add conversation and message

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

@ -1,11 +1,10 @@
from flask import Blueprint from flask import Blueprint
from libs.external_api import ExternalApi from libs.external_api import ExternalApi
bp = Blueprint("admin_api", __name__, url_prefix="/admin") bp = Blueprint("admin_api", __name__, url_prefix="/admin")
api = ExternalApi(bp) api = ExternalApi(bp)
from .auth import login from .auth import login
from .stats import stats
from .students import students
from .settings import settings 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 import api
from controllers.admin.wraps import validate_admin_token_and_extract_info 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 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 models.model import App
from services.end_user_service import EndUserService from services.end_user_service import EndUserService
class StudentList(Resource): class StudentList(Resource):
@validate_admin_token_and_extract_info @validate_admin_token_and_extract_info
@marshal_with(end_users_infinite_scroll_pagination_fields)
def get(self, app_model: App): def get(self, app_model: App):
"""Get all end_user list related with the app_model with filters with pagination. """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 # Get students with pagination
offset = (page - 1) * limit offset = (page - 1) * limit
# Query students from database (implementation will depend on your ORM/database structure) return EndUserService.pagination_by_filters(app_model=app_model, filters=filters, offset=offset, limit=limit)
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
class StudentAnalysis(Resource): class StudentAnalysis(Resource):
@ -443,7 +376,6 @@ class StudentNote(Resource):
api.add_resource(StudentList, '/students') 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(StudentAnalysis, '/students/<string:student_id>/analysis') # api.add_resource(StudentStatus, '/students/<string:student_id>/status')
api.add_resource(StudentStatus, '/students/<string:student_id>/status') # api.add_resource(StudentNote, '/students/<string:student_id>/note')
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 fields.member_fields import simple_account_fields
from flask_restful import fields # type: ignore
from libs.helper import TimestampField from libs.helper import TimestampField
from .raws import FilesContainedField from .raws import FilesContainedField

@ -1,4 +1,5 @@
from flask_restful import fields # type: ignore from flask_restful import fields # type: ignore
from libs.helper import TimestampField
simple_end_user_fields = { simple_end_user_fields = {
"id": fields.String, "id": fields.String,
@ -6,3 +7,23 @@ simple_end_user_fields = {
"is_anonymous": fields.Boolean, "is_anonymous": fields.Boolean,
"session_id": fields.String, "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 typing import Any, Dict, Optional, Tuple
from extensions.ext_database import db 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 models.model import App, Conversation, EndUser, Message
from sqlalchemy import and_, desc, func from sqlalchemy import and_, desc, func
class EndUserService: class EndUserService:
@staticmethod @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 Get a list of end users with filtering and pagination
@ -98,7 +100,7 @@ class EndUserService:
# Convert to dictionary for JSON serialization # Convert to dictionary for JSON serialization
end_user_dict = { end_user_dict = {
'id': end_user.id, 'id': end_user.external_user_id,
'email': end_user.email, 'email': end_user.email,
'first_chat_at': end_user.first_chat_at, 'first_chat_at': end_user.first_chat_at,
'last_chat_at': end_user.last_chat_at, 'last_chat_at': end_user.last_chat_at,
@ -113,7 +115,11 @@ class EndUserService:
users.append(end_user_dict) users.append(end_user_dict)
# Format and return results # 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 @staticmethod
def get_user_profile(end_user_id: str) -> Dict[str, Any]: def get_user_profile(end_user_id: str) -> Dict[str, Any]:

Loading…
Cancel
Save