add conversation and message
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")
|
||||||
Loading…
Reference in New Issue