bugfix:修复一些bug
This commit is contained in:
+3
-13
@@ -7,7 +7,7 @@ from copy import deepcopy
|
|||||||
|
|
||||||
from app.actions.registry import apply_action
|
from app.actions.registry import apply_action
|
||||||
from app.actions.update import fit_existing_path
|
from app.actions.update import fit_existing_path
|
||||||
from app.resources.material_fields import check_update_field
|
from app.resources.material_fields import check_update_field, find_material
|
||||||
from app.schemas.actions import Action, AddAction, UpdateAction
|
from app.schemas.actions import Action, AddAction, UpdateAction
|
||||||
from app.schemas.capabilities import DATA_BINDING_ERROR, DATA_BINDING_SUPPORTED
|
from app.schemas.capabilities import DATA_BINDING_ERROR, DATA_BINDING_SUPPORTED
|
||||||
from app.schemas.component_resource import ComponentCatalog
|
from app.schemas.component_resource import ComponentCatalog
|
||||||
@@ -39,18 +39,8 @@ def merge_config(defaults: dict, overrides: dict) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def complete_add(action: AddAction, catalog: ComponentCatalog) -> AddAction:
|
def complete_add(action: AddAction, catalog: ComponentCatalog) -> AddAction:
|
||||||
"""用物料默认配置补全新增组件,模型显式写出的字段优先,并按物料设置 sample_data"""
|
"""按 node.type 找物料补全新增组件,模型显式写出的字段优先,并按物料设置 sample_data"""
|
||||||
material = next(
|
material = find_material(catalog, action.node.type)
|
||||||
(item for item in catalog.components if item.key == action.component_key), None
|
|
||||||
)
|
|
||||||
if material is None:
|
|
||||||
raise ValueError(
|
|
||||||
f"物料 {action.component_key} 不存在,可用物料用 list_components 查看"
|
|
||||||
)
|
|
||||||
if material.type != action.node.type:
|
|
||||||
raise ValueError(
|
|
||||||
f"物料 {material.key} 的类型是 {material.type},node.type 必须与之一致"
|
|
||||||
)
|
|
||||||
if not DATA_BINDING_SUPPORTED and action.node.model_extra.get("dataId"):
|
if not DATA_BINDING_SUPPORTED and action.node.model_extra.get("dataId"):
|
||||||
raise ValueError(DATA_BINDING_ERROR)
|
raise ValueError(DATA_BINDING_ERROR)
|
||||||
overrides = action.node.model_dump(mode="json", exclude_unset=True)
|
overrides = action.node.model_dump(mode="json", exclude_unset=True)
|
||||||
|
|||||||
+21
-1
@@ -8,6 +8,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import socket
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
@@ -29,18 +30,37 @@ CONFIG_PATH = PROJECT_ROOT / "mcp_servers.json"
|
|||||||
# 物料服务返回的目录版本须与主进程一致,否则模型查到的字段和校验依据不同
|
# 物料服务返回的目录版本须与主进程一致,否则模型查到的字段和校验依据不同
|
||||||
MATERIAL_SERVER = "materials"
|
MATERIAL_SERVER = "materials"
|
||||||
LOCAL_HOSTS = {"localhost", "127.0.0.1"}
|
LOCAL_HOSTS = {"localhost", "127.0.0.1"}
|
||||||
|
# 配置中引用环境变量的写法
|
||||||
|
ENV_REFERENCE = re.compile(r"\$\{(\w+)\}")
|
||||||
|
|
||||||
|
|
||||||
class MCPServiceError(RuntimeError):
|
class MCPServiceError(RuntimeError):
|
||||||
"""MCP 配置错误、服务不可用,或物料目录版本不一致"""
|
"""MCP 配置错误、服务不可用,或物料目录版本不一致"""
|
||||||
|
|
||||||
|
|
||||||
|
def _expand_env(value):
|
||||||
|
"""只展开字符串值里的 ${NAME};变量未设置时报错,不把字面量当成地址或密钥"""
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return {key: _expand_env(item) for key, item in value.items()}
|
||||||
|
if isinstance(value, list):
|
||||||
|
return [_expand_env(item) for item in value]
|
||||||
|
if not isinstance(value, str):
|
||||||
|
return value
|
||||||
|
|
||||||
|
def lookup(match: re.Match) -> str:
|
||||||
|
if (found := os.environ.get(match[1])) is None:
|
||||||
|
raise MCPServiceError(f"MCP 服务配置引用的环境变量 {match[1]} 未设置")
|
||||||
|
return found
|
||||||
|
|
||||||
|
return ENV_REFERENCE.sub(lookup, value)
|
||||||
|
|
||||||
|
|
||||||
def load_mcp_config(path: Path = CONFIG_PATH) -> dict[str, dict]:
|
def load_mcp_config(path: Path = CONFIG_PATH) -> dict[str, dict]:
|
||||||
"""读取服务列表并展开环境变量;文件缺失或格式不对时报错"""
|
"""读取服务列表并展开环境变量;文件缺失或格式不对时报错"""
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
raise MCPServiceError(f"未找到 MCP 服务配置: {path}")
|
raise MCPServiceError(f"未找到 MCP 服务配置: {path}")
|
||||||
try:
|
try:
|
||||||
config = json.loads(os.path.expandvars(path.read_text(encoding="utf-8")))
|
config = _expand_env(json.loads(path.read_text(encoding="utf-8")))
|
||||||
except json.JSONDecodeError as error:
|
except json.JSONDecodeError as error:
|
||||||
raise MCPServiceError(f"MCP 服务配置不是合法 JSON: {error}") from error
|
raise MCPServiceError(f"MCP 服务配置不是合法 JSON: {error}") from error
|
||||||
if not isinstance(config, dict) or not all(
|
if not isinstance(config, dict) or not all(
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from app.core.logging import setup_logging
|
|||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(application: FastAPI):
|
async def lifespan(application: FastAPI):
|
||||||
setup_logging()
|
setup_logging()
|
||||||
|
from app.agent.screen_agent import get_screen_agent
|
||||||
from app.clients.decision import close_decision_engine
|
from app.clients.decision import close_decision_engine
|
||||||
from app.clients.mcp import start_mcp_servers, stop_mcp_servers
|
from app.clients.mcp import start_mcp_servers, stop_mcp_servers
|
||||||
from app.clients.user_center import close_user_center
|
from app.clients.user_center import close_user_center
|
||||||
@@ -35,6 +36,11 @@ async def lifespan(application: FastAPI):
|
|||||||
await warm_material_cache(persistence.db)
|
await warm_material_cache(persistence.db)
|
||||||
# 拉起 mcp_servers.json 中由应用管理的服务;单个服务失败只记录,用到它的请求执行时报错
|
# 拉起 mcp_servers.json 中由应用管理的服务;单个服务失败只记录,用到它的请求执行时报错
|
||||||
mcp_processes = await start_mcp_servers()
|
mcp_processes = await start_mcp_servers()
|
||||||
|
# 提前拉取 MCP 工具并组装 agent,问题在启动时暴露;失败只记录,首个请求会重试
|
||||||
|
try:
|
||||||
|
await get_screen_agent()
|
||||||
|
except Exception as error:
|
||||||
|
logger.opt(exception=error).error("screen agent 预先组装失败")
|
||||||
application.state.agent_service = AgentService(
|
application.state.agent_service = AgentService(
|
||||||
build_graph(persistence.checkpointer), persistence.store
|
build_graph(persistence.checkpointer), persistence.store
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -55,26 +55,6 @@ def flush_tracing() -> None:
|
|||||||
logger.opt(exception=exc).warning("Langfuse flush 失败")
|
logger.opt(exception=exc).warning("Langfuse flush 失败")
|
||||||
|
|
||||||
|
|
||||||
async def ainvoke_screen(
|
|
||||||
graph: Any,
|
|
||||||
payload: Any,
|
|
||||||
*,
|
|
||||||
thread_id: str,
|
|
||||||
session_id: str | None = None,
|
|
||||||
user_id: int | None = None,
|
|
||||||
) -> Any:
|
|
||||||
"""每轮调用记录一个根 span,返回、等待用户或异常时结束。
|
|
||||||
|
|
||||||
session_id 由调用方在首次与恢复时一致传入(通常为 conversation_id)。
|
|
||||||
未传时始终使用 thread_id,不从仅首次请求才有的 payload 推断。
|
|
||||||
user_id 为用户中心的用户 ID,接口调用时传入,本地脚本不传。
|
|
||||||
"""
|
|
||||||
with _screen_trace(payload, thread_id, session_id, user_id) as (config, root):
|
|
||||||
result = await graph.ainvoke(payload, config=config)
|
|
||||||
_finish_trace(root, result)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
async def astream_screen(graph, payload, *, thread_id, session_id=None, user_id=None):
|
async def astream_screen(graph, payload, *, thread_id, session_id=None, user_id=None):
|
||||||
"""同一轮流保持同一个追踪上下文,关闭流时结束追踪"""
|
"""同一轮流保持同一个追踪上下文,关闭流时结束追踪"""
|
||||||
from contextlib import aclosing
|
from contextlib import aclosing
|
||||||
|
|||||||
@@ -81,7 +81,3 @@ def build_graph(checkpointer=None):
|
|||||||
if checkpointer is None:
|
if checkpointer is None:
|
||||||
checkpointer = InMemorySaver(serde=build_checkpoint_serde())
|
checkpointer = InMemorySaver(serde=build_checkpoint_serde())
|
||||||
return workflow.compile(checkpointer=checkpointer)
|
return workflow.compile(checkpointer=checkpointer)
|
||||||
|
|
||||||
|
|
||||||
# 模块加载时创建共享图实例,复用该实例的调用也共用其检查点存储
|
|
||||||
query_app = build_graph()
|
|
||||||
|
|||||||
@@ -62,8 +62,6 @@ class ScreenAgentNode(BaseNode):
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
context=ScreenAgentContext(screen=context.screen, catalog=catalog),
|
context=ScreenAgentContext(screen=context.screen, catalog=catalog),
|
||||||
# 调用次数由 ModelCallLimitMiddleware 限制,这里只防止异常循环
|
|
||||||
config={"recursion_limit": 4 * self.config.max_agent_steps + 10},
|
|
||||||
)
|
)
|
||||||
if result.get("structured_response") is None:
|
if result.get("structured_response") is None:
|
||||||
last = result["messages"][-1]
|
last = result["messages"][-1]
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ def list_components(catalog: ComponentCatalog) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def get_materials(catalog: ComponentCatalog, component_types: list[str]) -> dict:
|
def get_materials(catalog: ComponentCatalog, component_types: list[str]) -> dict:
|
||||||
"""指定类型的物料默认配置,同一类型的多个预设全部返回"""
|
"""指定类型的物料默认配置"""
|
||||||
known = {item.type for item in catalog.components}
|
known = {item.type for item in catalog.components}
|
||||||
if unknown := [item for item in component_types if item not in known]:
|
if unknown := [item for item in component_types if item not in known]:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ from typing import Literal
|
|||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
|
|
||||||
from app.schemas.actions.base import ActionModel
|
from app.schemas.actions.base import ActionModel
|
||||||
from app.schemas.base import NonBlankString
|
|
||||||
from app.schemas.screen import ScreenNode
|
from app.schemas.screen import ScreenNode
|
||||||
|
|
||||||
|
|
||||||
@@ -13,13 +12,10 @@ class AddAction(ActionModel):
|
|||||||
"""按物料新增组件"""
|
"""按物料新增组件"""
|
||||||
|
|
||||||
type: Literal["add"] = "add"
|
type: Literal["add"] = "add"
|
||||||
component_key: NonBlankString = Field(
|
|
||||||
description="物料目录编号,即 list_components 返回的 key"
|
|
||||||
)
|
|
||||||
node: ScreenNode = Field(
|
node: ScreenNode = Field(
|
||||||
description="新组件。id、type、layout 必填,其余只写需要覆盖物料默认配置的字段;"
|
description="新组件。id、type、layout 必填,其余只写需要覆盖物料默认配置的字段;"
|
||||||
"后端按物料默认配置补全:对象逐层覆盖,数组整体替换,null 明确覆盖。"
|
"后端按 type 找到物料,用默认配置补全:对象逐层覆盖,数组整体替换,null 明确覆盖。"
|
||||||
"id 不能与现有组件重复,同一批里先 delete 的 ID 可以再用;type 必须与物料一致"
|
"id 不能与现有组件重复,同一批里先 delete 的 ID 可以再用;type 用 list_components 返回的 type"
|
||||||
)
|
)
|
||||||
sample_data: bool = Field(
|
sample_data: bool = Field(
|
||||||
default=False,
|
default=False,
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
# 这是一个示例 Python 脚本。
|
|
||||||
|
|
||||||
# 按 Shift+F10 执行或将其替换为您的代码。
|
|
||||||
# 按 双击 Shift 在所有地方搜索类、文件、工具窗口、操作和设置。
|
|
||||||
|
|
||||||
|
|
||||||
def print_hi(name):
|
|
||||||
# 在下面的代码行中使用断点来调试脚本。
|
|
||||||
print(f'Hi, {name}') # 按 Ctrl+F8 切换断点。
|
|
||||||
|
|
||||||
|
|
||||||
# 按装订区域中的绿色按钮以运行脚本。
|
|
||||||
if __name__ == '__main__':
|
|
||||||
print_hi('PyCharm')
|
|
||||||
|
|
||||||
# 访问 https://www.jetbrains.com/help/pycharm/ 获取 PyCharm 帮助
|
|
||||||
@@ -46,10 +46,7 @@ def _require_catalog() -> ComponentCatalog:
|
|||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def list_components() -> dict[str, Any]:
|
async def list_components() -> dict[str, Any]:
|
||||||
"""列出可以新增的组件:key、type、名称、用途和默认宽高
|
"""列出可以新增的组件:key、type、名称、用途和默认宽高"""
|
||||||
|
|
||||||
新建大屏、新增组件或更换类型时,先从这里选类型,再用 get_component_materials 查看默认配置
|
|
||||||
"""
|
|
||||||
return material_fields.list_components(_require_catalog())
|
return material_fields.list_components(_require_catalog())
|
||||||
|
|
||||||
|
|
||||||
@@ -60,10 +57,7 @@ async def get_component_materials(
|
|||||||
Field(description="要使用的物料类型,可一次传多个", min_length=1),
|
Field(description="要使用的物料类型,可一次传多个", min_length=1),
|
||||||
],
|
],
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""获取物料的完整默认配置(schema)和使用说明
|
"""获取物料的完整默认配置(schema)和使用说明"""
|
||||||
|
|
||||||
新增组件前先查看所选类型的默认配置,只覆盖需要修改的字段,不从零编写 props
|
|
||||||
"""
|
|
||||||
return material_fields.get_materials(_require_catalog(), component_types)
|
return material_fields.get_materials(_require_catalog(), component_types)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ description: 把已有组件换成另一种组件类型,例如"把折线图换
|
|||||||
2. 用 list_components 选出用户要的新类型,用 get_component_materials 查看它的默认配置
|
2. 用 list_components 选出用户要的新类型,用 get_component_materials 查看它的默认配置
|
||||||
3. 在同一批动作里先 delete 原组件,再 add 新组件:
|
3. 在同一批动作里先 delete 原组件,再 add 新组件:
|
||||||
- node.id 沿用原组件 ID,layout 沿用原组件的位置和尺寸(用户另有要求除外)
|
- node.id 沿用原组件 ID,layout 沿用原组件的位置和尺寸(用户另有要求除外)
|
||||||
- component_key 选新类型对应的物料
|
- node.type 填新类型
|
||||||
4. 用 apply_actions 提交
|
4. 用 apply_actions 提交
|
||||||
|
|
||||||
## 数据迁移
|
## 数据迁移
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ description: 新建大屏,或在大屏上新增组件、整体重新布局、
|
|||||||
## 选组件
|
## 选组件
|
||||||
|
|
||||||
1. 用 list_components 查看可用组件,按用户要展示的内容选类型;不编造目录中没有的组件类型
|
1. 用 list_components 查看可用组件,按用户要展示的内容选类型;不编造目录中没有的组件类型
|
||||||
2. 用 get_component_materials 查看所选类型的默认配置(可一次查多个类型),新组件以默认配置为基础,只写需要改的字段,例如标题
|
2. 用 get_component_materials 查看所选类型的默认配置
|
||||||
3. 新建大屏时,根据主题安排一个标题文本和若干图表;图表标题按要展示的内容写,不沿用物料的示例标题
|
3. 新建大屏时,根据主题安排一个标题文本和若干图表;图表标题按要展示的内容写,不沿用物料的示例标题
|
||||||
|
|
||||||
## 布局
|
## 布局
|
||||||
@@ -22,7 +22,3 @@ description: 新建大屏,或在大屏上新增组件、整体重新布局、
|
|||||||
|
|
||||||
- 重新布局:用 move 和 resize 调整已有组件,不删除、不重建
|
- 重新布局:用 move 和 resize 调整已有组件,不删除、不重建
|
||||||
- 统一风格:先用 get_nodes 读取组件配置,用 search_fields 找到颜色等字段,再逐个修改;画布背景用 update_canvas
|
- 统一风格:先用 get_nodes 读取组件配置,用 search_fields 找到颜色等字段,再逐个修改;画布背景用 update_canvas
|
||||||
|
|
||||||
## 提交
|
|
||||||
|
|
||||||
本次的所有新增和调整放在同一批,用 apply_actions 提交
|
|
||||||
|
|||||||
@@ -23,7 +23,3 @@ description: 修改大屏上已有的组件或画布:改标题、文字、颜
|
|||||||
|
|
||||||
- 相对变化("放大一倍""往右移 100")依据大屏摘要中的当前位置和尺寸换算
|
- 相对变化("放大一倍""往右移 100")依据大屏摘要中的当前位置和尺寸换算
|
||||||
- "放到正中间"等按画布尺寸计算目标位置
|
- "放到正中间"等按画布尺寸计算目标位置
|
||||||
|
|
||||||
## 提交
|
|
||||||
|
|
||||||
本次的所有修改放在同一批,用 apply_actions 提交
|
|
||||||
|
|||||||
Reference in New Issue
Block a user