add conversation and message

pull/21891/head
ytqh 1 year ago
parent 5c04969714
commit 42dc11206a

@ -1,11 +1,10 @@
from flask import Blueprint
from libs.external_api import ExternalApi
bp = Blueprint("admin_api", __name__, url_prefix="/admin")
api = ExternalApi(bp)
from .auth import login
from .stats import stats
from .students import students
from .settings import settings
from .stats import stats
from .students import conversation, message, students

@ -0,0 +1,118 @@
import services
from controllers.admin import api
from controllers.admin.students.error import NotChatAppError
from controllers.admin.wraps import validate_admin_token_and_extract_info
from core.app.entities.app_invoke_entities import InvokeFrom
from extensions.ext_database import db
from fields.conversation_fields import conversation_infinite_scroll_pagination_fields
from flask_restful import Resource, marshal_with, reqparse # type: ignore
from flask_restful.inputs import int_range # type: ignore
from libs.helper import uuid_value
from models.model import App, AppMode
from services.conversation_service import ConversationService
from services.end_user_service import EndUserService
from sqlalchemy.orm import Session # type: ignore
from werkzeug.exceptions import NotFound
class StudentConversation(Resource):
@validate_admin_token_and_extract_info
@marshal_with(conversation_infinite_scroll_pagination_fields)
def get(self, app_model: App, student_id: str):
"""Get student's conversation history.
---
tags:
- admin/students
summary: Get student conversation history
description: Get complete conversation history for a specific student
security:
- ApiKeyAuth: []
parameters:
- name: student_id
in: path
type: string
required: true
description: ID of the student
- name: start_time
in: query
type: string
format: date-time
description: Filter conversations after this time
- name: end_time
in: query
type: string
format: date-time
description: Filter conversations before this time
- name: page
in: query
type: integer
default: 1
description: Page number
- name: per_page
in: query
type: integer
default: 50
description: Conversations per page
responses:
200:
description: Conversation history retrieved successfully
schema:
type: object
properties:
total:
type: integer
conversations:
type: array
items:
type: object
properties:
timestamp:
type: string
format: date-time
role:
type: string
enum: [user, assistant]
content:
type: string
401:
description: Invalid or missing API key
404:
description: Student not found
"""
end_user = EndUserService.load_end_user_by_id(student_id)
if not end_user:
raise NotFound("Student not found")
app_mode = AppMode.value_of(app_model.mode)
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}:
raise NotChatAppError()
parser = reqparse.RequestParser()
parser.add_argument("last_id", type=uuid_value, location="args")
parser.add_argument("limit", type=int_range(1, 100), required=False, default=20, location="args")
parser.add_argument(
"sort_by",
type=str,
choices=["created_at", "-created_at", "updated_at", "-updated_at"],
required=False,
default="-updated_at",
location="args",
)
args = parser.parse_args()
try:
with Session(db.engine) as session:
return ConversationService.pagination_by_last_id(
session=session,
app_model=app_model,
user=end_user,
last_id=args["last_id"],
limit=args["limit"],
invoke_from=InvokeFrom.SERVICE_API,
sort_by=args["sort_by"],
)
except services.errors.conversation.LastConversationNotExistsError:
raise NotFound("Last Conversation Not Exists.")
api.add_resource(StudentConversation, '/students/<string:student_id>/conversation')

