mirror of https://github.com/langgenius/dify.git
feat: add api to retrieving logs from text generation applications and chat applications
This commit is contained in:
parent
2c919efa69
commit
c47dc8f18b
|
|
@ -51,7 +51,9 @@ from .app import (
|
|||
annotation,
|
||||
app,
|
||||
audio,
|
||||
chat_app_log,
|
||||
completion,
|
||||
completion_app_log,
|
||||
conversation,
|
||||
conversation_variables,
|
||||
generator,
|
||||
|
|
@ -147,7 +149,9 @@ __all__ = [
|
|||
"audio",
|
||||
"billing",
|
||||
"bp",
|
||||
"chat_app_log",
|
||||
"completion",
|
||||
"completion_app_log",
|
||||
"compliance",
|
||||
"console_ns",
|
||||
"conversation",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
from datetime import datetime
|
||||
|
||||
from dateutil.parser import isoparse
|
||||
from flask import request
|
||||
from flask_restx import Resource, marshal_with
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from extensions.ext_database import db
|
||||
from fields.chat_app_log_fields import build_chat_app_log_pagination_model
|
||||
from libs.login import login_required
|
||||
from models import App
|
||||
from models.model import AppMode
|
||||
from services.chat_app_log_service import ChatAppLogService
|
||||
|
||||
DEFAULT_REF_TEMPLATE_SWAGGER_2_0 = "#/definitions/{model}"
|
||||
|
||||
|
||||
class ChatAppLogQuery(BaseModel):
|
||||
status: str | None = Field(default=None, description="Message status filter")
|
||||
created_at__before: datetime | None = Field(default=None, description="Filter logs created before this timestamp")
|
||||
created_at__after: datetime | None = Field(default=None, description="Filter logs created after this timestamp")
|
||||
created_by_end_user_session_id: str | None = Field(default=None, description="Filter by end user session ID")
|
||||
created_by_account: str | None = Field(default=None, description="Filter by account")
|
||||
page: int = Field(default=1, ge=1, le=99999, description="Page number (1-99999)")
|
||||
limit: int = Field(default=20, ge=1, le=100, description="Number of items per page (1-100)")
|
||||
|
||||
@field_validator("created_at__before", "created_at__after", mode="before")
|
||||
@classmethod
|
||||
def parse_datetime(cls, value: str | None) -> datetime | None:
|
||||
if value in (None, ""):
|
||||
return None
|
||||
return isoparse(value) # type: ignore
|
||||
|
||||
|
||||
console_ns.schema_model(
|
||||
ChatAppLogQuery.__name__, ChatAppLogQuery.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_SWAGGER_2_0)
|
||||
)
|
||||
|
||||
# Register model for flask_restx to avoid dict type issues in Swagger
|
||||
chat_app_log_pagination_model = build_chat_app_log_pagination_model(console_ns)
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/chat-app-logs")
|
||||
class ChatAppLogApi(Resource):
|
||||
@console_ns.doc("get_chat_app_logs")
|
||||
@console_ns.doc(description="Get chat application execution logs with token consumption")
|
||||
@console_ns.doc(params={"app_id": "Application ID"})
|
||||
@console_ns.expect(console_ns.models[ChatAppLogQuery.__name__])
|
||||
@console_ns.response(200, "Chat app logs retrieved successfully", chat_app_log_pagination_model)
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT])
|
||||
@marshal_with(chat_app_log_pagination_model)
|
||||
def get(self, app_model: App):
|
||||
"""
|
||||
Get chat app logs with token consumption information
|
||||
"""
|
||||
args = ChatAppLogQuery.model_validate(request.args.to_dict(flat=True)) # type: ignore
|
||||
|
||||
# get paginate chat app logs
|
||||
chat_app_log_service = ChatAppLogService()
|
||||
with Session(db.engine) as session:
|
||||
chat_app_log_pagination = chat_app_log_service.get_paginate_chat_app_logs(
|
||||
session=session,
|
||||
app_model=app_model,
|
||||
status=args.status,
|
||||
created_at_before=args.created_at__before,
|
||||
created_at_after=args.created_at__after,
|
||||
page=args.page,
|
||||
limit=args.limit,
|
||||
created_by_end_user_session_id=args.created_by_end_user_session_id,
|
||||
created_by_account=args.created_by_account,
|
||||
)
|
||||
|
||||
return chat_app_log_pagination
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
from datetime import datetime
|
||||
|
||||
from dateutil.parser import isoparse
|
||||
from flask import request
|
||||
from flask_restx import Resource, marshal_with
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from extensions.ext_database import db
|
||||
from fields.completion_app_log_fields import build_completion_app_log_pagination_model
|
||||
from libs.login import login_required
|
||||
from models import App
|
||||
from models.model import AppMode
|
||||
from services.completion_app_log_service import CompletionAppLogService
|
||||
|
||||
DEFAULT_REF_TEMPLATE_SWAGGER_2_0 = "#/definitions/{model}"
|
||||
|
||||
|
||||
class CompletionAppLogQuery(BaseModel):
|
||||
status: str | None = Field(default=None, description="Message status filter")
|
||||
created_at__before: datetime | None = Field(default=None, description="Filter logs created before this timestamp")
|
||||
created_at__after: datetime | None = Field(default=None, description="Filter logs created after this timestamp")
|
||||
created_by_end_user_session_id: str | None = Field(default=None, description="Filter by end user ID")
|
||||
created_by_account: str | None = Field(default=None, description="Filter by account")
|
||||
page: int = Field(default=1, ge=1, le=99999, description="Page number (1-99999)")
|
||||
limit: int = Field(default=20, ge=1, le=100, description="Number of items per page (1-100)")
|
||||
|
||||
@field_validator("created_at__before", "created_at__after", mode="before")
|
||||
@classmethod
|
||||
def parse_datetime(cls, value: str | None) -> datetime | None:
|
||||
if value in (None, ""):
|
||||
return None
|
||||
return isoparse(value) # type: ignore
|
||||
|
||||
|
||||
console_ns.schema_model(
|
||||
CompletionAppLogQuery.__name__,
|
||||
CompletionAppLogQuery.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_SWAGGER_2_0),
|
||||
)
|
||||
|
||||
# Register model for flask_restx to avoid dict type issues in Swagger
|
||||
completion_app_log_pagination_model = build_completion_app_log_pagination_model(console_ns)
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/completion-app-logs")
|
||||
class CompletionAppLogApi(Resource):
|
||||
@console_ns.doc("get_completion_app_logs")
|
||||
@console_ns.doc(description="Get completion application execution logs with token consumption")
|
||||
@console_ns.doc(params={"app_id": "Application ID"})
|
||||
@console_ns.expect(console_ns.models[CompletionAppLogQuery.__name__])
|
||||
@console_ns.response(200, "Completion app logs retrieved successfully", completion_app_log_pagination_model)
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=[AppMode.COMPLETION])
|
||||
@marshal_with(completion_app_log_pagination_model)
|
||||
def get(self, app_model: App):
|
||||
"""
|
||||
Get completion app logs with token consumption information
|
||||
"""
|
||||
args = CompletionAppLogQuery.model_validate(request.args.to_dict(flat=True)) # type: ignore
|
||||
|
||||
# get paginate completion app logs
|
||||
completion_app_log_service = CompletionAppLogService()
|
||||
with Session(db.engine) as session:
|
||||
completion_app_log_pagination = completion_app_log_service.get_paginate_completion_app_logs(
|
||||
session=session,
|
||||
app_model=app_model,
|
||||
status=args.status,
|
||||
created_at_before=args.created_at__before,
|
||||
created_at_after=args.created_at__after,
|
||||
page=args.page,
|
||||
limit=args.limit,
|
||||
created_by_end_user_session_id=args.created_by_end_user_session_id,
|
||||
created_by_account=args.created_by_account,
|
||||
)
|
||||
|
||||
return completion_app_log_pagination
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
from flask_restx import Api, Namespace, fields
|
||||
|
||||
from fields.end_user_fields import build_simple_end_user_model, simple_end_user_fields
|
||||
from fields.member_fields import build_simple_account_model, simple_account_fields
|
||||
from libs.helper import TimestampField
|
||||
|
||||
chat_conversation_fields = {
|
||||
"id": fields.String,
|
||||
"name": fields.String,
|
||||
"status": fields.String,
|
||||
}
|
||||
|
||||
chat_message_fields = {
|
||||
"id": fields.String,
|
||||
"conversation_id": fields.String,
|
||||
"query": fields.String,
|
||||
"answer": fields.String,
|
||||
"status": fields.String,
|
||||
"message_tokens": fields.Integer,
|
||||
"total_tokens": fields.Integer,
|
||||
"created_at": TimestampField,
|
||||
"error": fields.String,
|
||||
"provider_response_latency": fields.Float,
|
||||
"from_source": fields.String,
|
||||
"from_end_user_id": fields.String,
|
||||
"from_account_id": fields.String,
|
||||
}
|
||||
|
||||
chat_app_log_partial_fields = {
|
||||
"id": fields.String,
|
||||
"conversation": fields.Nested(chat_conversation_fields, attribute="conversation", allow_null=True),
|
||||
"message": fields.Nested(chat_message_fields, attribute="message", allow_null=False),
|
||||
"created_from": fields.String,
|
||||
"created_by_role": fields.String,
|
||||
"created_by_account": fields.Nested(simple_account_fields, attribute="created_by_account", allow_null=True),
|
||||
"created_by_end_user": fields.Nested(simple_end_user_fields, attribute="created_by_end_user", allow_null=True),
|
||||
"created_at": TimestampField,
|
||||
}
|
||||
|
||||
|
||||
def build_chat_conversation_model(api_or_ns: Api | Namespace):
|
||||
"""Build the chat conversation model for the API or Namespace."""
|
||||
return api_or_ns.model("ChatConversation", chat_conversation_fields)
|
||||
|
||||
|
||||
def build_chat_message_model(api_or_ns: Api | Namespace):
|
||||
"""Build the chat message model for the API or Namespace."""
|
||||
return api_or_ns.model("ChatMessage", chat_message_fields)
|
||||
|
||||
|
||||
def build_chat_app_log_partial_model(api_or_ns: Api | Namespace):
|
||||
"""Build the chat app log partial model for the API or Namespace."""
|
||||
simple_account_model = build_simple_account_model(api_or_ns)
|
||||
simple_end_user_model = build_simple_end_user_model(api_or_ns)
|
||||
chat_conversation_model = build_chat_conversation_model(api_or_ns)
|
||||
chat_message_model = build_chat_message_model(api_or_ns)
|
||||
|
||||
copied_fields = chat_app_log_partial_fields.copy()
|
||||
copied_fields["conversation"] = fields.Nested(chat_conversation_model, attribute="conversation", allow_null=True)
|
||||
copied_fields["message"] = fields.Nested(chat_message_model, attribute="message", allow_null=False)
|
||||
copied_fields["created_by_account"] = fields.Nested(
|
||||
simple_account_model, attribute="created_by_account", allow_null=True
|
||||
)
|
||||
copied_fields["created_by_end_user"] = fields.Nested(
|
||||
simple_end_user_model, attribute="created_by_end_user", allow_null=True
|
||||
)
|
||||
return api_or_ns.model("ChatAppLogPartial", copied_fields)
|
||||
|
||||
|
||||
chat_app_log_pagination_fields = {
|
||||
"page": fields.Integer,
|
||||
"limit": fields.Integer,
|
||||
"total": fields.Integer,
|
||||
"has_more": fields.Boolean,
|
||||
"data": fields.List(fields.Nested(chat_app_log_partial_fields)),
|
||||
}
|
||||
|
||||
|
||||
def build_chat_app_log_pagination_model(api_or_ns: Api | Namespace):
|
||||
"""Build the chat app log pagination model for the API or Namespace."""
|
||||
# Build the nested partial model first
|
||||
chat_app_log_partial_model = build_chat_app_log_partial_model(api_or_ns)
|
||||
|
||||
copied_fields = chat_app_log_pagination_fields.copy()
|
||||
copied_fields["data"] = fields.List(fields.Nested(chat_app_log_partial_model))
|
||||
return api_or_ns.model("ChatAppLogPagination", copied_fields)
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
from flask_restx import Api, Namespace, fields
|
||||
|
||||
from fields.end_user_fields import build_simple_end_user_model, simple_end_user_fields
|
||||
from fields.member_fields import build_simple_account_model, simple_account_fields
|
||||
from libs.helper import TimestampField
|
||||
|
||||
completion_message_fields = {
|
||||
"id": fields.String,
|
||||
"query": fields.String,
|
||||
"answer": fields.String,
|
||||
"status": fields.String,
|
||||
"message_tokens": fields.Integer,
|
||||
"total_tokens": fields.Integer,
|
||||
"created_at": TimestampField,
|
||||
"error": fields.String,
|
||||
"provider_response_latency": fields.Float,
|
||||
"from_source": fields.String,
|
||||
"from_end_user_id": fields.String,
|
||||
"from_account_id": fields.String,
|
||||
}
|
||||
|
||||
completion_app_log_partial_fields = {
|
||||
"id": fields.String,
|
||||
"message": fields.Nested(completion_message_fields, attribute="message", allow_null=False),
|
||||
"created_from": fields.String,
|
||||
"created_by_role": fields.String,
|
||||
"created_by_account": fields.Nested(simple_account_fields, attribute="created_by_account", allow_null=True),
|
||||
"created_by_end_user": fields.Nested(simple_end_user_fields, attribute="created_by_end_user", allow_null=True),
|
||||
"created_at": TimestampField,
|
||||
}
|
||||
|
||||
|
||||
def build_completion_message_model(api_or_ns: Api | Namespace):
|
||||
"""Build the completion message model for the API or Namespace."""
|
||||
return api_or_ns.model("CompletionMessage", completion_message_fields)
|
||||
|
||||
|
||||
def build_completion_app_log_partial_model(api_or_ns: Api | Namespace):
|
||||
"""Build the completion app log partial model for the API or Namespace."""
|
||||
simple_account_model = build_simple_account_model(api_or_ns)
|
||||
simple_end_user_model = build_simple_end_user_model(api_or_ns)
|
||||
completion_message_model = build_completion_message_model(api_or_ns)
|
||||
|
||||
copied_fields = completion_app_log_partial_fields.copy()
|
||||
copied_fields["message"] = fields.Nested(completion_message_model, attribute="message", allow_null=False)
|
||||
copied_fields["created_by_account"] = fields.Nested(
|
||||
simple_account_model, attribute="created_by_account", allow_null=True
|
||||
)
|
||||
copied_fields["created_by_end_user"] = fields.Nested(
|
||||
simple_end_user_model, attribute="created_by_end_user", allow_null=True
|
||||
)
|
||||
return api_or_ns.model("CompletionAppLogPartial", copied_fields)
|
||||
|
||||
|
||||
completion_app_log_pagination_fields = {
|
||||
"page": fields.Integer,
|
||||
"limit": fields.Integer,
|
||||
"total": fields.Integer,
|
||||
"has_more": fields.Boolean,
|
||||
"data": fields.List(fields.Nested(completion_app_log_partial_fields)),
|
||||
}
|
||||
|
||||
|
||||
def build_completion_app_log_pagination_model(api_or_ns: Api | Namespace):
|
||||
"""Build the completion app log pagination model for the API or Namespace."""
|
||||
# Build the nested partial model first
|
||||
completion_app_log_partial_model = build_completion_app_log_partial_model(api_or_ns)
|
||||
|
||||
copied_fields = completion_app_log_pagination_fields.copy()
|
||||
copied_fields["data"] = fields.List(fields.Nested(completion_app_log_partial_model))
|
||||
return api_or_ns.model("CompletionAppLogPagination", copied_fields)
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import and_, func, or_, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models import Account, App, Conversation, EndUser, Message
|
||||
from models.enums import CreatorUserRole
|
||||
from models.model import AppMode
|
||||
from services.message_app_log_service import MessageAppLogServiceBase
|
||||
|
||||
|
||||
class ChatAppLogService(MessageAppLogServiceBase):
|
||||
def get_paginate_chat_app_logs(
|
||||
self,
|
||||
*,
|
||||
session: Session,
|
||||
app_model: App,
|
||||
status: str | None = None,
|
||||
created_at_before: datetime | None = None,
|
||||
created_at_after: datetime | None = None,
|
||||
page: int = 1,
|
||||
limit: int = 20,
|
||||
created_by_end_user_session_id: str | None = None,
|
||||
created_by_account: str | None = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Get paginated chat app logs with token consumption information.
|
||||
"""
|
||||
return self.get_paginate_app_logs(
|
||||
session=session,
|
||||
app_model=app_model,
|
||||
status=status,
|
||||
created_at_before=created_at_before,
|
||||
created_at_after=created_at_after,
|
||||
page=page,
|
||||
limit=limit,
|
||||
created_by_end_user_session_id=created_by_end_user_session_id,
|
||||
created_by_account=created_by_account,
|
||||
)
|
||||
|
||||
def get_app_mode_filter(self):
|
||||
"""Return the filter condition for chat app modes."""
|
||||
return or_(
|
||||
Message.app_mode == AppMode.CHAT.value,
|
||||
Message.app_mode == AppMode.AGENT_CHAT.value,
|
||||
Message.app_mode == AppMode.ADVANCED_CHAT.value,
|
||||
)
|
||||
|
||||
def build_log_data(self, session, message: Message, conversation=None):
|
||||
"""Build log data for chat app."""
|
||||
# For chat apps, we use from_account_id/from_end_user_id instead of created_by_*
|
||||
account_obj = None
|
||||
end_user_obj = None
|
||||
created_from = "api"
|
||||
created_by_role = None
|
||||
|
||||
if message.from_account_id:
|
||||
account_obj = session.get(Account, message.from_account_id)
|
||||
created_from = "web_app"
|
||||
created_by_role = CreatorUserRole.ACCOUNT.value
|
||||
elif message.from_end_user_id:
|
||||
end_user_obj = session.get(EndUser, message.from_end_user_id)
|
||||
created_from = "service_api"
|
||||
created_by_role = CreatorUserRole.END_USER.value
|
||||
|
||||
return {
|
||||
"id": str(message.id),
|
||||
"conversation": {
|
||||
"id": str(conversation.id) if conversation else None,
|
||||
"name": conversation.name if conversation else None,
|
||||
"status": conversation.status if conversation else None,
|
||||
},
|
||||
"message": {
|
||||
"id": str(message.id),
|
||||
"conversation_id": str(message.conversation_id),
|
||||
"query": message.query,
|
||||
"answer": message.answer,
|
||||
"status": message.status,
|
||||
"message_tokens": message.message_tokens,
|
||||
"total_tokens": message.total_tokens,
|
||||
"created_at": message.created_at,
|
||||
"error": message.error,
|
||||
"provider_response_latency": message.provider_response_latency,
|
||||
"from_source": message.from_source,
|
||||
"from_end_user_id": message.from_end_user_id,
|
||||
"from_account_id": message.from_account_id,
|
||||
},
|
||||
"created_from": created_from,
|
||||
"created_by_role": created_by_role,
|
||||
"created_by_account": account_obj,
|
||||
"created_by_end_user": end_user_obj,
|
||||
"created_at": message.created_at,
|
||||
}
|
||||
|
||||
def _build_base_query(self, app_model: App):
|
||||
"""Build the base query for chat apps."""
|
||||
return (
|
||||
select(Message)
|
||||
.join(Conversation, Message.conversation_id == Conversation.id)
|
||||
.where(
|
||||
and_(
|
||||
Message.app_id == app_model.id,
|
||||
self.get_app_mode_filter(),
|
||||
)
|
||||
)
|
||||
.order_by(Message.created_at.desc())
|
||||
)
|
||||
|
||||
def _build_total_count_query(self, app_model: App):
|
||||
"""Build the total count query for chat apps."""
|
||||
return (
|
||||
select(func.count(Message.id))
|
||||
.join(Conversation, Message.conversation_id == Conversation.id)
|
||||
.where(
|
||||
and_(
|
||||
Message.app_id == app_model.id,
|
||||
self.get_app_mode_filter(),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import and_, func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models import App, Message
|
||||
from models.model import AppMode
|
||||
from services.message_app_log_service import MessageAppLogServiceBase
|
||||
|
||||
|
||||
class CompletionAppLogService(MessageAppLogServiceBase):
|
||||
def get_paginate_completion_app_logs(
|
||||
self,
|
||||
*,
|
||||
session: Session,
|
||||
app_model: App,
|
||||
status: str | None = None,
|
||||
created_at_before: datetime | None = None,
|
||||
created_at_after: datetime | None = None,
|
||||
page: int = 1,
|
||||
limit: int = 20,
|
||||
created_by_end_user_session_id: str | None = None,
|
||||
created_by_account: str | None = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Get paginated completion app logs with token consumption information.
|
||||
"""
|
||||
return self.get_paginate_app_logs(
|
||||
session=session,
|
||||
app_model=app_model,
|
||||
status=status,
|
||||
created_at_before=created_at_before,
|
||||
created_at_after=created_at_after,
|
||||
page=page,
|
||||
limit=limit,
|
||||
created_by_end_user_session_id=created_by_end_user_session_id,
|
||||
created_by_account=created_by_account,
|
||||
)
|
||||
|
||||
def get_app_mode_filter(self):
|
||||
"""Return the filter condition for completion app mode."""
|
||||
return Message.app_mode == AppMode.COMPLETION.value
|
||||
|
||||
def build_log_data(self, session, message, conversation=None):
|
||||
"""Build log data for completion app."""
|
||||
account_obj, end_user_obj, created_from, created_by_role = self._get_creator_info(session, message)
|
||||
|
||||
return {
|
||||
"id": str(message.id),
|
||||
"message": {
|
||||
"id": str(message.id),
|
||||
"query": message.query,
|
||||
"answer": message.answer,
|
||||
"status": message.status,
|
||||
"message_tokens": message.message_tokens,
|
||||
"total_tokens": message.total_tokens,
|
||||
"created_at": message.created_at,
|
||||
"error": message.error,
|
||||
"provider_response_latency": message.provider_response_latency,
|
||||
"from_source": message.from_source,
|
||||
"from_end_user_id": message.from_end_user_id,
|
||||
"from_account_id": message.from_account_id,
|
||||
},
|
||||
"created_from": created_from,
|
||||
"created_by_role": created_by_role,
|
||||
"created_by_account": account_obj,
|
||||
"created_by_end_user": end_user_obj,
|
||||
"created_at": message.created_at,
|
||||
}
|
||||
|
||||
def _build_base_query(self, app_model: App):
|
||||
"""Build the base query for completion apps."""
|
||||
return (
|
||||
select(Message)
|
||||
.where(
|
||||
and_(
|
||||
Message.app_id == app_model.id,
|
||||
self.get_app_mode_filter(),
|
||||
)
|
||||
)
|
||||
.order_by(Message.created_at.desc())
|
||||
)
|
||||
|
||||
def _build_total_count_query(self, app_model: App):
|
||||
"""Build the total count query for completion apps."""
|
||||
return select(func.count(Message.id)).where(
|
||||
and_(
|
||||
Message.app_id == app_model.id,
|
||||
self.get_app_mode_filter(),
|
||||
)
|
||||
)
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models import Account, App, Conversation, EndUser, Message
|
||||
from models.enums import CreatorUserRole
|
||||
|
||||
|
||||
class MessageAppLogServiceBase(ABC):
|
||||
"""Base service for message app logs with common functionality."""
|
||||
|
||||
@abstractmethod
|
||||
def get_app_mode_filter(self) -> Any:
|
||||
"""Return the filter conditions for the specific app mode."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def build_log_data(self, session: Session, message, conversation=None) -> dict:
|
||||
"""Build the log data dictionary for a specific app type."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _build_base_query(self, app_model: App) -> Any:
|
||||
"""Build the base query for the specific app type."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _build_total_count_query(self, app_model: App) -> Any:
|
||||
"""Build the total count query for the specific app type."""
|
||||
pass
|
||||
|
||||
def _apply_base_filters(
|
||||
self,
|
||||
session,
|
||||
query,
|
||||
status,
|
||||
created_at_before,
|
||||
created_at_after,
|
||||
created_by_end_user_session_id,
|
||||
created_by_account,
|
||||
):
|
||||
"""Apply common filters to the query."""
|
||||
# Keyword search removed due to performance limitations
|
||||
# ILIKE with wildcards cannot use B-tree indexes effectively
|
||||
|
||||
if status:
|
||||
query = query.where(Message.status == status)
|
||||
|
||||
if created_at_before:
|
||||
query = query.where(Message.created_at <= created_at_before)
|
||||
|
||||
if created_at_after:
|
||||
query = query.where(Message.created_at >= created_at_after)
|
||||
|
||||
if created_by_end_user_session_id:
|
||||
query = query.where(Message.from_end_user_id == created_by_end_user_session_id)
|
||||
|
||||
if created_by_account:
|
||||
account = self._get_account_by_email(session, created_by_account)
|
||||
if not account:
|
||||
return None, True # Signal that account was not found
|
||||
query = query.where(Message.from_account_id == account.id)
|
||||
|
||||
return query, False
|
||||
|
||||
def _get_account_by_email(self, session, email):
|
||||
"""Get account by email from the database."""
|
||||
return session.scalar(select(Account).where(Account.email == email))
|
||||
|
||||
def _get_creator_info(self, session, message: Message):
|
||||
"""Get creator information for a message."""
|
||||
account_obj = None
|
||||
end_user_obj = None
|
||||
created_from = "api"
|
||||
created_by_role = None
|
||||
|
||||
if message.from_account_id:
|
||||
account_obj = session.get(Account, message.from_account_id)
|
||||
created_from = "web_app"
|
||||
created_by_role = CreatorUserRole.ACCOUNT.value
|
||||
elif message.from_end_user_id:
|
||||
end_user_obj = session.get(EndUser, message.from_end_user_id)
|
||||
created_from = "service_api"
|
||||
created_by_role = CreatorUserRole.END_USER.value
|
||||
|
||||
return account_obj, end_user_obj, created_from, created_by_role
|
||||
|
||||
def get_paginate_app_logs(
|
||||
self,
|
||||
session: Session,
|
||||
app_model: App,
|
||||
status: str | None = None,
|
||||
created_at_before: datetime | None = None,
|
||||
created_at_after: datetime | None = None,
|
||||
page: int = 1,
|
||||
limit: int = 20,
|
||||
created_by_end_user_session_id: str | None = None,
|
||||
created_by_account: str | None = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Get paginated app logs with token consumption information.
|
||||
This is the main method that coordinates the log retrieval process.
|
||||
"""
|
||||
# Build base query
|
||||
query = self._build_base_query(app_model)
|
||||
|
||||
# Apply filters
|
||||
query, account_not_found = self._apply_base_filters(
|
||||
session,
|
||||
query,
|
||||
status,
|
||||
created_at_before,
|
||||
created_at_after,
|
||||
created_by_end_user_session_id,
|
||||
created_by_account,
|
||||
)
|
||||
|
||||
# If account not found, return empty results
|
||||
if account_not_found or query is None:
|
||||
return self._empty_result(page, limit)
|
||||
|
||||
# Build and execute total count query
|
||||
total_query = self._build_total_count_query(app_model)
|
||||
total_query, account_not_found = self._apply_base_filters(
|
||||
session,
|
||||
total_query,
|
||||
status,
|
||||
created_at_before,
|
||||
created_at_after,
|
||||
created_by_end_user_session_id,
|
||||
created_by_account,
|
||||
)
|
||||
|
||||
# If account not found in count query, return empty results
|
||||
if account_not_found or total_query is None:
|
||||
return self._empty_result(page, limit)
|
||||
|
||||
# Get total count
|
||||
total_result = session.execute(total_query).scalar()
|
||||
total = total_result if total_result is not None else 0
|
||||
|
||||
# Apply pagination
|
||||
offset = (page - 1) * limit
|
||||
query = query.offset(offset).limit(limit)
|
||||
|
||||
# Execute query
|
||||
messages = session.execute(query).scalars().all()
|
||||
|
||||
# Transform to log format
|
||||
logs = []
|
||||
|
||||
conversation_ids = {msg.conversation_id for msg in messages if msg.conversation_id}
|
||||
conversations = {}
|
||||
if conversation_ids:
|
||||
conversation_results = session.query(Conversation).filter(Conversation.id.in_(conversation_ids)).all()
|
||||
conversations = {conv.id: conv for conv in conversation_results}
|
||||
|
||||
for message in messages:
|
||||
conversation = conversations.get(message.conversation_id)
|
||||
log_data = self.build_log_data(session, message, conversation)
|
||||
logs.append(log_data)
|
||||
|
||||
has_more = offset + limit < total
|
||||
|
||||
return {
|
||||
"data": logs,
|
||||
"has_more": has_more,
|
||||
"limit": limit,
|
||||
"total": total,
|
||||
"page": page,
|
||||
}
|
||||
|
||||
def _empty_result(self, page, limit):
|
||||
"""Return empty result set."""
|
||||
return {
|
||||
"data": [],
|
||||
"has_more": False,
|
||||
"limit": limit,
|
||||
"total": 0,
|
||||
"page": page,
|
||||
}
|
||||
|
|
@ -0,0 +1,329 @@
|
|||
"""Integration tests for Chat App Log API endpoints."""
|
||||
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.integration_tests.controllers.console.app.test_feedback_api_basic import TestFeedbackApiBasic
|
||||
|
||||
|
||||
class TestChatAppLogApiBasic(TestFeedbackApiBasic):
|
||||
"""Basic integration tests for Chat App Log API endpoints."""
|
||||
|
||||
def test_chat_app_logs_endpoint_exists(self, test_client, auth_header):
|
||||
"""Test that chat app logs endpoint exists and handles basic requests."""
|
||||
app_id = str(uuid.uuid4())
|
||||
|
||||
# Test endpoint exists (even if it fails, it should return 500 or 403, not 404)
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/chat-app-logs", headers=auth_header, query_string={"page": 1, "limit": 20}
|
||||
)
|
||||
|
||||
# Should not return 404 (endpoint exists)
|
||||
assert response.status_code != 404
|
||||
|
||||
# Should return authentication or permission error, or success if app exists
|
||||
assert response.status_code in [200, 401, 403, 500]
|
||||
|
||||
def test_chat_app_logs_endpoint_with_parameters(self, test_client, auth_header):
|
||||
"""Test chat app logs endpoint with various query parameters."""
|
||||
app_id = str(uuid.uuid4())
|
||||
|
||||
# Test with all chat-specific parameters
|
||||
params = {
|
||||
"status": "normal",
|
||||
"created_at__before": "2024-01-01T00:00:00Z",
|
||||
"created_at__after": "2023-12-01T00:00:00Z",
|
||||
"from_end_user_id": "user_session_456",
|
||||
"created_by_account": "user@example.com",
|
||||
"page": 2,
|
||||
"limit": 15,
|
||||
}
|
||||
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/chat-app-logs", headers=auth_header, query_string=params
|
||||
)
|
||||
|
||||
# Should not return 404 (endpoint exists)
|
||||
assert response.status_code != 404
|
||||
|
||||
def test_chat_app_logs_endpoint_invalid_parameters(self, test_client, auth_header):
|
||||
"""Test chat app logs endpoint with invalid parameters."""
|
||||
app_id = str(uuid.uuid4())
|
||||
|
||||
# Test with invalid page number
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/chat-app-logs",
|
||||
headers=auth_header,
|
||||
query_string={"page": 100000}, # Invalid: page should be <= 99999
|
||||
)
|
||||
|
||||
# Should return validation error
|
||||
assert response.status_code == 400
|
||||
|
||||
# Test with invalid limit
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/chat-app-logs",
|
||||
headers=auth_header,
|
||||
query_string={"limit": 101}, # Invalid: limit should be <= 100
|
||||
)
|
||||
|
||||
# Should return validation error
|
||||
assert response.status_code == 400
|
||||
|
||||
# Test with negative page
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/chat-app-logs",
|
||||
headers=auth_header,
|
||||
query_string={"page": -1}, # Invalid: page should be >= 1
|
||||
)
|
||||
|
||||
# Should return validation error
|
||||
assert response.status_code == 400
|
||||
|
||||
# Test with invalid datetime format
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/chat-app-logs",
|
||||
headers=auth_header,
|
||||
query_string={"created_at__before": "not-a-date"},
|
||||
)
|
||||
|
||||
# Should return validation error
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_chat_app_logs_endpoint_no_auth(self, test_client):
|
||||
"""Test chat app logs endpoint without authentication."""
|
||||
app_id = str(uuid.uuid4())
|
||||
|
||||
response = test_client.get(f"/console/api/apps/{app_id}/chat-app-logs", query_string={"page": 1, "limit": 20})
|
||||
|
||||
# Should return authentication error
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_chat_app_logs_response_structure(self, test_client, auth_header):
|
||||
"""Test that successful response has correct structure for chat logs."""
|
||||
app_id = str(uuid.uuid4())
|
||||
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/chat-app-logs", headers=auth_header, query_string={"page": 1, "limit": 20}
|
||||
)
|
||||
|
||||
# If we get a successful response, verify structure
|
||||
if response.status_code == 200:
|
||||
data = response.get_json()
|
||||
|
||||
# Verify response structure
|
||||
assert "data" in data
|
||||
assert "has_more" in data
|
||||
assert "limit" in data
|
||||
assert "total" in data
|
||||
assert "page" in data
|
||||
|
||||
# Verify data is a list
|
||||
assert isinstance(data["data"], list)
|
||||
|
||||
# If there are log entries, verify structure of each entry
|
||||
if len(data["data"]) > 0:
|
||||
entry = data["data"][0]
|
||||
|
||||
# Chat logs should have conversation data
|
||||
assert "conversation" in entry
|
||||
assert "message" in entry
|
||||
assert "created_from" in entry
|
||||
assert "created_by_role" in entry
|
||||
|
||||
# Conversation should have basic fields
|
||||
conversation = entry["conversation"]
|
||||
if conversation: # Can be None in some cases
|
||||
assert "id" in conversation
|
||||
assert "name" in conversation
|
||||
assert "status" in conversation
|
||||
|
||||
# Message should have token consumption fields
|
||||
message = entry["message"]
|
||||
assert "message_tokens" in message
|
||||
assert "total_tokens" in message
|
||||
assert "conversation_id" in message
|
||||
assert "query" in message
|
||||
assert "answer" in message
|
||||
|
||||
def test_chat_app_logs_endpoint_different_app_modes(self, test_client, auth_header):
|
||||
"""Test that endpoint works for different chat app modes."""
|
||||
app_id = str(uuid.uuid4())
|
||||
|
||||
# This should work for chat app modes (CHAT, AGENT_CHAT, ADVANCED_CHAT)
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/chat-app-logs", headers=auth_header, query_string={"page": 1, "limit": 20}
|
||||
)
|
||||
|
||||
# Should not return 404 (endpoint exists)
|
||||
# May return 403 if app doesn't exist or wrong mode, but not 404
|
||||
assert response.status_code != 404
|
||||
|
||||
def test_chat_app_logs_pagination_edge_cases(self, test_client, auth_header):
|
||||
"""Test pagination edge cases for chat app logs."""
|
||||
app_id = str(uuid.uuid4())
|
||||
|
||||
# Test minimum values
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/chat-app-logs", headers=auth_header, query_string={"page": 1, "limit": 1}
|
||||
)
|
||||
assert response.status_code in [200, 401, 403, 500]
|
||||
|
||||
# Test maximum values
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/chat-app-logs", headers=auth_header, query_string={"page": 99999, "limit": 100}
|
||||
)
|
||||
assert response.status_code in [200, 401, 403, 500]
|
||||
|
||||
def test_chat_app_logs_filtering_by_status(self, test_client, auth_header):
|
||||
"""Test filtering chat app logs by status."""
|
||||
app_id = str(uuid.uuid4())
|
||||
|
||||
# Test different status values
|
||||
statuses = ["normal", "error", "finished", "running"]
|
||||
|
||||
for status in statuses:
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/chat-app-logs",
|
||||
headers=auth_header,
|
||||
query_string={"status": status, "page": 1, "limit": 20},
|
||||
)
|
||||
assert response.status_code in [200, 401, 403, 500]
|
||||
|
||||
def test_chat_app_logs_filtering_by_dates(self, test_client, auth_header):
|
||||
"""Test filtering chat app logs by date ranges."""
|
||||
app_id = str(uuid.uuid4())
|
||||
|
||||
# Test date range filtering
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/chat-app-logs",
|
||||
headers=auth_header,
|
||||
query_string={"created_at__before": "2024-01-01T00:00:00Z", "created_at__after": "2023-01-01T00:00:00Z"},
|
||||
)
|
||||
assert response.status_code in [200, 401, 403, 500]
|
||||
|
||||
def test_chat_app_logs_filtering_by_users(self, test_client, auth_header):
|
||||
"""Test filtering chat app logs by user information."""
|
||||
app_id = str(uuid.uuid4())
|
||||
|
||||
# Test filtering by end user session ID
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/chat-app-logs",
|
||||
headers=auth_header,
|
||||
query_string={"from_end_user_id": "test_session_123456"},
|
||||
)
|
||||
assert response.status_code in [200, 401, 403, 500]
|
||||
|
||||
# Test filtering by account email
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/chat-app-logs",
|
||||
headers=auth_header,
|
||||
query_string={"created_by_account": "user@example.com"},
|
||||
)
|
||||
assert response.status_code in [200, 401, 403, 500]
|
||||
|
||||
# Test filtering by both
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/chat-app-logs",
|
||||
headers=auth_header,
|
||||
query_string={"from_end_user_id": "test_session", "created_by_account": "user@example.com"},
|
||||
)
|
||||
assert response.status_code in [200, 401, 403, 500]
|
||||
|
||||
def test_chat_app_logs_combined_filters(self, test_client, auth_header):
|
||||
"""Test chat app logs with multiple filters combined."""
|
||||
app_id = str(uuid.uuid4())
|
||||
|
||||
# Test combining multiple filters
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/chat-app-logs",
|
||||
headers=auth_header,
|
||||
query_string={
|
||||
"status": "normal",
|
||||
"created_at__before": "2024-01-01T00:00:00Z",
|
||||
"from_end_user_id": "user_session",
|
||||
"created_by_account": "support@company.com",
|
||||
"page": 2,
|
||||
"limit": 25,
|
||||
},
|
||||
)
|
||||
assert response.status_code in [200, 401, 403, 500]
|
||||
|
||||
def test_chat_app_logs_empty_parameters(self, test_client, auth_header):
|
||||
"""Test chat app logs with empty or null parameters."""
|
||||
app_id = str(uuid.uuid4())
|
||||
|
||||
# Test with empty string parameters (should be handled gracefully)
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/chat-app-logs",
|
||||
headers=auth_header,
|
||||
query_string={"status": "", "from_end_user_id": ""},
|
||||
)
|
||||
# Empty strings should not cause server errors
|
||||
assert response.status_code in [200, 401, 403, 500]
|
||||
|
||||
def test_chat_app_logs_large_page_numbers(self, test_client, auth_header):
|
||||
"""Test chat app logs with very large page numbers."""
|
||||
app_id = str(uuid.uuid4())
|
||||
|
||||
# Test with a very large page number (should return empty results if app exists)
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/chat-app-logs", headers=auth_header, query_string={"page": 999999, "limit": 20}
|
||||
)
|
||||
assert response.status_code in [200, 401, 403, 500]
|
||||
|
||||
@pytest.mark.parametrize("http_method", ["GET", "POST", "PUT", "DELETE", "PATCH"])
|
||||
def test_chat_app_logs_endpoint_http_methods(self, test_client, auth_header, http_method):
|
||||
"""Test that only GET method is supported for chat app logs."""
|
||||
app_id = str(uuid.uuid4())
|
||||
|
||||
if http_method == "GET":
|
||||
response = test_client.get(f"/console/api/apps/{app_id}/chat-app-logs", headers=auth_header)
|
||||
# GET should work (or return auth/permission errors)
|
||||
assert response.status_code in [200, 401, 403, 500]
|
||||
else:
|
||||
# Other methods should return 405 Method Not Allowed
|
||||
response = getattr(test_client, http_method.lower())(
|
||||
f"/console/api/apps/{app_id}/chat-app-logs", headers=auth_header
|
||||
)
|
||||
assert response.status_code == 405
|
||||
|
||||
def test_chat_app_logs_endpoint_content_type(self, test_client, auth_header):
|
||||
"""Test that chat app logs endpoint returns correct content type."""
|
||||
app_id = str(uuid.uuid4())
|
||||
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/chat-app-logs", headers=auth_header, query_string={"page": 1, "limit": 20}
|
||||
)
|
||||
|
||||
# If successful, should return JSON
|
||||
if response.status_code == 200:
|
||||
assert response.content_type == "application/json"
|
||||
|
||||
def test_chat_app_logs_query_string_encoding(self, test_client, auth_header):
|
||||
"""Test chat app logs with special characters in query parameters."""
|
||||
app_id = str(uuid.uuid4())
|
||||
|
||||
# Test with special characters in filter values
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/chat-app-logs",
|
||||
headers=auth_header,
|
||||
query_string={
|
||||
"from_end_user_id": "session+with-special&chars",
|
||||
"created_by_account": "user+test@example.com",
|
||||
},
|
||||
)
|
||||
assert response.status_code in [200, 401, 403, 500]
|
||||
|
||||
def test_chat_app_logs_unicode_support(self, test_client, auth_header):
|
||||
"""Test chat app logs with unicode characters in parameters."""
|
||||
app_id = str(uuid.uuid4())
|
||||
|
||||
# Test with unicode characters
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/chat-app-logs",
|
||||
headers=auth_header,
|
||||
query_string={"from_end_user_id": "用户会话123", "created_by_account": "user@测试.com"},
|
||||
)
|
||||
assert response.status_code in [200, 401, 403, 500]
|
||||
|
|
@ -0,0 +1,232 @@
|
|||
"""Integration tests for Completion App Log API endpoints."""
|
||||
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.integration_tests.controllers.console.app.test_feedback_api_basic import TestFeedbackApiBasic
|
||||
|
||||
|
||||
class TestCompletionAppLogApiBasic(TestFeedbackApiBasic):
|
||||
"""Basic integration tests for Completion App Log API endpoints."""
|
||||
|
||||
def test_completion_app_logs_endpoint_exists(self, test_client, auth_header):
|
||||
"""Test that completion app logs endpoint exists and handles basic requests."""
|
||||
app_id = str(uuid.uuid4())
|
||||
|
||||
# Test endpoint exists (even if it fails, it should return 500 or 403, not 404)
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/completion-app-logs",
|
||||
headers=auth_header,
|
||||
query_string={"page": 1, "limit": 20},
|
||||
)
|
||||
|
||||
# Should not return 404 (endpoint exists)
|
||||
assert response.status_code != 404
|
||||
|
||||
# Should return authentication or permission error, or success if app exists
|
||||
assert response.status_code in [200, 401, 403, 500]
|
||||
|
||||
def test_completion_app_logs_endpoint_with_parameters(self, test_client, auth_header):
|
||||
"""Test completion app logs endpoint with various query parameters."""
|
||||
app_id = str(uuid.uuid4())
|
||||
|
||||
# Test with all parameters
|
||||
params = {
|
||||
"status": "normal",
|
||||
"created_at__before": "2024-01-01T00:00:00Z",
|
||||
"created_at__after": "2023-12-01T00:00:00Z",
|
||||
"created_by_end_user_session_id": "test_session",
|
||||
"created_by_account": "user@example.com",
|
||||
"page": 1,
|
||||
"limit": 10,
|
||||
}
|
||||
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/completion-app-logs", headers=auth_header, query_string=params
|
||||
)
|
||||
|
||||
# Should not return 404 (endpoint exists)
|
||||
assert response.status_code != 404
|
||||
|
||||
def test_completion_app_logs_endpoint_invalid_parameters(self, test_client, auth_header):
|
||||
"""Test completion app logs endpoint with invalid parameters."""
|
||||
app_id = str(uuid.uuid4())
|
||||
|
||||
# Test with invalid page number
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/completion-app-logs",
|
||||
headers=auth_header,
|
||||
query_string={"page": 0}, # Invalid: page should be >= 1
|
||||
)
|
||||
|
||||
# Should return validation error
|
||||
assert response.status_code == 400
|
||||
|
||||
# Test with invalid limit
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/completion-app-logs",
|
||||
headers=auth_header,
|
||||
query_string={"limit": 0}, # Invalid: limit should be >= 1
|
||||
)
|
||||
|
||||
# Should return validation error
|
||||
assert response.status_code == 400
|
||||
|
||||
# Test with limit too large
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/completion-app-logs",
|
||||
headers=auth_header,
|
||||
query_string={"limit": 101}, # Invalid: limit should be <= 100
|
||||
)
|
||||
|
||||
# Should return validation error
|
||||
assert response.status_code == 400
|
||||
|
||||
# Test with invalid datetime format
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/completion-app-logs",
|
||||
headers=auth_header,
|
||||
query_string={"created_at__before": "invalid-date"},
|
||||
)
|
||||
|
||||
# Should return validation error
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_completion_app_logs_endpoint_no_auth(self, test_client):
|
||||
"""Test completion app logs endpoint without authentication."""
|
||||
app_id = str(uuid.uuid4())
|
||||
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/completion-app-logs", query_string={"page": 1, "limit": 20}
|
||||
)
|
||||
|
||||
# Should return authentication error
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_completion_app_logs_response_structure(self, test_client, auth_header):
|
||||
"""Test that successful response has correct structure."""
|
||||
app_id = str(uuid.uuid4())
|
||||
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/completion-app-logs",
|
||||
headers=auth_header,
|
||||
query_string={"page": 1, "limit": 20},
|
||||
)
|
||||
|
||||
# If we get a successful response, verify structure
|
||||
if response.status_code == 200:
|
||||
data = response.get_json()
|
||||
|
||||
# Verify response structure
|
||||
assert "data" in data
|
||||
assert "has_more" in data
|
||||
assert "limit" in data
|
||||
assert "total" in data
|
||||
assert "page" in data
|
||||
|
||||
# Verify data is a list
|
||||
assert isinstance(data["data"], list)
|
||||
|
||||
# Verify pagination fields are correct types
|
||||
assert isinstance(data["has_more"], bool)
|
||||
assert isinstance(data["limit"], int)
|
||||
assert isinstance(data["total"], int)
|
||||
assert isinstance(data["page"], int)
|
||||
|
||||
def test_completion_app_logs_endpoint_different_app_modes(self, test_client, auth_header):
|
||||
"""Test that endpoint is accessible for completion app mode."""
|
||||
app_id = str(uuid.uuid4())
|
||||
|
||||
# This should work for completion app mode
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/completion-app-logs",
|
||||
headers=auth_header,
|
||||
query_string={"page": 1, "limit": 20},
|
||||
)
|
||||
|
||||
# Should not return 404 (endpoint exists)
|
||||
# May return 403 if app doesn't exist or wrong mode, but not 404
|
||||
assert response.status_code != 404
|
||||
|
||||
def test_completion_app_logs_pagination_parameters(self, test_client, auth_header):
|
||||
"""Test pagination parameters work correctly."""
|
||||
app_id = str(uuid.uuid4())
|
||||
|
||||
# Test different page sizes
|
||||
for limit in [1, 5, 10, 20, 50, 100]:
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/completion-app-logs",
|
||||
headers=auth_header,
|
||||
query_string={"page": 1, "limit": limit},
|
||||
)
|
||||
|
||||
# Should handle valid limits without errors (except auth/permission issues)
|
||||
assert response.status_code in [200, 401, 403, 500]
|
||||
|
||||
# Test different page numbers
|
||||
for page in [1, 2, 5, 10]:
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/completion-app-logs",
|
||||
headers=auth_header,
|
||||
query_string={"page": page, "limit": 20},
|
||||
)
|
||||
|
||||
# Should handle valid pages without errors (except auth/permission issues)
|
||||
assert response.status_code in [200, 401, 403, 500]
|
||||
|
||||
def test_completion_app_logs_filter_parameters(self, test_client, auth_header):
|
||||
"""Test filter parameters work correctly."""
|
||||
app_id = str(uuid.uuid4())
|
||||
|
||||
# Test status filter
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/completion-app-logs", headers=auth_header, query_string={"status": "normal"}
|
||||
)
|
||||
assert response.status_code in [200, 401, 403, 500]
|
||||
|
||||
# Test date filters
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/completion-app-logs",
|
||||
headers=auth_header,
|
||||
query_string={"created_at__before": "2024-01-01T00:00:00Z", "created_at__after": "2023-12-01T00:00:00Z"},
|
||||
)
|
||||
assert response.status_code in [200, 401, 403, 500]
|
||||
|
||||
# Test user filters
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/completion-app-logs",
|
||||
headers=auth_header,
|
||||
query_string={"created_by_end_user_session_id": "session_123", "created_by_account": "user@example.com"},
|
||||
)
|
||||
assert response.status_code in [200, 401, 403, 500]
|
||||
|
||||
@pytest.mark.parametrize("http_method", ["GET", "POST", "PUT", "DELETE"])
|
||||
def test_completion_app_logs_endpoint_http_methods(self, test_client, auth_header, http_method):
|
||||
"""Test that only GET method is supported."""
|
||||
app_id = str(uuid.uuid4())
|
||||
|
||||
if http_method == "GET":
|
||||
response = test_client.get(f"/console/api/apps/{app_id}/completion-app-logs", headers=auth_header)
|
||||
# GET should work (or return auth/permission errors)
|
||||
assert response.status_code in [200, 401, 403, 500]
|
||||
else:
|
||||
# Other methods should return 405 Method Not Allowed
|
||||
response = getattr(test_client, http_method.lower())(
|
||||
f"/console/api/apps/{app_id}/completion-app-logs", headers=auth_header
|
||||
)
|
||||
assert response.status_code == 405
|
||||
|
||||
def test_completion_app_logs_endpoint_content_type(self, test_client, auth_header):
|
||||
"""Test that endpoint returns correct content type."""
|
||||
app_id = str(uuid.uuid4())
|
||||
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{app_id}/completion-app-logs",
|
||||
headers=auth_header,
|
||||
query_string={"page": 1, "limit": 20},
|
||||
)
|
||||
|
||||
# If successful, should return JSON
|
||||
if response.status_code == 200:
|
||||
assert response.content_type == "application/json"
|
||||
|
|
@ -0,0 +1,408 @@
|
|||
"""Unit tests for Chat App Log API."""
|
||||
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
|
||||
import pytest
|
||||
from flask_restx import marshal
|
||||
from pydantic import ValidationError
|
||||
|
||||
from controllers.console.app.chat_app_log import ChatAppLogQuery
|
||||
from fields.chat_app_log_fields import build_chat_app_log_pagination_model
|
||||
|
||||
|
||||
class TestChatAppLogQuery:
|
||||
"""Test cases for ChatAppLogQuery model validation."""
|
||||
|
||||
def test_valid_query_with_all_parameters(self):
|
||||
"""Test query validation with all valid parameters."""
|
||||
query_data = {
|
||||
"status": "normal",
|
||||
"created_at__before": "2024-01-01T00:00:00Z",
|
||||
"created_at__after": "2023-12-01T00:00:00Z",
|
||||
"created_by_end_user_session_id": "session_456",
|
||||
"created_by_account": "user@example.com",
|
||||
"page": 3,
|
||||
"limit": 30,
|
||||
}
|
||||
|
||||
# Should not raise any validation errors
|
||||
query = ChatAppLogQuery.model_validate(query_data)
|
||||
|
||||
assert query.status == "normal"
|
||||
assert query.created_at__before == datetime(2024, 1, 1, 0, 0, 0, tzinfo=UTC)
|
||||
assert query.created_at__after == datetime(2023, 12, 1, 0, 0, 0, tzinfo=UTC)
|
||||
assert query.created_by_end_user_session_id == "session_456"
|
||||
assert query.created_by_account == "user@example.com"
|
||||
assert query.page == 3
|
||||
assert query.limit == 30
|
||||
|
||||
def test_valid_query_with_minimal_parameters(self):
|
||||
"""Test query validation with minimal required parameters."""
|
||||
query_data = {"page": 1, "limit": 20}
|
||||
|
||||
query = ChatAppLogQuery.model_validate(query_data)
|
||||
|
||||
assert query.status is None
|
||||
assert query.created_at__before is None
|
||||
assert query.created_at__after is None
|
||||
assert query.created_by_end_user_session_id is None
|
||||
assert query.created_by_account is None
|
||||
assert query.page == 1
|
||||
assert query.limit == 20
|
||||
|
||||
def test_invalid_page_number_too_low(self):
|
||||
"""Test validation with page number too low."""
|
||||
query_data = {"page": 0} # Page should be >= 1
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
ChatAppLogQuery.model_validate(query_data)
|
||||
|
||||
def test_invalid_page_number_too_high(self):
|
||||
"""Test validation with page number too high."""
|
||||
query_data = {"page": 100000} # Page should be <= 99999
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
ChatAppLogQuery.model_validate(query_data)
|
||||
|
||||
def test_invalid_limit_too_small(self):
|
||||
"""Test validation with limit too small."""
|
||||
query_data = {"limit": 0} # Limit should be >= 1
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
ChatAppLogQuery.model_validate(query_data)
|
||||
|
||||
def test_invalid_limit_too_large(self):
|
||||
"""Test validation with limit too large."""
|
||||
query_data = {"limit": 101} # Limit should be <= 100
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
ChatAppLogQuery.model_validate(query_data)
|
||||
|
||||
def test_invalid_datetime_format(self):
|
||||
"""Test validation with invalid datetime format."""
|
||||
query_data = {"created_at__before": "not-a-date"}
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
ChatAppLogQuery.model_validate(query_data)
|
||||
|
||||
def test_empty_and_null_values(self):
|
||||
"""Test validation with empty and null values."""
|
||||
query_data = {
|
||||
"status": "",
|
||||
"created_at__before": "",
|
||||
"created_at__after": None,
|
||||
"created_by_end_user_session_id": "",
|
||||
"created_by_account": None,
|
||||
}
|
||||
|
||||
query = ChatAppLogQuery.model_validate(query_data)
|
||||
|
||||
# Empty strings should remain as empty strings, None should remain None
|
||||
assert query.status == ""
|
||||
assert query.created_at__before is None # Empty string becomes None for datetime
|
||||
assert query.created_at__after is None
|
||||
assert query.created_by_end_user_session_id == ""
|
||||
assert query.created_by_account is None
|
||||
|
||||
def test_edge_case_boundary_values(self):
|
||||
"""Test validation with boundary values."""
|
||||
# Test minimum valid values
|
||||
query_data = {"page": 1, "limit": 1}
|
||||
query = ChatAppLogQuery.model_validate(query_data)
|
||||
assert query.page == 1
|
||||
assert query.limit == 1
|
||||
|
||||
# Test maximum valid values
|
||||
query_data = {"page": 99999, "limit": 100}
|
||||
query = ChatAppLogQuery.model_validate(query_data)
|
||||
assert query.page == 99999
|
||||
assert query.limit == 100
|
||||
|
||||
def test_chat_specific_parameters(self):
|
||||
"""Test chat-specific parameter validation."""
|
||||
query_data = {
|
||||
"created_by_end_user_session_id": "user_session_12345",
|
||||
"status": "normal",
|
||||
}
|
||||
|
||||
query = ChatAppLogQuery.model_validate(query_data)
|
||||
|
||||
assert query.created_by_end_user_session_id == "user_session_12345"
|
||||
assert query.status == "normal"
|
||||
|
||||
def test_datetime_parsing_various_formats(self):
|
||||
"""Test datetime parsing with various ISO formats."""
|
||||
valid_datetimes = [
|
||||
"2024-01-01T00:00:00Z",
|
||||
"2024-01-01T12:30:45+00:00",
|
||||
"2024-01-01T12:30:45-05:00",
|
||||
]
|
||||
|
||||
for dt_str in valid_datetimes:
|
||||
query_data = {"created_at__before": dt_str}
|
||||
query = ChatAppLogQuery.model_validate(query_data)
|
||||
assert query.created_at__before is not None
|
||||
assert isinstance(query.created_at__before, datetime)
|
||||
assert query.created_at__before.tzinfo is not None # Should have timezone info
|
||||
|
||||
def test_model_to_dict_conversion(self):
|
||||
"""Test converting query model to dictionary."""
|
||||
query_data = {
|
||||
"status": "error",
|
||||
"created_by_end_user_session_id": "test_session",
|
||||
"page": 5,
|
||||
"limit": 15,
|
||||
}
|
||||
|
||||
query = ChatAppLogQuery.model_validate(query_data)
|
||||
query_dict = query.model_dump()
|
||||
|
||||
assert isinstance(query_dict, dict)
|
||||
assert query_dict["status"] == "error"
|
||||
assert query_dict["created_by_end_user_session_id"] == "test_session"
|
||||
assert query_dict["page"] == 5
|
||||
assert query_dict["limit"] == 15
|
||||
|
||||
|
||||
class TestChatAppLogFields:
|
||||
"""Test cases for chat app log field serialization."""
|
||||
|
||||
def test_build_pagination_model(self):
|
||||
"""Test building the pagination model for chat logs."""
|
||||
from flask_restx import Namespace
|
||||
|
||||
ns = Namespace("test")
|
||||
model = build_chat_app_log_pagination_model(ns)
|
||||
|
||||
assert model is not None
|
||||
assert model.name == "ChatAppLogPagination"
|
||||
|
||||
def test_marshal_empty_chat_response(self):
|
||||
"""Test marshaling an empty chat log response."""
|
||||
from flask_restx import Namespace
|
||||
|
||||
ns = Namespace("test")
|
||||
model = build_chat_app_log_pagination_model(ns)
|
||||
|
||||
empty_response = {
|
||||
"data": [],
|
||||
"has_more": False,
|
||||
"limit": 20,
|
||||
"total": 0,
|
||||
"page": 1,
|
||||
}
|
||||
|
||||
marshaled = marshal(empty_response, model)
|
||||
|
||||
assert marshaled["data"] == []
|
||||
assert marshaled["has_more"] is False
|
||||
assert marshaled["total"] == 0
|
||||
assert marshaled["page"] == 1
|
||||
|
||||
def test_marshal_chat_response_with_conversation(self):
|
||||
"""Test marshaling a response with conversation data."""
|
||||
from flask_restx import Namespace
|
||||
|
||||
ns = Namespace("test")
|
||||
model = build_chat_app_log_pagination_model(ns)
|
||||
|
||||
test_response = {
|
||||
"data": [
|
||||
{
|
||||
"id": str(uuid.uuid4()),
|
||||
"conversation": {
|
||||
"id": str(uuid.uuid4()),
|
||||
"name": "Customer Support Chat",
|
||||
"status": "active",
|
||||
},
|
||||
"message": {
|
||||
"id": str(uuid.uuid4()),
|
||||
"conversation_id": str(uuid.uuid4()),
|
||||
"query": "How can I reset my password?",
|
||||
"answer": "You can reset your password by clicking the 'Forgot Password' link.",
|
||||
"status": "normal",
|
||||
"message_tokens": 12,
|
||||
"total_tokens": 35,
|
||||
"created_at": datetime.utcnow(),
|
||||
"error": None,
|
||||
"provider_response_latency": 1.2,
|
||||
"from_source": "web_app",
|
||||
"from_end_user_id": str(uuid.uuid4()),
|
||||
"from_account_id": None,
|
||||
},
|
||||
"created_from": "web_app",
|
||||
"created_by_role": "end_user",
|
||||
"created_by_account": None,
|
||||
"created_by_end_user": {"id": str(uuid.uuid4())},
|
||||
"created_at": datetime.utcnow(),
|
||||
}
|
||||
],
|
||||
"has_more": True,
|
||||
"limit": 20,
|
||||
"total": 150,
|
||||
"page": 1,
|
||||
}
|
||||
|
||||
marshaled = marshal(test_response, model)
|
||||
|
||||
# Verify structure
|
||||
assert len(marshaled["data"]) == 1
|
||||
assert marshaled["has_more"] is True
|
||||
assert marshaled["total"] == 150
|
||||
|
||||
# Verify conversation data
|
||||
conversation = marshaled["data"][0]["conversation"]
|
||||
assert conversation["name"] == "Customer Support Chat"
|
||||
assert conversation["status"] == "active"
|
||||
|
||||
# Verify message token fields
|
||||
message = marshaled["data"][0]["message"]
|
||||
assert "message_tokens" in message
|
||||
assert "total_tokens" in message
|
||||
assert message["message_tokens"] == 12
|
||||
assert message["total_tokens"] == 35
|
||||
|
||||
def test_marshal_chat_response_multiple_entries(self):
|
||||
"""Test marshaling response with multiple chat log entries."""
|
||||
from flask_restx import Namespace
|
||||
|
||||
ns = Namespace("test")
|
||||
model = build_chat_app_log_pagination_model(ns)
|
||||
|
||||
# Create test data with multiple entries
|
||||
test_data = []
|
||||
for i in range(3):
|
||||
test_data.append(
|
||||
{
|
||||
"id": str(uuid.uuid4()),
|
||||
"conversation": {
|
||||
"id": str(uuid.uuid4()),
|
||||
"name": f"Chat {i + 1}",
|
||||
"status": "completed",
|
||||
},
|
||||
"message": {
|
||||
"id": str(uuid.uuid4()),
|
||||
"conversation_id": str(uuid.uuid4()),
|
||||
"query": f"Message {i + 1}",
|
||||
"answer": f"Response {i + 1}",
|
||||
"status": "normal",
|
||||
"message_tokens": 5 + i,
|
||||
"total_tokens": 15 + i * 2,
|
||||
"created_at": datetime.utcnow(),
|
||||
"error": None,
|
||||
"provider_response_latency": 0.5 + i * 0.1,
|
||||
"from_source": "api",
|
||||
"from_end_user_id": None,
|
||||
"from_account_id": str(uuid.uuid4()),
|
||||
},
|
||||
"created_from": "api",
|
||||
"created_by_role": "account",
|
||||
"created_by_account": {"id": str(uuid.uuid4())},
|
||||
"created_by_end_user": None,
|
||||
"created_at": datetime.utcnow(),
|
||||
}
|
||||
)
|
||||
|
||||
test_response = {
|
||||
"data": test_data,
|
||||
"has_more": False,
|
||||
"limit": 20,
|
||||
"total": 3,
|
||||
"page": 1,
|
||||
}
|
||||
|
||||
marshaled = marshal(test_response, model)
|
||||
|
||||
# Verify all entries are present
|
||||
assert len(marshaled["data"]) == 3
|
||||
assert marshaled["total"] == 3
|
||||
|
||||
# Verify each entry has conversation and message data
|
||||
for i, entry in enumerate(marshaled["data"]):
|
||||
assert "conversation" in entry
|
||||
assert "message" in entry
|
||||
assert entry["conversation"]["name"] == f"Chat {i + 1}"
|
||||
assert entry["message"]["query"] == f"Message {i + 1}"
|
||||
assert entry["message"]["message_tokens"] == 5 + i
|
||||
assert entry["message"]["total_tokens"] == 15 + i * 2
|
||||
|
||||
def test_marshal_response_with_creator_information(self):
|
||||
"""Test marshaling response with different creator types."""
|
||||
from flask_restx import Namespace
|
||||
|
||||
ns = Namespace("test")
|
||||
model = build_chat_app_log_pagination_model(ns)
|
||||
|
||||
# Test data for different creator scenarios
|
||||
scenarios = [
|
||||
{
|
||||
"name": "Account created",
|
||||
"created_from": "web_app",
|
||||
"created_by_role": "account",
|
||||
"created_by_account": {"id": str(uuid.uuid4()), "email": "user@example.com", "name": None},
|
||||
"created_by_end_user": None,
|
||||
},
|
||||
{
|
||||
"name": "End user created",
|
||||
"created_from": "service_api",
|
||||
"created_by_role": "end_user",
|
||||
"created_by_account": None,
|
||||
"created_by_end_user": {
|
||||
"id": str(uuid.uuid4()),
|
||||
"session_id": "session_123",
|
||||
"type": None,
|
||||
"is_anonymous": None,
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "Session created",
|
||||
"created_from": "service_api",
|
||||
"created_by_role": "end_user",
|
||||
"created_by_account": None,
|
||||
"created_by_end_user": None,
|
||||
},
|
||||
]
|
||||
|
||||
for scenario in scenarios:
|
||||
test_response = {
|
||||
"data": [
|
||||
{
|
||||
"id": str(uuid.uuid4()),
|
||||
"conversation": {
|
||||
"id": str(uuid.uuid4()),
|
||||
"name": scenario["name"],
|
||||
"status": "active",
|
||||
},
|
||||
"message": {
|
||||
"id": str(uuid.uuid4()),
|
||||
"conversation_id": str(uuid.uuid4()),
|
||||
"query": "Test query",
|
||||
"answer": "Test answer",
|
||||
"status": "normal",
|
||||
"message_tokens": 8,
|
||||
"total_tokens": 20,
|
||||
"created_at": datetime.utcnow(),
|
||||
"error": None,
|
||||
"provider_response_latency": 0.8,
|
||||
"from_source": "api",
|
||||
"from_end_user_id": None,
|
||||
"from_account_id": None,
|
||||
},
|
||||
**{k: v for k, v in scenario.items() if k != "name"},
|
||||
"created_at": datetime.utcnow(),
|
||||
}
|
||||
],
|
||||
"has_more": False,
|
||||
"limit": 20,
|
||||
"total": 1,
|
||||
"page": 1,
|
||||
}
|
||||
|
||||
marshaled = marshal(test_response, model)
|
||||
entry = marshaled["data"][0]
|
||||
|
||||
assert entry["created_from"] == scenario["created_from"]
|
||||
assert entry["created_by_role"] == scenario["created_by_role"]
|
||||
assert entry["created_by_account"] == scenario["created_by_account"]
|
||||
assert entry["created_by_end_user"] == scenario["created_by_end_user"]
|
||||
|
|
@ -0,0 +1,236 @@
|
|||
"""Unit tests for Completion App Log API."""
|
||||
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
|
||||
import pytest
|
||||
from flask_restx import marshal
|
||||
from pydantic import ValidationError
|
||||
|
||||
from controllers.console.app.completion_app_log import CompletionAppLogQuery
|
||||
from fields.completion_app_log_fields import build_completion_app_log_pagination_model
|
||||
|
||||
|
||||
class TestCompletionAppLogQuery:
|
||||
"""Test cases for CompletionAppLogQuery model validation."""
|
||||
|
||||
def test_valid_query_with_all_parameters(self):
|
||||
"""Test query validation with all valid parameters."""
|
||||
query_data = {
|
||||
"status": "normal",
|
||||
"created_at__before": "2024-01-01T00:00:00Z",
|
||||
"created_at__after": "2023-12-01T00:00:00Z",
|
||||
"created_by_end_user_session_id": "session_123",
|
||||
"created_by_account": "user@example.com",
|
||||
"page": 2,
|
||||
"limit": 50,
|
||||
}
|
||||
|
||||
# Should not raise any validation errors
|
||||
query = CompletionAppLogQuery.model_validate(query_data)
|
||||
|
||||
assert query.status == "normal"
|
||||
assert query.created_at__before == datetime(2024, 1, 1, 0, 0, 0, tzinfo=UTC)
|
||||
assert query.created_at__after == datetime(2023, 12, 1, 0, 0, 0, tzinfo=UTC)
|
||||
assert query.created_by_end_user_session_id == "session_123"
|
||||
assert query.created_by_account == "user@example.com"
|
||||
assert query.page == 2
|
||||
assert query.limit == 50
|
||||
|
||||
def test_valid_query_with_minimal_parameters(self):
|
||||
"""Test query validation with minimal required parameters."""
|
||||
query_data = {"page": 1, "limit": 20}
|
||||
|
||||
query = CompletionAppLogQuery.model_validate(query_data)
|
||||
|
||||
assert query.status is None
|
||||
assert query.created_at__before is None
|
||||
assert query.created_at__after is None
|
||||
assert query.created_by_end_user_session_id is None
|
||||
assert query.created_by_account is None
|
||||
assert query.page == 1
|
||||
assert query.limit == 20
|
||||
|
||||
def test_invalid_page_number(self):
|
||||
"""Test validation with invalid page number."""
|
||||
query_data = {"page": 0} # Page should be >= 1
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
CompletionAppLogQuery.model_validate(query_data)
|
||||
|
||||
def test_invalid_limit_too_small(self):
|
||||
"""Test validation with limit too small."""
|
||||
query_data = {"limit": 0} # Limit should be >= 1
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
CompletionAppLogQuery.model_validate(query_data)
|
||||
|
||||
def test_invalid_limit_too_large(self):
|
||||
"""Test validation with limit too large."""
|
||||
query_data = {"limit": 101} # Limit should be <= 100
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
CompletionAppLogQuery.model_validate(query_data)
|
||||
|
||||
def test_invalid_datetime_format(self):
|
||||
"""Test validation with invalid datetime format."""
|
||||
query_data = {"created_at__before": "invalid-date"}
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
CompletionAppLogQuery.model_validate(query_data)
|
||||
|
||||
def test_empty_datetime_values(self):
|
||||
"""Test validation with empty datetime values."""
|
||||
query_data = {
|
||||
"created_at__before": "",
|
||||
"created_at__after": None,
|
||||
}
|
||||
|
||||
query = CompletionAppLogQuery.model_validate(query_data)
|
||||
|
||||
assert query.created_at__before is None
|
||||
assert query.created_at__after is None
|
||||
|
||||
def test_edge_case_page_and_limit_values(self):
|
||||
"""Test validation with edge case values for page and limit."""
|
||||
query_data = {"page": 1, "limit": 1} # Minimum valid values
|
||||
|
||||
query = CompletionAppLogQuery.model_validate(query_data)
|
||||
|
||||
assert query.page == 1
|
||||
assert query.limit == 1
|
||||
|
||||
query_data = {"page": 99999, "limit": 100} # Maximum valid values
|
||||
|
||||
query = CompletionAppLogQuery.model_validate(query_data)
|
||||
|
||||
assert query.page == 99999
|
||||
assert query.limit == 100
|
||||
|
||||
def test_status_various_values(self):
|
||||
"""Test validation with various status values."""
|
||||
valid_statuses = ["normal", "error", "finished", "running"]
|
||||
|
||||
for status in valid_statuses:
|
||||
query_data = {"status": status}
|
||||
query = CompletionAppLogQuery.model_validate(query_data)
|
||||
assert query.status == status
|
||||
|
||||
def test_model_serialization(self):
|
||||
"""Test that the query model can be properly serialized."""
|
||||
query_data = {
|
||||
"status": "normal",
|
||||
"created_at__before": "2024-01-01T00:00:00Z",
|
||||
"page": 1,
|
||||
"limit": 20,
|
||||
}
|
||||
|
||||
query = CompletionAppLogQuery.model_validate(query_data)
|
||||
|
||||
# Should be able to convert to dict without errors
|
||||
query_dict = query.model_dump()
|
||||
assert isinstance(query_dict, dict)
|
||||
assert query_dict["status"] == "normal"
|
||||
assert query_dict["page"] == 1
|
||||
assert query_dict["limit"] == 20
|
||||
|
||||
def test_default_values(self):
|
||||
"""Test that default values are applied correctly."""
|
||||
query = CompletionAppLogQuery.model_validate({})
|
||||
|
||||
assert query.status is None
|
||||
assert query.created_at__before is None
|
||||
assert query.created_at__after is None
|
||||
assert query.created_by_end_user_session_id is None
|
||||
assert query.created_by_account is None
|
||||
assert query.page == 1 # Default page
|
||||
assert query.limit == 20 # Default limit
|
||||
|
||||
|
||||
class TestCompletionAppLogFields:
|
||||
"""Test cases for completion app log field serialization."""
|
||||
|
||||
def test_build_pagination_model(self):
|
||||
"""Test building the pagination model."""
|
||||
from flask_restx import Namespace
|
||||
|
||||
ns = Namespace("test")
|
||||
model = build_completion_app_log_pagination_model(ns)
|
||||
|
||||
assert model is not None
|
||||
assert model.name == "CompletionAppLogPagination"
|
||||
|
||||
def test_marshal_empty_response(self):
|
||||
"""Test marshaling an empty response."""
|
||||
from flask_restx import Namespace
|
||||
|
||||
ns = Namespace("test")
|
||||
model = build_completion_app_log_pagination_model(ns)
|
||||
|
||||
empty_response = {
|
||||
"data": [],
|
||||
"has_more": False,
|
||||
"limit": 20,
|
||||
"total": 0,
|
||||
"page": 1,
|
||||
}
|
||||
|
||||
marshaled = marshal(empty_response, model)
|
||||
|
||||
assert marshaled["data"] == []
|
||||
assert marshaled["has_more"] is False
|
||||
assert marshaled["limit"] == 20
|
||||
assert marshaled["total"] == 0
|
||||
assert marshaled["page"] == 1
|
||||
|
||||
def test_marshal_response_with_data(self):
|
||||
"""Test marshaling a response with data."""
|
||||
from flask_restx import Namespace
|
||||
|
||||
ns = Namespace("test")
|
||||
model = build_completion_app_log_pagination_model(ns)
|
||||
|
||||
test_response = {
|
||||
"data": [
|
||||
{
|
||||
"id": str(uuid.uuid4()),
|
||||
"message": {
|
||||
"id": str(uuid.uuid4()),
|
||||
"query": "Test query",
|
||||
"answer": "Test answer",
|
||||
"status": "normal",
|
||||
"message_tokens": 10,
|
||||
"total_tokens": 25,
|
||||
"created_at": datetime.utcnow(),
|
||||
"error": None,
|
||||
"provider_response_latency": 0.5,
|
||||
"from_source": "api",
|
||||
"from_end_user_id": None,
|
||||
"from_account_id": None,
|
||||
},
|
||||
"created_from": "api",
|
||||
"created_by_role": None,
|
||||
"created_by_account": None,
|
||||
"created_by_end_user": None,
|
||||
"created_at": datetime.utcnow(),
|
||||
}
|
||||
],
|
||||
"has_more": True,
|
||||
"limit": 20,
|
||||
"total": 100,
|
||||
"page": 1,
|
||||
}
|
||||
|
||||
marshaled = marshal(test_response, model)
|
||||
|
||||
assert len(marshaled["data"]) == 1
|
||||
assert marshaled["has_more"] is True
|
||||
assert marshaled["total"] == 100
|
||||
assert marshaled["page"] == 1
|
||||
|
||||
# Verify message token fields are present
|
||||
message = marshaled["data"][0]["message"]
|
||||
assert "message_tokens" in message
|
||||
assert "total_tokens" in message
|
||||
assert message["message_tokens"] == 10
|
||||
assert message["total_tokens"] == 25
|
||||
|
|
@ -0,0 +1,302 @@
|
|||
"""Unit tests for ChatAppLogService."""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models import Account, App, Conversation, EndUser, Message
|
||||
from models.model import AppMode
|
||||
from services.chat_app_log_service import ChatAppLogService
|
||||
|
||||
|
||||
class TestChatAppLogService:
|
||||
"""Test cases for ChatAppLogService."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.service = ChatAppLogService()
|
||||
self.app_id = str(uuid.uuid4())
|
||||
self.app_model = MagicMock(spec=App)
|
||||
self.app_model.id = self.app_id
|
||||
self.session = MagicMock(spec=Session)
|
||||
|
||||
def test_get_app_mode_filter(self):
|
||||
"""Test that app mode filter returns correct condition for chat modes."""
|
||||
filter_condition = self.service.get_app_mode_filter()
|
||||
# Should return a SQLAlchemy filter condition for chat modes
|
||||
assert filter_condition is not None
|
||||
|
||||
@patch("services.chat_app_log_service.or_")
|
||||
@patch("services.chat_app_log_service.Message")
|
||||
def test_get_app_mode_filter_values(self, mock_message_model, mock_or):
|
||||
"""Test that app mode filter includes all chat modes."""
|
||||
# Setup mocks
|
||||
mock_chat_condition = MagicMock()
|
||||
mock_agent_chat_condition = MagicMock()
|
||||
mock_advanced_chat_condition = MagicMock()
|
||||
|
||||
mock_message_model.app_mode.__eq__.side_effect = lambda value: {
|
||||
AppMode.CHAT.value: mock_chat_condition,
|
||||
AppMode.AGENT_CHAT.value: mock_agent_chat_condition,
|
||||
AppMode.ADVANCED_CHAT.value: mock_advanced_chat_condition,
|
||||
}[value]
|
||||
|
||||
mock_or.return_value = "combined_condition"
|
||||
|
||||
# Call the method
|
||||
result = self.service.get_app_mode_filter()
|
||||
|
||||
# Verify or_ was called with all three chat mode conditions
|
||||
mock_or.assert_called_once_with(
|
||||
mock_chat_condition,
|
||||
mock_agent_chat_condition,
|
||||
mock_advanced_chat_condition,
|
||||
)
|
||||
|
||||
assert result == "combined_condition"
|
||||
|
||||
def test_build_log_data_with_conversation(self):
|
||||
"""Test building log data with conversation information."""
|
||||
# Create mock conversation
|
||||
conversation = MagicMock(spec=Conversation)
|
||||
conversation.id = str(uuid.uuid4())
|
||||
conversation.name = "Test Conversation"
|
||||
conversation.status = "active"
|
||||
|
||||
# Create mock message
|
||||
message = MagicMock(spec=Message)
|
||||
message.id = str(uuid.uuid4())
|
||||
message.conversation_id = conversation.id
|
||||
message.query = "Hello"
|
||||
message.answer = "Hi there!"
|
||||
message.status = "normal"
|
||||
message.message_tokens = 5
|
||||
message.total_tokens = 12
|
||||
message.created_at = datetime.utcnow()
|
||||
message.error = None
|
||||
message.provider_response_latency = 0.4
|
||||
message.from_source = "web_app"
|
||||
message.from_end_user_id = None
|
||||
message.from_account_id = str(uuid.uuid4())
|
||||
|
||||
# Create mock account
|
||||
account = MagicMock(spec=Account)
|
||||
account.id = message.from_account_id
|
||||
|
||||
# Mock session.get to return the account
|
||||
self.session.get.return_value = account
|
||||
|
||||
# Build log data
|
||||
result = self.service.build_log_data(self.session, message, conversation)
|
||||
|
||||
# Verify the result structure
|
||||
assert result["id"] == message.id
|
||||
assert result["conversation"]["id"] == str(conversation.id)
|
||||
assert result["conversation"]["name"] == conversation.name
|
||||
assert result["conversation"]["status"] == conversation.status
|
||||
assert result["message"]["id"] == str(message.id)
|
||||
assert result["message"]["conversation_id"] == str(message.conversation_id)
|
||||
assert result["message"]["query"] == message.query
|
||||
assert result["message"]["answer"] == message.answer
|
||||
assert result["message"]["message_tokens"] == message.message_tokens
|
||||
assert result["message"]["total_tokens"] == message.total_tokens
|
||||
assert result["created_from"] == "web_app"
|
||||
assert result["created_by_role"] == "account"
|
||||
assert result["created_by_account"] == account
|
||||
assert result["created_by_end_user"] is None
|
||||
|
||||
def test_build_log_data_without_conversation(self):
|
||||
"""Test building log data when conversation is not provided."""
|
||||
# Create mock message
|
||||
message = MagicMock(spec=Message)
|
||||
message.id = str(uuid.uuid4())
|
||||
message.conversation_id = str(uuid.uuid4())
|
||||
message.query = "Hello"
|
||||
message.answer = "Hi there!"
|
||||
message.status = "normal"
|
||||
message.message_tokens = 8
|
||||
message.total_tokens = 15
|
||||
message.created_at = datetime.utcnow()
|
||||
message.from_end_user_id = str(uuid.uuid4())
|
||||
message.from_account_id = None
|
||||
|
||||
# Create mock end user
|
||||
end_user = MagicMock(spec=EndUser)
|
||||
end_user.id = message.from_end_user_id
|
||||
|
||||
# Mock session.get to return the end user
|
||||
def mock_get_side_effect(model, id):
|
||||
if model == EndUser:
|
||||
return end_user
|
||||
elif model == Conversation:
|
||||
return MagicMock(spec=Conversation, id=id, name="Test", status="active")
|
||||
return None
|
||||
|
||||
self.session.get.side_effect = mock_get_side_effect
|
||||
|
||||
# Build log data without conversation
|
||||
result = self.service.build_log_data(self.session, message)
|
||||
|
||||
# Verify conversation is None when not provided
|
||||
assert result["conversation"]["id"] is None
|
||||
assert result["conversation"]["name"] is None
|
||||
assert result["conversation"]["status"] is None
|
||||
assert result["created_from"] == "service_api"
|
||||
assert result["created_by_role"] == "end_user"
|
||||
assert result["created_by_account"] is None
|
||||
assert result["created_by_end_user"] == end_user
|
||||
|
||||
def test_build_log_data_with_session_id(self):
|
||||
"""Test building log data when created by session ID."""
|
||||
# Create mock message
|
||||
message = MagicMock(spec=Message)
|
||||
message.id = str(uuid.uuid4())
|
||||
message.conversation_id = str(uuid.uuid4())
|
||||
message.query = "Hello"
|
||||
message.answer = "Hi there!"
|
||||
message.status = "normal"
|
||||
message.message_tokens = 3
|
||||
message.total_tokens = 8
|
||||
message.created_at = datetime.utcnow()
|
||||
message.from_end_user_id = None
|
||||
message.from_account_id = None
|
||||
message.created_by_end_user_session_id = "test_session_123"
|
||||
|
||||
# Build log data
|
||||
result = self.service.build_log_data(self.session, message)
|
||||
|
||||
# Verify the result
|
||||
assert result["message"]["total_tokens"] == message.total_tokens
|
||||
assert result["created_from"] == "api" # Default when no from_end_user_id or from_account_id
|
||||
assert result["created_by_role"] is None # Default when no specific creator
|
||||
assert result["created_by_account"] is None
|
||||
assert result["created_by_end_user"] is None
|
||||
|
||||
# Verify session.get was not called for account/end user since using session ID
|
||||
calls = self.session.get.call_args_list
|
||||
account_calls = [call for call in calls if call[0][0] == Account]
|
||||
end_user_calls = [call for call in calls if call[0][0] == EndUser]
|
||||
assert len(account_calls) == 0
|
||||
assert len(end_user_calls) == 0
|
||||
|
||||
@patch.object(ChatAppLogService, "get_paginate_app_logs")
|
||||
def test_get_paginate_chat_app_logs(self, mock_get_paginate_app_logs):
|
||||
"""Test the main pagination method."""
|
||||
# Setup mock return value
|
||||
expected_result = {
|
||||
"data": [{"id": "test_chat_log"}],
|
||||
"has_more": True,
|
||||
"limit": 10,
|
||||
"total": 50,
|
||||
"page": 2,
|
||||
}
|
||||
mock_get_paginate_app_logs.return_value = expected_result
|
||||
|
||||
# Store datetime value to ensure consistency
|
||||
test_datetime = datetime.utcnow()
|
||||
|
||||
# Call the method
|
||||
result = self.service.get_paginate_chat_app_logs(
|
||||
session=self.session,
|
||||
app_model=self.app_model,
|
||||
status="normal",
|
||||
created_at_before=test_datetime,
|
||||
page=2,
|
||||
limit=10,
|
||||
created_by_end_user_session_id="session_123",
|
||||
)
|
||||
|
||||
# Verify the result
|
||||
assert result == expected_result
|
||||
|
||||
# Verify the base method was called with correct parameters
|
||||
mock_get_paginate_app_logs.assert_called_once_with(
|
||||
session=self.session,
|
||||
app_model=self.app_model,
|
||||
status="normal",
|
||||
created_at_before=test_datetime,
|
||||
created_at_after=None,
|
||||
page=2,
|
||||
limit=10,
|
||||
created_by_end_user_session_id="session_123",
|
||||
created_by_account=None,
|
||||
)
|
||||
|
||||
@patch("services.chat_app_log_service.select")
|
||||
@patch("services.chat_app_log_service.Message")
|
||||
@patch("services.chat_app_log_service.Conversation")
|
||||
def test_build_base_query(self, mock_conversation_model, mock_message_model, mock_select):
|
||||
"""Test building the base query for chat apps."""
|
||||
# Setup mocks
|
||||
mock_query = MagicMock()
|
||||
mock_join = MagicMock()
|
||||
mock_where = MagicMock()
|
||||
mock_where.order_by.return_value = mock_query
|
||||
mock_join.where.return_value = mock_where
|
||||
mock_select.return_value.join.return_value = mock_join
|
||||
|
||||
# Call the method
|
||||
result = self.service._build_base_query(self.app_model)
|
||||
|
||||
# Verify the query was built correctly
|
||||
mock_select.assert_called_once_with(mock_message_model)
|
||||
mock_select.return_value.join.assert_called_once_with(
|
||||
mock_conversation_model, mock_message_model.conversation_id == mock_conversation_model.id
|
||||
)
|
||||
mock_join.where.assert_called_once()
|
||||
mock_where.order_by.assert_called_once()
|
||||
|
||||
assert result == mock_query
|
||||
|
||||
@patch("services.chat_app_log_service.select")
|
||||
@patch("services.chat_app_log_service.func")
|
||||
@patch("services.chat_app_log_service.Message")
|
||||
@patch("services.chat_app_log_service.Conversation")
|
||||
def test_build_total_count_query(self, mock_conversation_model, mock_message_model, mock_func, mock_select):
|
||||
"""Test building the total count query."""
|
||||
# Setup mocks
|
||||
mock_count_query = MagicMock()
|
||||
mock_join = MagicMock()
|
||||
mock_join.where.return_value = mock_count_query
|
||||
mock_select.return_value.join.return_value = mock_join
|
||||
|
||||
# Call the method
|
||||
result = self.service._build_total_count_query(self.app_model)
|
||||
|
||||
# Verify the count query was built correctly
|
||||
mock_func.count.assert_called_once_with(mock_message_model.id)
|
||||
mock_select.return_value.join.assert_called_once_with(
|
||||
mock_conversation_model, mock_message_model.conversation_id == mock_conversation_model.id
|
||||
)
|
||||
mock_join.where.assert_called_once()
|
||||
|
||||
assert result == mock_count_query
|
||||
|
||||
def test_build_log_data_token_consumption_fields(self):
|
||||
"""Test that token consumption fields are properly included."""
|
||||
# Create mock message with token data
|
||||
message = MagicMock(spec=Message)
|
||||
message.id = str(uuid.uuid4())
|
||||
message.conversation_id = str(uuid.uuid4())
|
||||
message.query = "What is the weather?"
|
||||
message.answer = "The weather is sunny today."
|
||||
message.status = "normal"
|
||||
message.message_tokens = 7 # Input tokens
|
||||
message.total_tokens = 20 # Total tokens (input + output)
|
||||
message.created_at = datetime.utcnow()
|
||||
message.from_source = "api"
|
||||
message.from_end_user_id = None
|
||||
message.from_account_id = None
|
||||
message.created_by_end_user_session_id = "web_session"
|
||||
|
||||
# Build log data
|
||||
result = self.service.build_log_data(self.session, message)
|
||||
|
||||
# Verify token consumption fields are present and correct
|
||||
assert "message" in result
|
||||
assert result["message"]["message_tokens"] == 7
|
||||
assert result["message"]["total_tokens"] == 20
|
||||
assert result["message"]["query"] == message.query
|
||||
assert result["message"]["answer"] == message.answer
|
||||
|
|
@ -0,0 +1,219 @@
|
|||
"""Unit tests for CompletionAppLogService."""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models import Account, App, EndUser, Message
|
||||
from services.completion_app_log_service import CompletionAppLogService
|
||||
|
||||
|
||||
class TestCompletionAppLogService:
|
||||
"""Test cases for CompletionAppLogService."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.service = CompletionAppLogService()
|
||||
self.app_id = str(uuid.uuid4())
|
||||
self.app_model = MagicMock(spec=App)
|
||||
self.app_model.id = self.app_id
|
||||
self.session = MagicMock(spec=Session)
|
||||
|
||||
def test_get_app_mode_filter(self):
|
||||
"""Test that app mode filter returns correct condition."""
|
||||
filter_condition = self.service.get_app_mode_filter()
|
||||
# Should return a SQLAlchemy filter condition for COMPLETION mode
|
||||
assert filter_condition is not None
|
||||
|
||||
def test_build_log_data_with_account_creator(self):
|
||||
"""Test building log data when message was created by an account."""
|
||||
# Create mock message
|
||||
message = MagicMock(spec=Message)
|
||||
message.id = str(uuid.uuid4())
|
||||
message.query = "Test query"
|
||||
message.answer = "Test answer"
|
||||
message.status = "normal"
|
||||
message.message_tokens = 10
|
||||
message.total_tokens = 25
|
||||
message.created_at = datetime.utcnow()
|
||||
message.error = None
|
||||
message.provider_response_latency = 0.5
|
||||
message.from_source = "api"
|
||||
message.from_end_user_id = None
|
||||
message.from_account_id = str(uuid.uuid4())
|
||||
|
||||
# Create mock account
|
||||
account = MagicMock(spec=Account)
|
||||
account.id = message.from_account_id
|
||||
|
||||
# Mock session.get to return the account
|
||||
self.session.get.return_value = account
|
||||
|
||||
# Build log data
|
||||
result = self.service.build_log_data(self.session, message)
|
||||
|
||||
# Verify the result structure
|
||||
assert result["id"] == message.id
|
||||
assert result["message"]["id"] == message.id
|
||||
assert result["message"]["query"] == message.query
|
||||
assert result["message"]["answer"] == message.answer
|
||||
assert result["message"]["status"] == message.status
|
||||
assert result["message"]["message_tokens"] == message.message_tokens
|
||||
assert result["message"]["total_tokens"] == message.total_tokens
|
||||
assert result["message"]["created_at"] == message.created_at
|
||||
assert result["message"]["error"] == message.error
|
||||
assert result["message"]["provider_response_latency"] == message.provider_response_latency
|
||||
assert result["created_from"] == "web_app"
|
||||
assert result["created_by_role"] == "account"
|
||||
assert result["created_by_account"] == account
|
||||
assert result["created_by_end_user"] is None
|
||||
|
||||
# Verify session.get was called with correct parameters
|
||||
self.session.get.assert_called_with(Account, message.from_account_id)
|
||||
|
||||
def test_build_log_data_with_end_user_creator(self):
|
||||
"""Test building log data when message was created by an end user."""
|
||||
# Create mock message
|
||||
message = MagicMock(spec=Message)
|
||||
message.id = str(uuid.uuid4())
|
||||
message.query = "Test query"
|
||||
message.answer = "Test answer"
|
||||
message.status = "normal"
|
||||
message.message_tokens = 15
|
||||
message.total_tokens = 30
|
||||
message.created_at = datetime.utcnow()
|
||||
message.error = None
|
||||
message.provider_response_latency = 0.3
|
||||
message.from_source = "api"
|
||||
message.from_end_user_id = str(uuid.uuid4())
|
||||
message.from_account_id = None
|
||||
|
||||
# Create mock end user
|
||||
end_user = MagicMock(spec=EndUser)
|
||||
end_user.id = message.from_end_user_id
|
||||
|
||||
# Mock session.get to return the end user
|
||||
self.session.get.return_value = end_user
|
||||
|
||||
# Build log data
|
||||
result = self.service.build_log_data(self.session, message)
|
||||
|
||||
# Verify the result structure
|
||||
assert result["id"] == message.id
|
||||
assert result["message"]["total_tokens"] == message.total_tokens
|
||||
assert result["created_from"] == "service_api"
|
||||
assert result["created_by_role"] == "end_user"
|
||||
assert result["created_by_account"] is None
|
||||
assert result["created_by_end_user"] == end_user
|
||||
|
||||
# Verify session.get was called with correct parameters
|
||||
self.session.get.assert_called_with(EndUser, message.from_end_user_id)
|
||||
|
||||
def test_build_log_data_with_session_creator(self):
|
||||
"""Test building log data when message was created by session."""
|
||||
# Create mock message
|
||||
message = MagicMock(spec=Message)
|
||||
message.id = str(uuid.uuid4())
|
||||
message.query = "Test query"
|
||||
message.answer = "Test answer"
|
||||
message.status = "normal"
|
||||
message.message_tokens = 20
|
||||
message.total_tokens = 40
|
||||
message.created_at = datetime.utcnow()
|
||||
message.error = None
|
||||
message.provider_response_latency = 0.8
|
||||
message.from_source = "api"
|
||||
message.from_end_user_id = None
|
||||
message.from_account_id = None
|
||||
|
||||
# Build log data
|
||||
result = self.service.build_log_data(self.session, message)
|
||||
|
||||
# Verify the result structure
|
||||
assert result["id"] == message.id
|
||||
assert result["message"]["total_tokens"] == message.total_tokens
|
||||
assert result["created_from"] == "api" # Default when no from_end_user_id or from_account_id
|
||||
assert result["created_by_role"] is None # Default when no specific creator
|
||||
assert result["created_by_account"] is None
|
||||
assert result["created_by_end_user"] is None
|
||||
|
||||
# Verify session.get was not called
|
||||
self.session.get.assert_not_called()
|
||||
|
||||
@patch.object(CompletionAppLogService, "get_paginate_app_logs")
|
||||
def test_get_paginate_completion_app_logs(self, mock_get_paginate_app_logs):
|
||||
"""Test the main pagination method."""
|
||||
# Setup mock return value
|
||||
expected_result = {
|
||||
"data": [{"id": "test"}],
|
||||
"has_more": False,
|
||||
"limit": 20,
|
||||
"total": 1,
|
||||
"page": 1,
|
||||
}
|
||||
mock_get_paginate_app_logs.return_value = expected_result
|
||||
|
||||
# Call the method
|
||||
result = self.service.get_paginate_completion_app_logs(
|
||||
session=self.session,
|
||||
app_model=self.app_model,
|
||||
status="normal",
|
||||
page=1,
|
||||
limit=20,
|
||||
)
|
||||
|
||||
# Verify the result
|
||||
assert result == expected_result
|
||||
|
||||
# Verify the base method was called with correct parameters
|
||||
mock_get_paginate_app_logs.assert_called_once_with(
|
||||
session=self.session,
|
||||
app_model=self.app_model,
|
||||
status="normal",
|
||||
created_at_before=None,
|
||||
created_at_after=None,
|
||||
page=1,
|
||||
limit=20,
|
||||
created_by_end_user_session_id=None,
|
||||
created_by_account=None,
|
||||
)
|
||||
|
||||
@patch("services.completion_app_log_service.select")
|
||||
@patch("services.completion_app_log_service.Message")
|
||||
def test_build_base_query(self, mock_message_model, mock_select):
|
||||
"""Test building the base query for completion apps."""
|
||||
# Setup mocks
|
||||
mock_query = MagicMock()
|
||||
mock_where = MagicMock()
|
||||
mock_where.order_by.return_value = mock_query
|
||||
mock_select.return_value.where.return_value = mock_where
|
||||
|
||||
# Call the method
|
||||
result = self.service._build_base_query(self.app_model)
|
||||
|
||||
# Verify the query was built correctly
|
||||
mock_select.assert_called_once_with(mock_message_model)
|
||||
mock_select.return_value.where.assert_called_once()
|
||||
mock_where.order_by.assert_called_once()
|
||||
|
||||
assert result == mock_query
|
||||
|
||||
@patch("services.completion_app_log_service.select")
|
||||
@patch("services.completion_app_log_service.func")
|
||||
@patch("services.completion_app_log_service.Message")
|
||||
def test_build_total_count_query(self, mock_message_model, mock_func, mock_select):
|
||||
"""Test building the total count query."""
|
||||
# Setup mocks
|
||||
mock_count_query = MagicMock()
|
||||
mock_select.return_value.where.return_value = mock_count_query
|
||||
|
||||
# Call the method
|
||||
result = self.service._build_total_count_query(self.app_model)
|
||||
|
||||
# Verify the count query was built correctly
|
||||
mock_func.count.assert_called_once_with(mock_message_model.id)
|
||||
mock_select.return_value.where.assert_called_once()
|
||||
|
||||
assert result == mock_count_query
|
||||
Loading…
Reference in New Issue