From cf16b0b392caca73b45c00996ebf839cddb875de Mon Sep 17 00:00:00 2001 From: z30057876 Date: Mon, 22 Sep 2025 20:59:54 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8C=E6=AD=A5=E6=96=87=E6=A1=A3=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E3=80=81=E5=8E=BB=E9=99=A4=E5=86=97=E4=BD=99=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E7=BB=93=E6=9E=84=E3=80=81=E4=BF=AE=E6=AD=A3=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/llm/providers/base.py | 4 +- apps/llm/providers/ollama.py | 97 ++++------- apps/llm/providers/openai.py | 102 ++++++++--- apps/llm/reasoning.py | 5 + apps/models/mcp.py | 9 +- apps/routers/appcenter.py | 12 +- apps/routers/conversation.py | 8 +- apps/routers/document.py | 12 +- apps/routers/flow.py | 6 +- apps/routers/mcp_service.py | 4 +- apps/routers/user.py | 35 ++-- apps/scheduler/call/__init__.py | 2 + apps/scheduler/call/convert/convert.py | 86 ++++++--- apps/scheduler/call/convert/schema.py | 5 + apps/scheduler/call/llm/llm.py | 32 +++- apps/scheduler/call/llm/prompt.py | 4 +- apps/scheduler/call/rag/rag.py | 86 ++++----- apps/scheduler/call/suggest/prompt.py | 101 +++++------ apps/scheduler/call/suggest/suggest.py | 36 ++-- apps/scheduler/pool/check.py | 23 ++- apps/scheduler/pool/loader/app.py | 20 ++- apps/scheduler/pool/loader/service.py | 11 ++ apps/schemas/agent.py | 1 - apps/schemas/appcenter.py | 82 +++++++++ apps/schemas/conversation.py | 57 ++++++ apps/schemas/document.py | 43 ++--- apps/schemas/enum_var.py | 22 +-- apps/schemas/flow.py | 39 +++++ apps/schemas/mcp.py | 3 +- apps/schemas/mcp_service.py | 3 +- apps/schemas/message.py | 7 - apps/schemas/request_data.py | 12 +- apps/schemas/response_data.py | 164 ------------------ apps/services/flow.py | 32 ++-- apps/services/llm.py | 29 +--- apps/services/mcp_service.py | 66 ++++--- apps/services/user.py | 22 ++- deploy/chart/authhub/Chart.yaml | 4 +- deploy/chart/databases/Chart.yaml | 4 +- deploy/chart/euler_copilot/Chart.yaml | 4 +- ...50\347\275\262\346\214\207\345\215\227.md" | 2 +- ...50\347\275\262\346\214\207\345\215\227.md" | 1 + manual/source/conf.py | 4 +- 43 files changed, 698 insertions(+), 603 deletions(-) diff --git a/apps/llm/providers/base.py b/apps/llm/providers/base.py index 7067c0201..0f7687954 100644 --- a/apps/llm/providers/base.py +++ b/apps/llm/providers/base.py @@ -51,6 +51,7 @@ class BaseProvider: async def chat( self, messages: list[dict[str, str]], *, include_thinking: bool = False, + tools: list[LLMFunctions] | None = None, ) -> AsyncGenerator[LLMChunk, None]: """聊天""" raise NotImplementedError @@ -61,6 +62,3 @@ class BaseProvider: raise NotImplementedError - async def tool_call(self, messages: list[dict[str, str]], tools: list[LLMFunctions]) -> str: - """工具调用""" - raise NotImplementedError diff --git a/apps/llm/providers/ollama.py b/apps/llm/providers/ollama.py index 92bcbc6e1..e54ac59cb 100644 --- a/apps/llm/providers/ollama.py +++ b/apps/llm/providers/ollama.py @@ -77,6 +77,7 @@ class OllamaProvider(BaseProvider): async def chat( self, messages: list[dict[str, str]], *, include_thinking: bool = False, + tools: list[LLMFunctions] | None = None, ) -> AsyncGenerator[LLMChunk, None]: # 检查能力 if not self._allow_chat: @@ -93,16 +94,32 @@ class OllamaProvider(BaseProvider): # 流式返回响应 last_chunk = None - async for chunk in await self._client.chat( - model=self.config.modelName, - messages=messages, - options={ + chat_kwargs = { + "model": self.config.modelName, + "messages": messages, + "options": { "temperature": self.config.temperature, "num_predict": self.config.maxToken, }, - stream=True, + "stream": True, **self.config.extraConfig, - ): + } + + # 如果提供了tools,则传入以启用function-calling模式 + if tools: + functions = [] + for tool in tools: + functions += [{ + "type": "function", + "function": { + "name": tool.name, + "description": tool.description, + "parameters": tool.param_schema, + }, + }] + chat_kwargs["tools"] = functions + + async for chunk in await self._client.chat(**chat_kwargs): last_chunk = chunk if chunk.message.thinking: self.full_thinking += chunk.message.thinking @@ -111,6 +128,16 @@ class OllamaProvider(BaseProvider): self.full_answer += chunk.message.content yield LLMChunk(content=chunk.message.content) + # 在chat中统一处理工具调用分块(当提供了tools时) + if tools and chunk.message.tool_calls: + tool_call_dict = {} + for tool_call in chunk.message.tool_calls: + tool_call_dict.update({ + tool_call.function.name: tool_call.function.arguments, + }) + if tool_call_dict: + yield LLMChunk(tool_call=tool_call_dict) + # 使用最后一个chunk的usage数据 self._process_usage_data(last_chunk, messages) @@ -138,62 +165,4 @@ class OllamaProvider(BaseProvider): ) return self._seq_to_list(result.embeddings) - @override - async def tool_call( - self, messages: list[dict[str, str]], tools: list[LLMFunctions], - ) -> AsyncGenerator[LLMChunk, None]: - """工具调用""" - if not self._allow_function: - err = "[OllamaProvider] 当前模型不支持Function Call" - _logger.error(err) - raise RuntimeError(err) - - # 检查消息 - messages = self._validate_messages(messages) - - # 组装functions - functions = [] - for tool in tools: - functions += [{ - "type": "function", - "function": { - "name": tool.name, - "description": tool.description, - "parameters": tool.param_schema, - }, - }] - - # 流式返回响应 - last_chunk = None - async for chunk in await self._client.chat( - model=self.config.modelName, - messages=messages, - tools=functions, - options={ - "temperature": self.config.temperature, - "num_predict": self.config.maxToken, - }, - stream=True, - **self.config.extraConfig, - ): - last_chunk = chunk - if chunk.message.thinking: - self.full_thinking += chunk.message.thinking - yield LLMChunk(reasoning_content=chunk.message.thinking) - if chunk.message.content: - self.full_answer += chunk.message.content - yield LLMChunk(content=chunk.message.content) - if chunk.message.tool_calls: - # 将ToolCall对象转换为字典格式 - for tool_call in chunk.message.tool_calls: - tool_call_dict = { - "function": { - "name": tool_call.function.name, - "arguments": tool_call.function.arguments, - }, - } - yield LLMChunk(tool_call=tool_call_dict) - - # 使用最后一个chunk的usage数据 - self._process_usage_data(last_chunk, messages) diff --git a/apps/llm/providers/openai.py b/apps/llm/providers/openai.py index 27d0e5a8f..44250ffab 100644 --- a/apps/llm/providers/openai.py +++ b/apps/llm/providers/openai.py @@ -5,7 +5,14 @@ import logging from collections.abc import AsyncGenerator from openai import AsyncOpenAI, AsyncStream -from openai.types.chat import ChatCompletionChunk +from openai.types.chat import ( + ChatCompletionAssistantMessageParam, + ChatCompletionChunk, + ChatCompletionMessageParam, + ChatCompletionSystemMessageParam, + ChatCompletionToolMessageParam, + ChatCompletionUserMessageParam, +) from typing_extensions import override from apps.llm.token import TokenCalculator @@ -83,9 +90,10 @@ class OpenAIProvider(BaseProvider): }]) @override - async def chat( + async def chat( # noqa: C901 self, messages: list[dict[str, str]], *, include_thinking: bool = False, + tools: list[LLMFunctions] | None = None, ) -> AsyncGenerator[LLMChunk, None]: """聊天""" # 检查能力 @@ -101,16 +109,31 @@ class OpenAIProvider(BaseProvider): # 检查消息 messages = self._validate_messages(messages) - stream: AsyncStream[ChatCompletionChunk] = self._client.chat.completions.create( - model=self.config.modelName, - messages=messages, # type: ignore[] - max_tokens=self.config.maxToken, - temperature=self.config.temperature, - stream=True, - stream_options={"include_usage": True}, + request_kwargs = { + "model": self.config.modelName, + "messages": self._convert_messages(messages), + "max_tokens": self.config.maxToken, + "temperature": self.config.temperature, + "stream": True, + "stream_options": {"include_usage": True}, **self.config.extraConfig, - ) - + } + + # 如果提供了tools,则启用function-calling模式 + if tools: + functions = [] + for tool in tools: + functions += [{ + "type": "function", + "function": { + "name": tool.name, + "description": tool.description, + "parameters": tool.param_schema, + }, + }] + request_kwargs["tools"] = functions + + stream: AsyncStream[ChatCompletionChunk] = await self._client.chat.completions.create(**request_kwargs) # 流式返回响应 last_chunk = None async for chunk in stream: @@ -133,6 +156,17 @@ class OpenAIProvider(BaseProvider): self.full_answer += chunk.choices[0].delta.content yield LLMChunk(content=chunk.choices[0].delta.content) + # 在chat中统一处理工具调用分块(当提供了tools时) + if tools and hasattr(delta, "tool_calls") and delta.tool_calls: + tool_call_dict = {} + for tool_call in delta.tool_calls: + if hasattr(tool_call, "function") and tool_call.function: + tool_call_dict.update({ + tool_call.function.name: tool_call.function.arguments, + }) + if tool_call_dict: + yield LLMChunk(tool_call=tool_call_dict) + # 处理最后一个Chunk的usage(仅在最后一个chunk会出现) self._handle_usage_chunk(last_chunk, messages) @@ -150,17 +184,37 @@ class OpenAIProvider(BaseProvider): ) return [data.embedding for data in response.data] - @override - async def tool_call(self, messages: list[dict[str, str]], tools: list[LLMFunctions]) -> str: - if not self._allow_function: - info = "[OpenAIProvider] 当前模型不支持Function Call,将使用模拟方式" - _logger.error(info) - return "" - - # 检查消息 - messages = self._validate_messages(messages) - - # 实现tool_call功能 - return "" - + def _convert_messages(self, messages: list[dict[str, str]]) -> list[ChatCompletionMessageParam]: + """将list[dict]形式的消息转换为list[ChatCompletionMessageParam]形式""" + converted_messages = [] + for msg in messages: + role = msg.get("role", "user") + content = msg.get("content", "") + + if role == "system": + converted_messages.append( + ChatCompletionSystemMessageParam(role="system", content=content), + ) + elif role == "user": + converted_messages.append( + ChatCompletionUserMessageParam(role="user", content=content), + ) + elif role == "assistant": + converted_messages.append( + ChatCompletionAssistantMessageParam(role="assistant", content=content), + ) + elif role == "tool": + tool_call_id = msg.get("tool_call_id", "") + converted_messages.append( + ChatCompletionToolMessageParam( + role="tool", + content=content, + tool_call_id=tool_call_id, + ), + ) + else: + err = f"[OpenAIProvider] 未知角色: {role}" + _logger.error(err) + raise ValueError(err) + return converted_messages diff --git a/apps/llm/reasoning.py b/apps/llm/reasoning.py index fc2d3c54b..c54d63636 100644 --- a/apps/llm/reasoning.py +++ b/apps/llm/reasoning.py @@ -67,3 +67,8 @@ class ReasoningLLM: def output_tokens(self) -> int: """获取输出token数""" return self._provider.output_tokens + + @property + def config(self) -> LLMData: + """获取大模型配置""" + return self._provider.config diff --git a/apps/models/mcp.py b/apps/models/mcp.py index 84db4bc0f..827414afc 100644 --- a/apps/models/mcp.py +++ b/apps/models/mcp.py @@ -1,9 +1,7 @@ """MCP 相关 数据库表""" -import uuid from datetime import UTC, datetime from enum import Enum as PyEnum -from hashlib import shake_128 from typing import Any from sqlalchemy import BigInteger, DateTime, Enum, ForeignKey, String, Text @@ -93,9 +91,10 @@ class MCPTools(Base): """MCP 工具输入参数""" outputSchema: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False) # noqa: N815 """MCP 工具输出参数""" - id: Mapped[str] = mapped_column( - String(32), + id: Mapped[int] = mapped_column( + BigInteger, primary_key=True, - default_factory=lambda: shake_128(uuid.uuid4().bytes).hexdigest(8), + autoincrement=True, + init=False, ) """主键ID""" diff --git a/apps/routers/appcenter.py b/apps/routers/appcenter.py index 3c35ffd86..30e1612a7 100644 --- a/apps/routers/appcenter.py +++ b/apps/routers/appcenter.py @@ -10,20 +10,24 @@ from fastapi.responses import JSONResponse from apps.dependency.user import verify_personal_token, verify_session from apps.exceptions import InstancePermissionError -from apps.schemas.appcenter import AppFlowInfo, AppPermissionData, ChangeFavouriteAppRequest, CreateAppRequest -from apps.schemas.enum_var import AppFilterType, AppType -from apps.schemas.response_data import ( +from apps.schemas.appcenter import ( + AppFlowInfo, + AppMcpServiceInfo, + AppPermissionData, BaseAppOperationMsg, BaseAppOperationRsp, ChangeFavouriteAppMsg, + ChangeFavouriteAppRequest, ChangeFavouriteAppRsp, + CreateAppRequest, GetAppListMsg, GetAppListRsp, GetAppPropertyMsg, GetAppPropertyRsp, GetRecentAppListRsp, - ResponseData, ) +from apps.schemas.enum_var import AppFilterType, AppType +from apps.schemas.response_data import ResponseData from apps.services.appcenter import AppCenterManager from apps.services.mcp_service import MCPServiceManager diff --git a/apps/routers/conversation.py b/apps/routers/conversation.py index 823a322ae..562e3e764 100644 --- a/apps/routers/conversation.py +++ b/apps/routers/conversation.py @@ -12,20 +12,18 @@ from fastapi.responses import JSONResponse from apps.dependency import verify_personal_token, verify_session from apps.models.conversation import Conversation from apps.schemas.conversation import ( - ChangeConversationData, - DeleteConversationData, -) -from apps.schemas.response_data import ( AddConversationMsg, AddConversationRsp, + ChangeConversationData, ConversationListItem, ConversationListMsg, ConversationListRsp, + DeleteConversationData, DeleteConversationMsg, DeleteConversationRsp, - ResponseData, UpdateConversationRsp, ) +from apps.schemas.response_data import ResponseData from apps.services.appcenter import AppCenterManager from apps.services.conversation import ConversationManager from apps.services.document import DocumentManager diff --git a/apps/routers/document.py b/apps/routers/document.py index 8c33d5738..27d0ed7fc 100644 --- a/apps/routers/document.py +++ b/apps/routers/document.py @@ -12,11 +12,11 @@ from fastapi.responses import JSONResponse from apps.dependency import verify_personal_token, verify_session from apps.schemas.document import ( + BaseDocumentItem, ConversationDocumentItem, ConversationDocumentMsg, ConversationDocumentRsp, UploadDocumentMsg, - UploadDocumentMsgItem, UploadDocumentRsp, ) from apps.schemas.enum_var import DocumentStatus @@ -46,9 +46,9 @@ async def document_upload( await KnowledgeBaseService.send_file_to_rag(request.state.session_id, result) # 返回所有Framework已知的文档 - succeed_document: list[UploadDocumentMsgItem] = [ - UploadDocumentMsgItem( - _id=doc.id, + succeed_document: list[BaseDocumentItem] = [ + BaseDocumentItem( + id=doc.id, name=doc.name, type=doc.extension, size=doc.size, @@ -90,7 +90,7 @@ async def get_document_list( docs = await DocumentManager.get_used_docs(conversation_id) result += [ ConversationDocumentItem( - _id=item.id, + id=item.id, name=item.name, type=item.extension, size=round(item.size, 2), @@ -120,7 +120,7 @@ async def get_document_list( result += [ ConversationDocumentItem( - _id=current_doc.id, + id=current_doc.id, name=current_doc.name, type=current_doc.extension, size=round(current_doc.size, 2), diff --git a/apps/routers/flow.py b/apps/routers/flow.py index 251d08dcb..2d87351e2 100644 --- a/apps/routers/flow.py +++ b/apps/routers/flow.py @@ -8,16 +8,16 @@ from fastapi import APIRouter, Body, Depends, Query, Request, status from fastapi.responses import JSONResponse from apps.dependency import verify_personal_token, verify_session -from apps.schemas.request_data import PutFlowReq -from apps.schemas.response_data import ( +from apps.schemas.flow import ( FlowStructureDeleteMsg, FlowStructureDeleteRsp, FlowStructureGetMsg, FlowStructureGetRsp, FlowStructurePutMsg, FlowStructurePutRsp, - ResponseData, ) +from apps.schemas.request_data import PutFlowReq +from apps.schemas.response_data import ResponseData from apps.schemas.service import NodeServiceListMsg, NodeServiceListRsp from apps.services.appcenter import AppCenterManager from apps.services.flow import FlowManager diff --git a/apps/routers/mcp_service.py b/apps/routers/mcp_service.py index 16e8c8c41..b16022a2d 100644 --- a/apps/routers/mcp_service.py +++ b/apps/routers/mcp_service.py @@ -99,7 +99,7 @@ async def create_or_update_mcpservice( data: UpdateMCPServiceRequest, ) -> JSONResponse: """PUT /mcp: 新建或更新MCP服务""" - if not data.service_id: + if not data.mcp_id: try: service_id = await MCPServiceManager.create_mcpservice(data, request.state.user_sub) except Exception as e: @@ -144,7 +144,7 @@ async def install_mcp_service( ) -> JSONResponse: """安装MCP服务""" try: - await MCPServiceManager.install_mcpservice(request.state.user_sub, service_id, install) + await MCPServiceManager.install_mcpservice(request.state.user_sub, service_id, install=install) except Exception as e: err = f"[MCPService] 安装mcp服务失败: {e!s}" if install else f"[MCPService] 卸载mcp服务失败: {e!s}" logger.exception(err) diff --git a/apps/routers/user.py b/apps/routers/user.py index a4957bf20..7316ffb29 100644 --- a/apps/routers/user.py +++ b/apps/routers/user.py @@ -5,9 +5,11 @@ from fastapi import APIRouter, Depends, Request, status from fastapi.responses import JSONResponse from apps.dependency import verify_personal_token, verify_session -from apps.schemas.response_data import ResponseData, UserGetMsp, UserGetRsp +from apps.schemas.request_data import UpdateUserSelectedLLMReq, UserUpdateRequest +from apps.schemas.response_data import ResponseData, UserGetMsp, UserGetRsp, UserSelectedLLMData from apps.schemas.tag import UserTagListResponse from apps.schemas.user import UserInfo +from apps.services.llm import LLMManager from apps.services.user import UserManager from apps.services.user_tag import UserTagManager @@ -22,7 +24,7 @@ router = APIRouter( async def update_user_info(request: Request, data: UserUpdateRequest) -> JSONResponse: """POST /auth/user: 更新当前用户信息""" # 更新用户信息 - await UserManager.update_userinfo_by_user_sub(request.state.user_sub, data) + await UserManager.update_user(request.state.user_sub, data) return JSONResponse( status_code=status.HTTP_200_OK, @@ -61,11 +63,26 @@ async def list_user( ) async def update_user_llm( request: Request, - llmId: str, # noqa: N803 + llm_request: UpdateUserSelectedLLMReq, ) -> JSONResponse: - """更新用户所选的大模型""" + """更新用户所选的EmbeddingLLM和FunctionLLM""" try: - await LLMManager.update_user_selected_llm(request.state.user_sub, llmId) + await LLMManager.update_user_selected_llm(request.state.user_sub, llm_request) + + # 返回更新后的LLM配置 + result_data = UserSelectedLLMData.model_validate({ + "functionLLM": llm_request.functionLLM, + "embeddingLLM": llm_request.embeddingLLM, + }) + + return JSONResponse( + status_code=status.HTTP_200_OK, + content=ResponseData( + code=status.HTTP_200_OK, + message="用户LLM配置更新成功", + result=result_data, + ).model_dump(exclude_none=True, by_alias=True), + ) except ValueError as e: return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -75,14 +92,6 @@ async def update_user_llm( result=None, ).model_dump(exclude_none=True, by_alias=True), ) - return JSONResponse( - status_code=status.HTTP_200_OK, - content=ResponseData( - code=status.HTTP_200_OK, - message="success", - result=llmId, - ).model_dump(exclude_none=True, by_alias=True), - ) @router.get("/tag", diff --git a/apps/scheduler/call/__init__.py b/apps/scheduler/call/__init__.py index 0fc7106df..4575aaa16 100644 --- a/apps/scheduler/call/__init__.py +++ b/apps/scheduler/call/__init__.py @@ -3,6 +3,7 @@ from apps.scheduler.call.api.api import API from apps.scheduler.call.choice.choice import Choice +from apps.scheduler.call.convert.convert import Convert from apps.scheduler.call.graph.graph import Graph from apps.scheduler.call.llm.llm import LLM from apps.scheduler.call.mcp.mcp import MCP @@ -18,6 +19,7 @@ __all__ = [ "RAG", "SQL", "Choice", + "Convert", "Graph", "Suggestion", ] diff --git a/apps/scheduler/call/convert/convert.py b/apps/scheduler/call/convert/convert.py index 09886d4eb..b7a860819 100644 --- a/apps/scheduler/call/convert/convert.py +++ b/apps/scheduler/call/convert/convert.py @@ -1,6 +1,7 @@ # Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. """提取或格式化Step输出""" +import json from collections.abc import AsyncGenerator from datetime import datetime @@ -9,7 +10,7 @@ from jinja2 import BaseLoader from jinja2.sandbox import SandboxedEnvironment from pydantic import Field -from apps.scheduler.call.core import CoreCall +from apps.scheduler.call.core import CallError, CoreCall from apps.schemas.enum_var import CallOutputType, LanguageType from apps.schemas.scheduler import ( CallInfo, @@ -33,11 +34,11 @@ class Convert(CoreCall, input_model=ConvertInput, output_model=ConvertOutput): i18n_info = { LanguageType.CHINESE: CallInfo( name="模板转换", - description="使用jinja2语法和jsonnet语法,将自然语言信息和原始数据进行格式化。", + description="使用jinja2语法将自然语言信息和原始数据进行格式化。", ), LanguageType.ENGLISH: CallInfo( name="Convert", - description="Use jinja2 and jsonnet syntax to format natural language information and original data.", + description="Use jinja2 syntax to format natural language information and original data.", ), } return i18n_info[language] @@ -54,35 +55,68 @@ class Convert(CoreCall, input_model=ConvertInput, output_model=ConvertOutput): trim_blocks=True, lstrip_blocks=True, ) - return ConvertInput() + # 获取当前时间 + time = datetime.now(tz=pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S") + # 返回空的ConvertInput,因为输入数据来自上一步的输出 + self._extras = { + "time": time, + "history": self._history, + "question": self._question, + "background": self._sys_vars.background, + "ids": self._sys_vars.ids, + } + return ConvertInput( + text_template=self.text_template, + data_template=self.data_template, + extras=self._extras, + ) - async def _exec(self) -> AsyncGenerator[CallOutputChunk, None]: - """ - 调用Convert工具 - :param _slot_data: 经用户确认后的参数(目前未使用) - :return: 提取出的字段 - """ - # 判断用户是否给了值 - time = datetime.now(tz=pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S") - if self.text_template is None: - result_message = last_output.get("message", "") + async def _exec(self) -> AsyncGenerator[CallOutputChunk, None]: + """调用Convert工具""" + # 处理文本模板 + result_message = "" + if self.text_template is not None: + try: + text_template = self._env.from_string(self.text_template) + result_message = text_template.render(**self._extras) + except Exception as e: + raise CallError( + message=f"文本模板渲染错误: {e!s}", + data={ + "template": self.text_template, + "error": str(e), + }, + ) from e else: - text_template = self._env.from_string(self.text_template) - result_message = text_template.render(time=time, history=self._history, question=self._question) - - if self.data_template is None: - result_data = last_output.get("output", {}) + result_message = "未提供文本模板" + + # 处理数据模板 + result_data = {} + if self.data_template is not None: + try: + data_template = self._env.from_string(self.data_template) + rendered_data_str = data_template.render(**self._extras) + # 尝试解析为JSON对象 + result_data = json.loads(rendered_data_str) + except Exception as e: + raise CallError( + message=f"数据模板渲染错误: {e!s}", + data={ + "template": self.data_template, + "error": str(e), + }, + ) from e else: - data_template = self._env.from_string(self.data_template) - result_data = data_template.render( - time=time, - question=self._question, - history=[item.output_data["output"] for item in self._history if "output" in item.output_data], - ) + result_data = {"message": "未提供数据模板"} + # 返回文本和数据两个部分 yield CallOutputChunk( - type=CallOutputType.DATA, + type=CallOutputType.TEXT, content=result_message, ) + yield CallOutputChunk( + type=CallOutputType.DATA, + content=result_data, + ) diff --git a/apps/scheduler/call/convert/schema.py b/apps/scheduler/call/convert/schema.py index 9a4fa1a76..09ee4ff8e 100644 --- a/apps/scheduler/call/convert/schema.py +++ b/apps/scheduler/call/convert/schema.py @@ -1,6 +1,8 @@ # Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. """Convert工具的Schema""" +from typing import Any + from pydantic import Field from apps.scheduler.call.core import DataBase @@ -9,6 +11,9 @@ from apps.scheduler.call.core import DataBase class ConvertInput(DataBase): """定义Convert工具的输入""" + text_template: str | None = Field(description="自然语言信息的格式化模板,jinja2语法", default=None) + data_template: str | None = Field(description="原始数据的格式化模板,jinja2语法", default=None) + extras: dict[str, Any] = Field(description="额外参数", default={}) class ConvertOutput(DataBase): diff --git a/apps/scheduler/call/llm/llm.py b/apps/scheduler/call/llm/llm.py index 3e2ac80c2..6d190d86d 100644 --- a/apps/scheduler/call/llm/llm.py +++ b/apps/scheduler/call/llm/llm.py @@ -33,8 +33,8 @@ class LLM(CoreCall, input_model=LLMInput, output_model=LLMOutput): # 大模型参数 temperature: float = Field(description="大模型温度(随机化程度)", default=0.7) - enable_context: bool = Field(description="是否启用上下文", default=True) - step_history_size: int = Field(description="上下文信息中包含的步骤历史数量", default=3, ge=1, le=10) + step_history_size: int = Field(description="上下文信息中包含的步骤历史数量", default=3, ge=0, le=10) + history_length: int = Field(description="历史对话记录数量", default=0, ge=0) system_prompt: str = Field(description="大模型系统提示词", default="You are a helpful assistant.") user_prompt: str = Field(description="大模型用户提示词", default=LLM_DEFAULT_PROMPT) @@ -70,15 +70,30 @@ class LLM(CoreCall, input_model=LLMInput, output_model=LLMOutput): for ids in call_vars.step_order[-self.step_history_size:]: step_history += [call_vars.step_data[ids]] - if self.enable_context: + if self.step_history_size > 0: context_tmpl = env.from_string(LLM_CONTEXT_PROMPT[self._sys_vars.language]) context_prompt = context_tmpl.render( reasoning=call_vars.thinking, - history_data=step_history, + context_data=step_history, ) else: context_prompt = "无背景信息。" + # 历史对话记录 + history_messages = [] + if self.history_length > 0: + # 从 conversation 中提取历史记录 + conversation = self._sys_vars.background.conversation + # 取最后 history_length 条记录 + recent_conversation = conversation[-self.history_length:] + # 将历史记录转换为消息格式 + for item in recent_conversation: + if "question" in item and "answer" in item: + history_messages.extend([ + {"role": "user", "content": item["question"]}, + {"role": "assistant", "content": item["answer"]}, + ]) + # 参数 time = datetime.now(tz=pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S") formatter = { @@ -99,10 +114,15 @@ class LLM(CoreCall, input_model=LLMInput, output_model=LLMOutput): except Exception as e: raise CallError(message=f"用户提示词渲染失败:{e!s}", data={}) from e - return [ + # 构建消息列表,将历史消息放在前面 + messages = [] + messages.extend(history_messages) + messages.extend([ {"role": "system", "content": system_input}, {"role": "user", "content": user_input}, - ] + ]) + + return messages async def _init(self, call_vars: CallVars) -> LLMInput: diff --git a/apps/scheduler/call/llm/prompt.py b/apps/scheduler/call/llm/prompt.py index b0a7f20dc..c7c7f9adc 100644 --- a/apps/scheduler/call/llm/prompt.py +++ b/apps/scheduler/call/llm/prompt.py @@ -16,7 +16,7 @@ LLM_CONTEXT_PROMPT: dict[LanguageType, str] = { 你作为AI,在完成用户指令前,需要获取必要的信息。为此,你调用了一些工具,并获得了它们的输出: 工具的输出数据将在中给出, 其中为工具的名称,为工具的输出数据。 - {% for tool in history_data %} + {% for tool in context_data %} {{ tool.step_name }} {{ tool.step_description }} @@ -38,7 +38,7 @@ purpose, you have called some tools and obtained their outputs: The output data of the tools will be given in , where is the name of the tool and \ is the output data of the tool. - {% for tool in history_data %} + {% for tool in context_data %} {{ tool.step_name }} {{ tool.step_description }} diff --git a/apps/scheduler/call/rag/rag.py b/apps/scheduler/call/rag/rag.py index 5e2b722ef..85914b902 100644 --- a/apps/scheduler/call/rag/rag.py +++ b/apps/scheduler/call/rag/rag.py @@ -86,56 +86,59 @@ class RAG(CoreCall, input_model=RAGInput, output_model=RAGOutput): ) - async def _get_doc_info(self, doc_ids: list[str], data: RAGInput) -> AsyncGenerator[CallOutputChunk, None]: - """获取文档信息""" + async def _fetch_doc_chunks(self, data: RAGInput) -> list[DocItem]: + """从知识库获取文档分片""" url = config.rag.rag_service.rstrip("/") + "/chunk/search" headers = { "Content-Type": "application/json", "Authorization": f"Bearer {self._sys_vars.ids.session_id}", } + + doc_chunk_list = [] + try: + async with httpx.AsyncClient(timeout=30) as client: + data_json = data.model_dump(exclude_none=True, by_alias=True) + response = await client.post(url, headers=headers, json=data_json) + # 检查响应状态码 + if response.status_code == status.HTTP_200_OK: + result = response.json() + # 对返回的docChunks进行校验 + try: + validated_chunks = [] + for chunk_data in result["result"]["docChunks"]: + validated_chunk = DocItem.model_validate(chunk_data) + validated_chunks.append(validated_chunk) + doc_chunk_list += validated_chunks + except Exception as e: + _logger.error(f"[RAG] chunk校验失败: {e}") + raise + except Exception: + _logger.exception("[RAG] 获取文档分片失败") + + return doc_chunk_list + + async def _get_doc_info(self, doc_ids: list[str], data: RAGInput) -> AsyncGenerator[CallOutputChunk, None]: + """获取文档信息""" doc_chunk_list = [] + + # 处理指定文档ID的情况 if doc_ids: tmp_data = deepcopy(data) tmp_data.kbIds = [ uuid.UUID("00000000-0000-0000-0000-000000000000") ] - try: - async with httpx.AsyncClient(timeout=30) as client: - data_json = tmp_data.model_dump(exclude_none=True, by_alias=True) - response = await client.post(url, headers=headers, json=data_json) - if response.status_code == status.HTTP_200_OK: - result = response.json() - # 对返回的docChunks进行校验 - try: - validated_chunks = [] - for chunk_data in result["result"]["docChunks"]: - validated_chunk = DocItem.model_validate(chunk_data) - validated_chunks.append(validated_chunk) - doc_chunk_list += validated_chunks - except Exception as e: - _logger.error(f"[RAG] chunk校验失败: {e}") - raise - except Exception: - _logger.exception("[RAG] 获取文档分片失败") + doc_chunk_list.extend(await self._fetch_doc_chunks(tmp_data)) + + # 处理知识库ID的情况 if data.kbIds: - try: - async with httpx.AsyncClient(timeout=30) as client: - data_json = data.model_dump(exclude_none=True, by_alias=True) - response = await client.post(url, headers=headers, json=data_json) - # 检查响应状态码 - if response.status_code == status.HTTP_200_OK: - result = response.json() - # 对返回的docChunks进行校验 - try: - validated_chunks = [] - for chunk_data in result["result"]["docChunks"]: - validated_chunk = DocItem.model_validate(chunk_data) - validated_chunks.append(validated_chunk) - doc_chunk_list += validated_chunks - except Exception as e: - _logger.error(f"[RAG] chunk校验失败: {e}") - raise - except Exception: - _logger.exception("[RAG] 获取文档分片失败") - return doc_chunk_list + doc_chunk_list.extend(await self._fetch_doc_chunks(data)) + + # 将文档分片转换为文本片段并返回 + for doc_chunk in doc_chunk_list: + for chunk in doc_chunk.chunks: + text = chunk.text.replace("\n", "") + yield CallOutputChunk( + type=CallOutputType.DATA, + content=text, + ) async def _exec(self, input_data: dict[str, Any]) -> AsyncGenerator[CallOutputChunk, None]: @@ -194,7 +197,8 @@ class RAG(CoreCall, input_model=RAGInput, output_model=RAGOutput): validated_chunks.append(validated_chunk) doc_chunk_list = validated_chunks except Exception as e: - _logger.error(f"[RAG] chunk校验失败: {e}") + err = f"[RAG] chunk校验失败: {e}" + _logger.error(err) # noqa: TRY400 raise corpus = [] diff --git a/apps/scheduler/call/suggest/prompt.py b/apps/scheduler/call/suggest/prompt.py index c63e13251..ef6d9035f 100644 --- a/apps/scheduler/call/suggest/prompt.py +++ b/apps/scheduler/call/suggest/prompt.py @@ -10,10 +10,10 @@ SUGGEST_PROMPT: dict[LanguageType, str] = { r""" - 根据提供的对话和附加信息(用户倾向、历史问题列表、工具信息等),生成三个预测问题。 - 历史提问列表展示的是用户发生在历史对话之前的提问,仅为背景参考作用。 - 对话将在标签中给出,用户倾向将在标签中给出,\ - 历史问题列表将在标签中给出,工具信息将在标签中给出。 + 根据先前的历史对话和提供的附加信息(用户倾向、问题列表、工具信息等)生成三个预测问题。 + 问题列表展示的是用户的过往问题,请避免重复生成这些问题。 + 用户倾向将在标签中给出,历史问题列表将在标签中给出, + 工具信息将在标签中给出。 生成预测问题时的要求: 1. 以用户口吻生成预测问题,数量必须为3个,必须为疑问句或祈使句,必须少于30字。 @@ -29,17 +29,13 @@ SUGGEST_PROMPT: dict[LanguageType, str] = { ] } ``` + - - 杭州有哪些著名景点? - 杭州西湖是中国浙江省杭州市的一个著名景点,以其美丽的自然风光和丰富的文化遗产而闻名。\ -西湖周围有许多著名的景点,包括著名的苏堤、白堤、断桥、三潭印月等。西湖以其清澈的湖水和周围的山脉而著名,是中国最著名的湖泊之一。 - - + 简单介绍一下杭州 杭州有哪些著名景点? - + 景点查询 查询景点信息 @@ -51,8 +47,8 @@ SUGGEST_PROMPT: dict[LanguageType, str] = { { "predicted_questions": [ "杭州西湖景区的门票价格是多少?", - "杭州有哪些著名景点?", - "杭州的天气怎么样?" + "杭州的天气怎么样?", + "杭州有什么特色美食?" ] } @@ -60,21 +56,16 @@ SUGGEST_PROMPT: dict[LanguageType, str] = { 下面是实际的数据: - - {% for message in conversation %} - <{{ message.role }}>{{ message.content }} + 请参考以下历史问题进行问题生成,避免重复已提出的问题: + {% if history %} + + {% for question in history %} + {{ question }} {% endfor %} - - - - {% if history %} - {% for question in history %} - {{ question }} - {% endfor %} - {% else %} - (无历史问题) - {% endif %} - + + {% else %} + (无历史问题) + {% endif %} {% if tool %} @@ -100,14 +91,13 @@ SUGGEST_PROMPT: dict[LanguageType, str] = { r""" - Generate three predicted questions based on the provided conversation and additional information \ -(user preferences, historical question list, tool information, etc.). - The historical question list displays questions asked by the user before the historical \ -conversation and is for background reference only. - The conversation will be given in the tag, the user preferences will be given in \ - the tag, - the historical question list will be given in the tag, and the tool information \ -will be given in the tag. + Generate three predicted questions based on the previous historical dialogue and provided \ +additional information (user preferences, historical question list, tool information, etc.). + The question list displays the user's past questions, which should be avoided when generating \ +predictions. + The user preferences will be given in the tag, + the historical question list will be given in the tag, and the tool \ +information will be given in the tag. Requirements for generating predicted questions: @@ -130,18 +120,10 @@ other than the question. ``` - - What are the famous attractions in Hangzhou ? - Hangzhou West Lake is a famous scenic spot in Hangzhou, Zhejiang Province, China, \ -known for its beautiful natural scenery and rich cultural heritage. There are many famous attractions around West Lake\ -, including the renowned Su Causeway, Bai Causeway, Broken Bridge, and the Three Pools Mirroring the Moon. \ -West Lake is renowned for its clear waters and surrounding mountains, making it one of China's most famous \ -lakes. - - + Briefly introduce Hangzhou What are the famous attractions in Hangzhou ? - + Scenic Spot Search Scenic Spot Information Search @@ -153,8 +135,8 @@ lakes. { "predicted_questions": [ "What is the ticket price for the West Lake Scenic Area in Hangzhou?", - "What are the famous attractions in Hangzhou?", - "What's the weather like in Hangzhou?" + "What's the weather like in Hangzhou?", + "What are the local specialties in Hangzhou?" ] } @@ -162,21 +144,16 @@ lakes. Here's the actual data: - - {% for message in conversation %} - <{{ message.role }}>{{ message.content }} + Please refer to the following history questions for question generation, avoiding duplicate questions: + {% if history %} + + {% for question in history %} + {{ question }} {% endfor %} - - - - {% if history %} - {% for question in history %} - {{ question }} - {% endfor %} - {% else %} - (No history question) - {% endif %} - + + {% else %} + (No history question) + {% endif %} {% if tool %} @@ -195,7 +172,7 @@ lakes. {% endif %} - Now, generate the question: + Now, generate questions: """, ), } diff --git a/apps/scheduler/call/suggest/suggest.py b/apps/scheduler/call/suggest/suggest.py index f58eae0eb..40f938c89 100644 --- a/apps/scheduler/call/suggest/suggest.py +++ b/apps/scheduler/call/suggest/suggest.py @@ -46,7 +46,6 @@ class Suggestion(CoreCall, input_model=SuggestionInput, output_model=SuggestionO configs: list[SingleFlowSuggestionConfig] = Field(description="问题推荐配置", default=[]) num: int = Field(default=3, ge=1, le=6, description="推荐问题的总数量(必须大于等于configs中涉及的Flow的数量)") - context: SkipJsonSchema[list[dict[str, str]]] = Field(description="Executor的上下文", exclude=True) conversation_id: SkipJsonSchema[uuid.UUID] = Field(description="对话ID", exclude=True) @@ -66,21 +65,10 @@ class Suggestion(CoreCall, input_model=SuggestionInput, output_model=SuggestionO @classmethod async def instance(cls, executor: "StepExecutor", node: NodeInfo | None, **kwargs: Any) -> Self: """初始化""" - context = [ - { - "role": "user", - "content": executor.task.runtime.userInput, - }, - { - "role": "assistant", - "content": executor.task.runtime.fullAnswer, - }, - ] obj = cls( name=executor.step.step.name, description=executor.step.step.description, node=node, - context=context, conversation_id=executor.task.metadata.conversationId, **kwargs, ) @@ -163,7 +151,6 @@ class Suggestion(CoreCall, input_model=SuggestionInput, output_model=SuggestionO question = config.question else: prompt = prompt_tpl.render( - conversation=self.context, history=self._history_questions, tool={ "name": config.flow_id, @@ -171,11 +158,14 @@ class Suggestion(CoreCall, input_model=SuggestionInput, output_model=SuggestionO }, preference=user_domain, ) + # 按照正确的顺序:system -> conversation -> prompt + messages = [ + {"role": "system", "content": "You are a helpful assistant."}, + *self._sys_vars.background.conversation, + {"role": "user", "content": prompt}, + ] result = await self._json( - messages=[ - {"role": "system", "content": "You are a helpful assistant."}, - {"role": "user", "content": prompt}, - ], + messages=messages, schema=SuggestGenResult.model_json_schema(), ) questions = SuggestGenResult.model_validate(result) @@ -195,7 +185,6 @@ class Suggestion(CoreCall, input_model=SuggestionInput, output_model=SuggestionO while pushed_questions < self.num: prompt = prompt_tpl.render( - conversation=self.context, history=self._history_questions, tool={ "name": self._flow_id, @@ -203,11 +192,14 @@ class Suggestion(CoreCall, input_model=SuggestionInput, output_model=SuggestionO }, preference=user_domain, ) + # 按照正确的顺序:system -> conversation -> prompt + messages = [ + {"role": "system", "content": "You are a helpful assistant."}, + *self._sys_vars.background.conversation, + {"role": "user", "content": prompt}, + ] result = await self._json( - messages=[ - {"role": "system", "content": "You are a helpful assistant."}, - {"role": "user", "content": prompt}, - ], + messages=messages, schema=SuggestGenResult.model_json_schema(), ) questions = SuggestGenResult.model_validate(result) diff --git a/apps/scheduler/pool/check.py b/apps/scheduler/pool/check.py index 2e1d5f0e5..76dfa3ee0 100644 --- a/apps/scheduler/pool/check.py +++ b/apps/scheduler/pool/check.py @@ -3,6 +3,7 @@ import logging import uuid +from collections.abc import Sequence from hashlib import sha256 from anyio import Path @@ -22,9 +23,13 @@ class FileChecker: def __init__(self) -> None: """初始化文件检查器""" - self.hashes = {} + self.hashes: dict[str, dict[str, str]] = {} self._dir_path = Path(config.deploy.data_dir) / "semantics" + async def _hashes_to_dict(self, hashes: Sequence[AppHashes] | Sequence[ServiceHashes]) -> dict[str, str]: + """将哈希对象列表转换为字典格式""" + return {hash_obj.filePath: hash_obj.hash for hash_obj in hashes} + async def check_one(self, path: Path) -> dict[str, str]: """检查单个App/Service文件是否有变动""" hashes = {} @@ -45,14 +50,20 @@ class FileChecker: return hashes - async def diff_one(self, path: Path, previous_hashes: AppHashes | ServiceHashes | None = None) -> bool: + async def diff_one( + self, path: Path, previous_hashes: Sequence[AppHashes] | Sequence[ServiceHashes] | None = None, + ) -> bool: """检查文件是否发生变化""" self._resource_path = path semantics_path = Path(config.deploy.data_dir) / "semantics" path_diff = self._resource_path.relative_to(semantics_path) - # FIXME 不能使用字典比对,必须一条条比对 self.hashes[path_diff.as_posix()] = await self.check_one(path) - return self.hashes[path_diff.as_posix()] != previous_hashes + if previous_hashes is None: + # 如果没有之前的哈希记录,则认为发生了变化 + return True + # 将数据库中的哈希记录转换为字典格式进行比较 + previous_hashes_dict = await self._hashes_to_dict(previous_hashes) + return self.hashes[path_diff.as_posix()] != previous_hashes_dict async def diff(self, check_type: MetadataType) -> tuple[list[uuid.UUID], list[uuid.UUID]]: @@ -80,11 +91,11 @@ class FileChecker: if check_type == MetadataType.APP: hashes = ( await session.scalars(select(AppHashes).where(AppHashes.appId == list_item)) - ).one() + ).all() elif check_type == MetadataType.SERVICE: hashes = ( await session.scalars(select(ServiceHashes).where(ServiceHashes.serviceId == list_item)) - ).one() + ).all() # 判断是否发生变化 if await self.diff_one(Path(self._dir_path / str(list_item)), hashes): changed_list.append(list_item) diff --git a/apps/scheduler/pool/loader/app.py b/apps/scheduler/pool/loader/app.py index 2cf593635..4ffb1c226 100644 --- a/apps/scheduler/pool/loader/app.py +++ b/apps/scheduler/pool/loader/app.py @@ -110,6 +110,8 @@ class AppLoader: # 重新载入 file_checker = FileChecker() await file_checker.diff_one(app_path) + # 保存文件hash到数据库 + await self._update_db(metadata, file_checker.hashes[f"app/{app_id}"]) await self.load(app_id, file_checker.hashes[f"app/{app_id}"]) @@ -134,7 +136,11 @@ class AppLoader: shutil.rmtree(str(app_path), ignore_errors=True) - async def _update_db(self, metadata: AppMetadata | AgentAppMetadata) -> None: + async def _update_db( + self, + metadata: AppMetadata | AgentAppMetadata, + file_hashes: dict[str, str] | None = None, + ) -> None: """更新数据库""" if not metadata.hashes: err = f"[AppLoader] 应用 {metadata.id} 的哈希值为空" @@ -166,6 +172,14 @@ class AppLoader: action="", )) # 保存AppHashes表 + if file_hashes: + # 清除旧的hash记录 + await session.execute(delete(AppHashes).where(AppHashes.appId == metadata.id)) + # 添加新的hash记录 + for file_path, hash_value in file_hashes.items(): + session.add(AppHashes( + appId=metadata.id, + filePath=file_path, + hash=hash_value, + )) await session.commit() - - # FIXME 更新Hash值? diff --git a/apps/scheduler/pool/loader/service.py b/apps/scheduler/pool/loader/service.py index 3b165edb9..949d84088 100644 --- a/apps/scheduler/pool/loader/service.py +++ b/apps/scheduler/pool/loader/service.py @@ -114,6 +114,7 @@ class ServiceLoader: # 删除旧的数据 await session.execute(delete(Service).where(Service.id == metadata.id)) await session.execute(delete(NodeInfo).where(NodeInfo.serviceId == metadata.id)) + await session.execute(delete(ServiceHashes).where(ServiceHashes.serviceId == metadata.id)) # 插入新的数据 service_data = Service( @@ -127,6 +128,16 @@ class ServiceLoader: for node in nodes: session.add(node) + + # 保存哈希值 + for file_path, hash_value in metadata.hashes.items(): + hash_data = ServiceHashes( + serviceId=metadata.id, + filePath=file_path, + hash=hash_value, + ) + session.add(hash_data) + await session.commit() diff --git a/apps/schemas/agent.py b/apps/schemas/agent.py index 1ffc56cae..3b7cc673f 100644 --- a/apps/schemas/agent.py +++ b/apps/schemas/agent.py @@ -19,4 +19,3 @@ class AgentAppMetadata(MetadataBase): history_len: int = Field(description="对话轮次", default=3, le=10) mcp_service: list[str] = Field(default=[], description="MCP服务id列表") permission: Permission | None = Field(description="应用权限配置", default=None) - version: str = Field(description="元数据版本") diff --git a/apps/schemas/appcenter.py b/apps/schemas/appcenter.py index 63f1251db..cf953c3d3 100644 --- a/apps/schemas/appcenter.py +++ b/apps/schemas/appcenter.py @@ -6,6 +6,7 @@ import uuid from pydantic import BaseModel, Field from .enum_var import AppType, PermissionType +from .response_data import ResponseData class AppCenterCardItem(BaseModel): @@ -69,6 +70,14 @@ class AppData(BaseModel): mcp_service: list[str] = Field(default=[], alias="mcpService", description="MCP服务id列表") +class AppMcpServiceInfo(BaseModel): + """MCP服务信息""" + + id: uuid.UUID = Field(description="MCP服务ID") + name: str = Field(description="MCP服务名称") + description: str = Field(description="MCP服务描述") + + class CreateAppRequest(AppData): """POST /api/app 请求数据结构""" @@ -79,3 +88,76 @@ class ChangeFavouriteAppRequest(BaseModel): """PUT /api/app/{appId} 请求数据结构""" favorited: bool = Field(..., description="是否收藏") + + +class GetAppPropertyMsg(AppData): + """GET /api/app/{appId} Result数据结构""" + + app_id: str = Field(..., alias="appId", description="应用ID") + published: bool = Field(..., description="是否已发布") + mcp_service: list[AppMcpServiceInfo] = Field(default=[], alias="mcpService", description="MCP服务信息列表") + + +class GetAppPropertyRsp(ResponseData): + """GET /api/app/{appId} 返回数据结构""" + + result: GetAppPropertyMsg + + +class ChangeFavouriteAppMsg(BaseModel): + """PUT /api/app/{appId} Result数据结构""" + + app_id: uuid.UUID = Field(..., alias="appId", description="应用ID") + favorited: bool = Field(..., description="是否已收藏") + + +class ChangeFavouriteAppRsp(ResponseData): + """PUT /api/app/{appId} 返回数据结构""" + + result: ChangeFavouriteAppMsg + + +class GetAppListMsg(BaseModel): + """GET /api/app Result数据结构""" + + page_number: int = Field(..., alias="currentPage", description="当前页码") + app_count: int = Field(..., alias="totalApps", description="总应用数") + applications: list[AppCenterCardItem] = Field(..., description="应用列表") + + +class GetAppListRsp(ResponseData): + """GET /api/app 返回数据结构""" + + result: GetAppListMsg + + +class RecentAppListItem(BaseModel): + """GET /api/app/recent 列表项数据结构""" + + app_id: uuid.UUID = Field(..., alias="appId", description="应用ID") + name: str = Field(..., description="应用名称") + + +class RecentAppList(BaseModel): + """GET /api/app/recent Result数据结构""" + + applications: list[RecentAppListItem] = Field(..., description="最近使用的应用列表") + + +class GetRecentAppListRsp(ResponseData): + """GET /api/app/recent 返回数据结构""" + + result: RecentAppList + + +class BaseAppOperationMsg(BaseModel): + """基础应用操作Result数据结构""" + + app_id: uuid.UUID = Field(..., alias="appId", description="应用ID") + + +class BaseAppOperationRsp(ResponseData): + """基础应用操作返回数据结构""" + + result: BaseAppOperationMsg + diff --git a/apps/schemas/conversation.py b/apps/schemas/conversation.py index 8a5507b93..3c310af85 100644 --- a/apps/schemas/conversation.py +++ b/apps/schemas/conversation.py @@ -1,7 +1,11 @@ +"""对话相关数据结构""" + import uuid from pydantic import BaseModel, Field +from .response_data import ResponseData + class ChangeConversationData(BaseModel): """修改会话信息""" @@ -13,3 +17,56 @@ class DeleteConversationData(BaseModel): """删除会话""" conversation_list: list[uuid.UUID] = Field(alias="conversationList") + + +class ConversationListItem(BaseModel): + """GET /api/conversation Result数据结构""" + + conversation_id: uuid.UUID = Field(alias="conversationId") + title: str + doc_count: int = Field(alias="docCount") + created_time: str = Field(alias="createdTime") + app_id: str = Field(alias="appId") + debug: bool = Field(alias="debug") + + +class ConversationListMsg(BaseModel): + """GET /api/conversation Result数据结构""" + + conversations: list[ConversationListItem] + + +class ConversationListRsp(ResponseData): + """GET /api/conversation 返回数据结构""" + + result: ConversationListMsg + + +class DeleteConversationMsg(BaseModel): + """DELETE /api/conversation Result数据结构""" + + conversation_id_list: list[str] = Field(alias="conversationIdList") + + +class DeleteConversationRsp(ResponseData): + """DELETE /api/conversation 返回数据结构""" + + result: DeleteConversationMsg + + +class AddConversationMsg(BaseModel): + """POST /api/conversation Result数据结构""" + + conversation_id: uuid.UUID = Field(alias="conversationId") + + +class AddConversationRsp(ResponseData): + """POST /api/conversation 返回数据结构""" + + result: AddConversationMsg + + +class UpdateConversationRsp(ResponseData): + """POST /api/conversation 返回数据结构""" + + result: ConversationListItem diff --git a/apps/schemas/document.py b/apps/schemas/document.py index 3e4d9901d..89da317a2 100644 --- a/apps/schemas/document.py +++ b/apps/schemas/document.py @@ -1,18 +1,24 @@ # Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. """FastAPI 返回数据结构 - 文档相关""" +import uuid +from datetime import datetime + from pydantic import BaseModel, Field from .enum_var import DocumentStatus -class ConversationDocumentItem(BaseModel): - """GET /api/document/{conversation_id} Result内元素数据结构""" +class BaseDocumentItem(BaseModel): + """文档公共字段""" - id: str = Field(alias="_id", default="") - user_sub: None = None - status: DocumentStatus - conversation_id: None = None + id: uuid.UUID = Field(description="文档ID") + name: str = Field(default="", description="文档名称") + type: str = Field(default="", description="文档类型") + size: float = Field(default=0.0, description="文档大小") + created_at: datetime | None = Field(default=None, description="创建时间") + user_sub: str | None = None + conversation_id: str | None = None class Config: """配置""" @@ -20,6 +26,12 @@ class ConversationDocumentItem(BaseModel): populate_by_name = True +class ConversationDocumentItem(BaseDocumentItem): + """GET /api/document/{conversation_id} Result内元素数据结构""" + + status: DocumentStatus + + class ConversationDocumentMsg(BaseModel): """GET /api/document/{conversation_id} Result数据结构""" @@ -34,27 +46,10 @@ class ConversationDocumentRsp(BaseModel): result: ConversationDocumentMsg -class UploadDocumentMsgItem(BaseModel): - """POST /api/document/{conversation_id} 返回数据结构""" - - id: str = Field(alias="_id", default="") - user_sub: None = None - name: str = Field(default="", description="文档名称") - type: str = Field(default="", description="文档类型") - size: float = Field(default=0.0, description="文档大小") - created_at: None = None - conversation_id: None = None - - class Config: - """配置""" - - populate_by_name = True - - class UploadDocumentMsg(BaseModel): """POST /api/document/{conversation_id} 返回数据结构""" - documents: list[UploadDocumentMsgItem] + documents: list[BaseDocumentItem] class UploadDocumentRsp(BaseModel): diff --git a/apps/schemas/enum_var.py b/apps/schemas/enum_var.py index c69ced6f6..53304d2eb 100644 --- a/apps/schemas/enum_var.py +++ b/apps/schemas/enum_var.py @@ -174,27 +174,9 @@ class AppType(str, Enum): class AppFilterType(str, Enum): """应用过滤类型""" - ALL = "all" # 所有已发布的应用 - USER = "user" # 用户创建的应用 - FAVORITE = "favorite" # 用户收藏的应用 - - -class Role(str, Enum): - """Message role类型""" - - SYSTEM = "system" + ALL = "all" USER = "user" - ASSISTANT = "assistant" - TOOL = "tool" - - -class AgentState(str, Enum): - """Agent执行状态""" - - IDLE = "IDLE" - RUNNING = "RUNNING" - FINISHED = "FINISHED" - ERROR = "ERROR" + FAVORITE = "favorite" class LanguageType(str, Enum): diff --git a/apps/schemas/flow.py b/apps/schemas/flow.py index 68f771991..46f750c86 100644 --- a/apps/schemas/flow.py +++ b/apps/schemas/flow.py @@ -13,6 +13,8 @@ from .enum_var import ( MetadataType, PermissionType, ) +from .flow_topology import FlowItem +from .response_data import ResponseData class Edge(BaseModel): @@ -160,3 +162,40 @@ class AppMetadata(MetadataBase): history_len: int = Field(description="对话轮次", default=3, le=10) permission: Permission | None = Field(description="应用权限配置", default=None) flows: list[AppFlow] = Field(description="Flow列表", default=[]) + + +class FlowStructureGetMsg(BaseModel): + """GET /api/flow result""" + + flow: FlowItem = Field(default=FlowItem()) + + +class FlowStructureGetRsp(ResponseData): + """GET /api/flow 返回数据结构""" + + result: FlowStructureGetMsg + + +class FlowStructurePutMsg(BaseModel): + """PUT /api/flow result""" + + flow: FlowItem = Field(default=FlowItem()) + + +class FlowStructurePutRsp(ResponseData): + """PUT /api/flow 返回数据结构""" + + result: FlowStructurePutMsg + + +class FlowStructureDeleteMsg(BaseModel): + """DELETE /api/flow/ result""" + + flow_id: str = Field(alias="flowId", default="") + + +class FlowStructureDeleteRsp(ResponseData): + """DELETE /api/flow/ 返回数据结构""" + + result: FlowStructureDeleteMsg + flows: list[AppFlow] = Field(description="Flow列表", default=[]) diff --git a/apps/schemas/mcp.py b/apps/schemas/mcp.py index 063b37a33..e64391f4b 100644 --- a/apps/schemas/mcp.py +++ b/apps/schemas/mcp.py @@ -104,6 +104,7 @@ class IsParamError(BaseModel): is_param_error: bool = Field(description="是否是参数错误", default=False) + class MCPSelectResult(BaseModel): """MCP选择结果""" @@ -135,7 +136,7 @@ class Step(BaseModel): class UpdateMCPServiceRequest(BaseModel): """POST /api/mcpservice 请求数据结构""" - service_id: str | None = Field(None, alias="serviceId", description="服务ID(更新时传递)") + mcp_id: str = Field(alias="mcpId", description="MCP服务ID(更新时传递)") name: str = Field(..., description="MCP服务名称") description: str = Field(..., description="MCP服务描述") overview: str = Field(..., description="MCP服务概述") diff --git a/apps/schemas/mcp_service.py b/apps/schemas/mcp_service.py index a2af0824e..cd26578d5 100644 --- a/apps/schemas/mcp_service.py +++ b/apps/schemas/mcp_service.py @@ -1,7 +1,6 @@ # Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. """MCP 服务相关数据结构""" -import uuid from typing import Any from pydantic import BaseModel, Field @@ -44,7 +43,7 @@ class GetMCPServiceListRsp(ResponseData): class UpdateMCPServiceMsg(BaseModel): """插件中心:MCP服务属性数据结构""" - service_id: uuid.UUID = Field(..., alias="serviceId", description="MCP服务ID") + service_id: str = Field(..., alias="serviceId", description="MCP服务ID") name: str = Field(..., description="MCP服务名称") diff --git a/apps/schemas/message.py b/apps/schemas/message.py index b8c137c76..1101ae4f7 100644 --- a/apps/schemas/message.py +++ b/apps/schemas/message.py @@ -90,13 +90,6 @@ class DocumentAddContent(BaseModel): ) -class FlowStartContent(BaseModel): - """flow.start消息的content""" - - question: str = Field(description="用户问题") - params: dict[str, Any] | None = Field(description="预先提供的参数", default=None) - - class MessageBase(HeartbeatData): """基础消息事件结构""" diff --git a/apps/schemas/request_data.py b/apps/schemas/request_data.py index aa588728b..de9d979d8 100644 --- a/apps/schemas/request_data.py +++ b/apps/schemas/request_data.py @@ -52,8 +52,8 @@ class UpdateLLMReq(BaseModel): """更新大模型请求体""" llm_id: str | None = Field(default=None, description="大模型ID", alias="id") - openai_base_url: str = Field(default="", description="OpenAI API Base URL", alias="openaiBaseUrl") - openai_api_key: str = Field(default="", description="OpenAI API Key", alias="openaiApiKey") + base_url: str = Field(default="", description="OpenAI API Base URL", alias="baseUrl") + api_key: str = Field(default="", description="OpenAI API Key", alias="apiKey") model_name: str = Field(default="", description="模型名称", alias="modelName") max_tokens: int = Field(default=8192, description="最大token数", alias="maxTokens") provider: LLMProvider = Field(description="大模型提供商", alias="provider") @@ -68,12 +68,6 @@ class UpdateUserSelectedLLMReq(BaseModel): embeddingLLM: str = Field(description="Embedding LLM ID") # noqa: N815 -class DeleteLLMReq(BaseModel): - """删除大模型请求体""" - - llm_id: str = Field(description="大模型ID", alias="llmId") - - class UpdateUserKnowledgebaseReq(BaseModel): """更新知识库请求体""" @@ -83,4 +77,6 @@ class UpdateUserKnowledgebaseReq(BaseModel): class UserUpdateRequest(BaseModel): """更新用户信息请求体""" + user_name: str | None = Field(default=None, description="用户名", alias="userName") auto_execute: bool = Field(default=False, description="是否自动执行", alias="autoExecute") + agreement_confirmed: bool | None = Field(default=None, description="协议确认状态", alias="agreementConfirmed") diff --git a/apps/schemas/response_data.py b/apps/schemas/response_data.py index db00fe71a..1b55b209c 100644 --- a/apps/schemas/response_data.py +++ b/apps/schemas/response_data.py @@ -6,10 +6,6 @@ from typing import Any from pydantic import BaseModel, Field -from .appcenter import AppCenterCardItem, AppData -from .flow_topology import ( - FlowItem, -) from .parameters import ( BoolOperate, DictOperate, @@ -51,58 +47,6 @@ class HealthCheckRsp(BaseModel): status: str -class ConversationListItem(BaseModel): - """GET /api/conversation Result数据结构""" - - conversation_id: uuid.UUID = Field(alias="conversationId") - title: str - doc_count: int = Field(alias="docCount") - created_time: str = Field(alias="createdTime") - app_id: str = Field(alias="appId") - debug: bool = Field(alias="debug") - - -class ConversationListMsg(BaseModel): - """GET /api/conversation Result数据结构""" - - conversations: list[ConversationListItem] - - -class ConversationListRsp(ResponseData): - """GET /api/conversation 返回数据结构""" - - result: ConversationListMsg - - -class DeleteConversationMsg(BaseModel): - """DELETE /api/conversation Result数据结构""" - - conversation_id_list: list[str] = Field(alias="conversationIdList") - - -class DeleteConversationRsp(ResponseData): - """DELETE /api/conversation 返回数据结构""" - - result: DeleteConversationMsg - - -class AddConversationMsg(BaseModel): - """POST /api/conversation Result数据结构""" - - conversation_id: uuid.UUID = Field(alias="conversationId") - - -class AddConversationRsp(ResponseData): - """POST /api/conversation 返回数据结构""" - - result: AddConversationMsg - - -class UpdateConversationRsp(ResponseData): - """POST /api/conversation 返回数据结构""" - - result: ConversationListItem - class RecordListMsg(BaseModel): """GET /api/record/{conversation_id} Result数据结构""" @@ -141,114 +85,6 @@ class ListTeamKnowledgeRsp(ResponseData): result: ListTeamKnowledgeMsg -class BaseAppOperationMsg(BaseModel): - """基础应用操作Result数据结构""" - - app_id: uuid.UUID = Field(..., alias="appId", description="应用ID") - - -class BaseAppOperationRsp(ResponseData): - """基础应用操作返回数据结构""" - - result: BaseAppOperationMsg - - -class GetAppPropertyMsg(AppData): - """GET /api/app/{appId} Result数据结构""" - - app_id: str = Field(..., alias="appId", description="应用ID") - published: bool = Field(..., description="是否已发布") - mcp_service: list[AppMcpServiceInfo] = Field(default=[], alias="mcpService", description="MCP服务信息列表") - - -class GetAppPropertyRsp(ResponseData): - """GET /api/app/{appId} 返回数据结构""" - - result: GetAppPropertyMsg - - -class ChangeFavouriteAppMsg(BaseModel): - """PUT /api/app/{appId} Result数据结构""" - - app_id: str = Field(..., alias="appId", description="应用ID") - favorited: bool = Field(..., description="是否已收藏") - - -class ChangeFavouriteAppRsp(ResponseData): - """PUT /api/app/{appId} 返回数据结构""" - - result: ChangeFavouriteAppMsg - - -class GetAppListMsg(BaseModel): - """GET /api/app Result数据结构""" - - page_number: int = Field(..., alias="currentPage", description="当前页码") - app_count: int = Field(..., alias="totalApps", description="总应用数") - applications: list[AppCenterCardItem] = Field(..., description="应用列表") - - -class GetAppListRsp(ResponseData): - """GET /api/app 返回数据结构""" - - result: GetAppListMsg - - -class RecentAppListItem(BaseModel): - """GET /api/app/recent 列表项数据结构""" - - app_id: uuid.UUID = Field(..., alias="appId", description="应用ID") - name: str = Field(..., description="应用名称") - - -class RecentAppList(BaseModel): - """GET /api/app/recent Result数据结构""" - - applications: list[RecentAppListItem] = Field(..., description="最近使用的应用列表") - - -class GetRecentAppListRsp(ResponseData): - """GET /api/app/recent 返回数据结构""" - - result: RecentAppList - - -class FlowStructureGetMsg(BaseModel): - """GET /api/flow result""" - - flow: FlowItem = Field(default=FlowItem()) - - -class FlowStructureGetRsp(ResponseData): - """GET /api/flow 返回数据结构""" - - result: FlowStructureGetMsg - - -class FlowStructurePutMsg(BaseModel): - """PUT /api/flow result""" - - flow: FlowItem = Field(default=FlowItem()) - - -class FlowStructurePutRsp(ResponseData): - """PUT /api/flow 返回数据结构""" - - result: FlowStructurePutMsg - - -class FlowStructureDeleteMsg(BaseModel): - """DELETE /api/flow/ result""" - - flow_id: str = Field(alias="flowId", default="") - - -class FlowStructureDeleteRsp(ResponseData): - """DELETE /api/flow/ 返回数据结构""" - - result: FlowStructureDeleteMsg - - class UserGetMsp(BaseModel): """GET /api/user result""" diff --git a/apps/services/flow.py b/apps/services/flow.py index ae07a394b..a8aa26901 100644 --- a/apps/services/flow.py +++ b/apps/services/flow.py @@ -12,8 +12,7 @@ from apps.models.flow import Flow as FlowInfo from apps.models.node import NodeInfo from apps.models.service import Service from apps.models.user import UserFavorite, UserFavoriteType -from apps.scheduler.pool.loader.app import AppLoader -from apps.scheduler.pool.loader.flow import FlowLoader +from apps.scheduler.pool.pool import Pool from apps.scheduler.slot.slot import Slot from apps.schemas.enum_var import EdgeType from apps.schemas.flow import AppMetadata, Edge, Flow, Step @@ -178,8 +177,7 @@ class FlowManager: logger.error(err) raise ValueError(err) - flow_loader = FlowLoader() - flow_config = await flow_loader.load(app_id, flow_id) + flow_config = await Pool().flow_loader.load(app_id, flow_id) flow_item = FlowItem( flowId=flow_id, name=flow_config.name, @@ -303,7 +301,6 @@ class FlowManager: raise ValueError(err) # Flow模版 - # TODO: 需要看前端能否直接组装basicConfig if not flow_item.basic_config: err = "[FlowManager] basic_config is required" logger.error(err) @@ -341,13 +338,11 @@ class FlowManager: flow_config.edges.append(edge_config) # 检查是否是修改动作;检查修改前后是否等价 - flow_loader = FlowLoader() - old_flow_config = await flow_loader.load(app_id, flow_id) + old_flow_config = await Pool().flow_loader.load(app_id, flow_id) if old_flow_config and old_flow_config.checkStatus.debug: flow_config.checkStatus.debug = await FlowManager.is_flow_config_equal(old_flow_config, flow_config) - flow_loader = FlowLoader() - await flow_loader.save(app_id, flow_id, flow_config) + await Pool().flow_loader.save(app_id, flow_id, flow_config) @staticmethod @@ -358,8 +353,7 @@ class FlowManager: :param app_id: 应用的id :param flow_id: 流的id """ - flow_loader = FlowLoader() - await flow_loader.delete(app_id, flow_id) + await Pool().flow_loader.delete(app_id, flow_id) async with postgres.session() as session: key = f"flow/{flow_id}.yaml" @@ -371,14 +365,19 @@ class FlowManager: ), ), ) - - metadata = await AppLoader.read_metadata(app_id) + # 同步更新metadata yaml文件中的hash值 + metadata = await Pool().app_loader.read_metadata(app_id) if not isinstance(metadata, AppMetadata): err = f"[FlowManager] 应用 {app_id} 不是Flow应用" logger.error(err) raise TypeError(err) + + # 从metadata的hashes中移除对应的flow文件hash + if metadata.hashes and key in metadata.hashes: + del metadata.hashes[key] + metadata.flows = [flow for flow in metadata.flows if flow.id != flow_id] - await AppLoader.save(metadata, app_id) + await Pool().app_loader.save(metadata, app_id) @staticmethod @@ -392,12 +391,11 @@ class FlowManager: :return: 是否更新成功 """ # 由于调用位置,从文件系统中获取Flow数据 - flow_loader = FlowLoader() - flow = await flow_loader.load(app_id, flow_id) + flow = await Pool().flow_loader.load(app_id, flow_id) if flow is None: return False flow.checkStatus.debug = debug # 保存到文件系统 - await flow_loader.save(app_id=app_id, flow_id=flow_id, flow=flow) + await Pool().flow_loader.save(app_id=app_id, flow_id=flow_id, flow=flow) return True diff --git a/apps/services/llm.py b/apps/services/llm.py index 893301587..a61356534 100644 --- a/apps/services/llm.py +++ b/apps/services/llm.py @@ -13,11 +13,9 @@ from apps.schemas.request_data import ( UpdateUserSelectedLLMReq, ) from apps.schemas.response_data import ( - LLMProvider, LLMProviderInfo, UserSelectedLLMData, ) -from apps.templates.generate_llm_operator_config import llm_provider_dict logger = logging.getLogger(__name__) @@ -25,25 +23,6 @@ logger = logging.getLogger(__name__) class LLMManager: """大模型管理""" - @staticmethod - async def list_llm_provider() -> list[LLMProvider]: - """ - 获取大模型提供商列表 - - :return: 大模型提供商列表 - """ - provider_list = [] - for provider in llm_provider_dict.values(): - item = LLMProvider( - provider=provider["provider"], - url=provider["url"], - description=provider["description"], - icon=provider["icon"], - ) - provider_list.append(item) - return provider_list - - @staticmethod async def get_user_selected_llm(user_sub: str) -> UserSelectedLLMData | None: """ @@ -142,8 +121,8 @@ class LLMManager: if not llm: err = f"[LLMManager] LLM {llm_id} 不存在" raise ValueError(err) - llm.baseUrl = req.openai_base_url - llm.apiKey = req.openai_api_key + llm.baseUrl = req.base_url + llm.apiKey = req.api_key llm.modelName = req.model_name llm.maxToken = req.max_tokens llm.provider = req.provider @@ -153,8 +132,8 @@ class LLMManager: else: llm = LLMData( id=llm_id, - baseUrl=req.openai_base_url, - apiKey=req.openai_api_key, + baseUrl=req.base_url, + apiKey=req.api_key, modelName=req.model_name, maxToken=req.max_tokens, provider=req.provider, diff --git a/apps/services/mcp_service.py b/apps/services/mcp_service.py index b77801e3a..a1c7c276f 100644 --- a/apps/services/mcp_service.py +++ b/apps/services/mcp_service.py @@ -34,7 +34,7 @@ from apps.schemas.mcp import ( MCPServerStdioConfig, UpdateMCPServiceRequest, ) -from apps.schemas.response_data import MCPServiceCardItem +from apps.schemas.mcp_service import MCPServiceCardItem logger = logging.getLogger(__name__) MCP_ICON_PATH = ICON_PATH / "mcp" @@ -222,7 +222,7 @@ class MCPServiceManager: @staticmethod - async def create_mcpservice(data: UpdateMCPServiceRequest, user_sub: str) -> uuid.UUID: + async def create_mcpservice(data: UpdateMCPServiceRequest, user_sub: str) -> str: """ 创建MCP服务 @@ -235,16 +235,13 @@ class MCPServiceManager: else: config = MCPServerStdioConfig.model_validate(data.config) - if not data.service_id: - data.service_id = str(uuid.uuid4()) - # 构造Server mcp_server = MCPServerConfig( name=await MCPServiceManager.clean_name(data.name), overview=data.overview, description=data.description, mcpServers={ - data.service_id: config, + data.mcp_id: config, }, mcpType=data.mcp_type, author=user_sub, @@ -259,7 +256,7 @@ class MCPServiceManager: # 保存并载入配置 logger.info("[MCPServiceManager] 创建mcp:%s", mcp_server.name) - mcp_path = MCP_PATH / "template" / data.service_id / "project" + mcp_path = MCP_PATH / "template" / data.mcp_id / "project" index = None if isinstance(config, MCPServerStdioConfig): index = None @@ -275,28 +272,37 @@ class MCPServiceManager: config.args[index] = str(mcp_path) else: config.args += ["--directory", str(mcp_path)] - await MCPLoader._insert_template_db(mcp_id=mcp_id, config=mcp_server) - await MCPLoader.save_one(mcp_server.id, mcp_server) - await MCPLoader.update_template_status(mcp_id, MCPInstallStatus.INIT) - return mcp_server.id + async with postgres.session() as session: + await session.merge(MCPInfo( + id=data.mcp_id, + name=mcp_server.name, + overview=mcp_server.overview, + description=mcp_server.description, + mcpType=mcp_server.mcpType, + author=mcp_server.author or "", + )) + await session.commit() + await MCPLoader.save_one(data.mcp_id, mcp_server) + await MCPLoader.update_template_status(data.mcp_id, MCPInstallStatus.INIT) + return data.mcp_id @staticmethod - async def update_mcpservice(data: UpdateMCPServiceRequest, user_sub: str) -> uuid.UUID: + async def update_mcpservice(data: UpdateMCPServiceRequest, user_sub: str) -> str: """ 更新MCP服务 :param UpdateMCPServiceRequest data: MCP服务配置 :return: MCP服务ID """ - if not data.service_id: + if not data.mcp_id: msg = "[MCPServiceManager] MCP服务ID为空" raise ValueError(msg) async with postgres.session() as session: db_service = (await session.scalars(select(MCPInfo).where( and_( - MCPInfo.id == data.service_id, + MCPInfo.id == data.mcp_id, MCPInfo.author == user_sub, ), ))).one_or_none() @@ -304,27 +310,33 @@ class MCPServiceManager: msg = "[MCPServiceManager] MCP服务未找到或无权限" raise ValueError(msg) - db_service = MCPCollection.model_validate(db_service) + db_service = MCPInfo.model_validate(db_service) for user_id in db_service.activated: - await MCPServiceManager.deactive_mcpservice(user_sub=user_id, mcp_id=data.service_id) + await MCPServiceManager.deactive_mcpservice(user_sub=user_id, mcp_id=data.mcp_id) mcp_config = MCPServerConfig( name=data.name, overview=data.overview, description=data.description, - mcpServers=MCPServerStdioConfig.model_validate( - data.config, - ) if data.mcp_type == MCPType.STDIO else MCPServerSSEConfig.model_validate( - data.config, - ), + mcpServers={ + data.mcp_id: config, + }, mcpType=data.mcp_type, author=user_sub, ) - await MCPLoader._insert_template_db(mcp_id=data.service_id, config=mcp_config) - await MCPLoader.save_one(mcp_id=data.service_id, config=mcp_config) - await MCPLoader.update_template_status(data.service_id, MCPInstallStatus.INIT) - # 返回服务ID - return data.service_id + async with postgres.session() as session: + await session.merge(MCPInfo( + id=data.mcp_id, + name=mcp_config.name, + overview=mcp_config.overview, + description=mcp_config.description, + mcpType=mcp_config.mcpType, + author=mcp_config.author or "", + )) + await session.commit() + await MCPLoader.save_one(mcp_id=data.mcp_id, config=mcp_config) + await MCPLoader.update_template_status(data.mcp_id, MCPInstallStatus.INIT) + return data.mcp_id @staticmethod @@ -469,7 +481,7 @@ class MCPServiceManager: @staticmethod - async def install_mcpservice(user_sub: str, service_id: str, install: bool) -> None: + async def install_mcpservice(user_sub: str, service_id: str, *, install: bool) -> None: """ 安装或卸载MCP服务 diff --git a/apps/services/user.py b/apps/services/user.py index 8a74d2467..86cde5919 100644 --- a/apps/services/user.py +++ b/apps/services/user.py @@ -8,6 +8,7 @@ from sqlalchemy import func, select from apps.common.postgres import postgres from apps.models.user import User +from apps.schemas.request_data import UserUpdateRequest from .conversation import ConversationManager @@ -47,12 +48,16 @@ class UserManager: @staticmethod - async def update_user(user_sub: str) -> None: + async def update_user(user_sub: str, data: UserUpdateRequest) -> None: """ 根据用户sub更新用户信息 :param user_sub: 用户sub + :param data: 更新数据 """ + # 将 Pydantic 模型转换为字典 + update_data = data.model_dump(exclude_unset=True, exclude_none=True) + async with postgres.session() as session: user = ( await session.scalars(select(User).where(User.userSub == user_sub)) @@ -68,9 +73,24 @@ class UserManager: await session.commit() return + # 更新指定字段 + for key, value in update_data.items(): + if hasattr(user, key) and value is not None: + setattr(user, key, value) + user.lastLogin = datetime.now(tz=UTC) await session.commit() + @staticmethod + async def update_userinfo_by_user_sub(user_sub: str, data: UserUpdateRequest) -> None: + """ + 根据用户sub更新用户信息(兼容旧接口) + + :param user_sub: 用户sub + :param data: 更新数据 + """ + await UserManager.update_user(user_sub, data) + @staticmethod async def delete_user(user_sub: str) -> None: """ diff --git a/deploy/chart/authhub/Chart.yaml b/deploy/chart/authhub/Chart.yaml index c1029a168..59e2499f4 100644 --- a/deploy/chart/authhub/Chart.yaml +++ b/deploy/chart/authhub/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: authhub-chart description: AuthHub Helm部署包 type: application -version: 0.9.6 -appVersion: "0.9.6" +version: 0.10.0 +appVersion: "0.10.0" diff --git a/deploy/chart/databases/Chart.yaml b/deploy/chart/databases/Chart.yaml index f75228d53..448a3c5f8 100644 --- a/deploy/chart/databases/Chart.yaml +++ b/deploy/chart/databases/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: euler-copilot-databases description: Euler Copilot 数据库 Helm部署包 type: application -version: 0.9.6 -appVersion: "0.9.6" +version: 0.10.0 +appVersion: "0.10.0" diff --git a/deploy/chart/euler_copilot/Chart.yaml b/deploy/chart/euler_copilot/Chart.yaml index 9d9ffecff..0d0497a95 100644 --- a/deploy/chart/euler_copilot/Chart.yaml +++ b/deploy/chart/euler_copilot/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: euler-copilot description: Euler Copilot Helm部署包 type: application -version: 0.9.6 -appVersion: "0.9.6" +version: 0.10.0 +appVersion: "0.10.0" diff --git "a/documents/user-guide/\351\203\250\347\275\262\346\214\207\345\215\227/\346\217\222\344\273\266\351\203\250\347\275\262\346\214\207\345\215\227/\346\231\272\350\203\275\350\260\203\344\274\230/\346\217\222\344\273\266\342\200\224\346\231\272\350\203\275\350\260\203\344\274\230\351\203\250\347\275\262\346\214\207\345\215\227.md" "b/documents/user-guide/\351\203\250\347\275\262\346\214\207\345\215\227/\346\217\222\344\273\266\351\203\250\347\275\262\346\214\207\345\215\227/\346\231\272\350\203\275\350\260\203\344\274\230/\346\217\222\344\273\266\342\200\224\346\231\272\350\203\275\350\260\203\344\274\230\351\203\250\347\275\262\346\214\207\345\215\227.md" index 14d4712bd..2e96643e5 100644 --- "a/documents/user-guide/\351\203\250\347\275\262\346\214\207\345\215\227/\346\217\222\344\273\266\351\203\250\347\275\262\346\214\207\345\215\227/\346\231\272\350\203\275\350\260\203\344\274\230/\346\217\222\344\273\266\342\200\224\346\231\272\350\203\275\350\260\203\344\274\230\351\203\250\347\275\262\346\214\207\345\215\227.md" +++ "b/documents/user-guide/\351\203\250\347\275\262\346\214\207\345\215\227/\346\217\222\344\273\266\351\203\250\347\275\262\346\214\207\345\215\227/\346\231\272\350\203\275\350\260\203\344\274\230/\346\217\222\344\273\266\342\200\224\346\231\272\350\203\275\350\260\203\344\274\230\351\203\250\347\275\262\346\214\207\345\215\227.md" @@ -82,7 +82,7 @@ helm install -n euler-copilot agents . 如果之前有执行过安装,则按下面指令更新插件服务 ```bash -helm upgrade-n euler-copilot agents . +helm upgrade -n euler-copilot agents . ``` 如果 framework未重启,则需要重启framework配置 diff --git "a/documents/user-guide/\351\203\250\347\275\262\346\214\207\345\215\227/\346\227\240\347\275\221\347\273\234\347\216\257\345\242\203\344\270\213\351\203\250\347\275\262\346\214\207\345\215\227.md" "b/documents/user-guide/\351\203\250\347\275\262\346\214\207\345\215\227/\346\227\240\347\275\221\347\273\234\347\216\257\345\242\203\344\270\213\351\203\250\347\275\262\346\214\207\345\215\227.md" index be09376c0..56db1e828 100644 --- "a/documents/user-guide/\351\203\250\347\275\262\346\214\207\345\215\227/\346\227\240\347\275\221\347\273\234\347\216\257\345\242\203\344\270\213\351\203\250\347\275\262\346\214\207\345\215\227.md" +++ "b/documents/user-guide/\351\203\250\347\275\262\346\214\207\345\215\227/\346\227\240\347\275\221\347\273\234\347\216\257\345\242\203\344\270\213\351\203\250\347\275\262\346\214\207\345\215\227.md" @@ -310,6 +310,7 @@ k3s ctr image import $image_tar ``` 修改如下部分 + ```yaml # 模型设置 models: diff --git a/manual/source/conf.py b/manual/source/conf.py index 2085545c3..5b61cf42b 100644 --- a/manual/source/conf.py +++ b/manual/source/conf.py @@ -4,9 +4,9 @@ import sys from pathlib import Path project = "openEuler Intelligence Framework" -copyright = "2025, Huawei Technologies Co., Ltd." +copyright = "2025, Huawei Technologies Co., Ltd." # noqa: A001 author = "sig-intelligence" -release = "0.9.6" +release = "0.10.0" extensions = [ "sphinx.ext.autodoc", -- Gitee