@ -0,0 +1,109 @@
from libs.exception import BaseHTTPException
class AppUnavailableError(BaseHTTPException):
error_code = "app_unavailable"
description = "App unavailable, please check your app configurations."
code = 400
class NotCompletionAppError(BaseHTTPException):
error_code = "not_completion_app"
description = "Please check if your Completion app mode matches the right API route."
code = 400
class NotChatAppError(BaseHTTPException):
error_code = "not_chat_app"
description = "Please check if your app mode matches the right API route."
code = 400
class NotWorkflowAppError(BaseHTTPException):
error_code = "not_workflow_app"
description = "Please check if your app mode matches the right API route."
code = 400
class ConversationCompletedError(BaseHTTPException):
error_code = "conversation_completed"
description = "The conversation has ended. Please start a new conversation."
code = 400
class ProviderNotInitializeError(BaseHTTPException):
error_code = "provider_not_initialize"
description = (
"No valid model provider credentials found. "
"Please go to Settings -> Model Provider to complete your provider credentials."
)
code = 400
class ProviderQuotaExceededError(BaseHTTPException):
error_code = "provider_quota_exceeded"
description = (
"Your quota for Dify Hosted OpenAI has been exhausted. "
"Please go to Settings -> Model Provider to complete your own provider credentials."
)
code = 400
class ProviderModelCurrentlyNotSupportError(BaseHTTPException):
error_code = "model_currently_not_support"
description = "Dify Hosted OpenAI trial currently not support the GPT-4 model."
code = 400
class CompletionRequestError(BaseHTTPException):
error_code = "completion_request_error"
description = "Completion request failed."
code = 400
class NoAudioUploadedError(BaseHTTPException):
error_code = "no_audio_uploaded"
description = "Please upload your audio."
code = 400
class AudioTooLargeError(BaseHTTPException):
error_code = "audio_too_large"
description = "Audio size exceeded. {message}"
code = 413
class UnsupportedAudioTypeError(BaseHTTPException):
error_code = "unsupported_audio_type"
description = "Audio type not allowed."
code = 415
class ProviderNotSupportSpeechToTextError(BaseHTTPException):
error_code = "provider_not_support_speech_to_text"
description = "Provider not support speech to text."
code = 400
class NoFileUploadedError(BaseHTTPException):
error_code = "no_file_uploaded"
description = "Please upload your file."
code = 400
class TooManyFilesError(BaseHTTPException):
error_code = "too_many_files"
description = "Only one file is allowed."
code = 400
class FileTooLargeError(BaseHTTPException):
error_code = "file_too_large"
description = "File size exceeded. {message}"
code = 413
class UnsupportedFileTypeError(BaseHTTPException):
error_code = "unsupported_file_type"
description = "File type not allowed."
code = 415

