已合并
hotfix #99
张雅晴创建于 4月17日
hotfix #99
已合并
张雅晴创建于 4月17日
develop合入到main
5 个文件变更+23-4150
@@ -5,25 +5,18 @@ FROM ${BASE_IMAGE} AS builder
5ARG INDEX_URL5ARG INDEX_URL
6 6 
7WORKDIR /app7WORKDIR /app
8-COPY agent-studio ./agent-studio
9COPY applications ./applications8COPY applications ./applications
10COPY service ./service9COPY service ./service
11COPY foundation ./foundation10COPY foundation ./foundation
12 11 
13-RUN sed -i '/openjiuwen_studio/d' /app/applications/lowcode_agent/pyproject.toml
14RUN sed -i '/openjiuwen-runtime-service/d' /app/applications/lowcode_agent/pyproject.toml12RUN sed -i '/openjiuwen-runtime-service/d' /app/applications/lowcode_agent/pyproject.toml
15RUN sed -i '/openjiuwen-runtime-foundation/d' /app/applications/lowcode_agent/pyproject.toml13RUN sed -i '/openjiuwen-runtime-foundation/d' /app/applications/lowcode_agent/pyproject.toml
16 14 
17RUN pip install uv -i ${INDEX_URL}15RUN pip install uv -i ${INDEX_URL}
18-RUN sed -i "s#url = \"[^\"]*\"#url = \"${INDEX_URL}\"#g" /app/agent-studio/backend/pyproject.toml
19RUN sed -i "s#url = \"[^\"]*\"#url = \"${INDEX_URL}\"#g" /app/applications/lowcode_agent/pyproject.toml16RUN sed -i "s#url = \"[^\"]*\"#url = \"${INDEX_URL}\"#g" /app/applications/lowcode_agent/pyproject.toml
20RUN sed -i "s#url = \"[^\"]*\"#url = \"${INDEX_URL}\"#g" /app/service/pyproject.toml17RUN sed -i "s#url = \"[^\"]*\"#url = \"${INDEX_URL}\"#g" /app/service/pyproject.toml
21RUN sed -i "s#url = \"[^\"]*\"#url = \"${INDEX_URL}\"#g" /app/foundation/pyproject.toml18RUN sed -i "s#url = \"[^\"]*\"#url = \"${INDEX_URL}\"#g" /app/foundation/pyproject.toml
22 19 
23-WORKDIR /app/agent-studio/backend
24-RUN uv sync --group dev
25-RUN uv build --out-dir /app/dist
26- 
27WORKDIR /app/applications/lowcode_agent20WORKDIR /app/applications/lowcode_agent
28RUN uv sync --group dev21RUN uv sync --group dev
29RUN uv build --out-dir /app/dist22RUN uv build --out-dir /app/dist
@@ -40,7 +33,6 @@ RUN uv build --out-dir /app/dist
40ARG BASE_IMAGE33ARG BASE_IMAGE
41FROM ${BASE_IMAGE} AS runtime34FROM ${BASE_IMAGE} AS runtime
42 35 
43-ARG STUDIO_VERSION
44ARG LOWCODE_AGENT_VERSION36ARG LOWCODE_AGENT_VERSION
45ARG RUNTIME_SERVICE_VERSION37ARG RUNTIME_SERVICE_VERSION
46ARG RUNTIME_FOUNDATION_VERSION38ARG RUNTIME_FOUNDATION_VERSION
@@ -48,7 +40,6 @@ ARG INDEX_URL
48 40 
49RUN mkdir -p /app/dist41RUN mkdir -p /app/dist
50 42 
51-COPY --from=builder /app/dist/openjiuwen_studio-${STUDIO_VERSION}-py3-none-any.whl /app/dist
52COPY --from=builder /app/dist/lowcode_agent_runner-${LOWCODE_AGENT_VERSION}-py3-none-any.whl /app/dist43COPY --from=builder /app/dist/lowcode_agent_runner-${LOWCODE_AGENT_VERSION}-py3-none-any.whl /app/dist
53COPY --from=builder /app/dist/openjiuwen_runtime_service-${RUNTIME_SERVICE_VERSION}-py3-none-any.whl /app/dist44COPY --from=builder /app/dist/openjiuwen_runtime_service-${RUNTIME_SERVICE_VERSION}-py3-none-any.whl /app/dist
54COPY --from=builder /app/dist/openjiuwen_runtime_foundation-${RUNTIME_FOUNDATION_VERSION}-py3-none-any.whl /app/dist45COPY --from=builder /app/dist/openjiuwen_runtime_foundation-${RUNTIME_FOUNDATION_VERSION}-py3-none-any.whl /app/dist
@@ -58,7 +49,6 @@ COPY --from=builder /app/dist/openjiuwen_runtime_foundation-${RUNTIME_FOUNDATION
58# 必须要用uv(新一代 Python 包管理器),专门修复了这个 bug,安装共享目录包时,只新增 / 覆盖文件,绝不删除整个目录49# 必须要用uv(新一代 Python 包管理器),专门修复了这个 bug,安装共享目录包时,只新增 / 覆盖文件,绝不删除整个目录
59 50 
60RUN pip install uv -i ${INDEX_URL}51RUN pip install uv -i ${INDEX_URL}
61-RUN uv pip install /app/dist/openjiuwen_studio-${STUDIO_VERSION}-py3-none-any.whl --index-url ${INDEX_URL} --target=/app/site-packages
62RUN uv pip install /app/dist/lowcode_agent_runner-${LOWCODE_AGENT_VERSION}-py3-none-any.whl --index-url ${INDEX_URL} --target=/app/site-packages52RUN uv pip install /app/dist/lowcode_agent_runner-${LOWCODE_AGENT_VERSION}-py3-none-any.whl --index-url ${INDEX_URL} --target=/app/site-packages
63RUN uv pip install /app/dist/openjiuwen_runtime_service-${RUNTIME_SERVICE_VERSION}-py3-none-any.whl --index-url ${INDEX_URL} --target=/app/site-packages53RUN uv pip install /app/dist/openjiuwen_runtime_service-${RUNTIME_SERVICE_VERSION}-py3-none-any.whl --index-url ${INDEX_URL} --target=/app/site-packages
64RUN uv pip install /app/dist/openjiuwen_runtime_foundation-${RUNTIME_FOUNDATION_VERSION}-py3-none-any.whl --index-url ${INDEX_URL} --target=/app/site-packages54RUN uv pip install /app/dist/openjiuwen_runtime_foundation-${RUNTIME_FOUNDATION_VERSION}-py3-none-any.whl --index-url ${INDEX_URL} --target=/app/site-packages
@@ -96,14 +96,16 @@ try {
96 Invoke-CheckedCommand -FilePath "git" -Arguments @("submodule", "update", "--init", "--recursive") -ErrorMessage "git submodule update --init failed"96 Invoke-CheckedCommand -FilePath "git" -Arguments @("submodule", "update", "--init", "--recursive") -ErrorMessage "git submodule update --init failed"
97 Invoke-CheckedCommand -FilePath "git" -Arguments @("submodule", "update", "--remote", "--recursive") -ErrorMessage "git submodule update --remote failed"97 Invoke-CheckedCommand -FilePath "git" -Arguments @("submodule", "update", "--remote", "--recursive") -ErrorMessage "git submodule update --remote failed"
98 98 
99- Remove-LinesContaining -Path (Join-Path $ProjectDir "applications\lowcode_agent\pyproject.toml") -Keyword "openjiuwen_studio"
100 Remove-LinesContaining -Path (Join-Path $ProjectDir "applications\lowcode_agent\pyproject.toml") -Keyword "openjiuwen-runtime-service"99 Remove-LinesContaining -Path (Join-Path $ProjectDir "applications\lowcode_agent\pyproject.toml") -Keyword "openjiuwen-runtime-service"
101 Remove-LinesContaining -Path (Join-Path $ProjectDir "management\pyproject.toml") -Keyword "openjiuwen-runtime-foundation"100 Remove-LinesContaining -Path (Join-Path $ProjectDir "management\pyproject.toml") -Keyword "openjiuwen-runtime-foundation"
102 Remove-LinesContaining -Path (Join-Path $ProjectDir "server\pyproject.toml") -Keyword "openjiuwen-runtime-management"101 Remove-LinesContaining -Path (Join-Path $ProjectDir "server\pyproject.toml") -Keyword "openjiuwen-runtime-management"
103 Convert-FileToUtf8NoBom -Path (Join-Path $ProjectDir "applications\lowcode_agent\pyproject.toml")102 Convert-FileToUtf8NoBom -Path (Join-Path $ProjectDir "applications\lowcode_agent\pyproject.toml")
104 Convert-FileToUtf8NoBom -Path (Join-Path $ProjectDir "management\pyproject.toml")103 Convert-FileToUtf8NoBom -Path (Join-Path $ProjectDir "management\pyproject.toml")
105 Convert-FileToUtf8NoBom -Path (Join-Path $ProjectDir "server\pyproject.toml")104 Convert-FileToUtf8NoBom -Path (Join-Path $ProjectDir "server\pyproject.toml")
106- Convert-FileToUtf8NoBom -Path (Join-Path $ProjectDir "agent-studio\backend\pyproject.toml")105+ $StudioBackendDir = Join-Path $ProjectDir "agent-studio\backend"
106+ if (Test-Path $StudioBackendDir) {
107+ Convert-FileToUtf8NoBom -Path (Join-Path $StudioBackendDir "pyproject.toml")
108+ }
107 109 
108 $FinalDistDir = Resolve-DistDir -ProjectDir $ProjectDir110 $FinalDistDir = Resolve-DistDir -ProjectDir $ProjectDir
109 Write-Info "Final dist dir: $FinalDistDir"111 Write-Info "Final dist dir: $FinalDistDir"
@@ -117,12 +119,16 @@ try {
117 $UvExtraArgs = $env:UV_EXTRA_ARGS -split "\s+"119 $UvExtraArgs = $env:UV_EXTRA_ARGS -split "\s+"
118 }120 }
119 121 
120- Push-Location (Join-Path $ProjectDir "agent-studio\backend")122+ if (Test-Path $StudioBackendDir) {
121- try {123+ Push-Location $StudioBackendDir
122- Invoke-CheckedCommand -FilePath "uv" -Arguments (@("sync") + $UvExtraArgs) -ErrorMessage "uv sync failed in agent-studio/backend"124+ try {
123- if (Test-Path "dist") { Remove-Item "dist" -Recurse -Force -ErrorAction SilentlyContinue }125+ Invoke-CheckedCommand -FilePath "uv" -Arguments (@("sync") + $UvExtraArgs) -ErrorMessage "uv sync failed in agent-studio/backend"
124- Invoke-CheckedCommand -FilePath "uv" -Arguments (@("build", "--out-dir", $FinalDistDir) + $UvExtraArgs) -ErrorMessage "uv build failed in agent-studio/backend"126+ if (Test-Path "dist") { Remove-Item "dist" -Recurse -Force -ErrorAction SilentlyContinue }
125- } finally { Pop-Location }127+ Invoke-CheckedCommand -FilePath "uv" -Arguments (@("build", "--out-dir", $FinalDistDir) + $UvExtraArgs) -ErrorMessage "uv build failed in agent-studio/backend"
128+ } finally { Pop-Location }
129+ } else {
130+ Write-Info "agent-studio/backend not found, use openjiuwen_studio from package index."
131+ }
126 132 
127 $AgentCoreDir = Join-Path $ProjectDir "..\agent-core"133 $AgentCoreDir = Join-Path $ProjectDir "..\agent-core"
128 if (Test-Path $AgentCoreDir) {134 if (Test-Path $AgentCoreDir) {
@@ -43,7 +43,6 @@ else
43 SED_I_FLAG="-i"43 SED_I_FLAG="-i"
44fi44fi
45 45 
46-sed ${SED_I_FLAG} '/openjiuwen_studio/d' ${PROJECT_DIR}/applications/lowcode_agent/pyproject.toml
47sed ${SED_I_FLAG} '/openjiuwen-runtime-service/d' ${PROJECT_DIR}/applications/lowcode_agent/pyproject.toml46sed ${SED_I_FLAG} '/openjiuwen-runtime-service/d' ${PROJECT_DIR}/applications/lowcode_agent/pyproject.toml
48sed ${SED_I_FLAG} '/openjiuwen-runtime-foundation/d' ${PROJECT_DIR}/management/pyproject.toml47sed ${SED_I_FLAG} '/openjiuwen-runtime-foundation/d' ${PROJECT_DIR}/management/pyproject.toml
49sed ${SED_I_FLAG} '/openjiuwen-runtime-management/d' ${PROJECT_DIR}/server/pyproject.toml48sed ${SED_I_FLAG} '/openjiuwen-runtime-management/d' ${PROJECT_DIR}/server/pyproject.toml
@@ -61,11 +60,15 @@ echo "✅ Final build output directory (absolute path resolved): ${FINAL_DIST_DI
61rm -rf ${FINAL_DIST_DIR}60rm -rf ${FINAL_DIST_DIR}
62mkdir ${FINAL_DIST_DIR}61mkdir ${FINAL_DIST_DIR}
63 62 
64-# complie dist/openjiuwen_studio-0.1.5-py3-none-any.whl63+# compile studio package from local workspace when available
65-cd ${PROJECT_DIR}/agent-studio/backend64+if [ -d "${PROJECT_DIR}/agent-studio/backend" ]; then
66-uv sync ${UV_EXTRA_ARGS}65+ cd ${PROJECT_DIR}/agent-studio/backend
67-rm -rf dist66+ uv sync ${UV_EXTRA_ARGS}
68-uv build --out-dir ${FINAL_DIST_DIR} ${UV_EXTRA_ARGS}67+ rm -rf dist
68+ uv build --out-dir ${FINAL_DIST_DIR} ${UV_EXTRA_ARGS}
69+else
70+ echo "⚠️ ${PROJECT_DIR}/agent-studio/backend not found, use openjiuwen_studio from index."
71+fi
69 72 
70# complie dist/openjiuwen-0.1.9-py3-none-any.whl (core library)73# complie dist/openjiuwen-0.1.9-py3-none-any.whl (core library)
71if [ -d "${PROJECT_DIR}/../agent-core" ]; then74if [ -d "${PROJECT_DIR}/../agent-core" ]; then
@@ -1,1248 +0,0 @@
1-# openjiuwen-runtime-sdk 设计文档 v2.0
2- 
3-**版本**: v2.0
4-**更新时间**: 2025-01-08
5-**状态**: 设计草案
6- 
7----
8- 
9-## 目录
10- 
11-- [1. 项目概述](#1-项目概述)
12-- [2. 核心架构](#2-核心架构)
13-- [3. 详细设计](#3-详细设计)
14-- [4. 使用示例](#4-使用示例)
15- 
16----
17- 
18-## 1. 项目概述
19- 
20-### 1.1 定位
21- 
22-**openjiuwen-runtime-sdk** 是 agent-studio Agent 和 Plugin 的**生产级部署运行时**,提供:
23- 
24-- **Agent 部署能力**: 将开发好的 Agent 部署为生产服务
25-- **Plugin 部署能力**: 将一组工具部署为 RESTful 服务
26-- **执行能力**: 运行 agent-studio 的 ReAct Agent 和 Workflow Agent
27-- **会话管理**: 支持多用户、多会话的并发执行
28-- **扩展能力**: 沙箱工具、RESTful 工具服务集成
29- 
30----
31- 
32-## 2. 核心架构
33- 
34-### 2.1 整体架构图
35- 
36-```
37-┌─────────────────────────────────────────────────────────────┐
38-│ openjiuwen-runtime-sdk │
39-├─────────────────────────────────────────────────────────────┤
40-│ │
41-│ ┌─────────────────────────────────────────────────────┐ │
42-│ │ BaseApp (基类) │ │
43-│ │ - FastAPI 应用管理 │ │
44-│ │ - 生命周期管理 (@init, @shutdown) │ │
45-│ │ - 健康检查 (/health) │ │
46-│ │ - 运行方法 (run) │ │
47-│ └─────────────────┬───────────────────────────────────┘ │
48-│ │ │
49-│ ┌─────────────┴─────────────┐ │
50-│ ↓ ↓ │
51-│ ┌──────────────────┐ ┌──────────────────┐ │
52-│ │ AgentApp │ │ PluginApp │ │
53-│ │ - Agent 实例 │ │ - 工具注册表 │ │
54-│ │ - @query │ │ - @tool │ │
55-│ │ - /query 路由 │ │ - /tools/* 路由 │ │
56-│ └──────────────────┘ └──────────────────┘ │
57-│ │
58-│ ┌─────────────────────────────────────────────────────┐ │
59-│ │ AppGroup (应用容器) │ │
60-│ │ - 多应用共享单端口 │ │
61-│ │ - 路径前缀区分 (/customer, /assistant) │ │
62-│ │ - 聚合生命周期管理 │ │
63-│ │ - 聚合健康检查 │ │
64-│ └─────────────────────────────────────────────────────┘ │
65-│ ↓ │
66-│ ┌─────────────────────────────────────────────────────┐ │
67-│ │ Services Layer (可选) │ │
68-│ │ ┌──────────┬──────────┬──────────────────────────┐ │ │
69-│ │ │ Sandbox │ Tools │ Observability │ │ │
70-│ │ │ Service │ Service │ Service │ │ │
71-│ │ └──────────┴──────────┴──────────────────────────┘ │ │
72-│ └─────────────────────────────────────────────────────┘ │
73-│ │
74-└─────────────────────────────────────────────────────────────┘
75-```
76- 
77-### 2.2 目录结构
78- 
79-```
80-agent-runtime/
81-├── src/
82-│ ├── sdk/ # openjiuwen-runtime-sdk 组件
83-│ │ ├── pyproject.toml
84-│ │ └── src/
85-│ │ ├── core/ # AgentApp, PluginApp, BaseApp, AppGroup
86-│ │ │ ├── __init__.py
87-│ │ │ ├── base_app.py # 核心: BaseApp 基类
88-│ │ │ ├── agent_app.py # AgentApp 类
89-│ │ │ ├── plugin_app.py # PluginApp 类
90-│ │ │ ├── app_group.py # AppGroup 多应用容器
91-│ │ │ ├── restful.py # @restful.tool() 装饰器
92-│ │ │ ├── loader.py # Agent 加载器
93-│ │ │ └── stream_handler.py # 流式输出处理
94-│ │ ├── services/ (可选)
95-│ │ │ ├── __init__.py
96-│ │ │ ├── sandbox/ # 沙箱服务 (复用 agentscope-runtime)
97-│ │ │ │ ├── __init__.py
98-│ │ │ │ └── sandbox_service.py
99-│ │ │ ├── tools/ # RESTful 工具服务
100-│ │ │ │ ├── __init__.py
101-│ │ │ │ ├── http_client.py # HTTP 工具客户端
102-│ │ │ │ └── tool_registry.py # 工具注册表
103-│ │ │ └── observability/ # 观测性服务
104-│ │ │ ├── __init__.py
105-│ │ │ └── tracing.py
106-│ │ └── models/ # 数据模型
107-│ │ ├── __init__.py
108-│ │ ├── agent.py
109-│ │ ├── message.py
110-│ │ ├── request.py
111-│ │ └── tool.py # 工具相关模型
112-│ │
113-│ └── manager/ # runtime-manager 组件
114-│ ├── cli/ # CLI 子组件
115-│ │ ├── pyproject.toml
116-│ │ └── src/
117-│ │ ├── __init__.py
118-│ │ └── main.py # CLI 入口
119-│ ├── server/ # Server 子组件
120-│ │ ├── pyproject.toml
121-│ │ └── src/
122-│ │ ├── __init__.py
123-│ │ ├── main.py # FastAPI 应用
124-│ │ ├── config.py # 配置管理
125-│ │ └── models/
126-│ │ ├── __init__.py
127-│ │ ├── request.py # 请求模型
128-│ │ └── response.py # 响应模型
129-│ └── sdk/ # Manager SDK 子组件
130-│ ├── pyproject.toml
131-│ └── src/
132-│ ├── __init__.py
133-│ ├── manager.py # DeploymentManager 核心类
134-│ ├── storage.py # OSS/本地存储抽象
135-│ ├── database.py # 数据库抽象
136-│ ├── mysql_models.py # MySQL ORM 模型
137-│ ├── config.py # 配置项定义
138-│ ├── models/
139-│ │ ├── __init__.py
140-│ │ ├── deployment.py # 部署记录模型
141-│ │ └── enums.py # 枚举定义
142-│ ├── deployers/ # 部署器实现 (subprocess/进程管理)
143-│ │ ├── __init__.py
144-│ │ ├── base.py # Deployer 基类
145-│ │ ├── local_subprocess.py # 本地进程部署器
146-│ │ ├── kubernetes.py # K8s 部署器 (待实现)
147-│ │ └── docker.py # Docker 部署器 (待实现)
148-│ └── utils/
149-│ ├── __init__.py
150-│ └── id_generator.py # ID 生成器
151-
152-└── README.md
153-```
154- 
155-**目录说明:**
156-- `src/sdk/` - openjiuwen-runtime-sdk,提供 AgentApp/PluginApp 基类
157-- `src/manager/` - runtime-manager,提供部署管理服务和部署器实现
158- - `cli/` - 命令行工具组件
159- - `server/` - RESTful API 服务组件
160- - `sdk/` - Manager SDK (被 CLI 和 Server 共用)
161- 
162-**包结构说明:**
163-- 每个组件都有自己的 `src/` 目录,Python 模块直接位于各自的 `src/`
164-- 例如:`agent-runtime/src/sdk/src/core/`
165-- 部署器实现在 `manager/sdk/src/deployers/` 中,使用 subprocess 启动 Python 进程
166- 
167----
168- 
169-## 3. 详细设计
170- 
171-### 3.1 BaseApp 基类设计
172- 
173-```python
174-# agent-runtime/src/sdk/src/openjiuwen_runtime/core/base_app.py
175- 
176-from typing import Callable, Optional
177-from fastapi import FastAPI
178- 
179- 
180-class BaseApp:
181- """
182- openjiuwen 应用基类
183- 
184- 提供 AgentApp 和 PluginApp 的公共功能:
185- - FastAPI 应用管理
186- - 生命周期管理 (@init, @shutdown)
187- - 健康检查 (/health)
188- - 运行方法 (run)
189- """
190- 
191- def __init__(
192- self,
193- app_name: str,
194- app_description: str = "",
195- version: str = "1.0.0",
196- ):
197- self.app_name = app_name
198- self.app_description = app_description
199- self.version = version
200- 
201- # FastAPI 应用
202- self.app = FastAPI(
203- title=self.app_name,
204- description=self.app_description,
205- version=self.version,
206- )
207- 
208- # 生命周期钩子
209- self._init_hook: Optional[Callable] = None
210- self._shutdown_hook: Optional[Callable] = None
211- 
212- # 注册基础路由
213- self._register_base_routes()
214- 
215- def init(self, func: Callable) -> Callable:
216- """初始化钩子装饰器"""
217- self._init_hook = func
218- return func
219- 
220- def shutdown(self, func: Callable) -> Callable:
221- """清理钩子装饰器"""
222- self._shutdown_hook = func
223- return func
224- 
225- def _register_base_routes(self):
226- """注册基础路由"""
227- @self.app.on_event("startup")
228- async def startup():
229- # 执行用户初始化钩子
230- if self._init_hook:
231- await self._init_hook()
232- 
233- @self.app.on_event("shutdown")
234- async def shutdown():
235- # 执行用户清理钩子
236- if self._shutdown_hook:
237- await self._shutdown_hook()
238- 
239- @self.app.get("/health")
240- async def health():
241- """健康检查"""
242- return {
243- "status": "healthy",
244- "app": self.app_name,
245- "version": self.version,
246- }
247- 
248- def run(self, host: str = "0.0.0.0", port: int = 8090, **kwargs):
249- """运行应用"""
250- import uvicorn
251- uvicorn.run(self.app, host=host, port=port, **kwargs)
252-```
253- 
254-### 3.2 AgentApp 核心设计
255- 
256-```python
257-# agent-runtime/src/sdk/src/openjiuwen_runtime/core/agent_app.py
258- 
259-from typing import Callable, Optional
260-from .base_app import BaseApp
261- 
262- 
263-class AgentApp(BaseApp):
264- """
265- openjiuwen Agent 应用类
266- 
267- 提供简洁的 API 用于部署 agent-studio Agent 到生产环境。
268- 
269- 设计理念:
270- - 每个 AgentApp 只保存一个 Agent 实例
271- - 一个 Agent 实例服务所有 conversation
272- - Agent 内部通过 conversation_id 自动隔离 session
273- 
274- TODO: 引入 agent 实例池或 provider 模式
275- - 当前单实例模式在高并发场景下可能成为瓶颈
276- - 后续引入 agent 实例池: 预创建多个 agent 实例,按需分配
277- - 或引入 provider 模式: 动态创建/获取 agent 实例
278- - 目标: 解决并行处理问题,提升并发能力
279- 
280- 用法:
281- app = AgentApp("MyAgent", agent_config_path="agent_export.json")
282- 
283- @app.init
284- async def init():
285- app.agent = await load_agent_from_config(app.agent_config_path)
286- 
287- @app.query
288- async def query(msgs, request):
289- async for msg, last in app.agent.stream(
290- messages=msgs,
291- conversation_id=request.conversation_id,
292- ):
293- yield msg, last
294- 
295- app.run(port=8090)
296- """
297- 
298- def __init__(
299- self,
300- app_name: str,
301- app_description: str = "",
302- version: str = "1.0.0",
303- agent_config_path: str = None,
304- ):
305- super().__init__(app_name, app_description, version)
306- 
307- # 查询钩子
308- self._query_hook: Optional[Callable] = None
309- 
310- # Agent 配置路径
311- self.agent_config_path = agent_config_path
312- 
313- # TODO: 引入 agent 实例池或 provider 模式
314- # 当前单实例模式,后续改为实例池或动态创建以支持高并发
315- self.agent = None
316- 
317- # 可选服务
318- self.sandbox_service = None
319- 
320- # 注册 Agent 特定路由
321- self._register_agent_routes()
322- 
323- def query(self, func: Callable) -> Callable:
324- """查询钩子装饰器"""
325- self._query_hook = func
326- return func
327- 
328- def _register_agent_routes(self):
329- """注册 Agent 特定路由"""
330- from fastapi import Request
331- 
332- @self.app.post("/query")
333- async def query_endpoint(request: Request):
334- """查询端点"""
335- from fastapi.responses import StreamingResponse
336- import json
337- 
338- # 解析请求
339- body = await request.body()
340- data = json.loads(body)
341- messages = data.get("messages", [])
342- conversation_id = data.get("conversation_id")
343- 
344- if not conversation_id:
345- from fastapi import HTTPException
346- raise HTTPException(
347- status_code=400,
348- detail="conversation_id is required",
349- )
350- 
351- # 构造请求对象
352- from openjiuwen_runtime.models.request import QueryRequest
353- query_request = QueryRequest(
354- messages=messages,
355- conversation_id=conversation_id,
356- user_id=data.get("user_id", "anonymous"),
357- )
358- 
359- # 执行查询钩子
360- async def generate():
361- if self._query_hook:
362- async for msg, last in self._query_hook(messages, query_request):
363- yield f"data: {msg.model_dump_json()}\n\n"
364- 
365- return StreamingResponse(
366- generate(),
367- media_type="text/event-stream",
368- )
369- 
370- @self.app.post("/reset_conversation")
371- async def reset_conversation_endpoint(request: Request):
372- """重置对话端点"""
373- body = await request.body()
374- data = json.loads(body)
375- conversation_id = data.get("conversation_id")
376- 
377- if not conversation_id:
378- from fastapi import HTTPException
379- raise HTTPException(
380- status_code=400,
381- detail="conversation_id is required",
382- )
383- 
384- # 调用 Agent 内部的 session 清理方法
385- if self.agent and hasattr(self.agent, 'clear_session'):
386- await self.agent.clear_session(conversation_id)
387- 
388- return {"status": "ok", "message": f"Conversation {conversation_id} reset"}
389- 
390- # 更新健康检查,添加 agent_loaded 状态
391- original_health = self.app.routes[-1] # 获取 BaseApp 注册的 /health 端点
392- 
393- @self.app.get("/health")
394- async def health_with_agent():
395- """健康检查(包含 Agent 状态)"""
396- return {
397- "status": "healthy",
398- "app": self.app_name,
399- "version": self.version,
400- "agent_loaded": self.agent is not None,
401- }
402-```
403- 
404-### 3.3 PluginApp 核心设计
405- 
406-```python
407-# agent-runtime/src/sdk/src/openjiuwen_runtime/core/plugin_app.py
408- 
409-from typing import Callable, Dict, Any, Optional
410-from fastapi import Request, HTTPException
411-from fastapi.responses import JSONResponse
412-from .base_app import BaseApp
413- 
414- 
415-class PluginApp(BaseApp):
416- """
417- openjiuwen Plugin 应用类
418- 
419- 提供简洁的 API 用于部署同类工具集为 RESTful 服务。
420- 
421- 设计理念:
422- - 一个 PluginApp 包含同一类别的多个工具
423- - 工具应具有主题相关性(如:天气工具、搜索工具、数学工具)
424- - 使用 @restful.tool() 装饰器注册工具
425- - 自动为每个工具生成 RESTful 端点
426- - 支持工具元数据定义
427- 
428- 用法 - 天气工具 Plugin:
429- app = PluginApp(
430- app_name="WeatherTools",
431- app_description="天气查询工具集"
432- )
433- 
434- @app.restful.tool(name="current_weather", description="查询实时天气")
435- async def current_weather(city: str) -> Dict[str, Any]:
436- return {"city": city, "temperature": 25, "condition": "晴"}
437- 
438- @app.restful.tool(name="weather_forecast", description="查询天气预报")
439- async def weather_forecast(city: str, days: int = 3) -> Dict[str, Any]:
440- return {"city": city, "forecast": [...]}
441- 
442- app.run(port=8091)
443- 
444- 用法 - 搜索工具 Plugin:
445- app = PluginApp(
446- app_name="SearchTools",
447- app_description="搜索和查询工具集"
448- )
449- 
450- @app.restful.tool(name="web_search", description="网络搜索")
451- async def web_search(query: str) -> Dict[str, Any]:
452- return {"results": [...]}
453- 
454- @app.restful.tool(name="image_search", description="图片搜索")
455- async def image_search(query: str) -> Dict[str, Any]:
456- return {"images": [...]}
457- """
458- 
459- def __init__(
460- self,
461- app_name: str,
462- app_description: str = "",
463- version: str = "1.0.0",
464- ):
465- super().__init__(app_name, app_description, version)
466- 
467- # 工具注册表
468- self._tools: Dict[str, Dict[str, Any]] = {}
469- 
470- # 创建工具装饰器(绑定到当前 app)
471- from .restful import RestfulToolDecorator
472- self.restful = RestfulToolDecorator(self)
473- 
474- # 注册 Plugin 特定路由
475- self._register_plugin_routes()
476- 
477- def register_tool(
478- self,
479- name: str,
480- func: Callable,
481- description: str = "",
482- parameters: Dict[str, Any] = None,
483- ):
484- """
485- 注册工具
486- 
487- Args:
488- name: 工具名称
489- func: 工具函数
490- description: 工具描述
491- parameters: 参数定义(可选)
492- """
493- self._tools[name] = {
494- "name": name,
495- "func": func,
496- "description": description,
497- "parameters": parameters or {},
498- }
499- 
500- # 为工具创建路由
501- self._create_tool_route(name, func)
502- 
503- def _create_tool_route(self, name: str, func: Callable):
504- """为工具创建 RESTful 路由"""
505- 
506- @self.app.post(f"/tools/{name}")
507- async def tool_endpoint(request: Request):
508- """工具执行端点"""
509- try:
510- import json
511- 
512- # 解析请求
513- body = await request.body()
514- data = json.loads(body) if body else {}
515- 
516- # 执行工具函数
517- result = await func(**data) if asyncio.iscoroutinefunction(func) else func(**data)
518- 
519- return {
520- "status": "success",
521- "tool": name,
522- "result": result,
523- }
524- except Exception as e:
525- raise HTTPException(
526- status_code=500,
527- detail=f"Tool execution failed: {str(e)}",
528- )
529- 
530- def _register_plugin_routes(self):
531- """注册 Plugin 特定路由"""
532- 
533- @self.app.get("/tools")
534- async def list_tools():
535- """列出所有工具"""
536- tools_info = []
537- for name, tool in self._tools.items():
538- tools_info.append({
539- "name": tool["name"],
540- "description": tool["description"],
541- "parameters": tool["parameters"],
542- "endpoint": f"/tools/{name}",
543- })
544- return {
545- "status": "success",
546- "tools": tools_info,
547- }
548- 
549- # 更新健康检查,添加工具数量
550- @self.app.get("/health")
551- async def health_with_tools():
552- """健康检查(包含工具状态)"""
553- return {
554- "status": "healthy",
555- "app": self.app_name,
556- "version": self.version,
557- "tools_count": len(self._tools),
558- }
559-```
560- 
561-### 3.4 @restful.tool() 装饰器
562- 
563-```python
564-# agent-runtime/src/sdk/src/openjiuwen_runtime/core/restful.py
565- 
566-import asyncio
567-import inspect
568-from typing import Dict, Any, Callable, TYPE_CHECKING
569- 
570-if TYPE_CHECKING:
571- from .plugin_app import PluginApp
572- 
573- 
574-class RestfulToolDecorator:
575- """
576- @restful.tool() 装饰器实现
577- 
578- 用于将 Python 函数注册为 RESTful 工具。
579- 
580- 支持的功能:
581- - 自动提取函数签名生成参数定义
582- - 支持同步和异步函数
583- - 支持工具元数据定义
584- """
585- 
586- def __init__(self, plugin_app: "PluginApp"):
587- """
588- 初始化装饰器
589- 
590- Args:
591- plugin_app: PluginApp 实例(必需)
592- """
593- self.plugin_app = plugin_app
594- 
595- def tool(
596- self,
597- name: str = None,
598- description: str = "",
599- ):
600- """
601- 工具装饰器
602- 
603- Args:
604- name: 工具名称(默认使用函数名)
605- description: 工具描述
606- """
607- 
608- def decorator(func: Callable) -> Callable:
609- # 提取函数签名
610- sig = inspect.signature(func)
611- parameters = {}
612- for param_name, param in sig.parameters.items():
613- param_info = {"type": str(param.annotation if param.annotation != inspect.Parameter.empty else "any")}
614- if param.default != inspect.Parameter.empty:
615- param_info["default"] = param.default
616- parameters[param_name] = param_info
617- 
618- # 确定工具名称
619- tool_name = name or func.__name__
620- 
621- # 直接注册到 PluginApp
622- self.plugin_app.register_tool(
623- name=tool_name,
624- func=func,
625- description=description,
626- parameters=parameters,
627- )
628- 
629- return func
630- 
631- return decorator
632-```
633- 
634-### 3.5 Agent 加载器
635- 
636-```python
637-# agent-runtime/src/sdk/src/openjiuwen_runtime/core/loader.py
638- 
639-from typing import Union, TYPE_CHECKING
640-from pathlib import Path
641- 
642-if TYPE_CHECKING:
643- from openjiuwen.Agent import Agent
644- 
645- 
646-async def load_agent_from_config(
647- config_path: Union[str, Path],
648- current_user: dict = None,
649-) -> "Agent":
650- """
651- 从配置文件加载并创建 Agent 实例
652- 
653- 此函数通过调用 openjiuwen studio 提供的 Agent 编译接口来加载 Agent。
654- 
655- Args:
656- config_path: Agent 配置文件路径(JSON 格式)
657- current_user: 当前用户信息
658- 
659- Returns:
660- 编译后的 Agent 实例
661- 
662- Raises:
663- FileNotFoundError: 配置文件不存在
664- ValueError: 配置文件格式错误或 agent_type 不支持
665- 
666- Note:
667- 依赖 openjiuwen studio 提供的 Agent 编译接口。
668- 支持的 agent_type:
669- - "react": ReAct Agent
670- - "workflow": Workflow Agent
671- 
672- Example:
673- >>> agent = await load_agent_from_config(
674- ... "agent_export.json",
675- ... current_user={"user_id": "system"}
676- ... )
677- """
678- import json
679- from openjiuwen.studio import AgentCompiler # 由 openjiuwen studio 提供
680- 
681- # 1. 读取 JSON 配置文件
682- config_path = Path(config_path)
683- if not config_path.exists():
684- raise FileNotFoundError(f"Agent config not found: {config_path}")
685- 
686- with open(config_path, 'r', encoding='utf-8') as f:
687- agent_config = json.load(f)
688- 
689- # 2. 调用 openjiuwen studio 的 Agent 编译接口
690- compiler = AgentCompiler()
691- agent = await compiler.compile(
692- config=agent_config,
693- current_user=current_user or {"user_id": "system"},
694- )
695- 
696- return agent
697-```
698- 
699-### 3.6 请求模型
700- 
701-```python
702-# agent-runtime/src/sdk/src/openjiuwen_runtime/models/request.py
703- 
704-from pydantic import BaseModel
705-from typing import List, Dict, Any
706- 
707- 
708-class QueryRequest(BaseModel):
709- """查询请求模型"""
710- 
711- messages: List[Dict[str, Any]] # 当前消息(非完整历史)
712- conversation_id: str # 对话 ID(必需)
713- user_id: str # 用户 ID
714- space_id: str = "default" # 工作空间 ID
715- stream: bool = True # 是否流式输出
716-```
717- 
718-### 3.7 AppGroup 多应用容器
719- 
720-```python
721-# agent-runtime/src/sdk/src/openjiuwen_runtime/core/app_group.py
722- 
723-from typing import Dict
724-from fastapi import FastAPI
725-import uvicorn
726- 
727-from .base_app import BaseApp
728-from .agent_app import AgentApp
729-from .plugin_app import PluginApp
730- 
731- 
732-class AppGroup:
733- """
734- 多应用容器,允许多个 AgentApp/PluginApp 共享一个 FastAPI 服务。
735- 
736- 设计理念:
737- - 单一 FastAPI 服务托管多个应用
738- - 通过路径前缀区分应用 (如 /customer/query)
739- - 聚合生命周期钩子执行
740- - 聚合健康检查
741- 
742- 用法:
743- group = AppGroup("MyAgentGroup")
744- 
745- customer_app = AgentApp("CustomerAgent")
746- @customer_app.query
747- async def query(msgs, request): ...
748- 
749- assistant_app = AgentApp("AssistantAgent")
750- @assistant_app.query
751- async def query(msgs, request): ...
752- 
753- group.mount("/customer", customer_app)
754- group.mount("/assistant", assistant_app)
755- group.run(port=8090)
756- """
757- 
758- def __init__(
759- self,
760- group_name: str,
761- description: str = "",
762- version: str = "1.0.0",
763- ):
764- self.group_name = group_name
765- self.description = description
766- self.version = version
767- 
768- # 主 FastAPI 应用
769- self.app = FastAPI(
770- title=group_name,
771- description=description,
772- version=version,
773- )
774- 
775- # 已挂载的应用注册表: prefix -> BaseApp
776- self._mounted_apps: Dict[str, BaseApp] = {}
777- 
778- def mount(self, prefix: str, app: BaseApp) -> "AppGroup":
779- """
780- 挂载应用到指定路径前缀。
781- 
782- Args:
783- prefix: URL 路径前缀 (如 "/customer", "/assistant")
784- app: BaseApp 实例 (AgentApp 或 PluginApp)
785- 
786- Returns:
787- Self,支持链式调用
788- """
789- # 规范化前缀
790- if not prefix.startswith("/"):
791- prefix = f"/{prefix}"
792- prefix = prefix.rstrip("/")
793- 
794- # 检查重复前缀
795- if prefix in self._mounted_apps:
796- raise ValueError(f"前缀 '{prefix}' 已被挂载")
797- 
798- self._mounted_apps[prefix] = app
799- self.app.mount(prefix, app.app)
800- 
801- return self
802- 
803- def _register_lifecycle_events(self):
804- """注册聚合的启动和关闭事件"""
805- 
806- @self.app.on_event("startup")
807- async def startup():
808- """按顺序执行所有挂载应用的 init 钩子"""
809- for prefix, app in self._mounted_apps.items():
810- if app._init_hook:
811- await app._init_hook()
812- 
813- @self.app.on_event("shutdown")
814- async def shutdown():
815- """按逆序执行所有挂载应用的 shutdown 钩子"""
816- for prefix, app in reversed(list(self._mounted_apps.items())):
817- if app._shutdown_hook:
818- await app._shutdown_hook()
819- 
820- def _register_routes(self):
821- """注册 AppGroup 特定路由"""
822- 
823- @self.app.get("/health")
824- async def health():
825- """聚合健康检查"""
826- apps_status = {}
827- overall_healthy = True
828- 
829- for prefix, app in self._mounted_apps.items():
830- app_info = {
831- "name": app.app_name,
832- "version": app.version,
833- "mount_path": prefix,
834- }
835- 
836- if isinstance(app, AgentApp):
837- app_info["type"] = "agent"
838- app_info["agent_loaded"] = app.agent is not None
839- if app.agent is None:
840- overall_healthy = False
841- elif isinstance(app, PluginApp):
842- app_info["type"] = "plugin"
843- app_info["tools_count"] = len(app._tools)
844- else:
845- app_info["type"] = "base"
846- 
847- apps_status[prefix] = app_info
848- 
849- return {
850- "status": "healthy" if overall_healthy else "degraded",
851- "group": self.group_name,
852- "version": self.version,
853- "apps_count": len(self._mounted_apps),
854- "apps": apps_status,
855- }
856- 
857- @self.app.get("/")
858- async def root():
859- """根端点,列出所有挂载的应用"""
860- return {
861- "group": self.group_name,
862- "version": self.version,
863- "mounted_apps": [
864- {"prefix": prefix, "name": app.app_name}
865- for prefix, app in self._mounted_apps.items()
866- ],
867- }
868- 
869- def run(self, host: str = "0.0.0.0", port: int = 8090, **kwargs):
870- """运行所有挂载应用"""
871- self._register_routes()
872- self._register_lifecycle_events()
873- uvicorn.run(self.app, host=host, port=port, **kwargs)
874-```
875- 
876-**端点结构:**
877- 
878-| 端点 | 方法 | 描述 |
879-|------|------|------|
880-| `/` | GET | 组信息,列出所有挂载应用 |
881-| `/health` | GET | 聚合健康检查 |
882-| `/docs` | GET | OpenAPI 文档 |
883-| `/{prefix}/health` | GET | 单应用健康检查 |
884-| `/{prefix}/query` | POST | AgentApp 查询端点 (SSE) |
885-| `/{prefix}/reset_conversation` | POST | AgentApp 重置对话 |
886-| `/{prefix}/tools` | GET | PluginApp 工具列表 |
887-| `/{prefix}/tools/{name}` | POST | PluginApp 执行工具 |
888- 
889----
890- 
891-## 4. 使用示例
892- 
893-### 4.1 AgentApp 示例
894- 
895-```python
896-# examples/simple_agent.py
897- 
898-from openjiuwen_runtime import AgentApp
899-from openjiuwen_runtime.core.loader import load_agent_from_config
900- 
901-app = AgentApp(
902- app_name="SimpleAgent",
903- agent_config_path="configs/customer_service.json",
904-)
905- 
906-@app.init
907-async def init():
908- """创建 Agent 实例"""
909- app.agent = await load_agent_from_config(
910- config_path=app.agent_config_path,
911- current_user={"user_id": "system"},
912- )
913- 
914-@app.query
915-async def query(msgs, request):
916- """处理查询"""
917- async for msg, last in app.agent.stream(
918- messages=msgs,
919- conversation_id=request.conversation_id,
920- ):
921- yield msg, last
922- 
923-@app.shutdown
924-async def shutdown():
925- """清理资源"""
926- if app.agent and hasattr(app.agent, 'cleanup'):
927- await app.agent.cleanup()
928- 
929-if __name__ == "__main__":
930- app.run(port=8090)
931-```
932- 
933-### 4.2 PluginApp 示例
934- 
935-```python
936-# examples/weather_plugin.py
937- 
938-from openjiuwen_runtime import PluginApp
939-import httpx
940- 
941-app = PluginApp(
942- app_name="WeatherTools",
943- app_description="天气查询工具集",
944- version="1.0.0",
945-)
946- 
947-@app.init
948-async def init():
949- """初始化 HTTP 客户端"""
950- app.http_client = httpx.AsyncClient(timeout=30.0)
951- 
952-@app.shutdown
953-async def shutdown():
954- """清理资源"""
955- if hasattr(app, 'http_client'):
956- await app.http_client.aclose()
957- 
958-@app.restful.tool(name="current_weather", description="查询实时天气")
959-async def current_weather(city: str, unit: str = "celsius") -> dict:
960- """
961- 获取指定城市的实时天气
962- 
963- Args:
964- city: 城市名称
965- unit: 温度单位 (celsius/fahrenheit)
966- 
967- Returns:
968- 实时天气信息
969- """
970- response = await app.http_client.get(
971- f"https://api.weather.example.com/current",
972- params={"city": city, "unit": unit},
973- )
974- return response.json()
975- 
976-@app.restful.tool(name="weather_forecast", description="查询天气预报")
977-async def weather_forecast(city: str, days: int = 3) -> dict:
978- """
979- 获取指定城市的天气预报
980- 
981- Args:
982- city: 城市名称
983- days: 预报天数
984- 
985- Returns:
986- 天气预报信息
987- """
988- response = await app.http_client.get(
989- f"https://api.weather.example.com/forecast",
990- params={"city": city, "days": days},
991- )
992- return response.json()
993- 
994-@app.restful.tool(name="weather_alerts", description="查询天气预警")
995-async def weather_alerts(city: str) -> dict:
996- """
997- 获取指定城市的天气预警信息
998- 
999- Args:
1000- city: 城市名称
1001- 
1002- Returns:
1003- 天气预警列表
1004- """
1005- response = await app.http_client.get(
1006- f"https://api.weather.example.com/alerts",
1007- params={"city": city},
1008- )
1009- return response.json()
1010- 
1011-if __name__ == "__main__":
1012- app.run(port=8091)
1013-```
1014- 
1015-### 4.3 向 Agent 发送请求
1016- 
1017-假设 Agent 已经通过 4.1 部署在 `http://localhost:8090`,下面演示如何向其发送请求。
1018- 
1019-#### 使用 curl 测试
1020- 
1021-**1. 健康检查**
1022-```bash
1023-curl http://localhost:8090/health
1024-```
1025-响应:
1026-```json
1027-{
1028- "status": "healthy",
1029- "app": "CustomerService",
1030- "version": "1.0.0",
1031- "agent_loaded": true
1032-}
1033-```
1034- 
1035-**2. 发送查询请求**
1036-```bash
1037-curl -X POST http://localhost:8090/query \
1038- -H "Content-Type: application/json" \
1039- -d '{
1040- "messages": [
1041- {"role": "user", "content": "你好,我想查询订单状态"}
1042- ],
1043- "conversation_id": "conv_001",
1044- "user_id": "user_123"
1045- }'
1046-```
1047- 
1048-**3. 查看流式响应**
1049-```bash
1050-# 方式 1: 直接输出 SSE 流
1051-curl -N -X POST http://localhost:8090/query \
1052- -H "Content-Type: application/json" \
1053- -d '{
1054- "messages": [{"role": "user", "content": "你好"}],
1055- "conversation_id": "conv_002",
1056- "user_id": "user_123"
1057- }'
1058- 
1059-# 方式 2: 格式化输出(使用 jq)
1060-curl -N -X POST http://localhost:8090/query \
1061- -H "Content-Type: application/json" \
1062- -d '{
1063- "messages": [{"role": "user", "content": "帮我查一下北京天气"}],
1064- "conversation_id": "conv_003",
1065- "user_id": "user_123"
1066- }' | while read -r line; do
1067- if [[ $line == data:* ]]; then
1068- echo "${line#data: }" | jq .
1069- fi
1070- done
1071-```
1072- 
1073-**4. 重置对话**
1074-```bash
1075-curl -X POST http://localhost:8090/reset_conversation \
1076- -H "Content-Type: application/json" \
1077- -d '{
1078- "conversation_id": "conv_001"
1079- }'
1080-```
1081-响应:
1082-```json
1083-{
1084- "status": "ok",
1085- "message": "Conversation conv_001 reset"
1086-}
1087-```
1088- 
1089-#### 使用 Python 测试
1090- 
1091-```python
1092-import asyncio
1093-import json
1094-import httpx
1095- 
1096-async def query_agent():
1097- agent_url = "http://localhost:8090"
1098- 
1099- async with httpx.AsyncClient(timeout=30.0) as client:
1100- # 健康检查
1101- health = await client.get(f"{agent_url}/health")
1102- print(f"Health: {health.json()}")
1103- 
1104- # 发送查询
1105- request_data = {
1106- "messages": [
1107- {"role": "user", "content": "帮我查询订单 12345 的状态"}
1108- ],
1109- "conversation_id": "conv_python_001",
1110- "user_id": "user_123",
1111- }
1112- 
1113- # 接收流式响应
1114- async with client.stream(
1115- "POST",
1116- f"{agent_url}/query",
1117- json=request_data,
1118- ) as response:
1119- print(f"\nAgent Response:")
1120- async for line in response.aiter_lines():
1121- if line.startswith("data: "):
1122- data = line[6:]
1123- try:
1124- message = json.loads(data)
1125- if message.get("type") == "text":
1126- print(f"🤖 {message.get('content', '')}")
1127- elif message.get("type") == "tool_call":
1128- print(f"🔧 调用工具: {message.get('tool_name', '')}")
1129- except json.JSONDecodeError:
1130- pass
1131- 
1132-if __name__ == "__main__":
1133- asyncio.run(query_agent())
1134-```
1135- 
1136-#### API 端点总结
1137- 
1138-| 端点 | 方法 | 描述 |
1139-|------|------|------|
1140-| `/health` | GET | 健康检查 |
1141-| `/query` | POST | 发送查询(返回 SSE 流) |
1142-| `/reset_conversation` | POST | 重置指定对话的上下文 |
1143- 
1144-### 4.4 AppGroup 示例 - 多 Agent 共享端口
1145- 
1146-```python
1147-# examples/multi_agent_group.py
1148- 
1149-from openjiuwen_runtime import AgentApp, AppGroup
1150- 
1151- 
1152-# 创建客服 Agent
1153-customer_app = AgentApp(
1154- app_name="CustomerService",
1155- app_description="客服 Agent",
1156- version="1.0.0",
1157-)
1158- 
1159-@customer_app.init
1160-async def init_customer():
1161- print("客服 Agent 初始化完成")
1162- 
1163-@customer_app.query
1164-async def query_customer(msgs, request):
1165- # 客服逻辑
1166- yield {"type": "text", "content": f"客服回复: {msgs[-1]['content']}"}, True
1167- 
1168- 
1169-# 创建助手 Agent
1170-assistant_app = AgentApp(
1171- app_name="PersonalAssistant",
1172- app_description="个人助手 Agent",
1173- version="1.0.0",
1174-)
1175- 
1176-@assistant_app.init
1177-async def init_assistant():
1178- print("助手 Agent 初始化完成")
1179- 
1180-@assistant_app.query
1181-async def query_assistant(msgs, request):
1182- # 助手逻辑
1183- yield {"type": "text", "content": f"助手回复: {msgs[-1]['content']}"}, True
1184- 
1185- 
1186-# 创建应用组并挂载
1187-group = AppGroup(
1188- group_name="MultiAgentService",
1189- description="多 Agent 服务组",
1190- version="1.0.0",
1191-)
1192- 
1193-group.mount("/customer", customer_app)
1194-group.mount("/assistant", assistant_app)
1195- 
1196- 
1197-if __name__ == "__main__":
1198- # 支持 --host 和 --port 命令行参数
1199- group.run(port=8090)
1200-```
1201- 
1202-**测试 AppGroup:**
1203- 
1204-```bash
1205-# 查看组信息
1206-curl http://localhost:8090/
1207- 
1208-# 组健康检查
1209-curl http://localhost:8090/health
1210- 
1211-# 查询客服 Agent
1212-curl -X POST http://localhost:8090/customer/query \
1213- -H "Content-Type: application/json" \
1214- -d '{"messages": [{"role": "user", "content": "你好"}], "conversation_id": "test1"}'
1215- 
1216-# 查询助手 Agent
1217-curl -X POST http://localhost:8090/assistant/query \
1218- -H "Content-Type: application/json" \
1219- -d '{"messages": [{"role": "user", "content": "你好"}], "conversation_id": "test2"}'
1220-```
1221- 
1222-**响应示例 - 组健康检查:**
1223- 
1224-```json
1225-{
1226- "status": "healthy",
1227- "group": "MultiAgentService",
1228- "version": "1.0.0",
1229- "apps_count": 2,
1230- "apps": {
1231- "/customer": {
1232- "name": "CustomerService",
1233- "version": "1.0.0",
1234- "mount_path": "/customer",
1235- "type": "agent",
1236- "agent_loaded": true
1237- },
1238- "/assistant": {
1239- "name": "PersonalAssistant",
1240- "version": "1.0.0",
1241- "mount_path": "/assistant",
1242- "type": "agent",
1243- "agent_loaded": true
1244- }
1245- }
1246-}
1247-```
1248-