diff --git a/apps/application/chat_pipeline/I_base_chat_pipeline.py b/apps/application/chat_pipeline/I_base_chat_pipeline.py
deleted file mode 100644
index f231c2c4514..00000000000
--- a/apps/application/chat_pipeline/I_base_chat_pipeline.py
+++ /dev/null
@@ -1,185 +0,0 @@
-# coding=utf-8
-"""
- @project: maxkb
- @Author:虎
- @file: I_base_chat_pipeline.py
- @date:2024/1/9 17:25
- @desc:
-"""
-import time
-from abc import abstractmethod
-from typing import Type
-import uuid_utils.compat as uuid
-from rest_framework import serializers
-
-from knowledge.models import Paragraph
-
-
-class ParagraphPipelineModel:
-
- def __init__(self, _id: str, document_id: str, knowledge_id: str, content: str, title: str, status: str,
- is_active: bool, comprehensive_score: float, similarity: float, knowledge_name: str,
- document_name: str,
- hit_handling_method: str, directly_return_similarity: float, knowledge_type, meta: dict = None):
- self.id = _id
- self.document_id = document_id
- self.knowledge_id = knowledge_id
- self.content = content
- self.title = title
- self.status = status
- self.is_active = is_active
- self.comprehensive_score = comprehensive_score
- self.similarity = similarity
- self.knowledge_name = knowledge_name
- self.document_name = document_name
- self.hit_handling_method = hit_handling_method
- self.directly_return_similarity = directly_return_similarity
- self.meta = meta
- self.knowledge_type = knowledge_type
-
- def to_dict(self):
- return {
- 'id': self.id,
- 'document_id': self.document_id,
- 'knowledge_id': self.knowledge_id,
- 'content': self.content,
- 'title': self.title,
- 'status': self.status,
- 'is_active': self.is_active,
- 'comprehensive_score': self.comprehensive_score,
- 'similarity': self.similarity,
- 'knowledge_name': self.knowledge_name,
- 'document_name': self.document_name,
- 'knowledge_type': self.knowledge_type,
- 'meta': self.meta,
- }
-
- class builder:
- def __init__(self):
- self.similarity = None
- self.paragraph = {}
- self.comprehensive_score = None
- self.document_name = None
- self.knowledge_name = None
- self.knowledge_type = None
- self.hit_handling_method = None
- self.directly_return_similarity = 0.9
- self.meta = {}
-
- def add_paragraph(self, paragraph):
- if isinstance(paragraph, Paragraph):
- self.paragraph = {'id': paragraph.id,
- 'document_id': paragraph.document_id,
- 'knowledge_id': paragraph.knowledge_id,
- 'content': paragraph.content,
- 'title': paragraph.title,
- 'status': paragraph.status,
- 'is_active': paragraph.is_active,
- }
- else:
- self.paragraph = paragraph
- return self
-
- def add_knowledge_name(self, knowledge_name):
- self.knowledge_name = knowledge_name
- return self
-
- def add_knowledge_type(self, knowledge_type):
- self.knowledge_type = knowledge_type
- return self
-
- def add_document_name(self, document_name):
- self.document_name = document_name
- return self
-
- def add_hit_handling_method(self, hit_handling_method):
- self.hit_handling_method = hit_handling_method
- return self
-
- def add_directly_return_similarity(self, directly_return_similarity):
- self.directly_return_similarity = directly_return_similarity
- return self
-
- def add_comprehensive_score(self, comprehensive_score: float):
- self.comprehensive_score = comprehensive_score
- return self
-
- def add_similarity(self, similarity: float):
- self.similarity = similarity
- return self
-
- def add_meta(self, meta: dict):
- self.meta = meta
- return self
-
- def build(self):
- return ParagraphPipelineModel(str(self.paragraph.get('id')), str(self.paragraph.get('document_id')),
- str(self.paragraph.get('knowledge_id')),
- self.paragraph.get('content'), self.paragraph.get('title'),
- self.paragraph.get('status'),
- self.paragraph.get('is_active'),
- self.comprehensive_score, self.similarity, self.knowledge_name,
- self.document_name, self.hit_handling_method, self.directly_return_similarity,
- self.knowledge_type,
- self.meta)
-
-
-class IBaseChatPipelineStep:
- def __init__(self):
- # 当前步骤上下文,用于存储当前步骤信息
- self.context = {}
- self.status = 200
- self.err_message = ''
-
- @abstractmethod
- def get_step_serializer(self, manage) -> Type[serializers.Serializer]:
- pass
-
- def valid_args(self, manage):
- step_serializer_clazz = self.get_step_serializer(manage)
- step_serializer = step_serializer_clazz(data=manage.context)
- step_serializer.is_valid(raise_exception=True)
- self.context['step_args'] = step_serializer.data
-
- def run(self, manage):
- """
-
- :param manage: 步骤管理器
- :return: 执行结果
- """
- try:
- start_time = time.time()
- self.context['start_time'] = start_time
- # 校验参数,
- self.valid_args(manage)
- self._run(manage)
- self.context['run_time'] = time.time() - start_time
- except Exception as e:
- self.err_message = str(e)
- self.status = 500
- chat_record_id = manage.context.get('chat_record_id') or str(uuid.uuid7())
- manage.context['message_tokens'] = 0
- manage.context['answer_tokens'] = 0
- end_time = time.time()
- manage.context['run_time'] = end_time - (manage.context.get('start_time') or end_time)
- post_response_handler = manage.context.get('post_response_handler')
- post_response_handler.handler(manage.context.get('chat_id'), chat_record_id,
- manage.context.get('paragraph_list') or [],
- manage.context.get('problem_text'),
- str(e), manage, self, manage.context.get('padding_problem_text'),
- reasoning_content='')
-
- raise e
-
- def _run(self, manage):
- pass
-
- def execute(self, **kwargs):
- pass
-
- def get_details(self, manage, **kwargs):
- """
- 运行详情
- :return: 步骤详情
- """
- return None
diff --git a/apps/application/chat_pipeline/__init__.py b/apps/application/chat_pipeline/__init__.py
deleted file mode 100644
index 719a7e29c90..00000000000
--- a/apps/application/chat_pipeline/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# coding=utf-8
-"""
- @project: maxkb
- @Author:虎
- @file: __init__.py.py
- @date:2024/1/9 17:23
- @desc:
-"""
diff --git a/apps/application/chat_pipeline/pipeline_manage.py b/apps/application/chat_pipeline/pipeline_manage.py
deleted file mode 100644
index 206df8a399e..00000000000
--- a/apps/application/chat_pipeline/pipeline_manage.py
+++ /dev/null
@@ -1,66 +0,0 @@
-# coding=utf-8
-"""
- @project: maxkb
- @Author:虎
- @file: pipeline_manage.py
- @date:2024/1/9 17:40
- @desc:
-"""
-import time
-from functools import reduce
-from typing import List, Type, Dict
-
-from application.chat_pipeline.I_base_chat_pipeline import IBaseChatPipelineStep
-from common.handle.base_to_response import BaseToResponse
-from common.handle.impl.response.system_to_response import SystemToResponse
-
-
-class PipelineManage:
- def __init__(self, step_list: List[Type[IBaseChatPipelineStep]],
- base_to_response: BaseToResponse = SystemToResponse(),
- debug=False):
- # 步骤执行器
- self.step_list = [step() for step in step_list]
- self.run_step_list = []
- # 上下文
- self.context = {'message_tokens': 0, 'answer_tokens': 0}
- self.base_to_response = base_to_response
- self.debug = debug
-
- def run(self, context: Dict = None):
- self.context['start_time'] = time.time()
- if context is not None:
- for key, value in context.items():
- self.context[key] = value
- for step in self.step_list:
- self.run_step_list.append(step)
- step.run(self)
-
- def get_details(self):
- return reduce(lambda x, y: {**x, **y}, [{item.get('step_type'): item} for item in
- filter(lambda r: r is not None,
- [row.get_details(self) for row in self.run_step_list])], {})
-
- def get_base_to_response(self):
- return self.base_to_response
-
- class builder:
- def __init__(self):
- self.step_list: List[Type[IBaseChatPipelineStep]] = []
- self.base_to_response = SystemToResponse()
- self.debug = False
-
- def append_step(self, step: Type[IBaseChatPipelineStep]):
- self.step_list.append(step)
- return self
-
- def add_base_to_response(self, base_to_response: BaseToResponse):
- self.base_to_response = base_to_response
- return self
-
- def add_debug(self, debug):
- self.debug = debug
- return self
-
- def build(self):
- return PipelineManage(step_list=self.step_list, base_to_response=self.base_to_response, debug=self.debug)
diff --git a/apps/application/chat_pipeline/step/__init__.py b/apps/application/chat_pipeline/step/__init__.py
deleted file mode 100644
index 5d9549cdc64..00000000000
--- a/apps/application/chat_pipeline/step/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# coding=utf-8
-"""
- @project: maxkb
- @Author:虎
- @file: __init__.py.py
- @date:2024/1/9 18:23
- @desc:
-"""
diff --git a/apps/application/chat_pipeline/step/chat_step/__init__.py b/apps/application/chat_pipeline/step/chat_step/__init__.py
deleted file mode 100644
index 5d9549cdc64..00000000000
--- a/apps/application/chat_pipeline/step/chat_step/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# coding=utf-8
-"""
- @project: maxkb
- @Author:虎
- @file: __init__.py.py
- @date:2024/1/9 18:23
- @desc:
-"""
diff --git a/apps/application/chat_pipeline/step/chat_step/i_chat_step.py b/apps/application/chat_pipeline/step/chat_step/i_chat_step.py
deleted file mode 100644
index 1c2ede64b40..00000000000
--- a/apps/application/chat_pipeline/step/chat_step/i_chat_step.py
+++ /dev/null
@@ -1,121 +0,0 @@
-# coding=utf-8
-"""
- @project: maxkb
- @Author:虎
- @file: i_chat_step.py
- @date:2024/1/9 18:17
- @desc: 对话
-"""
-from abc import abstractmethod
-from typing import Type, List
-
-from django.utils.translation import gettext_lazy as _
-from langchain.chat_models.base import BaseChatModel
-from langchain_core.messages import BaseMessage
-from rest_framework import serializers
-
-from application.chat_pipeline.I_base_chat_pipeline import IBaseChatPipelineStep, ParagraphPipelineModel
-from application.chat_pipeline.pipeline_manage import PipelineManage
-from application.serializers.application import NoReferencesSetting
-from common.field.common import InstanceField
-
-
-class ModelField(serializers.Field):
- def to_internal_value(self, data):
- if not isinstance(data, BaseChatModel):
- self.fail(_('Model type error'), value=data)
- return data
-
- def to_representation(self, value):
- return value
-
-
-class MessageField(serializers.Field):
- def to_internal_value(self, data):
- if not isinstance(data, BaseMessage):
- self.fail(_('Message type error'), value=data)
- return data
-
- def to_representation(self, value):
- return value
-
-
-class PostResponseHandler:
- @abstractmethod
- def handler(self, chat_id, chat_record_id, paragraph_list: List[ParagraphPipelineModel], problem_text: str,
- answer_text,
- manage, step, padding_problem_text: str = None, **kwargs):
- pass
-
-
-class IChatStep(IBaseChatPipelineStep):
- class InstanceSerializer(serializers.Serializer):
- # 对话列表
- message_list = serializers.ListField(required=True, child=MessageField(required=True),
- label=_("Conversation list"))
- model_id = serializers.UUIDField(required=False, allow_null=True, label=_("Model id"))
- # 段落列表
- paragraph_list = serializers.ListField(label=_("Paragraph List"))
- # 对话id
- chat_id = serializers.UUIDField(required=True, label=_("Conversation ID"))
- # 用户问题
- problem_text = serializers.CharField(required=True, label=_("User Questions"))
- # 后置处理器
- post_response_handler = InstanceField(model_type=PostResponseHandler,
- label=_("Post-processor"))
- # 补全问题
- padding_problem_text = serializers.CharField(required=False,
- label=_("Completion Question"))
- # 是否使用流的形式输出
- stream = serializers.BooleanField(required=False, label=_("Streaming Output"))
- chat_user_id = serializers.CharField(required=True, label=_("Chat user id"))
- chat_record_id = serializers.CharField(required=False, label=_("Chat record id"))
-
- chat_user_type = serializers.CharField(required=True, label=_("Chat user Type"))
- # 未查询到引用分段
- no_references_setting = NoReferencesSetting(required=True,
- label=_("No reference segment settings"))
-
- workspace_id = serializers.CharField(required=True, label=_("Workspace ID"))
-
- model_setting = serializers.DictField(required=True, allow_null=True,
- label=_("Model settings"))
-
- model_params_setting = serializers.DictField(required=False, allow_null=True,
- label=_("Model parameter settings"))
- mcp_tool_ids = serializers.JSONField(label="MCP工具ID列表", required=False, default=list)
- mcp_servers = serializers.JSONField(label="MCP服务列表", required=False, default=dict)
- mcp_source = serializers.CharField(label="MCP Source", required=False, default="referencing")
- tool_ids = serializers.JSONField(label="工具ID列表", required=False, default=list)
- application_ids = serializers.JSONField(label="应用ID列表", required=False, default=list)
- skill_tool_ids = serializers.JSONField(label="技能ID列表", required=False, default=list)
- mcp_output_enable = serializers.BooleanField(label="MCP输出是否启用", required=False, default=True)
-
- def is_valid(self, *, raise_exception=False):
- super().is_valid(raise_exception=True)
- message_list: List = self.initial_data.get('message_list')
- for message in message_list:
- if not isinstance(message, BaseMessage):
- raise Exception(_("message type error"))
-
- def get_step_serializer(self, manage: PipelineManage) -> Type[serializers.Serializer]:
- return self.InstanceSerializer
-
- def _run(self, manage: PipelineManage):
- chat_result = self.execute(**self.context['step_args'], manage=manage)
- manage.context['chat_result'] = chat_result
-
- @abstractmethod
- def execute(self, message_list: List[BaseMessage],
- chat_id, problem_text,
- post_response_handler: PostResponseHandler,
- model_id: str = None,
- workspace_id: str = None,
- paragraph_list=None,
- manage: PipelineManage = None,
- padding_problem_text: str = None, stream: bool = True, chat_user_id=None, chat_user_type=None,
- no_references_setting=None, model_params_setting=None, model_setting=None,
- mcp_tool_ids=None, mcp_servers='', mcp_source="referencing",
- tool_ids=None, application_ids=None, skill_tool_ids=None, mcp_output_enable=True,
- **kwargs):
- pass
diff --git a/apps/application/chat_pipeline/step/chat_step/impl/base_chat_step.py b/apps/application/chat_pipeline/step/chat_step/impl/base_chat_step.py
deleted file mode 100644
index 0b5d8af6852..00000000000
--- a/apps/application/chat_pipeline/step/chat_step/impl/base_chat_step.py
+++ /dev/null
@@ -1,799 +0,0 @@
-# coding=utf-8
-"""
-@project: maxkb
-@Author:虎
-@file: base_chat_step.py
-@date:2024/1/9 18:25
-@desc: 对话step Base实现
-"""
-
-import json
-import time
-import traceback
-from typing import List
-
-import uuid_utils.compat as uuid
-from application.chat_pipeline.I_base_chat_pipeline import ParagraphPipelineModel
-from application.chat_pipeline.pipeline_manage import PipelineManage
-from application.chat_pipeline.step.chat_step.i_chat_step import IChatStep, PostResponseHandler
-from application.flow.tools import Reasoning, get_tools, mcp_response_generator
-from application.long_term_memory import extract_long_term_memory
-from application.models import (
- Application,
- ApplicationAccessToken,
- ApplicationApiKey,
- ApplicationChatUserStats,
- ApplicationLongTermMemory,
- ChatUserType,
-)
-from common.exception.app_exception import AppApiException
-from common.utils.logger import maxkb_logger
-from common.utils.rsa_util import rsa_long_decrypt
-from common.utils.shared_resource_auth import filter_authorized_ids
-from common.utils.tool_code import ToolExecutor
-from django.db.models import QuerySet
-from django.http import StreamingHttpResponse
-from django.utils.translation import gettext as _
-from langchain.chat_models.base import BaseChatModel
-from langchain_core.messages import AIMessage, AIMessageChunk, BaseMessage, HumanMessage, SystemMessage
-from models_provider.tools import get_model_instance_by_model_workspace_id
-from rest_framework import status
-from tools.models import Tool, ToolType
-
-
-def add_access_num(chat_user_id=None, chat_user_type=None, application_id=None):
- if [ChatUserType.ANONYMOUS_USER.value, ChatUserType.CHAT_USER.value].__contains__(
- chat_user_type
- ) and application_id is not None:
- application_public_access_client = (
- QuerySet(ApplicationChatUserStats)
- .filter(chat_user_id=chat_user_id, chat_user_type=chat_user_type, application_id=application_id)
- .first()
- )
- if application_public_access_client is not None:
- application_public_access_client.access_num = application_public_access_client.access_num + 1
- application_public_access_client.intraday_access_num = (
- application_public_access_client.intraday_access_num + 1
- )
- application_public_access_client.save()
-
-
-def write_context(step, manage, request_token, response_token, all_text):
- step.context["message_tokens"] = request_token
- step.context["answer_tokens"] = response_token
- current_time = time.time()
- step.context["answer_text"] = all_text
- step.context["run_time"] = current_time - step.context["start_time"]
- manage.context["run_time"] = current_time - manage.context["start_time"]
- manage.context["message_tokens"] = manage.context["message_tokens"] + request_token
- manage.context["answer_tokens"] = manage.context["answer_tokens"] + response_token
-
-
-def event_content(
- response,
- chat_id,
- chat_record_id,
- paragraph_list: List[ParagraphPipelineModel],
- post_response_handler: PostResponseHandler,
- manage,
- step,
- chat_model,
- message_list: List[BaseMessage],
- problem_text: str,
- padding_problem_text: str = None,
- chat_user_id=None,
- chat_user_type=None,
- is_ai_chat: bool = None,
- model_setting=None,
-):
- if model_setting is None:
- model_setting = {}
- reasoning_content_enable = model_setting.get("reasoning_content_enable", False)
- reasoning_content_start = model_setting.get("reasoning_content_start", "")
- reasoning_content_end = model_setting.get("reasoning_content_end", "")
- reasoning = Reasoning(reasoning_content_start, reasoning_content_end)
- all_text = ""
- reasoning_content = ""
- try:
- response_reasoning_content = False
- for chunk in response:
- reasoning_chunk = reasoning.get_reasoning_content(chunk)
- content_chunk = reasoning_chunk.get("content")
- if "reasoning_content" in chunk.additional_kwargs:
- response_reasoning_content = True
- reasoning_content_chunk = chunk.additional_kwargs.get("reasoning_content", "")
- else:
- reasoning_content_chunk = reasoning_chunk.get("reasoning_content")
- content_chunk = reasoning._normalize_content(content_chunk)
- all_text += content_chunk
- if reasoning_content_chunk is None:
- reasoning_content_chunk = ""
- reasoning_content += reasoning_content_chunk
- yield manage.get_base_to_response().to_stream_chunk_response(
- chat_id,
- str(chat_record_id),
- "ai-chat-node",
- [],
- content_chunk,
- False,
- 0,
- 0,
- {
- "node_is_end": False,
- "view_type": "many_view",
- "node_type": "ai-chat-node",
- "real_node_id": "ai-chat-node",
- "reasoning_content": reasoning_content_chunk if reasoning_content_enable else "",
- },
- )
- reasoning_chunk = reasoning.get_end_reasoning_content()
- all_text += reasoning_chunk.get("content")
- reasoning_content_chunk = ""
- if not response_reasoning_content:
- reasoning_content_chunk = reasoning_chunk.get("reasoning_content")
- yield manage.get_base_to_response().to_stream_chunk_response(
- chat_id,
- str(chat_record_id),
- "ai-chat-node",
- [],
- reasoning_chunk.get("content"),
- False,
- 0,
- 0,
- {
- "node_is_end": False,
- "view_type": "many_view",
- "node_type": "ai-chat-node",
- "real_node_id": "ai-chat-node",
- "reasoning_content": reasoning_content_chunk if reasoning_content_enable else "",
- },
- )
- # 获取token
- if is_ai_chat:
- try:
- request_token = chat_model.get_num_tokens_from_messages(message_list)
- response_token = chat_model.get_num_tokens(all_text)
- except Exception as e:
- request_token = 0
- response_token = 0
- else:
- request_token = 0
- response_token = 0
- write_context(step, manage, request_token, response_token, all_text)
- post_response_handler.handler(
- chat_id,
- chat_record_id,
- paragraph_list,
- problem_text,
- all_text,
- manage,
- step,
- padding_problem_text,
- reasoning_content=reasoning_content if reasoning_content_enable else "",
- )
- yield manage.get_base_to_response().to_stream_chunk_response(
- chat_id,
- str(chat_record_id),
- "ai-chat-node",
- [],
- "",
- True,
- request_token,
- response_token,
- {"node_is_end": True, "view_type": "many_view", "node_type": "ai-chat-node"},
- )
- if not manage.debug:
- add_access_num(chat_user_id, chat_user_type, manage.context.get("application_id"))
- except BaseException as e:
- if isinstance(e, GeneratorExit):
- maxkb_logger.error(f"Generator was closed (client disconnected)")
- else:
- maxkb_logger.error(f"{str(e)}:{traceback.format_exc()}")
- all_text = "Exception:" + str(e)
- write_context(step, manage, 0, 0, all_text)
- post_response_handler.handler(
- chat_id,
- chat_record_id,
- paragraph_list,
- problem_text,
- all_text,
- manage,
- step,
- padding_problem_text,
- reasoning_content=reasoning_content if reasoning_content_enable else "",
- )
- if not manage.debug:
- add_access_num(chat_user_id, chat_user_type, manage.context.get("application_id"))
- yield manage.get_base_to_response().to_stream_chunk_response(
- chat_id,
- str(chat_record_id),
- "ai-chat-node",
- [],
- all_text,
- False,
- 0,
- 0,
- {
- "node_is_end": False,
- "view_type": "many_view",
- "node_type": "ai-chat-node",
- "real_node_id": "ai-chat-node",
- "reasoning_content": "",
- },
- )
-
-
-class BaseChatStep(IChatStep):
- def execute(
- self,
- message_list: List[BaseMessage],
- chat_id,
- problem_text,
- post_response_handler: PostResponseHandler,
- model_id: str = None,
- workspace_id: str = None,
- paragraph_list=None,
- manage: PipelineManage = None,
- padding_problem_text: str = None,
- stream: bool = True,
- chat_user_id=None,
- chat_user_type=None,
- no_references_setting=None,
- model_params_setting=None,
- model_setting=None,
- mcp_tool_ids=None,
- mcp_servers="",
- mcp_source="referencing",
- tool_ids=None,
- application_ids=None,
- skill_tool_ids=None,
- mcp_output_enable=True,
- **kwargs,
- ):
- chat_model = (
- get_model_instance_by_model_workspace_id(model_id, workspace_id, **(model_params_setting or {}))
- if model_id is not None
- else None
- )
- if stream:
- return self.execute_stream(
- message_list,
- chat_id,
- problem_text,
- post_response_handler,
- chat_model,
- paragraph_list,
- manage,
- padding_problem_text,
- chat_user_id,
- chat_user_type,
- no_references_setting,
- model_setting,
- mcp_tool_ids,
- mcp_servers,
- mcp_source,
- tool_ids,
- application_ids,
- skill_tool_ids,
- workspace_id,
- mcp_output_enable,
- )
- else:
- return self.execute_block(
- message_list,
- chat_id,
- problem_text,
- post_response_handler,
- chat_model,
- paragraph_list,
- manage,
- padding_problem_text,
- chat_user_id,
- chat_user_type,
- no_references_setting,
- model_setting,
- mcp_tool_ids,
- mcp_servers,
- mcp_source,
- tool_ids,
- application_ids,
- skill_tool_ids,
- workspace_id,
- mcp_output_enable,
- )
-
- def get_details(self, manage, **kwargs):
- # 提取长期记忆
- extract_long_term_memory.apply_async(
- args=(
- manage.context.get("workspace_id"),
- manage.context.get("application_id"),
- manage.context.get("chat_user_id"),
- ),
- countdown=1,
- )
- return {
- "status": self.status,
- "err_message": self.err_message,
- "step_type": "chat_step",
- "run_time": self.context.get("run_time") or 0,
- "model_id": str(manage.context["model_id"]),
- "message_list": self.reset_message_list(
- self.context["step_args"].get("message_list"), self.context.get("answer_text")
- ),
- "message_tokens": self.context.get("message_tokens"),
- "answer_tokens": self.context.get("answer_tokens"),
- "cost": 0,
- }
-
- @staticmethod
- def reset_message_list(message_list: List[BaseMessage], answer_text):
- result = [
- {
- "role": "user"
- if isinstance(message, HumanMessage)
- else ("system" if isinstance(message, SystemMessage) else "ai"),
- "content": message.content,
- }
- for message in message_list
- ]
- result.append({"role": "ai", "content": answer_text})
- return result
-
- def _handle_mcp_request(
- self,
- mcp_source,
- mcp_servers,
- mcp_tool_ids,
- tool_ids,
- application_ids,
- skill_tool_ids,
- mcp_output_enable,
- chat_model,
- system_prompt,
- message_list,
- agent_id,
- chat_id,
- workspace_id,
- ):
-
- mcp_servers_config = {}
-
- # 迁移过来mcp_source是None
- if mcp_source is None:
- mcp_source = "custom"
- # 兼容老数据
- if not mcp_tool_ids:
- mcp_tool_ids = []
- if mcp_source == "custom" and mcp_servers:
- mcp_servers_config = json.loads(mcp_servers)
- elif mcp_tool_ids:
- mcp_tools = QuerySet(Tool).filter(id__in=mcp_tool_ids).values()
- for mcp_tool in mcp_tools:
- if mcp_tool and mcp_tool["is_active"]:
- mcp_servers_config = {**mcp_servers_config, **json.loads(mcp_tool["code"])}
- # 校验代码是否包括禁止的关键字
- ToolExecutor().validate_mcp_transport(json.dumps(mcp_servers_config))
-
- tool_init_params = {}
- tools = get_tools("APPLICATION", agent_id, tool_ids, workspace_id)
- if tool_ids and len(tool_ids) > 0: # 如果有工具ID,则将其转换为MCP
- self.context["tool_ids"] = tool_ids
- for tool_id in tool_ids:
- tool = QuerySet(Tool).filter(id=tool_id, tool_type=ToolType.CUSTOM).first()
- if tool is None or tool.is_active is False:
- continue
- executor = ToolExecutor()
- init_params_default_value = {i["field"]: i.get('default_value') for i in tool.init_field_list}
- if tool.init_params is not None:
- tool_init_params = init_params_default_value | json.loads(rsa_long_decrypt(tool.init_params))
- else:
- tool_init_params = init_params_default_value
- tool_config = executor.get_tool_mcp_config(tool, tool_init_params)
-
- mcp_servers_config[str(tool.id)] = tool_config
-
- if application_ids and len(application_ids) > 0:
- self.context["application_ids"] = application_ids
- for application_id in application_ids:
- app = QuerySet(Application).filter(id=application_id, is_publish=True).first()
- if app is None:
- continue
- app_key = QuerySet(ApplicationApiKey).filter(application_id=application_id, is_active=True).first()
- if app_key is not None:
- api_key = app_key.secret_key
- application_access_token = (
- QuerySet(ApplicationAccessToken).filter(application_id=app_key.application_id).first()
- )
- if application_access_token is not None and application_access_token.authentication:
- raise AppApiException(
- 500,
- _("Agent 【{name}】 access token authentication is not supported for agent tool").format(
- name=app.name
- ),
- )
- else:
- raise AppApiException(
- 500, _("Agent Key is required for agent tool 【{name}】").format(name=app.name)
- )
- executor = ToolExecutor()
- app_config = executor.get_app_mcp_config(api_key)
- mcp_servers_config[app.name] = app_config
-
- if skill_tool_ids and len(skill_tool_ids) > 0:
- self.context["skill_tool_ids"] = skill_tool_ids
- skill_file_items = []
-
- for tool_id in skill_tool_ids:
- tool = QuerySet(Tool).filter(id=tool_id, is_active=True).first()
- if tool is None or tool.is_active is False:
- continue
- init_params_default_value = {i["field"]: i.get("default_value") for i in tool.init_field_list}
- if tool.init_params is not None:
- params = init_params_default_value | json.loads(rsa_long_decrypt(tool.init_params))
- else:
- params = init_params_default_value
-
- skill_file_items.append({"tool_id": str(tool.id), "file_id": tool.code, "params": params})
- mcp_servers_config["skills"] = skill_file_items
-
- if len(mcp_servers_config) > 0 or len(tools) > 0:
- source_id = agent_id
- source_type = "APPLICATION"
- return mcp_response_generator(
- chat_model,
- system_prompt,
- message_list,
- json.dumps(mcp_servers_config),
- mcp_output_enable,
- tool_init_params,
- source_id,
- source_type,
- chat_id,
- tools,
- )
-
- return None
-
- def get_stream_result(
- self,
- message_list: List[BaseMessage],
- chat_model: BaseChatModel = None,
- paragraph_list=None,
- no_references_setting=None,
- problem_text=None,
- mcp_tool_ids=None,
- mcp_servers="",
- mcp_source="referencing",
- tool_ids=None,
- application_ids=None,
- skill_tool_ids=None,
- workspace_id=None,
- mcp_output_enable=True,
- agent_id=None,
- chat_id=None,
- chat_user_id=None,
- chat_user_type=None,
- ):
- if paragraph_list is None:
- paragraph_list = []
- directly_return_chunk_list = [
- AIMessageChunk(content=paragraph.content)
- for paragraph in paragraph_list
- if (
- paragraph.hit_handling_method == "directly_return"
- and paragraph.similarity >= paragraph.directly_return_similarity
- )
- ]
- if directly_return_chunk_list is not None and len(directly_return_chunk_list) > 0:
- return iter(directly_return_chunk_list), False
- elif len(paragraph_list) == 0 and no_references_setting.get("status") == "designated_answer":
- return iter(
- [AIMessageChunk(content=no_references_setting.get("value").replace("{question}", problem_text))]
- ), False
- if chat_model is None:
- return iter(
- [
- AIMessageChunk(
- _(
- "Sorry, the AI model is not configured. Please go to the application to set up the AI model first."
- )
- )
- ]
- ), False
- else:
- user_system_prompt = None
- filtered_message_list = []
- long_term_memory = (
- QuerySet(ApplicationLongTermMemory).filter(chat_user_id=chat_user_id, application_id=agent_id).first()
- )
- if long_term_memory is not None:
- memory = long_term_memory.memory
- else:
- memory = ""
-
- # print(chat_user_id, chat_user_type)
- for msg in message_list:
- if isinstance(msg, SystemMessage):
- if isinstance(msg.content, str):
- user_system_prompt = msg.content.replace("{memory}", memory)
- msg.content = user_system_prompt
- elif isinstance(msg.content, list):
- user_system_prompt = "".join(
- item.get("text", "") if isinstance(item, dict) else str(item) for item in msg.content
- )
- else:
- user_system_prompt = str(msg.content)
- else:
- filtered_message_list.append(msg)
- # 过滤tool_id
- all_tool_ids = list(set((mcp_tool_ids or []) + (tool_ids or []) + (skill_tool_ids or [])))
- authorized_set = set(filter_authorized_ids("tool", all_tool_ids, workspace_id))
-
- mcp_tool_ids = [i for i in (mcp_tool_ids or []) if i in authorized_set]
- tool_ids = [i for i in (tool_ids or []) if i in authorized_set]
- skill_tool_ids = [i for i in (skill_tool_ids or []) if i in authorized_set]
- # 处理 MCP 请求
- mcp_result = self._handle_mcp_request(
- mcp_source,
- mcp_servers,
- mcp_tool_ids,
- tool_ids,
- application_ids,
- skill_tool_ids,
- mcp_output_enable,
- chat_model,
- user_system_prompt,
- filtered_message_list,
- agent_id,
- chat_id,
- workspace_id,
- )
- if mcp_result:
- return mcp_result, True
- return chat_model.stream(message_list), True
-
- def execute_stream(
- self,
- message_list: List[BaseMessage],
- chat_id,
- problem_text,
- post_response_handler: PostResponseHandler,
- chat_model: BaseChatModel = None,
- paragraph_list=None,
- manage: PipelineManage = None,
- padding_problem_text: str = None,
- chat_user_id=None,
- chat_user_type=None,
- no_references_setting=None,
- model_setting=None,
- mcp_tool_ids=None,
- mcp_servers="",
- mcp_source="referencing",
- tool_ids=None,
- application_ids=None,
- skill_tool_ids=None,
- workspace_id=None,
- mcp_output_enable=True,
- ):
- chat_result, is_ai_chat = self.get_stream_result(
- message_list,
- chat_model,
- paragraph_list,
- no_references_setting,
- problem_text,
- mcp_tool_ids,
- mcp_servers,
- mcp_source,
- tool_ids,
- application_ids,
- skill_tool_ids,
- workspace_id,
- mcp_output_enable,
- manage.context.get("application_id"),
- chat_id,
- chat_user_id,
- chat_user_type,
- )
- chat_record_id = (
- self.context.get("step_args", {}).get("chat_record_id")
- if self.context.get("step_args", {}).get("chat_record_id")
- else uuid.uuid7()
- )
- r = StreamingHttpResponse(
- streaming_content=event_content(
- chat_result,
- chat_id,
- chat_record_id,
- paragraph_list,
- post_response_handler,
- manage,
- self,
- chat_model,
- message_list,
- problem_text,
- padding_problem_text,
- chat_user_id,
- chat_user_type,
- is_ai_chat,
- model_setting,
- ),
- content_type="text/event-stream;charset=utf-8",
- )
-
- r["Cache-Control"] = "no-cache"
- return r
-
- def get_block_result(
- self,
- message_list: List[BaseMessage],
- chat_model: BaseChatModel = None,
- paragraph_list=None,
- no_references_setting=None,
- problem_text=None,
- mcp_tool_ids=None,
- mcp_servers="",
- mcp_source="referencing",
- tool_ids=None,
- application_ids=None,
- skill_tool_ids=None,
- workspace_id=None,
- mcp_output_enable=True,
- application_id=None,
- chat_id=None,
- ):
- if paragraph_list is None:
- paragraph_list = []
- directly_return_chunk_list = [
- AIMessageChunk(content=paragraph.content)
- for paragraph in paragraph_list
- if (
- paragraph.hit_handling_method == "directly_return"
- and paragraph.similarity >= paragraph.directly_return_similarity
- )
- ]
- if directly_return_chunk_list is not None and len(directly_return_chunk_list) > 0:
- return directly_return_chunk_list[0], False
- elif len(paragraph_list) == 0 and no_references_setting.get("status") == "designated_answer":
- return AIMessage(no_references_setting.get("value").replace("{question}", problem_text)), False
- if chat_model is None:
- return AIMessage(
- _("Sorry, the AI model is not configured. Please go to the application to set up the AI model first.")
- ), False
- else:
- # 过滤tool_id
- all_tool_ids = list(set((mcp_tool_ids or []) + (tool_ids or []) + (skill_tool_ids or [])))
- authorized_set = set(filter_authorized_ids("tool", all_tool_ids, workspace_id))
-
- mcp_tool_ids = [i for i in (mcp_tool_ids or []) if i in authorized_set]
- tool_ids = [i for i in (tool_ids or []) if i in authorized_set]
- skill_tool_ids = [i for i in (skill_tool_ids or []) if i in authorized_set]
- # 处理 MCP 请求
- mcp_result = self._handle_mcp_request(
- mcp_source,
- mcp_servers,
- mcp_tool_ids,
- tool_ids,
- application_ids,
- skill_tool_ids,
- mcp_output_enable,
- chat_model,
- "",
- message_list,
- application_id,
- chat_id,
- workspace_id,
- )
- if mcp_result:
- return mcp_result, True
- return chat_model.invoke(message_list), True
-
- def execute_block(
- self,
- message_list: List[BaseMessage],
- chat_id,
- problem_text,
- post_response_handler: PostResponseHandler,
- chat_model: BaseChatModel = None,
- paragraph_list=None,
- manage: PipelineManage = None,
- padding_problem_text: str = None,
- chat_user_id=None,
- chat_user_type=None,
- no_references_setting=None,
- model_setting=None,
- mcp_tool_ids=None,
- mcp_servers="",
- mcp_source="referencing",
- tool_ids=None,
- application_ids=None,
- skill_tool_ids=None,
- workspace_id=None,
- mcp_output_enable=True,
- ):
- reasoning_content_enable = model_setting.get("reasoning_content_enable", False)
- reasoning_content_start = model_setting.get("reasoning_content_start", "")
- reasoning_content_end = model_setting.get("reasoning_content_end", "")
- reasoning = Reasoning(reasoning_content_start, reasoning_content_end)
- chat_record_id = uuid.uuid7()
- # 调用模型
- try:
- chat_result, is_ai_chat = self.get_block_result(
- message_list,
- chat_model,
- paragraph_list,
- no_references_setting,
- problem_text,
- mcp_tool_ids,
- mcp_servers,
- mcp_source,
- tool_ids,
- application_ids,
- skill_tool_ids,
- workspace_id,
- mcp_output_enable,
- manage.context.get("application_id"),
- chat_id,
- )
- if is_ai_chat:
- request_token = chat_model.get_num_tokens_from_messages(message_list)
- response_token = chat_model.get_num_tokens(chat_result.content)
- else:
- request_token = 0
- response_token = 0
- write_context(self, manage, request_token, response_token, chat_result.content)
- reasoning_result = reasoning.get_reasoning_content(chat_result)
- reasoning_result_end = reasoning.get_end_reasoning_content()
- content = reasoning_result.get("content") + reasoning_result_end.get("content")
- if "reasoning_content" in chat_result.response_metadata:
- reasoning_content = chat_result.response_metadata.get("reasoning_content", "") or ""
- else:
- reasoning_content = (reasoning_result.get("reasoning_content") or "") + (
- reasoning_result_end.get("reasoning_content") or ""
- )
- post_response_handler.handler(
- chat_id,
- chat_record_id,
- paragraph_list,
- problem_text,
- content,
- manage,
- self,
- padding_problem_text,
- reasoning_content=reasoning_content,
- )
- if not manage.debug:
- add_access_num(chat_user_id, chat_user_type, manage.context.get("application_id"))
- return manage.get_base_to_response().to_block_response(
- str(chat_id),
- str(chat_record_id),
- content,
- True,
- request_token,
- response_token,
- {
- "reasoning_content": reasoning_content if reasoning_content_enable else "",
- "answer_list": [
- {"content": content, "reasoning_content": reasoning_content if reasoning_content_enable else ""}
- ],
- },
- )
- except Exception as e:
- all_text = "Exception:" + str(e)
- write_context(self, manage, 0, 0, all_text)
- post_response_handler.handler(
- chat_id,
- chat_record_id,
- paragraph_list,
- problem_text,
- all_text,
- manage,
- self,
- padding_problem_text,
- reasoning_content="",
- )
- if not manage.debug:
- add_access_num(chat_user_id, chat_user_type, manage.context.get("application_id"))
- return manage.get_base_to_response().to_block_response(
- str(chat_id), str(chat_record_id), all_text, True, 0, 0, _status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
diff --git a/apps/application/chat_pipeline/step/generate_human_message_step/__init__.py b/apps/application/chat_pipeline/step/generate_human_message_step/__init__.py
deleted file mode 100644
index 5d9549cdc64..00000000000
--- a/apps/application/chat_pipeline/step/generate_human_message_step/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# coding=utf-8
-"""
- @project: maxkb
- @Author:虎
- @file: __init__.py.py
- @date:2024/1/9 18:23
- @desc:
-"""
diff --git a/apps/application/chat_pipeline/step/generate_human_message_step/i_generate_human_message_step.py b/apps/application/chat_pipeline/step/generate_human_message_step/i_generate_human_message_step.py
deleted file mode 100644
index 0d49e9a5e2f..00000000000
--- a/apps/application/chat_pipeline/step/generate_human_message_step/i_generate_human_message_step.py
+++ /dev/null
@@ -1,82 +0,0 @@
-# coding=utf-8
-"""
- @project: maxkb
- @Author:虎
- @file: i_generate_human_message_step.py
- @date:2024/1/9 18:15
- @desc: 生成对话模板
-"""
-from abc import abstractmethod
-from typing import Type, List
-
-from django.utils.translation import gettext_lazy as _
-from langchain_core.messages import BaseMessage
-from rest_framework import serializers
-
-from application.chat_pipeline.I_base_chat_pipeline import IBaseChatPipelineStep, ParagraphPipelineModel
-from application.chat_pipeline.pipeline_manage import PipelineManage
-from application.models import ChatRecord
-from application.serializers.application import NoReferencesSetting
-from common.field.common import InstanceField
-
-
-class IGenerateHumanMessageStep(IBaseChatPipelineStep):
- class InstanceSerializer(serializers.Serializer):
- # 问题
- problem_text = serializers.CharField(required=True, label=_("question"))
- # 段落列表
- paragraph_list = serializers.ListField(child=InstanceField(model_type=ParagraphPipelineModel, required=True),
- label=_("Paragraph List"))
- # 历史对答
- history_chat_record = serializers.ListField(child=InstanceField(model_type=ChatRecord, required=True),
- label=_("History Questions"))
- # 多轮对话数量
- dialogue_number = serializers.IntegerField(required=True, label=_("Number of multi-round conversations"))
- # 最大携带知识库段落长度
- max_paragraph_char_number = serializers.IntegerField(required=True,
- label=_("Maximum length of the knowledge base paragraph"))
- # 模板
- prompt = serializers.CharField(required=True, label=_("Prompt word"))
- system = serializers.CharField(required=False, allow_null=True, allow_blank=True,
- label=_("System prompt words (role)"))
- # 补齐问题
- padding_problem_text = serializers.CharField(required=False,
- label=_("Completion problem"))
- # 未查询到引用分段
- no_references_setting = NoReferencesSetting(required=True,
- label=_("No reference segment settings"))
-
- def get_step_serializer(self, manage: PipelineManage) -> Type[serializers.Serializer]:
- return self.InstanceSerializer
-
- def _run(self, manage: PipelineManage):
- message_list = self.execute(**self.context['step_args'])
- manage.context['message_list'] = message_list
-
- @abstractmethod
- def execute(self,
- problem_text: str,
- paragraph_list: List[ParagraphPipelineModel],
- history_chat_record: List[ChatRecord],
- dialogue_number: int,
- max_paragraph_char_number: int,
- prompt: str,
- padding_problem_text: str = None,
- no_references_setting=None,
- system=None,
- **kwargs) -> List[BaseMessage]:
- """
-
- :param problem_text: 原始问题文本
- :param paragraph_list: 段落列表
- :param history_chat_record: 历史对话记录
- :param dialogue_number: 多轮对话数量
- :param max_paragraph_char_number: 最大段落长度
- :param prompt: 模板
- :param padding_problem_text 用户修改文本
- :param kwargs: 其他参数
- :param no_references_setting: 无引用分段设置
- :param system 系统提示称
- :return:
- """
- pass
diff --git a/apps/application/chat_pipeline/step/generate_human_message_step/impl/base_generate_human_message_step.py b/apps/application/chat_pipeline/step/generate_human_message_step/impl/base_generate_human_message_step.py
deleted file mode 100644
index 2fc62897eee..00000000000
--- a/apps/application/chat_pipeline/step/generate_human_message_step/impl/base_generate_human_message_step.py
+++ /dev/null
@@ -1,79 +0,0 @@
-# coding=utf-8
-"""
- @project: maxkb
- @Author:虎
- @file: base_generate_human_message_step.py.py
- @date:2024/1/10 17:50
- @desc:
-"""
-from typing import List, Dict
-
-from langchain_core.messages import SystemMessage, BaseMessage, HumanMessage
-
-from application.chat_pipeline.I_base_chat_pipeline import ParagraphPipelineModel
-from application.chat_pipeline.step.generate_human_message_step.i_generate_human_message_step import \
- IGenerateHumanMessageStep
-from application.models import ChatRecord
-from common.utils.common import flat_map
-
-
-class BaseGenerateHumanMessageStep(IGenerateHumanMessageStep):
-
- def execute(self, problem_text: str,
- paragraph_list: List[ParagraphPipelineModel],
- history_chat_record: List[ChatRecord],
- dialogue_number: int,
- max_paragraph_char_number: int,
- prompt: str,
- padding_problem_text: str = None,
- no_references_setting=None,
- system=None,
- **kwargs) -> List[BaseMessage]:
- prompt = prompt if (paragraph_list is not None and len(paragraph_list) > 0) else no_references_setting.get(
- 'value')
- exec_problem_text = padding_problem_text if padding_problem_text is not None else problem_text
- start_index = len(history_chat_record) - dialogue_number
- history_message = [[history_chat_record[index].get_human_message(), history_chat_record[index].get_ai_message()]
- for index in
- range(start_index if start_index > 0 else 0, len(history_chat_record))]
- if system is not None and len(system) > 0:
- return [SystemMessage(system), *flat_map(history_message),
- self.to_human_message(prompt, exec_problem_text, max_paragraph_char_number, paragraph_list,
- no_references_setting)]
-
- return [*flat_map(history_message),
- self.to_human_message(prompt, exec_problem_text, max_paragraph_char_number, paragraph_list,
- no_references_setting)]
-
- @staticmethod
- def to_human_message(prompt: str,
- problem: str,
- max_paragraph_char_number: int,
- paragraph_list: List[ParagraphPipelineModel],
- no_references_setting: Dict):
- if paragraph_list is None or len(paragraph_list) == 0:
- if no_references_setting.get('status') == 'ai_questioning':
- return HumanMessage(
- content=no_references_setting.get('value').replace('{question}', problem))
- else:
- return HumanMessage(content=prompt.replace('{data}', "").replace('{question}', problem))
- temp_len = 0
- data_list = []
- for p in paragraph_list:
- content = f"{p.title}:{p.content}"
- temp_len += len(content)
- if temp_len > max_paragraph_char_number:
- row_data = content[0:max_paragraph_char_number - temp_len]
- data_list.append(f"{row_data}")
- break
- else:
- data_list.append(f"{content}")
- data = "\n".join(data_list)
- return HumanMessage(content=prompt.replace('{data}', data).replace('{question}', problem))
-
- def get_details(self, manage, **kwargs):
- return {
- 'status': self.status,
- 'err_message': self.err_message,
- 'step_type': 'generate_human_message',
- }
diff --git a/apps/application/chat_pipeline/step/reset_problem_step/__init__.py b/apps/application/chat_pipeline/step/reset_problem_step/__init__.py
deleted file mode 100644
index 5d9549cdc64..00000000000
--- a/apps/application/chat_pipeline/step/reset_problem_step/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# coding=utf-8
-"""
- @project: maxkb
- @Author:虎
- @file: __init__.py.py
- @date:2024/1/9 18:23
- @desc:
-"""
diff --git a/apps/application/chat_pipeline/step/reset_problem_step/i_reset_problem_step.py b/apps/application/chat_pipeline/step/reset_problem_step/i_reset_problem_step.py
deleted file mode 100644
index a0e06204364..00000000000
--- a/apps/application/chat_pipeline/step/reset_problem_step/i_reset_problem_step.py
+++ /dev/null
@@ -1,55 +0,0 @@
-# coding=utf-8
-"""
- @project: maxkb
- @Author:虎
- @file: i_reset_problem_step.py
- @date:2024/1/9 18:12
- @desc: 重写处理问题
-"""
-from abc import abstractmethod
-from typing import Type, List
-
-from django.utils.translation import gettext_lazy as _
-from rest_framework import serializers
-
-from application.chat_pipeline.I_base_chat_pipeline import IBaseChatPipelineStep
-from application.chat_pipeline.pipeline_manage import PipelineManage
-from application.models import ChatRecord
-from common.field.common import InstanceField
-
-
-class IResetProblemStep(IBaseChatPipelineStep):
- class InstanceSerializer(serializers.Serializer):
- # 问题文本
- problem_text = serializers.CharField(required=True, label=_("question"))
- # 历史对答
- history_chat_record = serializers.ListField(child=InstanceField(model_type=ChatRecord, required=True),
- label=_("History Questions"))
- # 大语言模型
- model_id = serializers.UUIDField(required=False, allow_null=True, label=_("Model id"))
- workspace_id = serializers.CharField(required=True, label=_("User ID"))
- problem_optimization_prompt = serializers.CharField(required=False, max_length=102400,
- label=_("Question completion prompt"))
-
- def get_step_serializer(self, manage: PipelineManage) -> Type[serializers.Serializer]:
- return self.InstanceSerializer
-
- def _run(self, manage: PipelineManage):
- padding_problem = self.execute(**self.context.get('step_args'))
- # 用户输入问题
- source_problem_text = self.context.get('step_args').get('problem_text')
- self.context['problem_text'] = source_problem_text
- self.context['padding_problem_text'] = padding_problem
- manage.context['problem_text'] = source_problem_text
- manage.context['padding_problem_text'] = padding_problem
- # 累加tokens
- manage.context['message_tokens'] = manage.context.get('message_tokens', 0) + self.context.get('message_tokens',
- 0)
- manage.context['answer_tokens'] = manage.context.get('answer_tokens', 0) + self.context.get('answer_tokens', 0)
-
- @abstractmethod
- def execute(self, problem_text: str, history_chat_record: List[ChatRecord] = None, model_id: str = None,
- problem_optimization_prompt=None,
- workspace_id=None,
- **kwargs):
- pass
diff --git a/apps/application/chat_pipeline/step/reset_problem_step/impl/base_reset_problem_step.py b/apps/application/chat_pipeline/step/reset_problem_step/impl/base_reset_problem_step.py
deleted file mode 100644
index 47368ae65f5..00000000000
--- a/apps/application/chat_pipeline/step/reset_problem_step/impl/base_reset_problem_step.py
+++ /dev/null
@@ -1,69 +0,0 @@
-# coding=utf-8
-"""
- @project: maxkb
- @Author:虎
- @file: base_reset_problem_step.py
- @date:2024/1/10 14:35
- @desc:
-"""
-from typing import List
-
-from django.utils.translation import gettext as _
-from langchain_core.messages import HumanMessage
-
-from application.chat_pipeline.step.reset_problem_step.i_reset_problem_step import IResetProblemStep
-from application.models import ChatRecord
-from common.utils.split_model import flat_map
-from models_provider.tools import get_model_instance_by_model_workspace_id
-
-prompt = _(
- "() contains the user's question. Answer the guessed user's question based on the context ({question}) Requirement: Output a complete question and put it in the tag")
-
-
-class BaseResetProblemStep(IResetProblemStep):
- def execute(self, problem_text: str, history_chat_record: List[ChatRecord] = None, model_id: str = None,
- problem_optimization_prompt=None,
- workspace_id=None,
- **kwargs) -> str:
- chat_model = get_model_instance_by_model_workspace_id(model_id, workspace_id) if model_id is not None else None
- if chat_model is None:
- return problem_text
- start_index = len(history_chat_record) - 3
- history_message = [[history_chat_record[index].get_human_message(), history_chat_record[index].get_ai_message()]
- for index in
- range(start_index if start_index > 0 else 0, len(history_chat_record))]
- reset_prompt = problem_optimization_prompt if problem_optimization_prompt else prompt
- message_list = [*flat_map(history_message),
- HumanMessage(content=reset_prompt.replace('{question}', problem_text))]
- response = chat_model.invoke(message_list)
- padding_problem = problem_text
- if response.content.__contains__("") and response.content.__contains__(''):
- padding_problem_data = response.content[
- response.content.index('') + 6:response.content.index('')]
- if padding_problem_data is not None and len(padding_problem_data.strip()) > 0:
- padding_problem = padding_problem_data
- elif len(response.content) > 0:
- padding_problem = response.content
-
- try:
- request_token = chat_model.get_num_tokens_from_messages(message_list)
- response_token = chat_model.get_num_tokens(padding_problem)
- except Exception as e:
- request_token = 0
- response_token = 0
- self.context['message_tokens'] = request_token
- self.context['answer_tokens'] = response_token
- return padding_problem
-
- def get_details(self, manage, **kwargs):
- return {'status': self.status,
- 'err_message': self.err_message,
- 'step_type': 'problem_padding',
- 'run_time': self.context['run_time'],
- 'model_id': str(manage.context['model_id']) if 'model_id' in manage.context else None,
- 'message_tokens': self.context.get('message_tokens', 0),
- 'answer_tokens': self.context.get('answer_tokens', 0),
- 'cost': 0,
- 'padding_problem_text': self.context.get('padding_problem_text'),
- 'problem_text': self.context.get("step_args").get('problem_text'),
- }
diff --git a/apps/application/chat_pipeline/step/search_dataset_step/__init__.py b/apps/application/chat_pipeline/step/search_dataset_step/__init__.py
deleted file mode 100644
index 023c4bc387d..00000000000
--- a/apps/application/chat_pipeline/step/search_dataset_step/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# coding=utf-8
-"""
- @project: maxkb
- @Author:虎
- @file: __init__.py.py
- @date:2024/1/9 18:24
- @desc:
-"""
diff --git a/apps/application/chat_pipeline/step/search_dataset_step/i_search_dataset_step.py b/apps/application/chat_pipeline/step/search_dataset_step/i_search_dataset_step.py
deleted file mode 100644
index 373dc33d44b..00000000000
--- a/apps/application/chat_pipeline/step/search_dataset_step/i_search_dataset_step.py
+++ /dev/null
@@ -1,77 +0,0 @@
-# coding=utf-8
-"""
- @project: maxkb
- @Author:虎
- @file: i_search_dataset_step.py
- @date:2024/1/9 18:10
- @desc: 检索知识库
-"""
-import re
-from abc import abstractmethod
-from typing import List, Type
-
-from django.core import validators
-from django.utils.translation import gettext_lazy as _
-from rest_framework import serializers
-
-from application.chat_pipeline.I_base_chat_pipeline import IBaseChatPipelineStep, ParagraphPipelineModel
-from application.chat_pipeline.pipeline_manage import PipelineManage
-
-
-class ISearchDatasetStep(IBaseChatPipelineStep):
- class InstanceSerializer(serializers.Serializer):
- # 原始问题文本
- problem_text = serializers.CharField(required=True, label=_("question"))
- # 系统补全问题文本
- padding_problem_text = serializers.CharField(required=False,
- label=_("System completes question text"))
- # 需要查询的数据集id列表
- knowledge_id_list = serializers.ListField(required=True, child=serializers.UUIDField(required=True),
- label=_("Dataset id list"))
- # 需要排除的文档id
- exclude_document_id_list = serializers.ListField(required=True, child=serializers.UUIDField(required=True),
- label=_("List of document ids to exclude"))
- # 需要排除向量id
- exclude_paragraph_id_list = serializers.ListField(required=True, child=serializers.UUIDField(required=True),
- label=_("List of exclusion vector ids"))
- # 需要查询的条数
- top_n = serializers.IntegerField(required=True,
- label=_("Reference segment number"))
- # 相似度 0-1之间
- similarity = serializers.FloatField(required=True, max_value=1, min_value=0,
- label=_("Similarity"))
- search_mode = serializers.CharField(required=True, validators=[
- validators.RegexValidator(regex=re.compile("^embedding|keywords|blend$"),
- message=_("The type only supports embedding|keywords|blend"), code=500)
- ], label=_("Retrieval Mode"))
- workspace_id = serializers.CharField(required=True, label=_("Workspace ID"))
-
- def get_step_serializer(self, manage: PipelineManage) -> Type[InstanceSerializer]:
- return self.InstanceSerializer
-
- def _run(self, manage: PipelineManage):
- paragraph_list = self.execute(**self.context['step_args'], manage=manage)
- manage.context['paragraph_list'] = paragraph_list
- self.context['paragraph_list'] = paragraph_list
-
- @abstractmethod
- def execute(self, problem_text: str, knowledge_id_list: list[str], exclude_document_id_list: list[str],
- exclude_paragraph_id_list: list[str], top_n: int, similarity: float, padding_problem_text: str = None,
- search_mode: str = None,
- workspace_id=None,
- manage: PipelineManage = None,
- **kwargs) -> List[ParagraphPipelineModel]:
- """
- 关于 用户和补全问题 说明: 补全问题如果有就使用补全问题去查询 反之就用用户原始问题查询
- :param similarity: 相关性
- :param top_n: 查询多少条
- :param problem_text: 用户问题
- :param knowledge_id_list: 需要查询的数据集id列表
- :param exclude_document_id_list: 需要排除的文档id
- :param exclude_paragraph_id_list: 需要排除段落id
- :param padding_problem_text 补全问题
- :param search_mode 检索模式
- :param workspace_id 工作空间id
- :return: 段落列表
- """
- pass
diff --git a/apps/application/chat_pipeline/step/search_dataset_step/impl/base_search_dataset_step.py b/apps/application/chat_pipeline/step/search_dataset_step/impl/base_search_dataset_step.py
deleted file mode 100644
index b96c9ba2187..00000000000
--- a/apps/application/chat_pipeline/step/search_dataset_step/impl/base_search_dataset_step.py
+++ /dev/null
@@ -1,194 +0,0 @@
-# coding=utf-8
-"""
-@project: maxkb
-@Author:虎
-@file: base_search_dataset_step.py
-@date:2024/1/10 10:33
-@desc:
-"""
-
-import os
-from typing import List, Dict
-
-from django.db.models import QuerySet
-from django.utils.translation import gettext_lazy as _
-from rest_framework.utils.formatting import lazy_format
-
-from application.chat_pipeline.I_base_chat_pipeline import ParagraphPipelineModel
-from application.chat_pipeline.step.search_dataset_step.i_search_dataset_step import ISearchDatasetStep
-from common.config.embedding_config import VectorStore, ModelManage
-from common.auth.constants.role_constants import RoleConstants
-from common.database_model_manage.database_model_manage import DatabaseModelManage
-from common.db.search import native_search
-from common.utils.common import get_file_content
-from knowledge.models import Paragraph, Knowledge
-from knowledge.models import SearchMode
-from knowledge.services.retrieval_stats import get_recall_tracker, record_recall_safely
-from maxkb.conf import PROJECT_DIR
-from models_provider.models import Model
-from models_provider.tools import get_model, get_model_by_id, get_model_default_params
-
-
-def reset_meta(meta):
- if not meta.get("allow_download", False):
- return {"allow_download": False}
- return meta
-
-
-def get_embedding_id(knowledge_id_list):
- knowledge_list = QuerySet(Knowledge).filter(id__in=knowledge_id_list)
- if len(set([knowledge.embedding_model_id for knowledge in knowledge_list])) > 1:
- raise Exception(
- _(
- "The vector model of the associated knowledge base is inconsistent and the segmentation cannot be recalled."
- )
- )
- if len(knowledge_list) == 0:
- raise Exception(_("The knowledge base setting is wrong, please reset the knowledge base"))
- return knowledge_list[0].embedding_model_id
-
-
-class BaseSearchDatasetStep(ISearchDatasetStep):
- def execute(
- self,
- problem_text: str,
- knowledge_id_list: list[str],
- exclude_document_id_list: list[str],
- exclude_paragraph_id_list: list[str],
- top_n: int,
- similarity: float,
- padding_problem_text: str = None,
- search_mode: str = None,
- workspace_id=None,
- manage=None,
- **kwargs,
- ) -> List[ParagraphPipelineModel]:
- get_knowledge_list_of_authorized = DatabaseModelManage.get_model("get_knowledge_list_of_authorized")
- chat_user_type = manage.context.get("chat_user_type")
- if get_knowledge_list_of_authorized is not None and RoleConstants.CHAT_USER.value.name == chat_user_type:
- knowledge_id_list = get_knowledge_list_of_authorized(manage.context.get("chat_user_id"), knowledge_id_list)
- if len(knowledge_id_list) == 0:
- return []
- exec_problem_text = padding_problem_text if padding_problem_text is not None else problem_text
- model_id = get_embedding_id(knowledge_id_list)
- model = get_model_by_id(model_id, workspace_id)
- if model.model_type != "EMBEDDING":
- raise Exception(_("Model does not exist"))
- self.context["model_name"] = model.name
- default_params = get_model_default_params(model)
- embedding_model = ModelManage.get_model(model_id, lambda _id: get_model(model, **{**default_params}))
- embedding_value = embedding_model.embed_query(exec_problem_text)
- vector = VectorStore.get_embedding_vector()
- embedding_list = vector.query(
- exec_problem_text,
- embedding_value,
- knowledge_id_list,
- None,
- exclude_document_id_list,
- exclude_paragraph_id_list,
- True,
- top_n,
- similarity,
- SearchMode(search_mode),
- )
- if embedding_list is None:
- return []
- paragraph_list = self.list_paragraph(embedding_list, vector)
- result = [self.reset_paragraph(paragraph, embedding_list) for paragraph in paragraph_list]
- if not manage.debug:
- recalled_paragraph_ids = {paragraph.id for paragraph in result}
- record_recall_safely(
- [
- embedding
- for embedding in embedding_list
- if str(embedding.get("paragraph_id")) in recalled_paragraph_ids
- ],
- tracker=get_recall_tracker(manage),
- )
- return result
-
- @staticmethod
- def reset_paragraph(paragraph: Dict, embedding_list: List) -> ParagraphPipelineModel:
- filter_embedding_list = [
- embedding for embedding in embedding_list if str(embedding.get("paragraph_id")) == str(paragraph.get("id"))
- ]
- if filter_embedding_list is not None and len(filter_embedding_list) > 0:
- find_embedding = filter_embedding_list[-1]
- return (
- ParagraphPipelineModel.builder()
- .add_paragraph(paragraph)
- .add_similarity(find_embedding.get("similarity"))
- .add_comprehensive_score(find_embedding.get("comprehensive_score"))
- .add_knowledge_name(paragraph.get("knowledge_name"))
- .add_knowledge_type(paragraph.get("knowledge_type"))
- .add_document_name(paragraph.get("document_name"))
- .add_hit_handling_method(paragraph.get("hit_handling_method"))
- .add_directly_return_similarity(paragraph.get("directly_return_similarity"))
- .add_meta(reset_meta(paragraph.get("meta")))
- .build()
- )
-
- @staticmethod
- def get_similarity(paragraph, embedding_list: List):
- filter_embedding_list = [
- embedding for embedding in embedding_list if str(embedding.get("paragraph_id")) == str(paragraph.get("id"))
- ]
- if filter_embedding_list is not None and len(filter_embedding_list) > 0:
- find_embedding = filter_embedding_list[-1]
- return find_embedding.get("comprehensive_score")
- return 0
-
- @staticmethod
- def list_paragraph(embedding_list: List, vector):
- paragraph_id_list = [row.get("paragraph_id") for row in embedding_list]
- if paragraph_id_list is None or len(paragraph_id_list) == 0:
- return []
- paragraph_list = native_search(
- QuerySet(Paragraph).filter(id__in=paragraph_id_list),
- get_file_content(
- os.path.join(PROJECT_DIR, "apps", "application", "sql", "list_knowledge_paragraph_by_paragraph_id.sql")
- ),
- with_table_name=True,
- )
- # 如果向量库中存在脏数据 直接删除
- if len(paragraph_list) != len(paragraph_id_list):
- exist_paragraph_list = [row.get("id") for row in paragraph_list]
- for paragraph_id in paragraph_id_list:
- if not exist_paragraph_list.__contains__(paragraph_id):
- vector.delete_by_paragraph_id(paragraph_id)
- # 如果存在直接返回的则取直接返回段落
- hit_handling_method_paragraph = [
- paragraph
- for paragraph in paragraph_list
- if (
- paragraph.get("hit_handling_method") == "directly_return"
- and BaseSearchDatasetStep.get_similarity(paragraph, embedding_list)
- >= paragraph.get("directly_return_similarity")
- )
- ]
- if len(hit_handling_method_paragraph) > 0:
- # 找到评分最高的
- return [
- sorted(
- hit_handling_method_paragraph, key=lambda p: BaseSearchDatasetStep.get_similarity(p, embedding_list)
- )[-1]
- ]
- return paragraph_list
-
- def get_details(self, manage, **kwargs):
- step_args = self.context.get("step_args") or {}
-
- return {
- "status": self.status,
- "err_message": self.err_message,
- "step_type": "search_step",
- "paragraph_list": [row.to_dict() for row in (self.context.get("paragraph_list") or [])],
- "run_time": self.context.get("run_time") or 0,
- "problem_text": step_args.get("padding_problem_text")
- if "padding_problem_text" in step_args
- else step_args.get("problem_text"),
- "model_name": self.context.get("model_name"),
- "message_tokens": 0,
- "answer_tokens": 0,
- "cost": 0,
- }
diff --git a/apps/application/models/application_chat.py b/apps/application/models/application_chat.py
index d617b65649a..5b87fb26f38 100644
--- a/apps/application/models/application_chat.py
+++ b/apps/application/models/application_chat.py
@@ -1,11 +1,12 @@
# coding=utf-8
"""
- @project: MaxKB
- @Author:虎虎
- @file: application_chat_log.py
- @date:2025/5/29 17:12
- @desc:
+@project: MaxKB
+@Author:虎虎
+@file: application_chat_log.py
+@date:2025/5/29 17:12
+@desc:
"""
+
import uuid_utils.compat as uuid
from django.contrib.postgres.fields import ArrayField
from django.db import models
@@ -19,12 +20,12 @@
class ChatUserType(models.TextChoices):
- ANONYMOUS_USER = "ANONYMOUS_USER", '匿名用户'
+ ANONYMOUS_USER = "ANONYMOUS_USER", "匿名用户"
CHAT_USER = "CHAT_USER", "对话用户"
SYSTEM_API_KEY = "SYSTEM_API_KEY", "系统API_KEY"
APPLICATION_API_KEY = "APPLICATION_API_KEY", "应用API_KEY"
PLATFORM_USER = "PLATFORM_USER", "平台用户"
- SYSTEM_USER = "SYSTEM_USER", '系统用户'
+ SYSTEM_USER = "SYSTEM_USER", "系统用户"
class ExecuteType(models.TextChoices):
@@ -33,7 +34,7 @@ class ExecuteType(models.TextChoices):
def default_asker():
- return {'username': '游客'}
+ return {"username": "游客"}
class Chat(AppModelMixin):
@@ -41,10 +42,12 @@ class Chat(AppModelMixin):
application = models.ForeignKey(Application, on_delete=models.CASCADE)
abstract = models.CharField(max_length=1024, verbose_name="摘要")
chat_user_id = models.CharField(verbose_name="对话用户id", default=None, null=True)
- chat_user_type = models.CharField(max_length=64, verbose_name="客户端类型", choices=ChatUserType.choices,
- default=ChatUserType.ANONYMOUS_USER)
- execute_type = models.CharField(max_length=64, verbose_name="执行类型", choices=ChatUserType.choices,
- default=ExecuteType.CHAT)
+ chat_user_type = models.CharField(
+ max_length=64, verbose_name="客户端类型", choices=ChatUserType.choices, default=ChatUserType.ANONYMOUS_USER
+ )
+ execute_type = models.CharField(
+ max_length=64, verbose_name="执行类型", choices=ChatUserType.choices, default=ExecuteType.CHAT
+ )
is_deleted = models.BooleanField(verbose_name="逻辑删除", default=False)
asker = models.JSONField(verbose_name="访问者", default=default_asker, encoder=SystemEncoder)
meta = models.JSONField(verbose_name="元数据", default=dict)
@@ -53,7 +56,7 @@ class Chat(AppModelMixin):
chat_record_count = models.IntegerField(verbose_name="对话次数", default=0)
mark_sum = models.IntegerField(verbose_name="标记数量", default=0)
source = models.JSONField(verbose_name="来源", default=dict)
- ip_address = models.CharField(max_length=128, verbose_name="ip地址", default='')
+ ip_address = models.CharField(max_length=128, verbose_name="ip地址", default="")
class Meta:
db_table = "application_chat"
@@ -61,22 +64,23 @@ class Meta:
class VoteChoices(models.TextChoices):
"""订单类型"""
- UN_VOTE = "-1", '未投票'
- STAR = "0", '赞同'
- TRAMPLE = "1", '反对'
+
+ UN_VOTE = "-1", "未投票"
+ STAR = "0", "赞同"
+ TRAMPLE = "1", "反对"
class VoteReasonChoices(models.TextChoices):
- ACCURATE = 'accurate', '内容准确'
- COMPLETE = 'complete', '内容完善'
- INACCURATE = 'inaccurate', '内容不准确'
- INCOMPLETE = 'incomplete', '内容不完善'
- OTHER = 'other', '其他'
+ ACCURATE = "accurate", "内容准确"
+ COMPLETE = "complete", "内容完善"
+ INACCURATE = "inaccurate", "内容不准确"
+ INCOMPLETE = "incomplete", "内容不完善"
+ OTHER = "other", "其他"
class ShareLinkType(models.TextChoices):
- PUBLIC = "PUBLIC", 'public'
- PRIVATE = "PRIVATE", 'private'
+ PUBLIC = "PUBLIC", "public"
+ PRIVATE = "PRIVATE", "private"
class ChatSourceChoices(models.TextChoices):
@@ -95,48 +99,66 @@ class ChatRecord(AppModelMixin):
"""
对话日志 详情
"""
+
id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid7, editable=False, verbose_name="主键id")
chat = models.ForeignKey(Chat, on_delete=models.CASCADE)
- vote_status = models.CharField(verbose_name='投票', max_length=10, choices=VoteChoices.choices,
- default=VoteChoices.UN_VOTE)
- vote_reason = models.CharField(verbose_name='投票原因', max_length=50, choices=VoteReasonChoices.choices, null=True,
- blank=True)
- vote_other_content = models.CharField(verbose_name='其他原因', max_length=1024, default='')
+ vote_status = models.CharField(
+ verbose_name="投票", max_length=10, choices=VoteChoices.choices, default=VoteChoices.UN_VOTE
+ )
+ vote_reason = models.CharField(
+ verbose_name="投票原因", max_length=50, choices=VoteReasonChoices.choices, null=True, blank=True
+ )
+ vote_other_content = models.CharField(verbose_name="其他原因", max_length=1024, default="")
problem_text = models.CharField(max_length=10240, verbose_name="问题")
answer_text = models.CharField(max_length=40960, verbose_name="答案")
- answer_text_list = ArrayField(verbose_name="改进标注列表",
- base_field=models.JSONField()
- , default=list)
+ answer_text_list = ArrayField(verbose_name="改进标注列表", base_field=models.JSONField(), default=list)
message_tokens = models.IntegerField(verbose_name="请求token数量", default=0)
answer_tokens = models.IntegerField(verbose_name="响应token数量", default=0)
const = models.IntegerField(verbose_name="总费用", default=0)
details = models.JSONField(verbose_name="对话详情", default=dict, encoder=SystemEncoder)
- improve_paragraph_id_list = ArrayField(verbose_name="改进标注列表",
- base_field=models.UUIDField(max_length=128, blank=True)
- , default=list)
+ improve_paragraph_id_list = ArrayField(
+ verbose_name="改进标注列表", base_field=models.UUIDField(max_length=128, blank=True), default=list
+ )
run_time = models.FloatField(verbose_name="运行时长", default=0)
index = models.IntegerField(verbose_name="对话下标")
source = models.JSONField(verbose_name="来源", default=dict)
- ip_address = models.CharField(max_length=128, verbose_name="ip地址", default='')
+ ip_address = models.CharField(max_length=128, verbose_name="ip地址", default="")
version = models.IntegerField(verbose_name="版本号", default=1)
question = models.JSONField(verbose_name="用户的消息", default=dict, encoder=SystemEncoder)
- messages = ArrayField(verbose_name="响应message",
- base_field=models.JSONField()
- , default=list)
+ messages = ArrayField(verbose_name="响应message", base_field=models.JSONField(), default=list)
workflow_context = models.JSONField(verbose_name="工作流上下文", default=dict, null=True, blank=True)
def get_human_message(self):
- if 'problem_padding' in self.details:
- return HumanMessage(content=self.details.get('problem_padding').get('padding_problem_text'))
- return HumanMessage(content=self.problem_text)
+ # 用户消息取自 question({content, image_list, ...}),历史上下文用文本部分
+ question = self.question if isinstance(self.question, dict) else {"content": self.question or ""}
+ return [HumanMessage(content=question.get("content", "") or "")]
def get_ai_message(self):
- answer_text = self.answer_text
- if answer_text is None or len(str(answer_text).strip()) == 0:
- answer_text = _(
- 'Sorry, no relevant content was found. Please re-describe your problem or provide more information. ')
- return AIMessage(content=answer_text)
+ # 答案取自 messages 中的 TEXT / TOOL 内容块(REASONING/FORM/FAILURE 不进历史),按顺序保留交错。
+ # 注意:type 用字面量,避免 models 反向依赖 application.workflow.ContentType
+ ai_message_list = []
+ for m in self.messages or []:
+ if not isinstance(m, dict):
+ continue
+ m_type = m.get("type")
+ if m_type == "TEXT":
+ if m.get("content"):
+ ai_message_list.append(AIMessage(content=m.get("content")))
+ elif m_type == "TOOL":
+ # 工具调用:名称 + 入参 + 结果 拼成一段
+ tool_parts = [str(p) for p in (m.get("content"), m.get("arguments"), m.get("result")) if p]
+ if tool_parts:
+ ai_message_list.append(AIMessage(content="\n".join(tool_parts)))
+ if len(ai_message_list) == 0:
+ ai_message_list = [
+ AIMessage(
+ content=_(
+ "Sorry, no relevant content was found. Please re-describe your problem or provide more information. "
+ )
+ )
+ ]
+ return ai_message_list
def get_node_details_runtime_node_id(self, runtime_node_id):
return self.details.get(runtime_node_id, None)
@@ -148,8 +170,9 @@ class Meta:
class ApplicationChatUserStats(AppModelMixin):
id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid7, editable=False, verbose_name="主键id")
chat_user_id = models.UUIDField(max_length=128, default=uuid.uuid7, verbose_name="对话用户id")
- chat_user_type = models.CharField(max_length=64, verbose_name="对话用户类型", choices=ChatUserType.choices,
- default=ChatUserType.ANONYMOUS_USER)
+ chat_user_type = models.CharField(
+ max_length=64, verbose_name="对话用户类型", choices=ChatUserType.choices, default=ChatUserType.ANONYMOUS_USER
+ )
application = models.ForeignKey(Application, on_delete=models.CASCADE, verbose_name="应用id")
access_num = models.IntegerField(default=0, verbose_name="访问总次数次数")
intraday_access_num = models.IntegerField(default=0, verbose_name="当日访问次数")
@@ -157,7 +180,7 @@ class ApplicationChatUserStats(AppModelMixin):
class Meta:
db_table = "application_chat_user_stats"
indexes = [
- models.Index(fields=['application_id', 'chat_user_id']),
+ models.Index(fields=["application_id", "chat_user_id"]),
]
@@ -181,7 +204,7 @@ class ApplicationLongTermMemory(AppModelMixin):
class Meta:
db_table = "application_long_term_memory"
- unique_together = [('application', 'chat_user_id')]
+ unique_together = [("application", "chat_user_id")]
indexes = [
- models.Index(fields=['application_id', 'chat_user_id']),
+ models.Index(fields=["application_id", "chat_user_id"]),
]
diff --git a/apps/application/serializers/common.py b/apps/application/serializers/common.py
index a19b70760c4..e238984b5ed 100644
--- a/apps/application/serializers/common.py
+++ b/apps/application/serializers/common.py
@@ -121,6 +121,46 @@ def set_record(self, tool_record):
)
+def resolve_chat_user(chat_user_id, chat_user_type, asker=None):
+ """
+ 根据对话用户 id / 类型解析出对话用户信息。
+ - 登录的对话用户(CHAT_USER):从 ChatUser 表取真实信息
+ - 匿名/其他:优先用 asker(dict 或用户名),否则回退为“游客”
+ """
+ from system_manage.models import ChatUser
+
+ if chat_user_type == ChatUserType.CHAT_USER.value:
+ chat_user = QuerySet(ChatUser).filter(id=chat_user_id).first()
+ return {
+ "id": str(chat_user.id),
+ "email": chat_user.email,
+ "phone": chat_user.phone,
+ "nick_name": chat_user.nick_name,
+ "username": chat_user.username,
+ "source": chat_user.source,
+ }
+ if asker:
+ if isinstance(asker, dict):
+ return asker
+ return {"username": asker}
+ return {"username": "游客"}
+
+
+def resolve_chat_user_group(chat_user):
+ chat_user_id = chat_user.get("id")
+ if not chat_user_id:
+ return []
+ user_group_relation_model = DatabaseModelManage.get_model("user_group_relation")
+ if user_group_relation_model:
+ return [
+ {"id": user_group_relation.group_id, "name": user_group_relation.group.name}
+ for user_group_relation in QuerySet(user_group_relation_model)
+ .select_related("group")
+ .filter(user_id=chat_user_id)
+ ]
+ return []
+
+
class ChatInfo:
def __init__(
self,
@@ -207,45 +247,14 @@ def get_application(self):
def get_chat_user(self, asker=None):
if self.chat_user:
return self.chat_user
- from system_manage.models import ChatUser
-
- chat_user_model = ChatUser
- if self.chat_user_type == ChatUserType.CHAT_USER.value and chat_user_model:
- chat_user = QuerySet(chat_user_model).filter(id=self.chat_user_id).first()
- return {
- "id": str(chat_user.id),
- "email": chat_user.email,
- "phone": chat_user.phone,
- "nick_name": chat_user.nick_name,
- "username": chat_user.username,
- "source": chat_user.source,
- }
- else:
- if asker:
- if isinstance(asker, dict):
- self.chat_user = asker
- else:
- self.chat_user = {"username": asker}
- else:
- self.chat_user = {"username": "游客"}
- return self.chat_user
+ chat_user = resolve_chat_user(self.chat_user_id, self.chat_user_type, asker=asker)
+ # 保持原有语义:仅非登录用户缓存到实例上
+ if self.chat_user_type != ChatUserType.CHAT_USER.value:
+ self.chat_user = chat_user
+ return chat_user
def get_chat_user_group(self, asker=None):
- chat_user = self.get_chat_user(asker=asker)
- chat_user_id = chat_user.get("id")
-
- if not chat_user_id:
- return []
-
- user_group_relation_model = DatabaseModelManage.get_model("user_group_relation")
- if user_group_relation_model:
- return [
- {"id": user_group_relation.group_id, "name": user_group_relation.group.name}
- for user_group_relation in QuerySet(user_group_relation_model)
- .select_related("group")
- .filter(user_id=chat_user_id)
- ]
- return []
+ return resolve_chat_user_group(self.get_chat_user(asker=asker))
def to_base_pipeline_manage_params(self):
self.get_application()
diff --git a/apps/application/views/application_chat.py b/apps/application/views/application_chat.py
index 4537025d581..01dbcb464ec 100644
--- a/apps/application/views/application_chat.py
+++ b/apps/application/views/application_chat.py
@@ -22,7 +22,7 @@
from application.serializers.application_chat import ApplicationChatQuerySerializers
from chat.api.chat_api import ChatAPI, PromptGenerateAPI, PageHistoricalConversationAPI, HistoricalConversationRecordAPI
from chat.api.chat_authentication_api import ChatOpenAPI
-from chat.serializers.chat import OpenChatSerializers, DebugChatSerializers, PromptGenerateSerializer, ResumeSerializers
+from chat.serializers.chat import DebugChatSerializers, OpenChatSerializers, PromptGenerateSerializer, ResumeSerializers
from common.auth import TokenAuth
from common.auth.authentication import has_permissions
from common.auth.constants.compare_constants import CompareConstants
diff --git a/apps/application/workflow/nodes/ai_chat_node/ai_chat_node.py b/apps/application/workflow/nodes/ai_chat_node/ai_chat_node.py
index 2b4583324d4..b2205c020a8 100644
--- a/apps/application/workflow/nodes/ai_chat_node/ai_chat_node.py
+++ b/apps/application/workflow/nodes/ai_chat_node/ai_chat_node.py
@@ -100,7 +100,7 @@ def _get_node_message(chat_record, runtime_node_id):
def _get_workflow_message(chat_record):
- return [chat_record.get_human_message(), chat_record.get_ai_message()]
+ return [*chat_record.get_human_message(), *chat_record.get_ai_message()]
def _get_message(chat_record, dialogue_type, runtime_node_id):
diff --git a/apps/application/workflow/nodes/image_generate_node/image_generate_node.py b/apps/application/workflow/nodes/image_generate_node/image_generate_node.py
index 9611864b37c..a5d5a448e5e 100644
--- a/apps/application/workflow/nodes/image_generate_node/image_generate_node.py
+++ b/apps/application/workflow/nodes/image_generate_node/image_generate_node.py
@@ -130,7 +130,7 @@ def _get_history_message(self, history_chat_record, dialogue_number):
[
[
self._generate_history_human_message(history_chat_record[index]),
- self._generate_history_ai_message(history_chat_record[index]),
+ *self._generate_history_ai_message(history_chat_record[index]),
]
for index in range(max(start_index, 0), len(history_chat_record))
],
@@ -152,9 +152,11 @@ def _generate_history_ai_message(self, chat_record):
if val.get("dialogue_type") == "WORKFLOW":
return chat_record.get_ai_message()
image_list = val["image_list"]
- return AIMessage(
- content=[{"type": "image_url", "image_url": {"url": f"{file_url}"}} for file_url in image_list]
- )
+ return [
+ AIMessage(
+ content=[{"type": "image_url", "image_url": {"url": f"{file_url}"}} for file_url in image_list]
+ )
+ ]
return chat_record.get_ai_message()
def _upload_file(self, file, workflow_params, workflow_type):
diff --git a/apps/application/workflow/nodes/image_to_video_node/image_to_video_node.py b/apps/application/workflow/nodes/image_to_video_node/image_to_video_node.py
index 31df25c47b6..7c1ba29c06b 100644
--- a/apps/application/workflow/nodes/image_to_video_node/image_to_video_node.py
+++ b/apps/application/workflow/nodes/image_to_video_node/image_to_video_node.py
@@ -213,9 +213,13 @@ def _generate_history_ai_message(self, chat_record):
if val["dialogue_type"] == "WORKFLOW":
return chat_record.get_ai_message()
image_list = val["image_list"]
- return AIMessage(
- content=[*[{"type": "image_url", "image_url": {"url": f"{file_url}"}} for file_url in image_list]]
- )
+ return [
+ AIMessage(
+ content=[
+ *[{"type": "image_url", "image_url": {"url": f"{file_url}"}} for file_url in image_list]
+ ]
+ )
+ ]
return chat_record.get_ai_message()
def _get_history_message(self, history_chat_record, dialogue_number):
@@ -225,7 +229,7 @@ def _get_history_message(self, history_chat_record, dialogue_number):
[
[
self._generate_history_human_message(history_chat_record[index]),
- self._generate_history_ai_message(history_chat_record[index]),
+ *self._generate_history_ai_message(history_chat_record[index]),
]
for index in range(start_index if start_index > 0 else 0, len(history_chat_record))
],
diff --git a/apps/application/workflow/nodes/image_understand_node/image_understand_node.py b/apps/application/workflow/nodes/image_understand_node/image_understand_node.py
index 6882d3e5fc3..77b3d4d9708 100644
--- a/apps/application/workflow/nodes/image_understand_node/image_understand_node.py
+++ b/apps/application/workflow/nodes/image_understand_node/image_understand_node.py
@@ -237,7 +237,7 @@ def _get_history_message_for_details(self, history_chat_record, dialogue_number)
[
[
self._generate_history_human_message_for_details(history_chat_record[index]),
- self._generate_history_ai_message(history_chat_record[index]),
+ *self._generate_history_ai_message(history_chat_record[index]),
]
for index in range(start_index if start_index > 0 else 0, len(history_chat_record))
],
@@ -250,7 +250,7 @@ def _generate_history_ai_message(self, chat_record):
if self.node.id == val["node_id"] and "image_list" in val:
if val["dialogue_type"] == "WORKFLOW":
return chat_record.get_ai_message()
- return AIMessage(content=val["answer"])
+ return [AIMessage(content=val["answer"])]
return chat_record.get_ai_message()
def _generate_history_human_message_for_details(self, chat_record):
@@ -285,7 +285,7 @@ def _get_history_message(self, history_chat_record, dialogue_number):
[
[
self._generate_history_human_message(history_chat_record[index]),
- self._generate_history_ai_message(history_chat_record[index]),
+ *self._generate_history_ai_message(history_chat_record[index]),
]
for index in range(start_index if start_index > 0 else 0, len(history_chat_record))
],
diff --git a/apps/application/workflow/nodes/intent_node/intent_node.py b/apps/application/workflow/nodes/intent_node/intent_node.py
index 0137b12b3a8..6d1b61aed7b 100644
--- a/apps/application/workflow/nodes/intent_node/intent_node.py
+++ b/apps/application/workflow/nodes/intent_node/intent_node.py
@@ -134,7 +134,7 @@ def _get_history_message(self, history_chat_record, dialogue_number):
history_message = reduce(
lambda x, y: [*x, *y],
[
- [history_chat_record[index].get_human_message(), history_chat_record[index].get_ai_message()]
+ [*history_chat_record[index].get_human_message(), *history_chat_record[index].get_ai_message()]
for index in range(start_index if start_index > 0 else 0, len(history_chat_record))
],
[],
diff --git a/apps/application/workflow/nodes/question_node/question_node.py b/apps/application/workflow/nodes/question_node/question_node.py
index c11d568f92d..7509e209bed 100644
--- a/apps/application/workflow/nodes/question_node/question_node.py
+++ b/apps/application/workflow/nodes/question_node/question_node.py
@@ -47,7 +47,7 @@ def _get_history_message(history_chat_record, dialogue_number):
history_message = reduce(
lambda x, y: [*x, *y],
[
- [history_chat_record[index].get_human_message(), history_chat_record[index].get_ai_message()]
+ [*history_chat_record[index].get_human_message(), *history_chat_record[index].get_ai_message()]
for index in range(start_index if start_index > 0 else 0, len(history_chat_record))
],
[],
diff --git a/apps/application/workflow/nodes/text_to_video_node/text_to_video_node.py b/apps/application/workflow/nodes/text_to_video_node/text_to_video_node.py
index 0b8afe3b094..5fe0a47d5eb 100644
--- a/apps/application/workflow/nodes/text_to_video_node/text_to_video_node.py
+++ b/apps/application/workflow/nodes/text_to_video_node/text_to_video_node.py
@@ -178,9 +178,13 @@ def _generate_history_ai_message(self, chat_record):
if val["dialogue_type"] == "WORKFLOW":
return chat_record.get_ai_message()
image_list = val["image_list"]
- return AIMessage(
- content=[*[{"type": "image_url", "image_url": {"url": f"{file_url}"}} for file_url in image_list]]
- )
+ return [
+ AIMessage(
+ content=[
+ *[{"type": "image_url", "image_url": {"url": f"{file_url}"}} for file_url in image_list]
+ ]
+ )
+ ]
return chat_record.get_ai_message()
def _get_history_message(self, history_chat_record, dialogue_number):
@@ -190,7 +194,7 @@ def _get_history_message(self, history_chat_record, dialogue_number):
[
[
self._generate_history_human_message(history_chat_record[index]),
- self._generate_history_ai_message(history_chat_record[index]),
+ *self._generate_history_ai_message(history_chat_record[index]),
]
for index in range(start_index if start_index > 0 else 0, len(history_chat_record))
],
diff --git a/apps/chat/serializers/chat.py b/apps/chat/serializers/chat.py
index 918041823c6..c01a0a9f6aa 100644
--- a/apps/chat/serializers/chat.py
+++ b/apps/chat/serializers/chat.py
@@ -4,171 +4,87 @@
@Author:虎虎
@file: chat.py
@date:2025/6/9 11:23
-@desc:
+@desc: 对话新实现(统一走 workflow 引擎、去除 ChatInfo 与 Redis 会话缓存)。
"""
import json
import os
+import queue
import queue as thread_queue
import threading
-from gettext import gettext
-from typing import List, Dict
import uuid_utils
import uuid_utils.compat as uuid
from django.db.models import QuerySet
from django.http import StreamingHttpResponse
+from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from rest_framework import serializers
from rest_framework.request import Request
-from application.chat_pipeline.pipeline_manage import PipelineManage
-from application.chat_pipeline.step.chat_step.i_chat_step import PostResponseHandler
-from application.chat_pipeline.step.chat_step.impl.base_chat_step import BaseChatStep
-from application.chat_pipeline.step.generate_human_message_step.impl.base_generate_human_message_step import (
- BaseGenerateHumanMessageStep,
-)
-from application.chat_pipeline.step.reset_problem_step.impl.base_reset_problem_step import BaseResetProblemStep
-from application.chat_pipeline.step.search_dataset_step.impl.base_search_dataset_step import BaseSearchDatasetStep
-from application.flow.common import Answer
from application.flow.tools import to_stream_response_simple
from application.models import (
Application,
- ApplicationTypeChoices,
- ChatUserType,
- ApplicationChatUserStats,
+ ApplicationVersion,
ApplicationAccessToken,
- ChatRecord,
+ ApplicationChatUserStats,
Chat,
- ApplicationVersion,
+ ChatRecord,
+ ChatUserType,
+ ExecuteType,
)
from application.serializers.application import ApplicationOperateSerializer
-from application.serializers.common import ChatInfo
+from application.serializers.application_chat import ChatCountSerializer
+from application.serializers.common import resolve_chat_user, resolve_chat_user_group
+from chat.serializers.chat_history import ChatHistory
from application.workflow.common import WorkflowType, new_instance
from application.workflow.message.aggregator import AggregationManager
from application.workflow.message.struct.failure_content import FailureContent
-from application.workflow.message.struct.form_content import FormContent
-from application.workflow.message.struct.reasoning_content import ReasoningContent
-from application.workflow.message.struct.text_content import TextContent
-from application.workflow.message.struct.tool_content import ToolContent
from application.workflow.message_queue import get_message_queue
from application.workflow.nodes import get_start_node
from application.workflow.workflow_manage import WorkflowManage, CallBack
from application.workflow.workflow_run_registry import WorkflowRunRegistry
+from chat.template.agent_simple import build_workflow
from common import result
-from common.database_model_manage.database_model_manage import DatabaseModelManage
from common.exception.app_exception import AppApiException, AppChatNumOutOfBoundsFailed, ChatException
from common.handle.base_to_response import BaseToResponse
from common.handle.impl.response.openai_to_response import OpenaiToResponse
from common.handle.impl.response.system_to_response import SystemToResponse
-from common.utils.common import flat_map, get_file_content, is_valid_uuid
+from common.utils.common import get_file_content
from common.utils.logger import maxkb_logger
-from knowledge.models import Document, Paragraph
from maxkb.conf import PROJECT_DIR
from models_provider.models import Model, Status
from models_provider.tools import get_model_instance_by_model_workspace_id
from system_manage.models.chat_user_token_quota import ChatUserTokenQuota
-from system_manage.models.resource_mapping import ResourceMapping
+# 「Chat 行尚未查询」的哨兵,区别于「查询过但不存在(None)」
+_CHAT_UNSET = object()
-class ChatMessagesSerializers(serializers.Serializer):
- role = serializers.CharField(required=True, label=_("Role"))
- content = serializers.CharField(required=True, label=_("Content"))
-
-class GeneratePromptSerializers(serializers.Serializer):
- prompt = serializers.CharField(required=True, label=_("Prompt template"))
- messages = serializers.ListSerializer(child=ChatMessagesSerializers(), required=True, label=_("Chat context"))
-
- def is_valid(self, *, raise_exception=False):
- super().is_valid(raise_exception=True)
- messages = self.data.get("messages")
-
- if len(messages) > 30:
- raise AppApiException(400, _("Too many messages"))
-
- for index in range(len(messages)):
- role = messages[index].get("role")
- if role == "ai" and index % 2 != 1:
- raise AppApiException(400, _("Authentication failed. Please verify that the parameters are correct."))
- if role == "user" and index % 2 != 0:
- raise AppApiException(400, _("Authentication failed. Please verify that the parameters are correct."))
- if role not in ["user", "ai"]:
- raise AppApiException(400, _("Authentication failed. Please verify that the parameters are correct."))
+def get_work_flow(application):
+ if application.type == "WORK_FLOW":
+ return application.work_flow
+ return build_workflow(application)
class ChatMessageSerializers(serializers.Serializer):
+ """新流程的对话入参(去掉旧工作流调试字段 node_id/runtime_node_id/node_data/child_node)。"""
+
message = serializers.DictField(required=True, label=_("User Questions"))
- stream = serializers.BooleanField(required=True, label=_("Is the answer in streaming mode"))
- re_chat = serializers.BooleanField(required=True, label=_("Do you want to reply again"))
+ stream = serializers.BooleanField(required=False, default=True, label=_("Is the answer in streaming mode"))
+ re_chat = serializers.BooleanField(required=False, default=False, label=_("Do you want to reply again"))
chat_record_id = serializers.UUIDField(required=False, allow_null=True, label=_("Conversation record id"))
-
- node_id = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_("Node id"))
-
- runtime_node_id = serializers.CharField(
- required=False, allow_null=True, allow_blank=True, label=_("Runtime node id")
- )
-
- node_data = serializers.DictField(required=False, allow_null=True, label=_("Node parameters"))
-
form_data = serializers.DictField(required=False, label=_("Global variables"))
- child_node = serializers.DictField(required=False, allow_null=True, label=_("Child Nodes"))
-
-
-def get_post_handler(chat_info: ChatInfo):
- class PostHandler(PostResponseHandler):
- def handler(
- self,
- chat_id,
- chat_record_id,
- paragraph_list: List[Paragraph],
- problem_text: str,
- answer_text,
- manage: PipelineManage,
- step: BaseChatStep,
- padding_problem_text: str = None,
- **kwargs,
- ):
- answer_list = [
- [
- Answer(
- answer_text,
- "ai-chat-node",
- "ai-chat-node",
- "ai-chat-node",
- {},
- "ai-chat-node",
- kwargs.get("reasoning_content", ""),
- ).to_dict()
- ]
- ]
- chat_record = ChatRecord(
- id=chat_record_id,
- chat_id=chat_id,
- problem_text=problem_text,
- answer_text=answer_text,
- details=manage.get_details(),
- message_tokens=manage.context["message_tokens"],
- answer_tokens=manage.context["answer_tokens"],
- answer_text_list=answer_list,
- run_time=manage.context["run_time"],
- index=len(chat_info.chat_record_list) + 1,
- ip_address=chat_info.ip_address,
- source=chat_info.source,
- )
- chat_info.append_chat_record(chat_record)
- # 重新设置缓存
- chat_info.set_cache()
-
- return PostHandler()
+ # Form 提交时的定位信息 {id, index, children}
+ position = serializers.DictField(required=False, allow_null=True, label=_("Form position"))
+ chunk_id = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_("Chunk id"))
class DebugChatSerializers(serializers.Serializer):
chat_id = serializers.UUIDField(required=True, label=_("Conversation ID"))
- # 以下字段用于「缓存缺失时按前端提供的 chat_id 现开会话」(open-if-missing)
workspace_id = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_("Workspace ID"))
- application_id = serializers.UUIDField(required=False, allow_null=True, label=_("Application ID"))
+ application_id = serializers.UUIDField(required=True, label=_("Application ID"))
chat_user_id = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_("Client id"))
chat_user_type = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_("Client Type"))
ip_address = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_("IP Address"))
@@ -176,179 +92,19 @@ class DebugChatSerializers(serializers.Serializer):
def chat(self, instance: dict, base_to_response: BaseToResponse = SystemToResponse()):
self.is_valid(raise_exception=True)
- chat_id = self.data.get("chat_id")
- chat_info: ChatInfo = ChatInfo.get_cache(chat_id)
- if chat_info is None:
- # 前端本地生成的 chat_id 首次发消息时,缓存里还没有会话,按该 id 现开一个 debug 会话。
- OpenChatSerializers(
- data={
- "workspace_id": self.data.get("workspace_id"),
- "application_id": self.data.get("application_id"),
- "chat_user_id": self.data.get("chat_user_id"),
- "chat_user_type": self.data.get("chat_user_type"),
- "ip_address": self.data.get("ip_address"),
- "source": self.data.get("source"),
- "debug": True,
- }
- ).open(chat_id=str(chat_id))
- chat_info = ChatInfo.get_cache(chat_id)
- application = QuerySet(Application).filter(id=chat_info.application_id).first()
- chat_info.application = application
return ChatSerializers(
data={
- "chat_id": chat_id,
- "chat_user_id": chat_info.chat_user_id,
- "chat_user_type": chat_info.chat_user_type,
- "application_id": chat_info.application.id,
+ "chat_id": self.data.get("chat_id"),
+ "chat_user_id": self.data.get("chat_user_id"),
+ "chat_user_type": self.data.get("chat_user_type"),
+ "application_id": self.data.get("application_id"),
+ "ip_address": self.data.get("ip_address"),
+ "source": self.data.get("source"),
"debug": True,
}
).chat(instance, base_to_response)
-SYSTEM_ROLE = get_file_content(os.path.join(PROJECT_DIR, "apps", "chat", "template", "generate_prompt_system"))
-
-
-class PromptGenerateSerializer(serializers.Serializer):
- workspace_id = serializers.CharField(required=False, label=_("Workspace ID"))
- model_id = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("Model"))
- application_id = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("Application"))
-
- def is_valid(self, *, raise_exception=False):
- super().is_valid(raise_exception=True)
- workspace_id = self.data.get("workspace_id")
- query_set = QuerySet(Application).filter(id=self.data.get("application_id"))
- if workspace_id:
- query_set = query_set.filter(workspace_id=workspace_id)
- application = query_set.first()
- if application is None:
- raise AppApiException(500, _("Application id does not exist"))
- return application
-
- def generate_prompt(self, instance: dict):
- application = self.is_valid(raise_exception=True)
- GeneratePromptSerializers(data=instance).is_valid(raise_exception=True)
- workspace_id = self.data.get("workspace_id")
- model_id = self.data.get("model_id")
- prompt = instance.get("prompt")
- messages = instance.get("messages")
-
- message = messages[-1]["content"]
- q = prompt.replace("{userInput}", message)
-
- messages[-1]["content"] = q
- SUPPORTED_MODEL_TYPES = ["LLM", "IMAGE"]
- model_exist = QuerySet(Model).filter(id=model_id, model_type__in=SUPPORTED_MODEL_TYPES).exists()
- if not model_exist:
- raise Exception(_("Model does not exists or is not an LLM model"))
-
- def process():
- model = get_model_instance_by_model_workspace_id(
- model_id=model_id, workspace_id=workspace_id, **application.model_params_setting
- )
- try:
- for r in model.stream(
- [
- SystemMessage(content=SYSTEM_ROLE),
- *[
- HumanMessage(content=m.get("content"))
- if m.get("role") == "user"
- else AIMessage(content=m.get("content"))
- for m in messages
- ],
- ]
- ):
- yield "data: " + json.dumps({"content": r.content}) + "\n\n"
- except Exception as e:
- yield "data: " + json.dumps({"error": str(e)}) + "\n\n"
-
- return to_stream_response_simple(process())
-
-
-class OpenAIMessage(serializers.Serializer):
- content = serializers.CharField(required=True, label=_("content"))
- role = serializers.CharField(required=True, label=_("Role"))
-
-
-class OpenAIInstanceSerializer(serializers.Serializer):
- messages = serializers.ListField(child=OpenAIMessage())
- chat_id = serializers.UUIDField(required=False, label=_("Conversation ID"))
- re_chat = serializers.BooleanField(required=False, label=_("Regenerate"))
- stream = serializers.BooleanField(required=False, label=_("Streaming Output"))
-
-
-class OpenAIChatSerializer(serializers.Serializer):
- application_id = serializers.UUIDField(required=True, label=_("Application ID"))
- chat_user_id = serializers.CharField(required=True, label=_("Client id"))
- chat_user_type = serializers.CharField(required=True, label=_("Client Type"))
- ip_address = serializers.CharField(required=False, label=_("IP Address"))
- source = serializers.JSONField(required=False, label=_("Source"))
-
- @staticmethod
- def get_message(instance):
- return instance.get("messages")[-1].get("content")
-
- @staticmethod
- def generate_chat(chat_id, application_id, message, chat_user_id, chat_user_type, ip_address, source):
- if chat_id is None:
- chat_id = str(uuid.uuid1())
- chat_info = ChatInfo(chat_id, chat_user_id, chat_user_type, ip_address, source, [], [], application_id)
- chat_info.set_cache()
- else:
- chat_info = ChatInfo.get_cache(chat_id)
- if chat_info is None:
- open_chat = ChatSerializers(
- data={
- "chat_id": chat_id,
- "chat_user_id": chat_user_id,
- "chat_user_type": chat_user_type,
- "application_id": application_id,
- "ip_address": ip_address,
- "source": source,
- }
- )
- open_chat.is_valid(raise_exception=True)
- chat_info = open_chat.re_open_chat(chat_id)
- chat_info.set_cache()
- return chat_id
-
- def chat(self, instance: Dict, with_valid=True):
- if with_valid:
- self.is_valid(raise_exception=True)
- OpenAIInstanceSerializer(data=instance).is_valid(raise_exception=True)
- chat_id = instance.get("chat_id")
- message = self.get_message(instance)
- re_chat = instance.get("re_chat", False)
- stream = instance.get("stream", False)
- application_id = self.data.get("application_id")
- chat_user_id = self.data.get("chat_user_id")
- chat_user_type = self.data.get("chat_user_type")
- ip_address = self.data.get("ip_address")
- source = self.data.get("source")
- chat_id = self.generate_chat(chat_id, application_id, message, chat_user_id, chat_user_type, ip_address, source)
- return ChatSerializers(
- data={
- "chat_id": chat_id,
- "chat_user_id": chat_user_id,
- "chat_user_type": chat_user_type,
- "application_id": application_id,
- "ip_address": ip_address,
- "source": source,
- }
- ).chat(
- {
- "message": message,
- "re_chat": re_chat,
- "stream": stream,
- "form_data": instance.get("form_data", {}),
- "image_list": instance.get("image_list", []),
- "document_list": instance.get("document_list", []),
- "audio_list": instance.get("audio_list", []),
- "other_list": instance.get("other_list", []),
- },
- base_to_response=OpenaiToResponse(),
- )
-
-
class ChatSerializers(serializers.Serializer):
chat_id = serializers.UUIDField(required=True, label=_("Conversation ID"))
chat_user_id = serializers.CharField(required=True, label=_("Client id"))
@@ -358,12 +114,27 @@ class ChatSerializers(serializers.Serializer):
ip_address = serializers.CharField(required=False, label=_("IP Address"), allow_null=True, allow_blank=True)
source = serializers.JSONField(required=False, label=_("Source"))
- def is_valid_application_workflow(self, *, raise_exception=False):
- self.is_valid_intraday_access_num()
-
- def is_valid_chat_id(self, chat_info: ChatInfo):
- if self.data.get("application_id") is not None and self.data.get("application_id") != str(
- chat_info.application_id
+ # ---------- 会话行(一次查询,全程复用) ----------
+ def get_chat(self):
+ """查询 Chat 行并缓存到实例,全流程只查一次(区分未查询/不存在)。"""
+ cached = getattr(self, "_chat_cache", _CHAT_UNSET)
+ if cached is _CHAT_UNSET:
+ cached = QuerySet(Chat).filter(id=self.data.get("chat_id")).first()
+ self._chat_cache = cached
+ return cached
+
+ # ---------- 校验 ----------
+ def is_valid_chat(self):
+ """
+ 会话不存在 → 视为新会话,后续 ensure_chat_row 惰性创建,无需前端传标记;
+ 会话已存在 → 校验归属(必须属于当前应用与当前对话用户),防止越权写入。
+ debug 会话同样落库(execute_type=DEBUG)、同样按此校验,不再特殊放行。
+ """
+ chat = self.get_chat()
+ if chat is None:
+ return
+ if str(chat.application_id) != str(self.data.get("application_id")) or str(chat.chat_user_id) != str(
+ self.data.get("chat_user_id")
):
raise ChatException(500, _("Conversation does not exist"))
@@ -393,120 +164,127 @@ def is_valid_intraday_access_num(self):
if application_access_token.access_num <= access_client.intraday_access_num:
raise AppChatNumOutOfBoundsFailed(1002, _("The number of visits exceeds today's visits"))
- def is_valid_application_simple(self, *, chat_info: ChatInfo, raise_exception=False):
- self.is_valid_intraday_access_num()
- model_id = chat_info.application.model_id
- if model_id is None:
- return chat_info
- model = QuerySet(Model).filter(id=model_id).first()
- if model is None:
- return chat_info
- if model.status == Status.ERROR:
- raise ChatException(500, _("The current model is not available"))
- if model.status == Status.DOWNLOAD:
- raise ChatException(500, _("The model is downloading, please try again later"))
- return chat_info
-
- def chat_simple(self, chat_info: ChatInfo, instance, base_to_response):
- message_dict = instance.get("message")
- message = message_dict.get("content", "") if isinstance(message_dict, dict) else message_dict
- re_chat = instance.get("re_chat")
- stream = instance.get("stream")
- chat_user_id = self.data.get("chat_user_id")
- chat_user_type = self.data.get("chat_user_type")
- ip_address = self.data.get("ip_address")
- source = self.data.get("source")
- form_data = instance.get("form_data")
- chat_record_id = instance.get("chat_record_id")
- pipeline_manage_builder = PipelineManage.builder()
- # 如果开启了问题优化,则添加上问题优化步骤
- if chat_info.application.problem_optimization:
- pipeline_manage_builder.append_step(BaseResetProblemStep)
- # 构建流水线管理器
- pipeline_message = (
- pipeline_manage_builder.append_step(BaseSearchDatasetStep)
- .append_step(BaseGenerateHumanMessageStep)
- .append_step(BaseChatStep)
- .add_base_to_response(base_to_response)
- .add_debug(self.data.get("debug", False))
- .build()
- )
- exclude_paragraph_id_list = []
- # 相同问题是否需要排除已经查询到的段落
- if re_chat:
- paragraph_id_list = flat_map(
- [
- [paragraph.get("id") for paragraph in chat_record.details["search_step"]["paragraph_list"]]
- for chat_record in chat_info.chat_record_list
- if chat_record.problem_text == message
- and "search_step" in chat_record.details
- and "paragraph_list" in chat_record.details["search_step"]
- ]
+ # ---------- application ----------
+ def get_application(self):
+ """debug 取 Application 本体;非 debug 取最新发布的 ApplicationVersion。"""
+ application_id = self.data.get("application_id")
+ if self.data.get("debug"):
+ application = QuerySet(Application).filter(id=application_id).first()
+ if application is None:
+ raise ChatException(500, _("The application does not exist"))
+ else:
+ application = (
+ QuerySet(ApplicationVersion).filter(application_id=application_id).order_by("-create_time")[0:1].first()
)
- exclude_paragraph_id_list = list(set(paragraph_id_list))
- # 构建运行参数
- params = chat_info.to_pipeline_manage_params(
- message,
- get_post_handler(chat_info),
- exclude_paragraph_id_list,
- chat_user_id,
- chat_user_type,
- ip_address,
- source,
- stream,
- form_data,
+ if application is None:
+ raise ChatException(500, _("The application has not been published. Please use it after publishing."))
+ return application
+
+ def ensure_chat_row(self, question, asker):
+ """Chat 行不存在则创建(debug 记为 DEBUG 类型),返回该行。复用 get_chat 的一次查询。"""
+ chat = self.get_chat()
+ if chat is not None:
+ return chat
+ chat = Chat(
+ id=self.data.get("chat_id"),
+ application_id=self.data.get("application_id"),
+ abstract=(question or "")[0:1024],
+ execute_type=ExecuteType.DEBUG if self.data.get("debug") else ExecuteType.CHAT,
+ chat_user_id=self.data.get("chat_user_id"),
+ chat_user_type=self.data.get("chat_user_type"),
+ ip_address=self.data.get("ip_address"),
+ source=self.data.get("source"),
+ asker=asker,
+ )
+ chat.save()
+ self._chat_cache = chat
+ return chat
+
+ def save_chat_record(self, chat_record_id, question, asker, new_record):
+ """插入一条占位 ChatRecord(workflow 完成后由 update_chat_record 回填)。"""
+ chat_id = self.data.get("chat_id")
+ q_text = question.get("content", "") if isinstance(question, dict) else question
+ self.ensure_chat_row(q_text, asker)
+ defaults = {
+ "chat_id": chat_id,
+ "problem_text": "",
+ "answer_text": "",
+ "details": {},
+ "message_tokens": 0,
+ "answer_tokens": 0,
+ "answer_text_list": [[]],
+ "run_time": 0,
+ # index 现在用不上,字段 NOT NULL 故给常量 0
+ "index": 0,
+ "ip_address": self.data.get("ip_address") or "",
+ "source": self.data.get("source"),
+ "workflow_context": {},
+ "question": question,
+ "messages": [],
+ }
+ if new_record:
+ # 全新记录:直接插入,省掉 update_or_create 的一次 SELECT
+ ChatRecord(id=chat_record_id, **defaults).save(force_insert=True)
+ else:
+ # Form 提交 / 重答:复用同一 chat_record_id
+ QuerySet(ChatRecord).update_or_create(id=chat_record_id, defaults=defaults)
+
+ @staticmethod
+ def _usage_from_context(workflow_context):
+ """从 workflow_context 汇总 token 用量:prompt=message_tokens, completion=answer_tokens。"""
+ prompt_tokens = sum(
+ v.get("message_tokens", 0)
+ for v in workflow_context.values()
+ if isinstance(v, dict) and "message_tokens" in v
+ )
+ completion_tokens = sum(
+ v.get("answer_tokens", 0) for v in workflow_context.values() if isinstance(v, dict) and "answer_tokens" in v
)
- if chat_record_id:
- params["chat_record_id"] = chat_record_id
- chat_info.set_chat(message)
- # 运行流水线作业
- pipeline_message.run(params)
- return pipeline_message.context["chat_result"]
+ return {"prompt_tokens": prompt_tokens, "completion_tokens": completion_tokens}
@staticmethod
- def get_chat_record(chat_info, chat_record_id):
- if chat_info is not None:
- chat_record_list = [
- chat_record for chat_record in chat_info.chat_record_list if str(chat_record.id) == str(chat_record_id)
- ]
- if chat_record_list is not None and len(chat_record_list):
- return chat_record_list[-1]
- chat_record = QuerySet(ChatRecord).filter(id=chat_record_id, chat_id=chat_info.chat_id).first()
- if chat_record is None:
- if not is_valid_uuid(chat_record_id):
- raise ChatException(500, _("Conversation record does not exist"))
- chat_record = QuerySet(ChatRecord).filter(id=chat_record_id).first()
- return chat_record
-
- def chat_work_flow(self, chat_info: ChatInfo, instance: dict, base_to_response):
- import queue
+ def update_chat_record(chat_user_id, chat_record_id, workflow_context, messages):
+ usage = ChatSerializers._usage_from_context(workflow_context)
+ message_tokens = usage["prompt_tokens"]
+ answer_tokens = usage["completion_tokens"]
+ ChatUserTokenQuota.consume(chat_user_id, message_tokens + answer_tokens)
+ QuerySet(ChatRecord).filter(id=chat_record_id).update(
+ workflow_context=workflow_context,
+ messages=messages,
+ message_tokens=message_tokens,
+ answer_tokens=answer_tokens,
+ )
+ # ---------- 执行 ----------
+ def chat_work_flow(self, application, instance: dict, base_to_response):
message_dict = instance.get("message")
message = message_dict.get("content", "") if isinstance(message_dict, dict) else message_dict
re_chat = instance.get("re_chat")
stream = instance.get("stream")
+ chat_id = self.data.get("chat_id")
chat_user_id = self.data.get("chat_user_id")
chat_user_type = self.data.get("chat_user_type")
ip_address = self.data.get("ip_address")
source = self.data.get("source")
- form_data = instance.get("form_data")
+ form_data = instance.get("form_data") or {}
image_list = message_dict.get("image_list", []) if isinstance(message_dict, dict) else []
video_list = message_dict.get("video_list", []) if isinstance(message_dict, dict) else []
document_list = message_dict.get("document_list", []) if isinstance(message_dict, dict) else []
audio_list = message_dict.get("audio_list", []) if isinstance(message_dict, dict) else []
other_list = message_dict.get("other_list", []) if isinstance(message_dict, dict) else []
- workspace_id = chat_info.application.workspace_id
+ workspace_id = application.workspace_id
chat_record_id = instance.get("chat_record_id")
position = instance.get("position")
chunk_id = instance.get("chunk_id")
debug = self.data.get("debug", False)
- history_chat_record = chat_info.chat_record_list
- if chat_record_id is not None:
- chat_record = self.get_chat_record(chat_info, chat_record_id)
- if chat_record:
- history_chat_record = [r for r in chat_info.chat_record_list if str(r.id) != chat_record_id]
- work_flow = chat_info.application.work_flow
+ # 对话用户信息(asker 取自 form_data)
+ chat_user = resolve_chat_user(chat_user_id, chat_user_type, asker=form_data.get("asker"))
+ chat_user_group = resolve_chat_user_group(chat_user)
+
+ history_chat_record = ChatHistory(chat_id).load(exclude_record_id=chat_record_id)
+
+ work_flow = get_work_flow(application)
workflow = new_instance(work_flow, WorkflowType.APPLICATION)
chat_record_id_str = str(uuid.uuid7()) if chat_record_id is None else str(chat_record_id)
@@ -514,7 +292,7 @@ def chat_work_flow(self, chat_info: ChatInfo, instance: dict, base_to_response):
parameters = {
"history_chat_record": history_chat_record,
"question": message,
- "chat_id": chat_info.chat_id,
+ "chat_id": chat_id,
"chat_record_id": chat_record_id_str,
"stream": stream,
"re_chat": re_chat,
@@ -524,10 +302,10 @@ def chat_work_flow(self, chat_info: ChatInfo, instance: dict, base_to_response):
"source": source,
"workspace_id": workspace_id,
"debug": debug,
- "chat_user": chat_info.get_chat_user(),
- "chat_user_group": chat_info.get_chat_user_group(),
- "application_id": str(chat_info.application_id),
- "form_data": form_data or {},
+ "chat_user": chat_user,
+ "chat_user_group": chat_user_group,
+ "application_id": str(self.data.get("application_id")),
+ "form_data": form_data,
"position": position,
"chunk_id": chunk_id,
"image_list": image_list or [],
@@ -538,94 +316,18 @@ def chat_work_flow(self, chat_info: ChatInfo, instance: dict, base_to_response):
}
result_queue = queue.Queue()
-
aggregation = AggregationManager()
- self.save_chat_record(chat_info, chat_info.chat_id, chat_record_id_str, message_dict)
+ self.save_chat_record(chat_record_id_str, message_dict, chat_user, new_record=chat_record_id is None)
def on_next(wf_manage, content):
aggregation.aggregate(content)
- message_queue = get_message_queue()
- message_queue.produce(chat_record_id_str, content.to_dict())
- if isinstance(content, TextContent):
- result_queue.put(
- (
- "chunk",
- {
- "content": [
- {
- "id": content.id,
- "type": "TEXT",
- "content": content.content,
- }
- ]
- },
- )
- )
- elif isinstance(content, ReasoningContent):
- result_queue.put(
- (
- "chunk",
- {
- "content": [
- {
- "id": content.id,
- "type": "REASONING",
- "content": content.content,
- "status": content.status.value if content.status else None,
- }
- ]
- },
- )
- )
- elif isinstance(content, ToolContent):
- result_queue.put(
- (
- "chunk",
- {
- "content": [
- {
- "id": content.id,
- "type": "TOOL",
- "content": content.content,
- "arguments": content.arguments,
- "result": content.result,
- "status": content.status.value if content.status else None,
- }
- ]
- },
- )
- )
- elif isinstance(content, FormContent):
-
- def position_to_dict(pos):
- if pos is None:
- return None
- return {"id": pos.id, "index": pos.index, "children": position_to_dict(pos.children)}
-
- result_queue.put(
- (
- "chunk",
- {
- "content": [
- {
- "id": content.id,
- "type": "FORM",
- "form_field_list": content.form_field_list,
- "form_content_format": content.form_content_format,
- "is_submit": content.is_submit,
- "form_data": content.form_data,
- "status": content.status.value if content.status else None,
- "position": position_to_dict(content.position),
- "chat_record_id": chat_record_id_str,
- }
- ]
- },
- )
- )
+ block = content.to_dict()
+ # 持久化(resume)与 live 队列都放裸内容块,格式化统一交给消费端的 base_to_response
+ get_message_queue().produce(chat_record_id_str, block)
+ result_queue.put(("chunk", block))
def on_complete(wf_manage, error):
- # 注销工作流实例
- WorkflowRunRegistry.unregister(chat_record_id_str, str(chat_info.chat_id))
+ WorkflowRunRegistry.unregister(chat_record_id_str, str(chat_id))
message_queue = get_message_queue()
if error:
result_queue.put(("error", error))
@@ -633,9 +335,19 @@ def on_complete(wf_manage, error):
chat_record_id_str,
FailureContent(str(uuid_utils.uuid7()), str(error), Status.SUCCESS, None, None).to_dict(),
)
- QuerySet(ChatRecord).filter(id=chat_record_id).update()
- self.update_chat_record(
- chat_info, chat_info.chat_id, chat_record_id_str, wf_manage.context, aggregation.get_contents()
+ messages = aggregation.get_contents()
+ self.update_chat_record(chat_user_id, chat_record_id_str, wf_manage.context, messages)
+ # 计数统计放到内容落库之后再更新(挪出进对话前的关键路径)
+ ChatCountSerializer(data={"chat_id": chat_id}).update_chat()
+ # 定稿后把本轮记录追加进历史缓存(question 已在建占位时确定,messages 为聚合结果)
+ ChatHistory(chat_id).append(
+ ChatRecord(
+ id=chat_record_id_str,
+ chat_id=chat_id,
+ question=message_dict,
+ messages=messages,
+ create_time=timezone.now(),
+ )
)
result_queue.put(("done", None))
message_queue.produce_done(chat_record_id_str)
@@ -645,9 +357,8 @@ def on_complete(wf_manage, error):
def get_start_node_fn(wf, wm):
return get_start_node(wf, wm, WorkflowType.APPLICATION, position)
- # 判断是否是 Form 提交(有 position 和 chat_record_id)
+ # Form 提交(有 position 和 chat_record_id):从历史 context 恢复
if position and chat_record_id:
- # 从历史 context 恢复
work_flow_manage = WorkflowManage.from_context(
chat_record_id=chat_record_id,
workflow=workflow,
@@ -657,22 +368,16 @@ def get_start_node_fn(wf, wm):
get_start_node=get_start_node_fn,
)
if work_flow_manage is None:
- # 恢复失败,回退到正常流程
work_flow_manage = WorkflowManage(
workflow, parameters, WorkflowType.APPLICATION, call_back, get_start_node_fn
)
else:
- # 正常创建新的 WorkflowManage
work_flow_manage = WorkflowManage(
workflow, parameters, WorkflowType.APPLICATION, call_back, get_start_node_fn
)
work_flow_manage.start_node.workflow_manage = work_flow_manage
-
- # 注册工作流实例到注册表
- WorkflowRunRegistry.register(chat_record_id_str, str(chat_info.chat_id), work_flow_manage)
-
- chat_info.set_chat(message)
+ WorkflowRunRegistry.register(chat_record_id_str, str(chat_id), work_flow_manage)
if stream:
@@ -681,27 +386,27 @@ def generate():
while True:
msg_type, data = result_queue.get()
if msg_type == "done":
+ end_frame = base_to_response.to_stream_end(
+ chat_id,
+ chat_record_id_str,
+ usage=self._usage_from_context(work_flow_manage.context),
+ )
+ if end_frame is not None:
+ yield "data: " + end_frame + "\n\n"
yield "data: [DONE]\n\n"
break
if msg_type == "error":
- yield (
- "data: "
- + json.dumps(
- {
- "chat_id": str(chat_info.chat_id),
- "chat_record_id": chat_record_id_str,
- "content": [{"type": "FAILURE", "content": str(data)}],
- },
- ensure_ascii=False,
- )
- + "\n\n"
- )
+ error_block = {"id": str(uuid.uuid7()), "type": "FAILURE", "content": str(data)}
+ frame = base_to_response.to_stream(chat_id, chat_record_id_str, error_block)
+ if frame is not None:
+ yield "data: " + frame + "\n\n"
yield "data: [DONE]\n\n"
break
if msg_type == "chunk":
- data["chat_id"] = str(chat_info.chat_id)
- data["chat_record_id"] = chat_record_id_str
- yield "data: " + json.dumps(data, ensure_ascii=False) + "\n\n"
+ # data 是裸内容块(content.to_dict()),格式化交给 base_to_response
+ frame = base_to_response.to_stream(chat_id, chat_record_id_str, data)
+ if frame is not None:
+ yield "data: " + frame + "\n\n"
return to_stream_response_simple(generate())
else:
@@ -712,163 +417,127 @@ def generate():
break
if msg_type == "error":
raise data
- return base_to_response.to_block_response(chat_info.chat_id, chat_record_id_str, "", True, 0, 0)
+ usage = self._usage_from_context(work_flow_manage.context)
+ return base_to_response.to_block(chat_id, chat_record_id_str, aggregation.get_contents(), usage)
- @staticmethod
- def save_chat_record(chat_info, chat_id, chat_record_id, question):
- chat_record = ChatRecord(
- id=chat_record_id,
- chat_id=chat_id,
- problem_text="",
- answer_text="",
- details={},
- message_tokens=0,
- answer_tokens=0,
- answer_text_list=[[]],
- run_time=0,
- index=len(chat_info.chat_record_list) + 1,
- ip_address=chat_info.ip_address,
- source=chat_info.source,
- workflow_context={},
- question=question,
- messages=[],
- )
- chat_info.append_chat_record(chat_record)
- chat_info.set_cache()
+ def chat(self, instance: dict, base_to_response: BaseToResponse = SystemToResponse()):
+ self.is_valid(raise_exception=True)
+ ChatMessageSerializers(data=instance).is_valid(raise_exception=True)
+ self.is_valid_chat()
+ application = self.get_application()
+ self.is_valid_intraday_access_num()
+ return self.chat_work_flow(application, instance, base_to_response)
+
+
+class OpenAIMessage(serializers.Serializer):
+ content = serializers.CharField(required=True, label=_("content"))
+ role = serializers.CharField(required=True, label=_("Role"))
+
+
+class OpenAIInstanceSerializer(serializers.Serializer):
+ messages = serializers.ListField(child=OpenAIMessage())
+ chat_id = serializers.UUIDField(required=False, label=_("Conversation ID"))
+ re_chat = serializers.BooleanField(required=False, label=_("Regenerate"))
+ stream = serializers.BooleanField(required=False, label=_("Streaming Output"))
+
+
+class OpenAIChatSerializer(serializers.Serializer):
+ """OpenAI 兼容入口:走新 ChatSerializers + OpenaiToResponse,无 ChatInfo/缓存。"""
+
+ application_id = serializers.UUIDField(required=True, label=_("Application ID"))
+ chat_user_id = serializers.CharField(required=True, label=_("Client id"))
+ chat_user_type = serializers.CharField(required=True, label=_("Client Type"))
+ ip_address = serializers.CharField(required=False, label=_("IP Address"))
+ source = serializers.JSONField(required=False, label=_("Source"))
@staticmethod
- def update_chat_record(chat_info, chat_id, chat_record_id, workflow_context, messages):
- message_tokens = sum(
- v.get("message_tokens", 0)
- for v in workflow_context.values()
- if isinstance(v, dict) and "message_tokens" in v
- )
- answer_tokens = sum(
- v.get("answer_tokens", 0) for v in workflow_context.values() if isinstance(v, dict) and "answer_tokens" in v
- )
- ChatUserTokenQuota.consume(chat_info.chat_user_id, message_tokens + answer_tokens)
- QuerySet(ChatRecord).filter(id=chat_record_id).update(
- workflow_context=workflow_context,
- messages=messages,
- message_tokens=message_tokens,
- answer_tokens=answer_tokens,
+ def get_message(instance):
+ return instance.get("messages")[-1].get("content")
+
+ def chat(self, instance: dict, with_valid=True):
+ if with_valid:
+ self.is_valid(raise_exception=True)
+ OpenAIInstanceSerializer(data=instance).is_valid(raise_exception=True)
+ # 会话不存在则新开:新 ChatSerializers 会按 chat_id 惰性建 Chat 行,无需缓存
+ chat_id = instance.get("chat_id") or str(uuid.uuid7())
+ message = self.get_message(instance)
+ return ChatSerializers(
+ data={
+ "chat_id": chat_id,
+ "chat_user_id": self.data.get("chat_user_id"),
+ "chat_user_type": self.data.get("chat_user_type"),
+ "application_id": self.data.get("application_id"),
+ "ip_address": self.data.get("ip_address"),
+ "source": self.data.get("source"),
+ }
+ ).chat(
+ {
+ "message": {
+ "content": message,
+ "image_list": instance.get("image_list", []),
+ "document_list": instance.get("document_list", []),
+ "audio_list": instance.get("audio_list", []),
+ "video_list": instance.get("video_list", []),
+ "other_list": instance.get("other_list", []),
+ },
+ "re_chat": instance.get("re_chat", False),
+ "stream": instance.get("stream", False),
+ "form_data": instance.get("form_data", {}),
+ },
+ base_to_response=OpenaiToResponse(),
)
- def is_valid_chat_user(self):
- chat_user_id = self.data.get("chat_user_id")
- application_id = self.data.get("application_id")
- chat_user_type = self.data.get("chat_user_type")
- is_auth_chat_user = DatabaseModelManage.get_model("is_auth_chat_user")
- application_access_token = QuerySet(ApplicationAccessToken).filter(application_id=application_id).first()
- if (
- application_access_token
- and application_access_token.authentication
- and application_access_token.authentication_value.get("type") == "login"
- ):
- if chat_user_type == ChatUserType.ANONYMOUS_USER.value:
- raise ChatException(500, _("The chat user is not authorized."))
- if chat_user_type == ChatUserType.CHAT_USER.value and is_auth_chat_user:
- is_auth = is_auth_chat_user(chat_user_id, application_id)
- if not is_auth:
- raise ChatException(500, _("The chat user is not authorized."))
- def chat(self, instance: dict, base_to_response: BaseToResponse = SystemToResponse()):
+# ==================== 会话创建 ====================
+
+
+class OpenChatSerializers(serializers.Serializer):
+ workspace_id = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_("Workspace ID"))
+ application_id = serializers.UUIDField(required=True)
+ chat_user_id = serializers.CharField(required=True, label=_("Client id"))
+ chat_user_type = serializers.CharField(required=True, label=_("Client Type"))
+ debug = serializers.BooleanField(required=True, label=_("Debug"))
+ ip_address = serializers.CharField(required=False, label=_("IP Address"))
+ source = serializers.JSONField(required=False, label=_("Source"))
+
+ def is_valid(self, *, raise_exception=False):
super().is_valid(raise_exception=True)
- ChatMessageSerializers(data=instance).is_valid(raise_exception=True)
- chat_info = self.get_chat_info()
- chat_info.get_application()
- chat_info.get_chat_user(asker=(instance.get("form_data") or {}).get("asker"))
- self.is_valid_chat_id(chat_info)
- if not self.data.get("debug"):
- self.is_valid_chat_user()
- ChatUserTokenQuota.consume(chat_info.chat_user_id, 0) # 触发周期重置 + 配额预校验
- if chat_info.application.type == ApplicationTypeChoices.SIMPLE:
- self.is_valid_application_simple(raise_exception=True, chat_info=chat_info)
- return self.chat_simple(chat_info, instance, base_to_response)
- else:
- self.is_valid_application_workflow(raise_exception=True)
- return self.chat_work_flow(chat_info, instance, base_to_response)
+ workspace_id = self.data.get("workspace_id")
+ application_id = self.data.get("application_id")
+ query_set = QuerySet(Application).filter(id=application_id)
+ if workspace_id:
+ query_set = query_set.filter(workspace_id=workspace_id)
+ if not query_set.exists():
+ raise AppApiException(500, _("Application does not exist"))
- def get_chat_info(self):
+ def open(self, chat_id=None):
+ """新建会话:直接建 Chat 行(cache-free,无 ChatInfo)。SIMPLE/WORK_FLOW 一视同仁。"""
self.is_valid(raise_exception=True)
- chat_id = self.data.get("chat_id")
- chat_info: ChatInfo = ChatInfo.get_cache(chat_id)
- if chat_info is None:
- chat_info: ChatInfo = self.re_open_chat(chat_id)
- chat_info.set_cache()
- return chat_info
-
- def re_open_chat(self, chat_id: str):
- chat = QuerySet(Chat).filter(id=chat_id).first()
- if chat is None:
- raise ChatException(500, _("Conversation does not exist"))
- application = QuerySet(Application).filter(id=chat.application_id).first()
- if application is None:
- raise ChatException(500, _("Application does not exist"))
- application_version = (
- QuerySet(ApplicationVersion).filter(application_id=application.id).order_by("-create_time")[0:1].first()
- )
- if application_version is None:
- raise ChatException(500, _("The application has not been published. Please use it after publishing."))
- if application.type == ApplicationTypeChoices.SIMPLE:
- return self.re_open_chat_simple(chat_id, application)
- else:
- return self.re_open_chat_work_flow(chat_id, application)
-
- def re_open_chat_simple(self, chat_id, application):
- if self.data.get("debug"):
- # 数据集id列表
- knowledge_id_list = [
- str(row.target_id)
- for row in QuerySet(ResourceMapping).filter(
- source_id=str(application.id), source_type="APPLICATION", target_type="KNOWLEDGE"
- )
- ]
- else:
- application_version = (
- QuerySet(ApplicationVersion).filter(application_id=application.id).order_by("-create_time")[0:1].first()
+ application_id = self.data.get("application_id")
+ debug = self.data.get("debug")
+ if not debug:
+ published = (
+ QuerySet(ApplicationVersion).filter(application_id=application_id).order_by("-create_time")[0:1].first()
)
- knowledge_id_list = application_version.knowledge_ids
-
- # 需要排除的文档
- exclude_document_id_list = [
- str(document.id)
- for document in QuerySet(Document).filter(knowledge_id__in=knowledge_id_list, is_active=False)
- ]
- chat_info = ChatInfo(
- chat_id,
- self.data.get("chat_user_id"),
- self.data.get("chat_user_type"),
- self.data.get("ip_address"),
- self.data.get("source"),
- knowledge_id_list,
- exclude_document_id_list,
- application.id,
- )
- chat_record_list = list(QuerySet(ChatRecord).filter(chat_id=chat_id).order_by("-create_time")[0:5])
- chat_record_list.sort(key=lambda r: r.create_time)
- for chat_record in chat_record_list:
- chat_info.chat_record_list.append(chat_record)
- return chat_info
-
- def re_open_chat_work_flow(self, chat_id, application):
- chat_info = ChatInfo(
- chat_id,
- self.data.get("chat_user_id"),
- self.data.get("chat_user_type"),
- self.data.get("ip_address"),
- self.data.get("source"),
- [],
- [],
- application.id,
- )
- chat_record_list = list(QuerySet(ChatRecord).filter(chat_id=chat_id).order_by("-create_time")[0:5])
- chat_record_list.sort(key=lambda r: r.create_time)
- for chat_record in chat_record_list:
- chat_info.chat_record_list.append(chat_record)
- return chat_info
+ if published is None:
+ raise AppApiException(500, _("The application has not been published. Please use it after publishing."))
+ chat_id = chat_id or str(uuid.uuid7())
+ Chat(
+ id=chat_id,
+ application_id=application_id,
+ abstract="新建对话",
+ execute_type=ExecuteType.DEBUG if debug else ExecuteType.CHAT,
+ chat_user_id=self.data.get("chat_user_id"),
+ chat_user_type=self.data.get("chat_user_type"),
+ ip_address=self.data.get("ip_address"),
+ source=self.data.get("source"),
+ asker=resolve_chat_user(self.data.get("chat_user_id"), self.data.get("chat_user_type")),
+ ).save()
+ return chat_id
+# ==================== 断点续传 ====================
+
# consume 桥接队列的上限:满了会反压 pump 线程,防止慢客户端把消息全堆进内存
_BRIDGE_MAXSIZE = 1000
# 消费上限(秒),与桥接 get 的超时保持一致的量级
@@ -881,9 +550,6 @@ class ResumeSerializers(serializers.Serializer):
def resume(self, request):
self.is_valid(raise_exception=True)
- from application.workflow.message_queue import get_message_queue
- from application.models import ChatRecord
-
chat_record_id = self.data.get("chat_record_id")
mq = get_message_queue()
@@ -1008,93 +674,94 @@ def _stream_from_db(self, chat_record, start_id: str):
yield "data: [DONE]\n\n"
-class OpenChatSerializers(serializers.Serializer):
- workspace_id = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_("Workspace ID"))
- application_id = serializers.UUIDField(required=True)
- chat_user_id = serializers.CharField(required=True, label=_("Client id"))
- chat_user_type = serializers.CharField(required=True, label=_("Client Type"))
- debug = serializers.BooleanField(required=True, label=_("Debug"))
- ip_address = serializers.CharField(required=False, label=_("IP Address"))
- source = serializers.JSONField(required=False, label=_("Source"))
+# ==================== 提示词生成 ====================
+
+SYSTEM_ROLE = get_file_content(os.path.join(PROJECT_DIR, "apps", "chat", "template", "generate_prompt_system"))
+
+
+class ChatMessagesSerializers(serializers.Serializer):
+ role = serializers.CharField(required=True, label=_("Role"))
+ content = serializers.CharField(required=True, label=_("Content"))
+
+
+class GeneratePromptSerializers(serializers.Serializer):
+ prompt = serializers.CharField(required=True, label=_("Prompt template"))
+ messages = serializers.ListSerializer(child=ChatMessagesSerializers(), required=True, label=_("Chat context"))
+
+ def is_valid(self, *, raise_exception=False):
+ super().is_valid(raise_exception=True)
+ messages = self.data.get("messages")
+
+ if len(messages) > 30:
+ raise AppApiException(400, _("Too many messages"))
+
+ for index in range(len(messages)):
+ role = messages[index].get("role")
+ if role == "ai" and index % 2 != 1:
+ raise AppApiException(400, _("Authentication failed. Please verify that the parameters are correct."))
+ if role == "user" and index % 2 != 0:
+ raise AppApiException(400, _("Authentication failed. Please verify that the parameters are correct."))
+ if role not in ["user", "ai"]:
+ raise AppApiException(400, _("Authentication failed. Please verify that the parameters are correct."))
+
+
+class PromptGenerateSerializer(serializers.Serializer):
+ workspace_id = serializers.CharField(required=False, label=_("Workspace ID"))
+ model_id = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("Model"))
+ application_id = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("Application"))
def is_valid(self, *, raise_exception=False):
super().is_valid(raise_exception=True)
workspace_id = self.data.get("workspace_id")
- application_id = self.data.get("application_id")
- query_set = QuerySet(Application).filter(id=application_id)
+ query_set = QuerySet(Application).filter(id=self.data.get("application_id"))
if workspace_id:
query_set = query_set.filter(workspace_id=workspace_id)
- if not query_set.exists():
- raise AppApiException(500, gettext("Application does not exist"))
+ application = query_set.first()
+ if application is None:
+ raise AppApiException(500, _("Application id does not exist"))
+ return application
- def open(self, chat_id=None):
- self.is_valid(raise_exception=True)
- application_id = self.data.get("application_id")
- application = QuerySet(Application).get(id=application_id)
- debug = self.data.get("debug")
- if not debug:
- application_version = (
- QuerySet(ApplicationVersion).filter(application_id=application_id).order_by("-create_time")[0:1].first()
- )
- if application_version is None:
- raise AppApiException(500, _("The application has not been published. Please use it after publishing."))
- if application.type == ApplicationTypeChoices.SIMPLE:
- return self.open_simple(application, chat_id)
- else:
- return self.open_work_flow(application, chat_id)
+ def generate_prompt(self, instance: dict):
+ application = self.is_valid(raise_exception=True)
+ GeneratePromptSerializers(data=instance).is_valid(raise_exception=True)
+ workspace_id = self.data.get("workspace_id")
+ model_id = self.data.get("model_id")
+ prompt = instance.get("prompt")
+ messages = instance.get("messages")
- def open_work_flow(self, application, chat_id=None):
- self.is_valid(raise_exception=True)
- application_id = self.data.get("application_id")
- chat_user_id = self.data.get("chat_user_id")
- chat_user_type = self.data.get("chat_user_type")
- ip_address = self.data.get("ip_address")
- source = self.data.get("source")
- debug = self.data.get("debug")
- chat_id = chat_id or str(uuid.uuid7())
- chat_info = ChatInfo(chat_id, chat_user_id, chat_user_type, ip_address, source, [], [], application_id, debug)
- chat_info.save_chat()
- chat_info.set_cache()
- return chat_id
+ message = messages[-1]["content"]
+ q = prompt.replace("{userInput}", message)
- def open_simple(self, application, chat_id=None):
- application_id = self.data.get("application_id")
- chat_user_id = self.data.get("chat_user_id")
- chat_user_type = self.data.get("chat_user_type")
- ip_address = self.data.get("ip_address")
- source = self.data.get("source")
- debug = self.data.get("debug")
- if debug:
- knowledge_id_list = [
- str(row.target_id)
- for row in QuerySet(ResourceMapping).filter(
- source_id=str(application_id), source_type="APPLICATION", target_type="KNOWLEDGE"
- )
- ]
- else:
- application_version = (
- QuerySet(ApplicationVersion).filter(application_id=application_id).order_by("-create_time")[0:1].first()
+ messages[-1]["content"] = q
+ SUPPORTED_MODEL_TYPES = ["LLM", "IMAGE"]
+ model_exist = QuerySet(Model).filter(id=model_id, model_type__in=SUPPORTED_MODEL_TYPES).exists()
+ if not model_exist:
+ raise Exception(_("Model does not exists or is not an LLM model"))
+
+ def process():
+ model = get_model_instance_by_model_workspace_id(
+ model_id=model_id, workspace_id=workspace_id, **application.model_params_setting
)
- knowledge_id_list = application_version.knowledge_ids
+ try:
+ for r in model.stream(
+ [
+ SystemMessage(content=SYSTEM_ROLE),
+ *[
+ HumanMessage(content=m.get("content"))
+ if m.get("role") == "user"
+ else AIMessage(content=m.get("content"))
+ for m in messages
+ ],
+ ]
+ ):
+ yield "data: " + json.dumps({"content": r.content}) + "\n\n"
+ except Exception as e:
+ yield "data: " + json.dumps({"error": str(e)}) + "\n\n"
- chat_id = chat_id or str(uuid.uuid7())
- chat_info = ChatInfo(
- chat_id,
- chat_user_id,
- chat_user_type,
- ip_address,
- source,
- knowledge_id_list,
- [
- str(document.id)
- for document in QuerySet(Document).filter(knowledge_id__in=knowledge_id_list, is_active=False)
- ],
- application_id,
- debug=debug,
- )
- chat_info.save_chat()
- chat_info.set_cache()
- return chat_id
+ return to_stream_response_simple(process())
+
+
+# ==================== 语音 ====================
class TextToSpeechSerializers(serializers.Serializer):
diff --git a/apps/chat/serializers/chat_history.py b/apps/chat/serializers/chat_history.py
new file mode 100644
index 00000000000..d8c4499238e
--- /dev/null
+++ b/apps/chat/serializers/chat_history.py
@@ -0,0 +1,90 @@
+# coding=utf-8
+"""
+@project: MaxKB
+@Author:虎虎
+@file: chat_history.py
+@date:2025/6/9 11:23
+@desc: 会话历史的滚动窗口缓存(Redis,跨 worker 共享)。
+
+- 历史是 append-only:每轮末尾追加一条已完成记录,旧记录不再变。
+- 只缓存最近 LIMIT 条,且只存历史真正要用的字段:question + messages
+ (新流程用 question/messages 构造 Human/AI message,不再用 problem_text/answer_text)。
+- 缓存缺失时回落 DB 并回填;记录定稿(on_complete)后 append/按 id upsert;清历史时失效。
+"""
+
+from django.core.cache import cache
+from django.db.models import QuerySet
+
+from application.models import ChatRecord
+from common.constants.cache_version import Cache_Version
+
+
+class ChatHistory:
+ # 最近多少条历史进上下文(注意:若节点 dialogue_number 超过该值会喂不够)
+ LIMIT = 5
+ TIMEOUT = 60 * 30
+
+ def __init__(self, chat_id):
+ self.chat_id = str(chat_id)
+
+ def _key(self):
+ return Cache_Version.CHAT_HISTORY.get_key(key=self.chat_id)
+
+ def _version(self):
+ return Cache_Version.CHAT_HISTORY.get_version()
+
+ @staticmethod
+ def _to_map(r):
+ return {
+ "id": str(r.id),
+ "chat_id": str(r.chat_id),
+ "question": r.question,
+ "messages": r.messages,
+ "create_time": r.create_time,
+ }
+
+ @staticmethod
+ def _from_map(d):
+ return ChatRecord(
+ id=d.get("id"),
+ chat_id=d.get("chat_id"),
+ question=d.get("question"),
+ messages=d.get("messages"),
+ create_time=d.get("create_time"),
+ )
+
+ def _load_from_db(self):
+ records = list(QuerySet(ChatRecord).filter(chat_id=self.chat_id).order_by("-create_time")[0 : self.LIMIT])
+ records.sort(key=lambda r: r.create_time)
+ return records
+
+ def load(self, exclude_record_id=None):
+ """
+ 读历史:命中缓存则还原,未命中从 DB 取最近 N 条并回填。
+ exclude_record_id:重答/Form 提交时把当前这条从历史上下文里剔掉。
+ """
+ cached = cache.get(self._key(), version=self._version())
+ if cached is None:
+ records = self._load_from_db()
+ cache.set(self._key(), [self._to_map(r) for r in records], version=self._version(), timeout=self.TIMEOUT)
+ else:
+ records = [self._from_map(d) for d in cached]
+ if exclude_record_id is not None:
+ records = [r for r in records if str(r.id) != str(exclude_record_id)]
+ return records
+
+ def append(self, chat_record):
+ """
+ 记录定稿后追加进缓存(按 create_time 天然排在最后)。
+ re_chat 复用同一 id → 先按 id 去重再追加,等价 upsert。
+ 未预热(缓存为空)则跳过,下次 load 会从 DB 重建。
+ """
+ cached = cache.get(self._key(), version=self._version())
+ if cached is None:
+ return
+ cached = [d for d in cached if str(d.get("id")) != str(chat_record.id)]
+ cached.append(self._to_map(chat_record))
+ cache.set(self._key(), cached[-self.LIMIT :], version=self._version(), timeout=self.TIMEOUT)
+
+ def clear(self):
+ cache.delete(self._key(), version=self._version())
diff --git a/apps/chat/template/agent_simple.py b/apps/chat/template/agent_simple.py
new file mode 100644
index 00000000000..45db39983e4
--- /dev/null
+++ b/apps/chat/template/agent_simple.py
@@ -0,0 +1,655 @@
+from django.db.models import QuerySet
+
+template = {
+ "edges": [
+ {
+ "id": "6a8d23d9-5179-424e-80c2-f08d37cdb8d4",
+ "type": "app-edge",
+ "endPoint": {"x": 2760, "y": 1054.125},
+ "pointsList": [
+ {"x": 2620, "y": 1054.125},
+ {"x": 2730, "y": 1054.125},
+ {"x": 2650, "y": 1054.125},
+ {"x": 2760, "y": 1054.125},
+ ],
+ "properties": {},
+ "startPoint": {"x": 2620, "y": 1054.125},
+ "sourceNodeId": "fd0324fc-f5e4-4fa6-a2d9-cb251b467605",
+ "targetNodeId": "420a6e4f-44ff-4847-bb81-0923630846b5",
+ "sourceAnchorId": "fd0324fc-f5e4-4fa6-a2d9-cb251b467605_right",
+ "targetAnchorId": "420a6e4f-44ff-4847-bb81-0923630846b5_left",
+ },
+ {
+ "id": "56006748-d9fe-491b-a14b-04fd568cac08",
+ "type": "app-edge",
+ "endPoint": {"x": 3610, "y": 149.25},
+ "pointsList": [
+ {"x": 3340, "y": 913.75},
+ {"x": 3450, "y": 913.75},
+ {"x": 3500, "y": 149.25},
+ {"x": 3610, "y": 149.25},
+ ],
+ "properties": {},
+ "startPoint": {"x": 3340, "y": 913.75},
+ "sourceNodeId": "420a6e4f-44ff-4847-bb81-0923630846b5",
+ "targetNodeId": "36a440a9-5b00-4d82-b13a-8e7819112918",
+ "sourceAnchorId": "420a6e4f-44ff-4847-bb81-0923630846b5_7887_right",
+ "targetAnchorId": "36a440a9-5b00-4d82-b13a-8e7819112918_left",
+ },
+ {
+ "id": "9bc8721b-07aa-4730-9347-910ed64e26b9",
+ "type": "app-edge",
+ "endPoint": {"x": 3610, "y": 1054.125},
+ "pointsList": [
+ {"x": 3340, "y": 1043.125},
+ {"x": 3450, "y": 1043.125},
+ {"x": 3500, "y": 1054.125},
+ {"x": 3610, "y": 1054.125},
+ ],
+ "properties": {},
+ "startPoint": {"x": 3340, "y": 1043.125},
+ "sourceNodeId": "420a6e4f-44ff-4847-bb81-0923630846b5",
+ "targetNodeId": "f7c3b4a2-cb80-4e47-b050-7fef0315daaf",
+ "sourceAnchorId": "420a6e4f-44ff-4847-bb81-0923630846b5_6847_right",
+ "targetAnchorId": "f7c3b4a2-cb80-4e47-b050-7fef0315daaf_left",
+ },
+ {
+ "id": "e4b4bb4e-35ed-40a4-b4e7-b86f77131d92",
+ "type": "app-edge",
+ "endPoint": {"x": 550, "y": 1054.125},
+ "pointsList": [
+ {"x": 280, "y": 1054.125},
+ {"x": 390, "y": 1054.125},
+ {"x": 440, "y": 1054.125},
+ {"x": 550, "y": 1054.125},
+ ],
+ "properties": {},
+ "startPoint": {"x": 280, "y": 1054.125},
+ "sourceNodeId": "start-node",
+ "targetNodeId": "b4dd9d45-25f0-4b01-9ec3-557a46a97d94",
+ "sourceAnchorId": "start-node_right",
+ "targetAnchorId": "b4dd9d45-25f0-4b01-9ec3-557a46a97d94_left",
+ },
+ {
+ "id": "0ea723ab-bebd-4058-98af-74b6c5f03260",
+ "type": "app-edge",
+ "endPoint": {"x": 1270, "y": 1054.125},
+ "pointsList": [
+ {"x": 1130, "y": 978.4375},
+ {"x": 1240, "y": 978.4375},
+ {"x": 1160, "y": 1054.125},
+ {"x": 1270, "y": 1054.125},
+ ],
+ "properties": {},
+ "startPoint": {"x": 1130, "y": 978.4375},
+ "sourceNodeId": "b4dd9d45-25f0-4b01-9ec3-557a46a97d94",
+ "targetNodeId": "a0089772-3821-474f-bb4f-9bfe32c1d95f",
+ "sourceAnchorId": "b4dd9d45-25f0-4b01-9ec3-557a46a97d94_gWldyeZ3CMPKS9teLWQeI_right",
+ "targetAnchorId": "a0089772-3821-474f-bb4f-9bfe32c1d95f_left",
+ },
+ {
+ "id": "c0c675d3-cb0b-4b67-8009-16951303791d",
+ "type": "app-edge",
+ "endPoint": {"x": 1730, "y": 1054.125},
+ "pointsList": [
+ {"x": 1130, "y": 1069.125},
+ {"x": 1240, "y": 1069.125},
+ {"x": 1620, "y": 1054.125},
+ {"x": 1730, "y": 1054.125},
+ ],
+ "properties": {},
+ "startPoint": {"x": 1130, "y": 1069.125},
+ "sourceNodeId": "b4dd9d45-25f0-4b01-9ec3-557a46a97d94",
+ "targetNodeId": "124fe8a0-70fa-42cb-b854-4b6c02ebb836",
+ "sourceAnchorId": "b4dd9d45-25f0-4b01-9ec3-557a46a97d94_TvdY3NQkSdYbC8A15VrId_right",
+ "targetAnchorId": "124fe8a0-70fa-42cb-b854-4b6c02ebb836_left",
+ },
+ {
+ "id": "0c1d5fc1-6ab2-431e-afdc-9f332ce8b466",
+ "type": "app-edge",
+ "endPoint": {"x": 1730, "y": 1054.125},
+ "pointsList": [
+ {"x": 1590, "y": 1054.125},
+ {"x": 1700, "y": 1054.125},
+ {"x": 1620, "y": 1054.125},
+ {"x": 1730, "y": 1054.125},
+ ],
+ "properties": {},
+ "startPoint": {"x": 1590, "y": 1054.125},
+ "sourceNodeId": "a0089772-3821-474f-bb4f-9bfe32c1d95f",
+ "targetNodeId": "124fe8a0-70fa-42cb-b854-4b6c02ebb836",
+ "sourceAnchorId": "a0089772-3821-474f-bb4f-9bfe32c1d95f_right",
+ "targetAnchorId": "124fe8a0-70fa-42cb-b854-4b6c02ebb836_left",
+ },
+ {
+ "id": "422564a4-2b0a-469b-be86-ded4204e7742",
+ "type": "app-edge",
+ "endPoint": {"x": 2300, "y": 1054.125},
+ "pointsList": [
+ {"x": 2160, "y": 1054.125},
+ {"x": 2270, "y": 1054.125},
+ {"x": 2190, "y": 1054.125},
+ {"x": 2300, "y": 1054.125},
+ ],
+ "properties": {},
+ "startPoint": {"x": 2160, "y": 1054.125},
+ "sourceNodeId": "124fe8a0-70fa-42cb-b854-4b6c02ebb836",
+ "targetNodeId": "fd0324fc-f5e4-4fa6-a2d9-cb251b467605",
+ "sourceAnchorId": "124fe8a0-70fa-42cb-b854-4b6c02ebb836_right",
+ "targetAnchorId": "fd0324fc-f5e4-4fa6-a2d9-cb251b467605_left",
+ },
+ {
+ "id": "a0cee2ac-4d0d-4b68-8cb2-ca2cb39993e9",
+ "type": "app-edge",
+ "endPoint": {"x": 3480, "y": 1973.375},
+ "pointsList": [
+ {"x": 3340, "y": 1133.8125},
+ {"x": 3450, "y": 1133.8125},
+ {"x": 3370, "y": 1973.375},
+ {"x": 3480, "y": 1973.375},
+ ],
+ "properties": {},
+ "startPoint": {"x": 3340, "y": 1133.8125},
+ "sourceNodeId": "420a6e4f-44ff-4847-bb81-0923630846b5",
+ "targetNodeId": "f9ae6300-5b07-4244-9b88-2a5e7329e1d4",
+ "sourceAnchorId": "420a6e4f-44ff-4847-bb81-0923630846b5_2794_right",
+ "targetAnchorId": "f9ae6300-5b07-4244-9b88-2a5e7329e1d4_left",
+ },
+ {
+ "id": "cd66759a-bcb9-4d61-806b-7bde23ae4582",
+ "type": "app-edge",
+ "endPoint": {"x": 4200, "y": 1001.5},
+ "pointsList": [
+ {"x": 4060, "y": 1897.6875},
+ {"x": 4170, "y": 1897.6875},
+ {"x": 4090, "y": 1001.5},
+ {"x": 4200, "y": 1001.5},
+ ],
+ "properties": {},
+ "startPoint": {"x": 4060, "y": 1897.6875},
+ "sourceNodeId": "f9ae6300-5b07-4244-9b88-2a5e7329e1d4",
+ "targetNodeId": "dd02a0d8-0ea1-41c4-8b64-0cb7d8963fd9",
+ "sourceAnchorId": "f9ae6300-5b07-4244-9b88-2a5e7329e1d4_Iu8b0BMQU9xXWy5JbcTnz_right",
+ "targetAnchorId": "dd02a0d8-0ea1-41c4-8b64-0cb7d8963fd9_left",
+ },
+ {
+ "id": "7113c5b7-d9d6-4f49-a030-24eaeee00e7d",
+ "type": "app-edge",
+ "endPoint": {"x": 4200, "y": 1973.375},
+ "pointsList": [
+ {"x": 4060, "y": 1988.375},
+ {"x": 4170, "y": 1988.375},
+ {"x": 4090, "y": 1973.375},
+ {"x": 4200, "y": 1973.375},
+ ],
+ "properties": {},
+ "startPoint": {"x": 4060, "y": 1988.375},
+ "sourceNodeId": "f9ae6300-5b07-4244-9b88-2a5e7329e1d4",
+ "targetNodeId": "04dd6c1e-95f9-4757-bb3e-134d503fce54",
+ "sourceAnchorId": "f9ae6300-5b07-4244-9b88-2a5e7329e1d4_s-groW06vt6a7B-aqDqnX_right",
+ "targetAnchorId": "04dd6c1e-95f9-4757-bb3e-134d503fce54_left",
+ },
+ ],
+ "nodes": [
+ {
+ "x": 120,
+ "y": 120,
+ "id": "base-node",
+ "type": "base-node",
+ "properties": {
+ "config": {},
+ "height": 984.25,
+ "showNode": True,
+ "stepName": "基本信息",
+ "node_data": {
+ "desc": "www",
+ "name": "www",
+ "prologue": "您好,我是 XXX 小助手,您可以向我提出 XXX 使用问题。\n- XXX 主要功能有什么?\n- XXX 如何收费?\n- 需要转人工服务",
+ "tts_type": "BROWSER",
+ "stt_model_id_type": "default",
+ "long_term_model_id_type": "default",
+ },
+ "enableException": False,
+ "input_field_list": [],
+ "user_input_config": {"title": "用户输入"},
+ "api_input_field_list": [],
+ "chat_input_field_list": [],
+ "user_input_field_list": [
+ {
+ "attrs": {},
+ "field": "problem_optimization",
+ "label": {
+ "attrs": {"tooltip": "是否需要问题优化"},
+ "label": "问题优化",
+ "input_type": "TooltipLabel",
+ "props_info": {},
+ },
+ "required": True,
+ "input_type": "SwitchInput",
+ "default_value": False,
+ "visibility_rules": {
+ "action": "show",
+ "node_id": "base-node",
+ "condition": "and",
+ "node_name": "基本信息",
+ "conditions": [],
+ },
+ "show_default_value": True,
+ },
+ {
+ "attrs": {},
+ "field": "ai_questioning",
+ "label": {
+ "attrs": {"tooltip": "是否ai回复"},
+ "label": "是否ai回复",
+ "input_type": "TooltipLabel",
+ "props_info": {},
+ },
+ "required": True,
+ "input_type": "SwitchInput",
+ "default_value": False,
+ "visibility_rules": {
+ "action": "show",
+ "node_id": "base-node",
+ "condition": "and",
+ "node_name": "基本信息",
+ "conditions": [],
+ },
+ "show_default_value": True,
+ },
+ ],
+ },
+ },
+ {
+ "x": 120,
+ "y": 1054.125,
+ "id": "start-node",
+ "type": "start-node",
+ "properties": {
+ "config": {
+ "fields": [{"label": "用户问题", "value": "question"}],
+ "chatFields": [],
+ "globalFields": [
+ {"label": "当前时间", "value": "time"},
+ {"label": "历史聊天记录", "value": "history_context"},
+ {"label": "对话 ID", "value": "chat_id"},
+ {"label": "对话用户 ID", "value": "chat_user_id"},
+ {"label": "对话用户类型", "value": "chat_user_type"},
+ {"label": "对话用户组", "value": "chat_user_group"},
+ {"label": "对话用户", "value": "chat_user"},
+ {"label": "问题优化", "value": "problem_optimization"},
+ {"label": "是否ai回复", "value": "ai_questioning"},
+ ],
+ },
+ "fields": [{"label": "用户问题", "value": "question"}],
+ "height": 644,
+ "showNode": True,
+ "stepName": "开始",
+ "globalFields": [{"label": "当前时间", "value": "time"}],
+ "enableException": False,
+ },
+ },
+ {
+ "x": 2460,
+ "y": 1054.125,
+ "id": "fd0324fc-f5e4-4fa6-a2d9-cb251b467605",
+ "type": "search-knowledge-node",
+ "properties": {
+ "config": {
+ "fields": [
+ {"label": "检索结果的分段列表", "value": "paragraph_list"},
+ {"label": "满足直接回答的分段列表", "value": "is_hit_handling_method_list"},
+ {"label": "检索结果", "value": "data"},
+ {"label": "满足直接回答的分段内容", "value": "directly_return"},
+ ]
+ },
+ "height": 806.375,
+ "showNode": True,
+ "stepName": "知识库检索",
+ "condition": "AND",
+ "node_data": {
+ "knowledge_list": [],
+ "show_knowledge": True,
+ "knowledge_id_list": [],
+ "knowledge_setting": {
+ "top_n": 3,
+ "similarity": 0.6,
+ "search_mode": "embedding",
+ "max_paragraph_char_number": 5000,
+ },
+ "search_scope_type": "custom",
+ "search_scope_source": "knowledge",
+ "all_knowledge_id_list": [],
+ "question_reference_address": ["124fe8a0-70fa-42cb-b854-4b6c02ebb836", "Group1"],
+ "no_permission_knowledge_id_list": [],
+ },
+ "enableException": False,
+ },
+ },
+ {
+ "x": 3050,
+ "y": 1054.125,
+ "id": "420a6e4f-44ff-4847-bb81-0923630846b5",
+ "type": "condition-node",
+ "properties": {
+ "width": 600,
+ "config": {"fields": [{"label": "分支名称", "value": "branch_name"}]},
+ "height": 552.125,
+ "showNode": True,
+ "stepName": "判断器",
+ "condition": "AND",
+ "node_data": {
+ "branch": [
+ {
+ "id": "7887",
+ "type": "IF",
+ "condition": "and",
+ "conditions": [
+ {
+ "field": ["fd0324fc-f5e4-4fa6-a2d9-cb251b467605", "is_hit_handling_method_list"],
+ "value": 1,
+ "compare": "is_not_None",
+ }
+ ],
+ },
+ {
+ "id": "6847",
+ "type": "ELSE IF 1",
+ "condition": "and",
+ "conditions": [
+ {
+ "field": ["fd0324fc-f5e4-4fa6-a2d9-cb251b467605", "paragraph_list"],
+ "value": 1,
+ "compare": "is_not_None",
+ }
+ ],
+ },
+ {"id": "2794", "type": "ELSE", "condition": "and", "conditions": []},
+ ]
+ },
+ "enableException": False,
+ "branch_condition_list": [
+ {"id": "7887", "index": 0, "height": 121.375},
+ {"id": "6847", "index": 1, "height": 121.375},
+ {"id": "2794", "index": 2, "height": 44},
+ ],
+ },
+ },
+ {
+ "x": 3770,
+ "y": 149.25,
+ "id": "36a440a9-5b00-4d82-b13a-8e7819112918",
+ "type": "reply-node",
+ "properties": {
+ "config": {"fields": [{"label": "内容", "value": "answer"}]},
+ "height": 394,
+ "showNode": True,
+ "stepName": "指定回复",
+ "condition": "AND",
+ "node_data": {
+ "fields": ["fd0324fc-f5e4-4fa6-a2d9-cb251b467605", "directly_return"],
+ "content": "",
+ "is_result": True,
+ "reply_type": "referencing",
+ },
+ "enableException": False,
+ },
+ },
+ {
+ "x": 3770,
+ "y": 1054.125,
+ "id": "f7c3b4a2-cb80-4e47-b050-7fef0315daaf",
+ "type": "ai-chat-node",
+ "properties": {
+ "config": {
+ "fields": [
+ {"label": "AI 回答内容", "value": "answer"},
+ {"label": "思考过程", "value": "reasoning_content"},
+ {"label": "历史聊天记录", "value": "history_message"},
+ ]
+ },
+ "height": 1175.75,
+ "showNode": True,
+ "stepName": "AI 对话",
+ "condition": "AND",
+ "node_data": {
+ "prompt": "已知信息:\n{{知识库检索.data}}\n问题:\n{{开始.question}}",
+ "system": "",
+ "model_id": "",
+ "is_result": True,
+ "max_tokens": None,
+ "temperature": None,
+ "dialogue_type": "WORKFLOW",
+ "model_id_type": "custom",
+ "model_setting": {
+ "reasoning_content_end": "",
+ "reasoning_content_start": "",
+ "reasoning_content_enable": False,
+ },
+ "dialogue_number": 1,
+ "mcp_output_enable": True,
+ "model_id_reference": [],
+ },
+ "enableException": False,
+ },
+ },
+ {
+ "x": 4360,
+ "y": 1973.375,
+ "id": "04dd6c1e-95f9-4757-bb3e-134d503fce54",
+ "type": "reply-node",
+ "properties": {
+ "config": {"fields": [{"label": "内容", "value": "answer"}]},
+ "height": 512,
+ "showNode": True,
+ "stepName": "指定回复1",
+ "condition": "AND",
+ "node_data": {
+ "fields": [],
+ "content": "抱歉,没有在知识库查询到相关内容,请提供更详细的信息。",
+ "is_result": True,
+ "reply_type": "content",
+ },
+ "enableException": False,
+ },
+ },
+ {
+ "x": 840,
+ "y": 1054.125,
+ "id": "b4dd9d45-25f0-4b01-9ec3-557a46a97d94",
+ "type": "condition-node",
+ "properties": {
+ "width": 600,
+ "config": {"fields": [{"label": "分支名称", "value": "branch_name"}]},
+ "height": 422.75,
+ "showNode": True,
+ "stepName": "判断器1",
+ "condition": "AND",
+ "node_data": {
+ "branch": [
+ {
+ "id": "gWldyeZ3CMPKS9teLWQeI",
+ "type": "IF",
+ "condition": "and",
+ "conditions": [
+ {"field": ["global", "problem_optimization"], "value": 1, "compare": "is_True"}
+ ],
+ },
+ {"id": "TvdY3NQkSdYbC8A15VrId", "type": "ELSE", "condition": "and", "conditions": []},
+ ]
+ },
+ "enableException": False,
+ "branch_condition_list": [
+ {"id": "gWldyeZ3CMPKS9teLWQeI", "index": 0, "height": 121.375},
+ {"id": "TvdY3NQkSdYbC8A15VrId", "index": 1, "height": 44},
+ ],
+ },
+ },
+ {
+ "x": 1430,
+ "y": 1054.125,
+ "id": "a0089772-3821-474f-bb4f-9bfe32c1d95f",
+ "type": "question-node",
+ "properties": {
+ "config": {"fields": [{"label": "问题优化结果", "value": "answer"}]},
+ "height": 842,
+ "showNode": True,
+ "stepName": "问题优化",
+ "condition": "AND",
+ "node_data": {
+ "prompt": "{{开始.question}}",
+ "system": "# 角色\n你是一位问题优化大师,擅长根据上下文精准揣测用户意图,并对用户提出的问题进行优化。\n\n## 技能\n### 技能 1: 优化问题\n2. 接收用户输入的问题。\n3. 依据上下文仔细分析问题含义。\n4. 输出优化后的问题。\n\n## 限制:\n- 仅返回优化后的问题,不进行额外解释或说明。\n- 确保优化后的问题准确反映原始问题意图,不得改变原意。",
+ "model_id": "",
+ "is_result": False,
+ "model_id_type": "default",
+ "dialogue_number": 0,
+ "model_id_reference": [],
+ },
+ "enableException": False,
+ },
+ },
+ {
+ "x": 1945,
+ "y": 1054.125,
+ "id": "124fe8a0-70fa-42cb-b854-4b6c02ebb836",
+ "type": "variable-aggregation-node",
+ "properties": {
+ "config": {"fields": [{"label": "Group1", "value": "Group1"}]},
+ "height": 530.75,
+ "showNode": True,
+ "stepName": "变量聚合",
+ "condition": "AND",
+ "node_data": {
+ "strategy": "first_non_None",
+ "is_result": True,
+ "group_list": [
+ {
+ "id": "A5aBuBrQJ5hq12mKSJNiQ",
+ "field": "Group1",
+ "label": "Group1",
+ "variable_list": [
+ {
+ "v_id": "0bmeMSbo9696jwbfp3jDX",
+ "variable": ["a0089772-3821-474f-bb4f-9bfe32c1d95f", "answer"],
+ },
+ {"v_id": "1YHRj-fr3_IQpELv_HAdC", "variable": ["start-node", "question"]},
+ ],
+ }
+ ],
+ },
+ "enableException": False,
+ },
+ },
+ {
+ "x": 4360,
+ "y": 1001.5,
+ "id": "dd02a0d8-0ea1-41c4-8b64-0cb7d8963fd9",
+ "type": "ai-chat-node",
+ "properties": {
+ "config": {
+ "fields": [
+ {"label": "AI 回答内容", "value": "answer"},
+ {"label": "思考过程", "value": "reasoning_content"},
+ {"label": "历史聊天记录", "value": "history_message"},
+ ]
+ },
+ "height": 1191.75,
+ "showNode": True,
+ "stepName": "AI 对话1",
+ "condition": "AND",
+ "node_data": {
+ "prompt": "{{开始.question}}",
+ "system": "",
+ "model_id": "",
+ "is_result": True,
+ "max_tokens": None,
+ "temperature": None,
+ "dialogue_type": "WORKFLOW",
+ "model_id_type": "custom",
+ "model_setting": {
+ "reasoning_content_end": "",
+ "reasoning_content_start": "",
+ "reasoning_content_enable": False,
+ },
+ "dialogue_number": 0,
+ "mcp_output_enable": True,
+ "model_id_reference": [],
+ },
+ "enableException": False,
+ },
+ },
+ {
+ "x": 3770,
+ "y": 1973.375,
+ "id": "f9ae6300-5b07-4244-9b88-2a5e7329e1d4",
+ "type": "condition-node",
+ "properties": {
+ "width": 600,
+ "config": {"fields": [{"label": "分支名称", "value": "branch_name"}]},
+ "height": 422.75,
+ "showNode": True,
+ "stepName": "判断器2",
+ "condition": "AND",
+ "node_data": {
+ "branch": [
+ {
+ "id": "Iu8b0BMQU9xXWy5JbcTnz",
+ "type": "IF",
+ "condition": "and",
+ "conditions": [{"field": ["global", "ai_questioning"], "value": 1, "compare": "is_True"}],
+ },
+ {"id": "s-groW06vt6a7B-aqDqnX", "type": "ELSE", "condition": "and", "conditions": []},
+ ]
+ },
+ "enableException": False,
+ "branch_condition_list": [
+ {"id": "Iu8b0BMQU9xXWy5JbcTnz", "index": 0, "height": 121.375},
+ {"id": "s-groW06vt6a7B-aqDqnX", "index": 1, "height": 44},
+ ],
+ },
+ },
+ ],
+}
+
+
+def build_workflow(application):
+ from system_manage.models.resource_mapping import ResourceMapping, ResourceType
+
+ data = template.copy()
+ if application.knowledge_ids:
+ knowledge_ids = application.knowledge_ids
+ else:
+ knowledge_ids = (
+ QuerySet(ResourceMapping)
+ .filter(source_type=ResourceType.APPLICATION, source_id=application.id, target_type=ResourceType.KNOWLEDGE)
+ .values_list("target_id", flat=True)
+ )
+
+ data["nodes"][0]["properties"]["user_input_field_list"][0]["default_value"] = application.problem_optimization
+
+ data["nodes"][0]["properties"]["user_input_field_list"][1]["default_value"] = (
+ application.knowledge_setting.no_references_setting.status == "ai_questioning"
+ )
+ model_id = application.model
+ model_params_setting = application.model_params_setting or {}
+ ## 问题优化设置
+ data["nodes"][8]["properties"]["node_data"]["model_id"] = model_id
+ data["nodes"][8]["properties"]["node_data"]["prompt"] = application.problem_optimization_prompt.replace(
+ "{question}", "{{开始.question}}"
+ )
+ data["nodes"][8]["properties"]["node_data"]["model_params_setting"] = model_params_setting
+ ## 知识库检索
+ data["nodes"][2]["properties"]["node_data"]["knowledge_id_list"] = knowledge_ids
+ data["nodes"][2]["properties"]["node_data"]["knowledge_setting"] = application.knowledge_setting
+ ## ai对话
+ data["nodes"][5]["properties"]["node_data"]["model_id"] = model_id
+ data["nodes"][5]["properties"]["node_data"]["model_params_setting"] = model_params_setting
+ data["nodes"][5]["properties"]["node_data"]["prompt"] = application.model_setting.prompt
+ ## 未查询到知识库ai 回复
+ data["nodes"][10]["properties"]["node_data"]["model_id"] = model_id
+ data["nodes"][10]["properties"]["node_data"]["model_params_setting"] = model_params_setting
+ ## 未查询到知识库指定回复
+ if application.knowledge_setting.no_references_setting.status == "designated_answer":
+ data["nodes"][6]["properties"]["node_data"]["content"] = application.knowledge_setting.value
+
+ return data
diff --git a/apps/chat/views/v2/chat.py b/apps/chat/views/v2/chat.py
index 8f4816d33cf..a816df8a9e1 100644
--- a/apps/chat/views/v2/chat.py
+++ b/apps/chat/views/v2/chat.py
@@ -1,11 +1,12 @@
# coding=utf-8
"""
- @project: MaxKB
- @Author:虎虎
- @file: chat.py
- @date:2025/6/6 11:18
- @desc:
+@project: MaxKB
+@Author:虎虎
+@file: chat.py
+@date:2025/6/6 11:18
+@desc:
"""
+
import json
import requests
@@ -22,11 +23,24 @@
from application.api.application_api import SpeechToTextAPI, TextToSpeechAPI
from application.models import ChatUserType, ChatSourceChoices
from chat.api.chat_api import ChatAPI
-from chat.api.chat_authentication_api import ChatAuthenticationAPI, ChatAuthenticationProfileAPIV2, ChatOpenAPI, OpenAIAPI
-from chat.serializers.chat import OpenChatSerializers, ChatSerializers, SpeechToTextSerializers, \
- TextToSpeechSerializers, OpenAIChatSerializer
-from chat.serializers.chat_authentication import AnonymousAuthenticationV2Serializer, ApplicationProfileSerializer, \
- AuthProfileV2Serializer
+from chat.api.chat_authentication_api import (
+ ChatAuthenticationAPI,
+ ChatAuthenticationProfileAPIV2,
+ ChatOpenAPI,
+ OpenAIAPI,
+)
+from chat.serializers.chat import (
+ ChatSerializers,
+ OpenAIChatSerializer,
+ OpenChatSerializers,
+ SpeechToTextSerializers,
+ TextToSpeechSerializers,
+)
+from chat.serializers.chat_authentication import (
+ AnonymousAuthenticationV2Serializer,
+ ApplicationProfileSerializer,
+ AuthProfileV2Serializer,
+)
from common.auth import ChatTokenAuth
from common.auth.authentication import has_permissions
from common.auth.common import FileToken
@@ -63,19 +77,18 @@ def get(self, request: Request):
if not image_url:
return result.error("Missing 'url' parameter")
try:
-
# 发送GET请求,流式获取图片内容
response = requests.get(
image_url,
stream=True, # 启用流式响应
allow_redirects=True,
- timeout=10
+ timeout=10,
)
- content_type = response.headers.get('Content-Type', '').split(';')[0]
+ content_type = response.headers.get("Content-Type", "").split(";")[0]
# 创建Django流式响应
django_response = StreamingHttpResponse(
stream_image(response), # 使用生成器
- content_type=content_type
+ content_type=content_type,
)
return django_response
@@ -87,49 +100,59 @@ class OpenAIView(APIView):
authentication_classes = [ChatTokenAuth]
@extend_schema(
- methods=['POST'],
- description=_('OpenAI Interface Dialogue'),
- summary=_('OpenAI Interface Dialogue'),
- operation_id=_('OpenAI Interface Dialogue'), # type: ignore
+ methods=["POST"],
+ description=_("OpenAI Interface Dialogue"),
+ summary=_("OpenAI Interface Dialogue"),
+ operation_id=_("OpenAI Interface Dialogue"), # type: ignore
request=OpenAIAPI.get_request(),
responses=None,
- tags=[_('Chat')] # type: ignore
+ tags=[_("Chat")], # type: ignore
)
def post(self, request: Request, application_id: str):
ip_address = _get_ip_address(request)
- if application_id != str(request.user.kwargs.get('application_id')):
- raise AppAuthenticationFailed(500, _('Secret key is invalid'))
+ if application_id != str(request.user.kwargs.get("application_id")):
+ raise AppAuthenticationFailed(500, _("Secret key is invalid"))
return OpenAIChatSerializer(
- data={'application_id': application_id, 'chat_user_id': request.user.id,
- 'chat_user_type': request.user.type,
- 'ip_address': ip_address,
- 'source': {"type": ChatSourceChoices.API_CALL.value}}).chat(request.data)
+ data={
+ "application_id": application_id,
+ "chat_user_id": request.user.id,
+ "chat_user_type": request.user.type,
+ "ip_address": ip_address,
+ "source": {"type": ChatSourceChoices.API_CALL.value},
+ }
+ ).chat(request.data)
class AnonymousAuthentication(APIView):
def options(self, request, *args, **kwargs):
return HttpResponse(
- headers={"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Credentials": "true",
- "Access-Control-Allow-Methods": "POST",
- "Access-Control-Allow-Headers": "Origin,Content-Type,Cookie,Accept,Token"}, )
+ headers={
+ "Access-Control-Allow-Origin": "*",
+ "Access-Control-Allow-Credentials": "true",
+ "Access-Control-Allow-Methods": "POST",
+ "Access-Control-Allow-Headers": "Origin,Content-Type,Cookie,Accept,Token",
+ },
+ )
@extend_schema(
- methods=['POST'],
- description=_('Application Anonymous Certification'),
- summary=_('Application Anonymous Certification'),
- operation_id=_('Application Anonymous Certification'), # type: ignore
+ methods=["POST"],
+ description=_("Application Anonymous Certification"),
+ summary=_("Application Anonymous Certification"),
+ operation_id=_("Application Anonymous Certification"), # type: ignore
request=AnonymousAuthenticationV2Serializer,
responses=None,
- tags=[_('Chat')] # type: ignore
+ tags=[_("Chat")], # type: ignore
)
def post(self, request: Request):
- token, f_token = AnonymousAuthenticationV2Serializer(data=request.data).auth(
- request)
+ token, f_token = AnonymousAuthenticationV2Serializer(data=request.data).auth(request)
response = result.success(
token,
- headers={"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Credentials": "true",
- "Access-Control-Allow-Methods": "POST",
- "Access-Control-Allow-Headers": "Origin,Content-Type,Cookie,Accept,Token"}
+ headers={
+ "Access-Control-Allow-Origin": "*",
+ "Access-Control-Allow-Credentials": "true",
+ "Access-Control-Allow-Methods": "POST",
+ "Access-Control-Allow-Headers": "Origin,Content-Type,Cookie,Accept,Token",
+ },
)
is_https = request.scheme == "https"
@@ -137,7 +160,7 @@ def post(self, request: Request):
key="mk_file_auth",
value=f_token,
max_age=7 * 24 * 3600,
- path=f'{CONFIG.get_chat_path()}/{request.data.get("access_token")}',
+ path=f"{CONFIG.get_chat_path()}/{request.data.get('access_token')}",
secure=is_https,
httponly=True,
samesite="None" if is_https else "Lax",
@@ -149,160 +172,185 @@ class ApplicationProfile(APIView):
authentication_classes = [ChatTokenAuth]
@extend_schema(
- methods=['GET'],
+ methods=["GET"],
description=_("Get application related information"),
summary=_("Get application related information"),
operation_id=_("Get application related information"), # type: ignore
request=None,
responses=None,
- tags=[_('Chat')] # type: ignore
+ tags=[_("Chat")], # type: ignore
)
def get(self, request: Request):
- return result.success(ApplicationProfileSerializer(
- data={'application_id': request.user.kwargs.get('application_id')}).profile())
+ return result.success(
+ ApplicationProfileSerializer(data={"application_id": request.user.kwargs.get("application_id")}).profile()
+ )
class AuthProfile(APIView):
@extend_schema(
- methods=['GET'],
+ methods=["GET"],
description=_("Get application authentication information"),
summary=_("Get application authentication information"),
operation_id=_("Get application authentication information"), # type: ignore
parameters=ChatAuthenticationProfileAPIV2.get_parameters(),
responses=None,
- tags=[_('Chat')] # type: ignore
+ tags=[_("Chat")], # type: ignore
)
def get(self, request: Request):
return result.success(
- AuthProfileV2Serializer(data={'access_token': request.query_params.get("access_token")}).profile())
+ AuthProfileV2Serializer(data={"access_token": request.query_params.get("access_token")}).profile()
+ )
class ChatView(APIView):
authentication_classes = [ChatTokenAuth]
@extend_schema(
- methods=['POST'],
+ methods=["POST"],
description=_("dialogue"),
summary=_("dialogue"),
operation_id=_("dialogue"), # type: ignore
request=ChatAPI.get_request(),
parameters=ChatAPI.get_parameters(),
responses=None,
- tags=[_('Chat')] # type: ignore
+ tags=[_("Chat")], # type: ignore
)
def post(self, request: Request, chat_id: str):
ip_address = _get_ip_address(request)
- return ChatSerializers(data={'chat_id': chat_id,
- 'chat_user_id': request.user.id,
- 'chat_user_type': request.user.type,
- 'application_id': request.user.kwargs.get('application_id'),
- 'debug': False,
- 'ip_address': ip_address,
- 'source': {
- 'type': ChatSourceChoices.API_CALL.value if request.user.type == ChatUserType.APPLICATION_API_KEY.value else ChatSourceChoices.ONLINE.value}
- }
- ).chat(request.data)
+ return ChatSerializers(
+ data={
+ "chat_id": chat_id,
+ "chat_user_id": request.user.id,
+ "chat_user_type": request.user.type,
+ "application_id": request.user.kwargs.get("application_id"),
+ "debug": False,
+ "ip_address": ip_address,
+ "source": {
+ "type": ChatSourceChoices.API_CALL.value
+ if request.user.type == ChatUserType.APPLICATION_API_KEY.value
+ else ChatSourceChoices.ONLINE.value
+ },
+ }
+ ).chat(request.data)
class OpenView(APIView):
authentication_classes = [ChatTokenAuth]
@extend_schema(
- methods=['GET'],
+ methods=["GET"],
description=_("Get the session id according to the application id"),
summary=_("Get the session id according to the application id"),
operation_id=_("Get the session id according to the application id"), # type: ignore
parameters=ChatOpenAPI.get_parameters(),
responses=None,
- tags=[_('Chat')] # type: ignore
+ tags=[_("Chat")], # type: ignore
)
@has_permissions(ChatPermissionConstants.get_aggregate_permissions())
def get(self, request: Request):
ip_address = _get_ip_address(request)
- return result.success(OpenChatSerializers(
- data={'application_id': request.user.kwargs.get('application_id'),
- 'chat_user_id': request.user.id, 'chat_user_type': request.user.type,
- 'ip_address': ip_address,
- 'source': {
- 'type': ChatSourceChoices.API_CALL.value if request.user.type == ChatUserType.APPLICATION_API_KEY.value else ChatSourceChoices.ONLINE.value},
- 'debug': False}).open())
+ return result.success(
+ OpenChatSerializers(
+ data={
+ "application_id": request.user.kwargs.get("application_id"),
+ "chat_user_id": request.user.id,
+ "chat_user_type": request.user.type,
+ "ip_address": ip_address,
+ "source": {
+ "type": ChatSourceChoices.API_CALL.value
+ if request.user.type == ChatUserType.APPLICATION_API_KEY.value
+ else ChatSourceChoices.ONLINE.value
+ },
+ "debug": False,
+ }
+ ).open()
+ )
class CancelWorkflowView(APIView):
authentication_classes = [ChatTokenAuth]
@extend_schema(
- methods=['POST'],
+ methods=["POST"],
description=_("Cancel running workflow"),
summary=_("Cancel running workflow"),
operation_id=_("Cancel running workflow"), # type: ignore
parameters=[
- OpenApiParameter(name='chat_id', type=OpenApiTypes.UUID, location=OpenApiParameter.PATH,
- description=_('Chat ID')),
+ OpenApiParameter(
+ name="chat_id", type=OpenApiTypes.UUID, location=OpenApiParameter.PATH, description=_("Chat ID")
+ ),
],
responses=None,
- tags=[_('Chat')] # type: ignore
+ tags=[_("Chat")], # type: ignore
)
def post(self, request: Request, chat_id: str):
from application.workflow.workflow_run_registry import WorkflowRunRegistry, CancelResult
+
result_enum = WorkflowRunRegistry.cancel_by_chat_id(chat_id)
if result_enum == CancelResult.CANCELLED:
- return result.success({'status': 'cancelled', 'chat_id': chat_id})
+ return result.success({"status": "cancelled", "chat_id": chat_id})
elif result_enum == CancelResult.NOT_FOUND:
- return result.success({'status': 'not_found', 'chat_id': chat_id})
+ return result.success({"status": "not_found", "chat_id": chat_id})
else:
- return result.fail(500, _('Failed to cancel workflow'))
+ return result.fail(500, _("Failed to cancel workflow"))
class CaptchaView(APIView):
- @extend_schema(methods=['GET'],
- summary=_("Get Chat captcha"),
- description=_("Get Chat captcha"),
- operation_id=_("Get Chat captcha"), # type: ignore
- tags=[_("Chat")], # type: ignore
- responses=CaptchaAPI.get_response())
+ @extend_schema(
+ methods=["GET"],
+ summary=_("Get Chat captcha"),
+ description=_("Get Chat captcha"),
+ operation_id=_("Get Chat captcha"), # type: ignore
+ tags=[_("Chat")], # type: ignore
+ responses=CaptchaAPI.get_response(),
+ )
def get(self, request: Request):
- username = request.query_params.get('username', None)
- accessToken = request.query_params.get('accessToken', None)
- return result.success(CaptchaSerializer().chat_generate(username, 'chat', accessToken))
+ username = request.query_params.get("username", None)
+ accessToken = request.query_params.get("accessToken", None)
+ return result.success(CaptchaSerializer().chat_generate(username, "chat", accessToken))
class SpeechToText(APIView):
authentication_classes = [ChatTokenAuth]
@extend_schema(
- methods=['POST'],
+ methods=["POST"],
description=_("speech to text"),
summary=_("speech to text"),
operation_id=_("speech to text"), # type: ignore
request=SpeechToTextAPI.get_request(),
responses=SpeechToTextAPI.get_response(),
- tags=[_('Chat')] # type: ignore
+ tags=[_("Chat")], # type: ignore
)
def post(self, request: Request):
return result.success(
- SpeechToTextSerializers(
- data={'application_id': request.user.kwargs.get('application_id')})
- .speech_to_text({'file': request.FILES.get('file')}))
+ SpeechToTextSerializers(data={"application_id": request.user.kwargs.get("application_id")}).speech_to_text(
+ {"file": request.FILES.get("file")}
+ )
+ )
class TextToSpeech(APIView):
authentication_classes = [ChatTokenAuth]
@extend_schema(
- methods=['POST'],
+ methods=["POST"],
description=_("text to speech"),
summary=_("text to speech"),
operation_id=_("text to speech"), # type: ignore
request=TextToSpeechAPI.get_request(),
responses=TextToSpeechAPI.get_response(),
- tags=[_('Chat')] # type: ignore
+ tags=[_("Chat")], # type: ignore
)
def post(self, request: Request):
byte_data = TextToSpeechSerializers(
- data={'application_id': request.user.kwargs.get('application_id')}).text_to_speech(request.data)
- return HttpResponse(byte_data, status=200, headers={'Content-Type': 'audio/mp3',
- 'Content-Disposition': 'attachment; filename="abc.mp3"'})
+ data={"application_id": request.user.kwargs.get("application_id")}
+ ).text_to_speech(request.data)
+ return HttpResponse(
+ byte_data,
+ status=200,
+ headers={"Content-Type": "audio/mp3", "Content-Disposition": 'attachment; filename="abc.mp3"'},
+ )
class UploadFile(APIView):
@@ -310,23 +358,28 @@ class UploadFile(APIView):
parser_classes = [MultiPartParser]
@extend_schema(
- methods=['POST'],
+ methods=["POST"],
description=_("Upload files"),
summary=_("Upload files"),
operation_id=_("Upload files"), # type: ignore
request=TextToSpeechAPI.get_request(),
responses=TextToSpeechAPI.get_response(),
- tags=[_('Application')] # type: ignore
+ tags=[_("Application")], # type: ignore
)
def post(self, request: Request, chat_id: str):
- files = request.FILES.getlist('file')
+ files = request.FILES.getlist("file")
file_ids = []
meta = {}
for file in files:
file_url = FileSerializer(
- data={'file': file, 'meta': meta, 'source_id': chat_id, 'source_type': FileSourceType.CHAT, }).upload(
- request.user.id)
- file_ids.append({'name': file.name, 'url': file_url, 'file_id': file_url.split('/')[-1]})
+ data={
+ "file": file,
+ "meta": meta,
+ "source_id": chat_id,
+ "source_type": FileSourceType.CHAT,
+ }
+ ).upload(request.user.id)
+ file_ids.append({"name": file.name, "url": file_url, "file_id": file_url.split("/")[-1]})
return result.success(file_ids)
@@ -393,7 +446,7 @@ def create_token_and_cache(access_token, user, request):
return token, FileToken(str(user.id), AuthenticationType.CHAT_USER.value).to_token()
@classmethod
- def generate(self, request, f_token: str, response: HttpResponse, path: str = '/chat'):
+ def generate(self, request, f_token: str, response: HttpResponse, path: str = "/chat"):
secure = request.is_secure()
response.set_cookie(
"mk_file_auth",
@@ -422,8 +475,8 @@ def post(self, request: Request, access_token: str = None):
user = ChatUserAccessTokenSerializer.local_login(request.data, access_token)
user.source = "LOCAL"
token, f_token = self.create_token_and_cache(access_token, user, request)
- response = result.success({'token': token})
- return self.generate(request, f_token, response, path=f'/chat/{access_token}/')
+ response = result.success({"token": token})
+ return self.generate(request, f_token, response, path=f"/chat/{access_token}/")
class Logout(APIView):
diff --git a/apps/chat/views/v3/chat.py b/apps/chat/views/v3/chat.py
index 8fd53a8baf1..a4c48b7535e 100644
--- a/apps/chat/views/v3/chat.py
+++ b/apps/chat/views/v3/chat.py
@@ -25,11 +25,11 @@
from chat.api.chat_api import ChatAPI
from chat.api.chat_authentication_api import ChatAuthenticationAPI, ChatAuthenticationProfileAPI, ChatOpenAPI, OpenAIAPI
from chat.serializers.chat import (
- OpenChatSerializers,
+ OpenAIChatSerializer,
ChatSerializers,
+ OpenChatSerializers,
SpeechToTextSerializers,
TextToSpeechSerializers,
- OpenAIChatSerializer,
)
from chat.serializers.chat_authentication import (
AnonymousAuthenticationSerializer,
diff --git a/apps/common/constants/cache_version.py b/apps/common/constants/cache_version.py
index 5642b7929ae..64c29498515 100644
--- a/apps/common/constants/cache_version.py
+++ b/apps/common/constants/cache_version.py
@@ -1,11 +1,12 @@
# coding=utf-8
"""
- @project: MaxKB
- @Author:虎虎
- @file: cache_version.py
- @date:2025/4/14 19:09
- @desc:
+@project: MaxKB
+@Author:虎虎
+@file: cache_version.py
+@date:2025/4/14 19:09
+@desc:
"""
+
from enum import Enum
@@ -32,6 +33,9 @@ class Cache_Version(Enum):
CHAT_INFO = "CHAT_INFO", lambda key: key
+ # 会话历史滚动窗口缓存(只存最近 N 条已完成记录,append-only)
+ CHAT_HISTORY = "CHAT_HISTORY", lambda key: key
+
CHAT_VARIABLE = "CHAT_VARIABLE", lambda key: key
# 应用API KEY
diff --git a/apps/common/handle/base_to_response.py b/apps/common/handle/base_to_response.py
index 376d1a9ddd7..8f03a68f7f2 100644
--- a/apps/common/handle/base_to_response.py
+++ b/apps/common/handle/base_to_response.py
@@ -1,30 +1,40 @@
# coding=utf-8
"""
- @project: MaxKB
- @Author:虎
- @file: base_to_response.py
- @date:2024/9/6 16:04
- @desc:
+@project: MaxKB
+@Author:虎
+@file: base_to_response.py
+@date:2024/9/6 16:04
+@desc:
"""
+
from abc import ABC, abstractmethod
from rest_framework import status
class BaseToResponse(ABC):
+ @abstractmethod
+ def to_stream(self, chat_id, chat_record_id, block: dict):
+ """
+ 把一个内容块(content.to_dict())格式化成一帧 SSE 的 data 载荷(JSON 字符串)。
+ 返回 None 表示该块类型在此格式下不表达(消费方跳过)。
+ 只返回 data 载荷,不含 'data:'/'id:' 帧壳,帧壳由消费方拼。
+ """
+ pass
@abstractmethod
- def to_block_response(self, chat_id, chat_record_id, content, is_end, completion_tokens,
- prompt_tokens, other_params: dict = None,
- _status=status.HTTP_200_OK):
+ def to_stream_end(self, chat_id, chat_record_id, usage: dict = None):
+ """
+ 流结束帧(如 OpenAI 的空 delta + finish_reason=stop + 最终用量)。
+ 返回 None 表示该格式无需单独结束帧(如系统格式以 [DONE] 收尾)。
+ """
pass
@abstractmethod
- def to_stream_chunk_response(self, chat_id, chat_record_id, node_id, up_node_id_list, content, is_end,
- completion_tokens,
- prompt_tokens, other_params: dict = None):
+ def to_block(self, chat_id, chat_record_id, contents: list, usage: dict = None, _status=status.HTTP_200_OK):
+ """从聚合后的内容块列表(content.to_dict() 的 list)构造非流式响应。"""
pass
@staticmethod
def format_stream_chunk(response_str):
- return 'data: ' + response_str + '\n\n'
+ return "data: " + response_str + "\n\n"
diff --git a/apps/common/handle/impl/response/openai_to_response.py b/apps/common/handle/impl/response/openai_to_response.py
index b4eda362555..98023aef364 100644
--- a/apps/common/handle/impl/response/openai_to_response.py
+++ b/apps/common/handle/impl/response/openai_to_response.py
@@ -1,12 +1,11 @@
# coding=utf-8
"""
- @project: MaxKB
- @Author:虎
- @file: openai_to_response.py
- @date:2024/9/6 16:08
- @desc:
+@project: MaxKB
+@Author:虎
+@file: openai_to_response.py
+@date:2024/9/6 16:08
+@desc:
"""
-import datetime
from django.http import JsonResponse
from django.utils import timezone
@@ -20,34 +19,102 @@
class OpenaiToResponse(BaseToResponse):
- def to_block_response(self, chat_id, chat_record_id, content, is_end, prompt_tokens, completion_tokens,
- other_params: dict = None,
- _status=status.HTTP_200_OK):
- if other_params is None:
- other_params = {}
- data = ChatCompletion(id=chat_record_id, choices=[
- BlockChoice(finish_reason='stop', index=0, chat_id=chat_id,
- answer_list=other_params.get('answer_list', ""),
- message=ChatCompletionMessage(role='assistant', content=content))],
- created=timezone.now().second, model='', object='chat.completion',
- usage=CompletionUsage(completion_tokens=completion_tokens,
- prompt_tokens=prompt_tokens,
- total_tokens=completion_tokens + prompt_tokens)
- ).dict()
- return JsonResponse(data=data, status=_status)
+ def __init__(self):
+ # per-response 状态:tool_id -> index,逐帧分配,客户端按 index 累加 arguments
+ self._tool_index = {}
+
+ def _to_tool_call_delta(self, block: dict) -> dict:
+ """把一个 ToolContent 块转成 OpenAI 的 delta.tool_calls 项;靠稳定 id 分帧、不缓冲。"""
+ tool_id = block.get("id")
+ first = tool_id not in self._tool_index
+ if first:
+ self._tool_index[tool_id] = len(self._tool_index)
+ index = self._tool_index[tool_id]
+ function = {"arguments": block.get("arguments") or ""}
+ if first:
+ function["name"] = block.get("content") or "" # ToolContent.content = 工具名
+ tool_call = {"index": index, "type": "function", "function": function}
+ if first:
+ tool_call["id"] = tool_id
+ # 非标扩展:result(与 reasoning_content/chat_id 一致),标准客户端忽略、自家客户端读
+ if block.get("result"):
+ tool_call["result"] = block.get("result")
+ return tool_call
+
+ def to_stream(self, chat_id, chat_record_id, block: dict):
+ block_type = block.get("type")
+ delta_kwargs = {"chat_id": chat_id}
+ if block_type == "TEXT":
+ delta_kwargs["content"] = block.get("content", "")
+ elif block_type == "REASONING":
+ delta_kwargs["reasoning_content"] = block.get("content", "")
+ elif block_type == "TOOL":
+ delta_kwargs["tool_calls"] = [self._to_tool_call_delta(block)]
+ else:
+ # FORM / FAILURE 等:OpenAI 流不表达,跳过
+ return None
+ # 内容帧:finish_reason=None、usage=None(用量只在结束帧给,符合 OpenAI 规范)
+ return ChatCompletionChunk(
+ id=str(chat_record_id),
+ model="",
+ object="chat.completion.chunk",
+ created=int(timezone.now().timestamp()),
+ choices=[Choice(delta=ChoiceDelta(**delta_kwargs), finish_reason=None, index=0)],
+ ).json()
- def to_stream_chunk_response(self, chat_id, chat_record_id, node_id, up_node_id_list, content, is_end,
- prompt_tokens,
- completion_tokens, other_params: dict = None):
- if other_params is None:
- other_params = {}
- chunk = ChatCompletionChunk(id=chat_record_id, model='', object='chat.completion.chunk',
- created=timezone.now().second, choices=[
- Choice(delta=ChoiceDelta(content=content, reasoning_content=other_params.get('reasoning_content', ""),
- chat_id=chat_id),
- finish_reason='stop' if is_end else None,
- index=0)],
- usage=CompletionUsage(completion_tokens=completion_tokens,
- prompt_tokens=prompt_tokens,
- total_tokens=completion_tokens + prompt_tokens)).json()
- return super().format_stream_chunk(chunk)
+ def to_stream_end(self, chat_id, chat_record_id, usage: dict = None):
+ # 结束帧:空 delta + finish_reason=stop + 最终用量
+ usage = usage or {}
+ completion_tokens = usage.get("completion_tokens", 0)
+ prompt_tokens = usage.get("prompt_tokens", 0)
+ return ChatCompletionChunk(
+ id=str(chat_record_id),
+ model="",
+ object="chat.completion.chunk",
+ created=int(timezone.now().timestamp()),
+ choices=[Choice(delta=ChoiceDelta(chat_id=chat_id), finish_reason="stop", index=0)],
+ usage=CompletionUsage(
+ completion_tokens=completion_tokens,
+ prompt_tokens=prompt_tokens,
+ total_tokens=completion_tokens + prompt_tokens,
+ ),
+ ).json()
+
+ def to_block(self, chat_id, chat_record_id, contents: list, usage: dict = None, _status=status.HTTP_200_OK):
+ usage = usage or {}
+ answer = "".join(c.get("content", "") for c in (contents or []) if c.get("type") == "TEXT")
+ tool_calls = []
+ for c in contents or []:
+ if c.get("type") != "TOOL":
+ continue
+ tc = {
+ "index": len(tool_calls),
+ "id": c.get("id"),
+ "type": "function",
+ "function": {"name": c.get("content") or "", "arguments": c.get("arguments") or ""},
+ }
+ if c.get("result"):
+ tc["result"] = c.get("result")
+ tool_calls.append(tc)
+ message_kwargs = {"role": "assistant", "content": answer}
+ if tool_calls:
+ message_kwargs["tool_calls"] = tool_calls
+ completion_tokens = usage.get("completion_tokens", 0)
+ prompt_tokens = usage.get("prompt_tokens", 0)
+ data = ChatCompletion(
+ id=str(chat_record_id),
+ choices=[
+ BlockChoice(
+ finish_reason="stop", index=0, chat_id=chat_id, message=ChatCompletionMessage(**message_kwargs)
+ )
+ ],
+ created=int(timezone.now().timestamp()),
+ model="",
+ object="chat.completion",
+ usage=CompletionUsage(
+ completion_tokens=completion_tokens,
+ prompt_tokens=prompt_tokens,
+ total_tokens=completion_tokens + prompt_tokens,
+ ),
+ ).dict()
+ return JsonResponse(data=data, status=_status)
diff --git a/apps/common/handle/impl/response/system_to_response.py b/apps/common/handle/impl/response/system_to_response.py
index a1a530dba08..29243613ae4 100644
--- a/apps/common/handle/impl/response/system_to_response.py
+++ b/apps/common/handle/impl/response/system_to_response.py
@@ -1,11 +1,12 @@
# coding=utf-8
"""
- @project: MaxKB
- @Author:虎
- @file: system_to_response.py
- @date:2024/9/6 18:03
- @desc:
+@project: MaxKB
+@Author:虎
+@file: system_to_response.py
+@date:2024/9/6 18:03
+@desc:
"""
+
import json
from rest_framework import status
@@ -15,27 +16,35 @@
class SystemToResponse(BaseToResponse):
- def to_block_response(self, chat_id, chat_record_id, content, is_end, completion_tokens,
- prompt_tokens, other_params: dict = None,
- _status=status.HTTP_200_OK):
- if other_params is None:
- other_params = {}
- return result.success({'chat_id': str(chat_id), 'id': str(chat_record_id), 'operate': True,
- 'content': content, 'is_end': is_end, **other_params,
- 'completion_tokens': completion_tokens, 'prompt_tokens': prompt_tokens},
- response_status=_status,
- code=_status)
+ def to_stream(self, chat_id, chat_record_id, block: dict):
+ # 沿用前端在解析的信封 shape:{chat_id, chat_record_id, content:[block]}
+ # 系统格式所有块类型都原样下发(block 即 content.to_dict())
+ return json.dumps(
+ {
+ "chat_id": str(chat_id),
+ "chat_record_id": str(chat_record_id),
+ "content": [block],
+ },
+ ensure_ascii=False,
+ )
+
+ def to_stream_end(self, chat_id, chat_record_id, usage: dict = None):
+ # 系统格式以 [DONE] 收尾,无需单独结束帧
+ return None
- def to_stream_chunk_response(self, chat_id, chat_record_id, node_id, up_node_id_list, content, is_end,
- completion_tokens,
- prompt_tokens, other_params: dict = None):
- if other_params is None:
- other_params = {}
- chunk = json.dumps({'chat_id': str(chat_id), 'chat_record_id': str(chat_record_id), 'operate': True,
- 'content': content, 'node_id': node_id, 'up_node_id_list': up_node_id_list,
- 'is_end': is_end,
- 'usage': {'completion_tokens': completion_tokens,
- 'prompt_tokens': prompt_tokens,
- 'total_tokens': completion_tokens + prompt_tokens},
- **other_params})
- return super().format_stream_chunk(chunk)
+ def to_block(self, chat_id, chat_record_id, contents: list, usage: dict = None, _status=status.HTTP_200_OK):
+ usage = usage or {}
+ answer = "".join(c.get("content", "") for c in (contents or []) if c.get("type") == "TEXT")
+ return result.success(
+ {
+ "chat_id": str(chat_id),
+ "id": str(chat_record_id),
+ "operate": True,
+ "content": answer,
+ "is_end": True,
+ "completion_tokens": usage.get("completion_tokens", 0),
+ "prompt_tokens": usage.get("prompt_tokens", 0),
+ },
+ response_status=_status,
+ code=_status,
+ )