@ -0,0 +1,155 @@
import services
from controllers.admin import api
from controllers.admin.students.error import NotChatAppError
from controllers.admin.wraps import validate_admin_token_and_extract_info
from fields.conversation_fields import message_file_fields
from fields.raws import FilesContainedField
from flask_restful import Resource, fields, marshal_with, reqparse # type: ignore
from flask_restful.inputs import int_range # type: ignore
from libs.helper import TimestampField, uuid_value
from models.model import App, AppMode, EndUser
from services.end_user_service import EndUserService
from services.errors.message import SuggestedQuestionsAfterAnswerDisabledError
from services.message_service import MessageService
from werkzeug.exceptions import NotFound
class MessageListApi(Resource):
feedback_fields = {"rating": fields.String}
retriever_resource_fields = {
"id": fields.String,
"message_id": fields.String,
"position": fields.Integer,
"dataset_id": fields.String,
"dataset_name": fields.String,
"document_id": fields.String,
"document_name": fields.String,
"data_source_type": fields.String,
"segment_id": fields.String,
"score": fields.Float,
"hit_count": fields.Integer,
"word_count": fields.Integer,
"segment_position": fields.Integer,
"index_node_hash": fields.String,
"content": fields.String,
"created_at": TimestampField,
}
agent_thought_fields = {
"id": fields.String,
"chain_id": fields.String,
"message_id": fields.String,
"position": fields.Integer,
"thought": fields.String,
"tool": fields.String,
"tool_labels": fields.Raw,
"tool_input": fields.String,
"created_at": TimestampField,
"observation": fields.String,
"message_files": fields.List(fields.Nested(message_file_fields)),
}
message_fields = {
"id": fields.String,
"conversation_id": fields.String,
"parent_message_id": fields.String,
"inputs": FilesContainedField,
"query": fields.String,
"answer": fields.String(attribute="re_sign_file_url_answer"),
"message_files": fields.List(fields.Nested(message_file_fields)),
"feedback": fields.Nested(feedback_fields, attribute="user_feedback", allow_null=True),
"retriever_resources": fields.List(fields.Nested(retriever_resource_fields)),
"created_at": TimestampField,
"agent_thoughts": fields.List(fields.Nested(agent_thought_fields)),
"status": fields.String,
"error": fields.String,
}
message_infinite_scroll_pagination_fields = {
"limit": fields.Integer,
"has_more": fields.Boolean,
"data": fields.List(fields.Nested(message_fields)),
}
@validate_admin_token_and_extract_info
@marshal_with(message_infinite_scroll_pagination_fields)
def get(self, app_model: App, student_id: str):
"""Get messages list.
---
tags:
- admin/students
summary: List messages
description: Get a paginated list of messages for a conversation
security:
- ApiKeyAuth: []
parameters:
- name: student_id
in: path
required: true
type: string
format: uuid
description: ID of the student to get messages for
- name: conversation_id
in: query
required: true
type: string
format: uuid
description: ID of the conversation to get messages for
- name: first_id
in: query
type: string
format: uuid
description: ID of the first message for pagination
- name: limit
in: query
type: integer
minimum: 1
maximum: 100
default: 20
description: Number of messages to return
responses:
200:
description: Messages retrieved successfully
schema:
type: object
properties:
limit:
type: integer
has_more:
type: boolean
data:
type: array
items:
type: object
400:
description: Invalid request
401:
description: Invalid or missing token
404:
description: Conversation not found or not a chat app
"""
app_mode = AppMode.value_of(app_model.mode)
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}:
raise NotChatAppError()
end_user = EndUserService.load_end_user_by_id(student_id)
if not end_user:
raise NotFound("Student not found")
parser = reqparse.RequestParser()
parser.add_argument("conversation_id", required=True, type=uuid_value, location="args")
parser.add_argument("first_id", type=uuid_value, location="args")
parser.add_argument("limit", type=int_range(1, 100), required=False, default=20, location="args")
args = parser.parse_args()
try:
return MessageService.pagination_by_first_id(
app_model, end_user, args["conversation_id"], args["first_id"], args["limit"]
)
except services.errors.conversation.ConversationNotExistsError:
raise NotFound("Conversation Not Exists.")
except services.errors.message.FirstMessageNotExistsError:
raise NotFound("First Message Not Exists.")
api.add_resource(MessageListApi, "/students/<string:student_id>/messages")

@ -1,13 +1,15 @@
from controllers.admin import api
from controllers.admin.wraps import validate_admin_token_and_extract_info
from fields.end_user_fields import end_users_infinite_scroll_pagination_fields
from flask import Blueprint
from flask_restful import Api, Resource # type: ignore
from flask_restful import Api, Resource, marshal_with # type: ignore
from models.model import App
from services.end_user_service import EndUserService
class StudentList(Resource):
@validate_admin_token_and_extract_info
@marshal_with(end_users_infinite_scroll_pagination_fields)
def get(self, app_model: App):
"""Get all end_user list related with the app_model with filters with pagination.
---
@ -131,76 +133,7 @@ class StudentList(Resource):
# Get students with pagination
offset = (page - 1) * limit
# Query students from database (implementation will depend on your ORM/database structure)
data = EndUserService.get_user_list(app_model=app_model, filters=filters, offset=offset, limit=limit)
return {"result": "success", "data": data}, 200
class StudentConversation(Resource):
@validate_admin_token_and_extract_info
def get(self, app_model: App):
"""Get student's conversation history.
---
tags:
- admin/students
summary: Get student conversation history
description: Get complete conversation history for a specific student
security:
- ApiKeyAuth: []
parameters:
- name: student_id
in: path
type: string
required: true
description: ID of the student
- name: start_time
in: query
type: string
format: date-time
description: Filter conversations after this time
- name: end_time
in: query
type: string
format: date-time
description: Filter conversations before this time
- name: page
in: query
type: integer
default: 1
description: Page number
- name: per_page
in: query
type: integer
default: 50
description: Conversations per page
responses:
200:
description: Conversation history retrieved successfully
schema:
type: object
properties:
total:
type: integer
conversations:
type: array
items:
type: object
properties:
timestamp:
type: string
format: date-time
role:
type: string
enum: [user, assistant]
content:
type: string
401:
description: Invalid or missing API key
404:
description: Student not found
"""
pass
return EndUserService.pagination_by_filters(app_model=app_model, filters=filters, offset=offset, limit=limit)
class StudentAnalysis(Resource):
@ -443,7 +376,6 @@ class StudentNote(Resource):
api.add_resource(StudentList, '/students')
api.add_resource(StudentConversation, '/students/<string:student_id>/conversation')
api.add_resource(StudentAnalysis, '/students/<string:student_id>/analysis')
api.add_resource(StudentStatus, '/students/<string:student_id>/status')
api.add_resource(StudentNote, '/students/<string:student_id>/note')
# api.add_resource(StudentAnalysis, '/students/<string:student_id>/analysis')
# api.add_resource(StudentStatus, '/students/<string:student_id>/status')
# api.add_resource(StudentNote, '/students/<string:student_id>/note')

@ -1,6 +1,5 @@
from flask_restful import fields # type: ignore
from fields.member_fields import simple_account_fields
from flask_restful import fields # type: ignore
from libs.helper import TimestampField
from .raws import FilesContainedField

@ -1,4 +1,5 @@
from flask_restful import fields # type: ignore
from libs.helper import TimestampField
simple_end_user_fields = {
"id": fields.String,
@ -6,3 +7,23 @@ simple_end_user_fields = {
"is_anonymous": fields.Boolean,
"session_id": fields.String,
}
detailed_end_user_fields = {
"id": fields.String,
"name": fields.String,
"email": fields.String,
"first_chat_at": TimestampField,
"last_chat_at": TimestampField,
"total_messages": fields.Integer,
"active_days": fields.Integer,
"health_status": fields.String,
"topics": fields.String,
"summary": fields.String,
"major": fields.String,
}
end_users_infinite_scroll_pagination_fields = {
"limit": fields.Integer,
"has_more": fields.Boolean,
"data": fields.List(fields.Nested(detailed_end_user_fields)),
}

@ -1,14 +1,16 @@
from typing import Any, Dict, Optional, Tuple
from extensions.ext_database import db
from models.account import Account, TenantAccountJoin
from libs.infinite_scroll_pagination import InfiniteScrollPagination
from models.model import App, Conversation, EndUser, Message
from sqlalchemy import and_, desc, func
class EndUserService:
@staticmethod
def get_user_list(app_model: App, filters: Dict[str, Any], offset: int, limit: int) -> Dict[str, Any]:
def pagination_by_filters(
app_model: App, filters: Dict[str, Any], offset: int, limit: int
) -> InfiniteScrollPagination:
"""
Get a list of end users with filtering and pagination
@ -98,7 +100,7 @@ class EndUserService:
# Convert to dictionary for JSON serialization
end_user_dict = {
'id': end_user.id,
'id': end_user.external_user_id,
'email': end_user.email,
'first_chat_at': end_user.first_chat_at,
'last_chat_at': end_user.last_chat_at,
@ -113,7 +115,11 @@ class EndUserService:
users.append(end_user_dict)
# Format and return results
return {'total': total_count, 'users': users}
return InfiniteScrollPagination(data=users, limit=limit, has_more=total_count > offset + limit)
@staticmethod
def load_end_user_by_id(end_user_id: str) -> EndUser:
return db.session.query(EndUser).filter(EndUser.external_user_id == end_user_id).first()
@staticmethod
def get_user_profile(end_user_id: str) -> Dict[str, Any]:

Loading…
Cancel
Save