Metadata-Version: 2.3
Name: mh-gateway
Version: 0.1.1
Summary: Orchestration Gateway SDK — load scenarios, route agents, stream events. Built on top of minimal-harness.
Requires-Dist: fastapi>=0.115.0
Requires-Dist: minimal-harness>=0.8.0
Requires-Dist: mh-service-kit>=0.1.2
Requires-Dist: python-multipart>=0.0.20
Requires-Python: >=3.12
Description-Content-Type: text/markdown

# mh-gateway — 编排网关

核心网关服务，依赖 [minimal-harness](https://github.com/J0ey1iu/minimal-harness) SDK。负责场景加载、用户权限校验、事件流归集，协调前端与各 worker 服务的通信。

- 版本：**0.1.1**

- 端口：`8005`
- Swagger：`http://localhost:8005/docs`

> **开发者指南**：[docs/dev-guide.md](./docs/dev-guide.md)（中文） · [docs/dev-guide.agent.md](./docs/dev-guide.agent.md)（英文，面向 Coding Agent）
>
> **企业适配指导**：[docs/customer-adaptation-guide.md](./docs/customer-adaptation-guide.md)（中文） · [docs/customer-adaptation-guide.agent.md](./docs/customer-adaptation-guide.agent.md)（英文，面向 Coding Agent）
>
> **构建分发**：[docs/build-guide.md](./docs/build-guide.md)

## 在 mh 生态中的位置

| 包 | 角色 | 仓库 |
|---|---|---|
| [minimal-harness](https://github.com/J0ey1iu/minimal-harness) | 核心 SDK（类型、协议、Agent 运行时、LLM 抽象、Memory/Session）。本服务依赖它。 | [J0ey1iu/minimal-harness](https://github.com/J0ey1iu/minimal-harness) |
| [mh-service-kit](https://github.com/J0ey1iu/mh-service-kit) | FastAPI 服务工具包。本服务使用 `ServiceApp` 来托管 in-cluster Agent（如 dev-mode 下的 `triage`），并通过它提供的 SSE 客户端调用远程 Agent。 | [J0ey1iu/mh-service-kit](https://github.com/J0ey1iu/mh-service-kit) |
| [mh-tui](https://github.com/J0ey1iu/mh-tui) | 本地 Textual TUI。本服务是它的云端多租户对等形态；二者共享 `minimal-harness` 的 Agent / Tool / Memory 抽象。 | [J0ey1iu/mh-tui](https://github.com/J0ey1iu/mh-tui) |
| [agent-tool-service](https://github.com/J0ey1iu/mh-incubator/tree/main/packages/agent-tool-service) | 内置于 umbrella 仓的示例 Agent & Tool 服务，可被本服务通过 M2M 端点调用。 | [J0ey1iu/mh-incubator](https://github.com/J0ey1iu/mh-incubator) |
| [mh-incubator](https://github.com/J0ey1iu/mh-incubator) | umbrella 工作区，串联本服务、agent-tool-service、web-frontend、`minimal-harness` 一起做端到端演示。 | [J0ey1iu/mh-incubator](https://github.com/J0ey1iu/mh-incubator) |

## 适配层架构

orchestration 通过 LifespanHook 接口与外部系统解耦。所有适配器通过 `create_app()` 参数注入：

| 接口 | 默认实现 | 企业部署替换 |
|------|----------|----------|
| `UserAuthProvider` | `_DefaultAuthProvider` — 提取 `X-User-Id` header/cookie | 实现 `verify(request) → UserIdentity` |
| `PermissionChecker` | `_DefaultAuthProvider` — 内置权限表 | 实现 `check/get_permissions` |
| `MetadataManager` | `InMemoryManagementProvider` — 内存数据（受 `dev_mode` 控制） | 实现读 (`get_agent/list_agents/...`) + CRUD (`create_agent/update_agent/...`) |
| `OutboundAuthProvider` | `_DefaultOutboundAuthProvider` — 透传请求 header | 实现 `get_headers(request, url, type) → dict` |
| `M2MAuthProvider` | `_DefaultM2MAuthProvider` — 允许所有请求 | 实现 `authenticate(request) → str\|None`（Chat/Sessions 端点也支持 M2M 鉴权回退） |
| `ToolScriptStore` | orch-app: `LocalFileScriptStore` — 保存到 `./data/scripts/`；mh-local: `LocalScriptStore` — 保存到 `~/.config/mh-local/scripts/` | 实现 `save/read/delete/exists/close` 定义 `.py` 工具脚本落盘位置；不启用文件型工具可传 `None` |
| `ConfigProvider` | 无（仅环境变量） | 实现 `get(key) → str` 对接 Apollo/Nacos/Vault 等 |
| `LLMProvider` | 通过 `LLMProviderRegistry` + 环境变量 `ORCH_PROVIDER_*` 配置 | 注入自定义 `llm_provider_factory` 或 `llm_provider_registry` LifespanHook |

`UserAuthProvider`、`PermissionChecker`、`MetadataManager` Protocol 定义在 `mh_gateway` 内。`OutboundAuthProvider`、`M2MAuthProvider`、`ConfigProvider` Protocol 也定义在 `mh_gateway` 内。

## `create_app()` 工厂函数（客户部署入口）

`create_app()` 是 mh-gateway 的唯一入口，所有适配器通过 LifespanHook 参数注入：

```python
import asyncio
import logging
from contextlib import asynccontextmanager

from fastapi import FastAPI
from mh_gateway import (
    ConfigManager, ConfigSchema, create_app,
)
from my_adapters import (
    CorpUserAuthProvider, CorpPermissionChecker, CorpRegistry,
)

# 1. 解析配置（env → 可选配置中心 → 报错）
config_mgr = ConfigManager()
settings = asyncio.run(config_mgr.resolve(ConfigSchema, prefix="ORCH"))

# 2. 配置 root logger（可选，不配置则使用 SDK 内置默认日志）
root = logging.getLogger()
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter(
    "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
))
root.addHandler(handler)
root.setLevel(logging.DEBUG)

# 3. 定义 Adapter LifespanHook（应用启动时注入）
@asynccontextmanager
async def token_verifier(app: FastAPI):
    app.state.adapters.token_verifier = CorpUserAuthProvider()
    yield

@asynccontextmanager
async def permission_checker(app: FastAPI):
    app.state.adapters.permission_checker = CorpPermissionChecker()
    yield

@asynccontextmanager
async def management_provider(app: FastAPI):
    app.state.adapters.management_provider = CorpRegistry()
    yield

# 4. 注入你的企业适配器
app = create_app(
    settings=settings,
    token_verifier=token_verifier,
    permission_checker=permission_checker,
    management_provider=management_provider,
)
```

省略的适配器参数会使用内置默认实现，适合开发和演示。

部署后以 `uvicorn my_app:app` 启动。

## AppState — 运行时可访问适配器

所有注入的适配器实例通过 `request.app.state.adapters` 访问：

```python
from mh_gateway import AppState

adapters: AppState = request.app.state.adapters
identity = await adapters.token_verifier.verify(request)
perms = await adapters.permission_checker.get_permissions(user_id)
agents = await adapters.management_provider.list_agents()
```

## API

### 用户面 API

| 端点 | 方法 | 说明 |
|------|------|------|
| `/api/v1/auth/me` | GET | 当前用户信息（含权限列表） |
| `/api/v1/scenarios` | GET | 场景列表（按权限过滤） |
| `/api/v1/scenarios/{id}` | GET | 场景详情（含 Agent/Tool） |
| `/api/v1/chat/{memory_id}` | POST | SSE 流式聊天（支持 `session_id` 续传）<br/>*支持用户 Token 或 M2M 鉴权* |
| `/api/v1/sessions` | GET | 当前用户的 Session 列表（支持 `?scenario_id=` 过滤）<br/>*支持用户 Token 或 M2M 鉴权* |
| `/api/v1/sessions` | POST | 创建 Session<br/>*支持用户 Token 或 M2M 鉴权* |
| `/api/v1/sessions/{id}` | GET | Session 详情（含消息数）<br/>*支持用户 Token 或 M2M 鉴权* |
| `/api/v1/sessions/{id}/messages` | GET | Session 消息历史<br/>*支持用户 Token 或 M2M 鉴权* |
| `/api/v1/sessions/{id}` | DELETE | 删除 Session<br/>*支持用户 Token 或 M2M 鉴权* |
| `/api/v1/agents` | GET | Agent 列表（按权限过滤，支持 `?scenario=` 过滤） |
| `/api/v1/tools` | GET | Tool 列表（按权限过滤） |
| `/api/v1/auth/logout` | POST | 用户登出（清除认证态） |
| `/api/v1/feedback` | GET/POST | 反馈列表（按 session 过滤）/ 提交用户反馈（自动关联 session/target 消息） |
| `/api/v1/feedback/{id}` | PUT/DELETE | 更新/删除反馈内容 |
| `/api/v1/health` | GET | 健康检查（始终返回 `{"status":"ok"}`） |
| `/ready` | GET | 就绪检查（检查数据库连接） |
| `/api/v1/metrics` | GET | 运行时指标快照（仅 `metrics_enabled=true` 时可用） |

### 管理面 API（需 `MetadataManager` + 对应资源管理权限：`manage:scene:*` / `manage:agent:*` / `manage:tool:*`）

| 端点 | 方法 | 说明 |
|------|------|------|
| `/api/v1/management/scenarios` | GET/POST | 场景列表/创建 |
| `/api/v1/management/scenarios/{id}` | GET/PUT/DELETE | 场景详情/更新/删除 |
| `/api/v1/management/scenarios/{id}/agents` | POST/DELETE | 场景-Agent 关系管理 |
| `/api/v1/management/scenarios/{id}/agents/{name}/tools` | POST/DELETE | Agent-Tool 关系管理 |
| `/api/v1/management/agents` | GET/POST | Agent 列表/创建 |
| `/api/v1/management/agents/{name}` | GET/PUT/DELETE | Agent 详情/更新/删除 |
| `/api/v1/management/tools` | GET/POST | Tool 列表/创建 |
| `/api/v1/management/tools/{name}` | GET/PUT/DELETE | Tool 详情/更新/删除<br/>*支持 `?force=true` 强制删除（自动解除被 scenario 引用）* |
| `/api/v1/management/tools/upload` | POST | 上传单个 `.py` 工具脚本（multipart `file`），自动解析 `TOOL_NAME` / `TOOL_PARAMETERS` / locale 元数据并校验 shebang 解释器存在性 |
| `/api/v1/management/tools/upload-batch` | POST | 批量上传多个 `.py` 工具脚本，单文件错误不影响其他文件 |
| `/api/v1/management/providers` | GET | LLM Provider 列表 |
| `/api/v1/management/controllers` | GET | Controller 目录（default / goal / timer，供前端渲染） |
| `/api/v1/management/metrics` | GET | 数据概览聚合指标（需 `manage:metrics:*` 权限，支持 `?date_from=&date_to=` 日期区间，精确到日） |

### M2M 端点

| 端点 | 方法 | 说明 |
|------|------|------|
| `/api/v1/agents/{name}/run` | POST | 运行 Agent（M2M 鉴权） |

### AI 生成端点

| 端点 | 方法 | 说明 |
|------|------|------|


### 开发模式端点（仅 `dev_mode=true`）

| 端点 | 方法 | 说明 |
|------|------|------|

## 文件型 Tool（ToolScriptStore + 脚本上传）

除了在 UI 中以表单方式逐字段创建 tool，平台还支持把 Python 文件上传后自动解析为 tool：

1. 用户在管理 UI 拖拽 `.py` 文件 → `POST /api/v1/management/tools/upload`
2. 服务端用 AST 解析脚本顶部变量，校验必填字段（`TOOL_NAME`、`TOOL_DESCRIPTION`、`TOOL_DISPLAY_NAME_LOCALE`、`TOOL_DESCRIPTION_LOCALE`、`TOOL_PARAMETERS`、`async def execute()`）和 shebang 解释器是否可执行
3. 通过后把脚本文件保存到 `ToolScriptStore`（每个 app 实现各自的存储位置），并写入 `ToolCreate.script_path` 创建 `ExternalScriptToolBinding`
4. 运行时由 `ExternalToolWrapper` 在子进程中执行 `execute()`，每次 `yield` 序列化为 JSON 推到前端作为 `ToolProgress` 事件，支持取消和异常传播

每个 `.py` 文件对应一个 tool，禁止在文件里定义 `register()` 函数。最简模板：

```python
TOOL_NAME = "greeter"
TOOL_DESCRIPTION = "Greet a user."
TOOL_DISPLAY_NAME_LOCALE = {"zh": "问候", "en": "Greeter"}
TOOL_DESCRIPTION_LOCALE = {"zh": "问候用户", "en": "Greet a user."}
TOOL_PARAMETERS = {
    "type": "object",
    "properties": {"name": {"type": "string"}},
    "required": ["name"],
}

async def execute(name: str):
    yield {"result": f"Hello, {name}!"}
```

## AuditMiddleware

每个 Agent 执行周期自动记录审计日志（包括 `agent_start/end`、`llm_start/end`、`tool_start/end/error`、token 用量）。
日志级别为 `INFO`，可通过 `orchestration.audit` logger 配置。

## AccessLogMiddleware

每个 HTTP 请求自动输出一条结构化 JSON 访问日志，包含 `method`、`path`、`status`、`duration_ms`、`trace_id`、`user_id` 等字段。
日志级别为 `INFO`，可通过 `orchestration.access` logger 配置。

## 监控指标

当 `metrics_enabled=true` 时，服务会自动注册 `MetricsCollector`，在内存中采集如下指标并通过后台定时任务推送到日志：

| 指标 | 类型 | 标签 |
|------|------|------|
| `http_requests_total` | Counter | method, path, status |
| `http_request_duration_ms` | Histogram | method, path |
| `llm_requests_total` | Counter | provider, model, status |
| `llm_tokens_total` | Counter | provider, model, type (prompt/completion) |
| `llm_request_duration_ms` | Histogram | provider, model |
| `agent_runs_total` | Counter | agent_id, status |
| `tool_calls_total` | Counter | tool_name, status |
| `sessions_active` | Gauge | — |

指标通过 AuditMiddleware 的生命周期钩子自动采集。可通过 `/api/v1/metrics` 获取实时快照。

## 指标持久化（MetricsRepository）

管理面数据概览（`/api/v1/management/metrics`）基于 **可选** 的持久化指标仓库，采用与
`MetricsCollector` 相同的单例注入模式，**不修改 `GatewayAdapters`**：

```python
from mh_gateway.metrics_repo import MetricsRepository, LLMCallRecord, set_metrics_repo

class MyMetricsRepository(MetricsRepository):
    async def record_llm_call(self, record: LLMCallRecord) -> None: ...
    async def record_tool_call(self, record: ToolCallRecord) -> None: ...
    async def query_summary(self, date_from=None, date_to=None) -> MetricsSummary: ...
    async def close(self) -> None: ...

# 部署方在 lifespan hook 中注册（create_app 已支持 lifespan_hooks 参数）
set_metrics_repo(MyMetricsRepository())
```

- 记录写入：由独立的 `MetricsPersistenceMiddleware`（单一职责，区别于审计）在
  `on_llm_end` / `on_tool_end` 钩子中写入，每条 LLM 调用记录一次（含 user/session/agent/scenario/provider/model/token/耗时）。
- 查询聚合：`query_summary` 按日期区间（`YYYY-MM-DD`）聚合调用次数、Token、Top N、模型性能。
- 存储介质：协议对后端完全中立 —— 单实例可用 SQLite/JSONL 文件，水平扩展时部署方可换共享数据库实现。
- 权限：端点需要 `manage:metrics:*` 权限（`require_permission`）。
- 未注册仓库时端点返回 `503`；中间件在未注册时完全静默。

## PermissionMiddleware

每个 Agent 运行时自动校验工具调用权限。可通过 `check(user_id, perm)` 返回 `bool` 实现自定义逻辑。

## 环境变量

所有环境变量以 `ORCH_` 为前缀：

| 变量 | 默认值 | 说明 |
|------|--------|------|
| `ORCH_DB_PATH` | `./sessions.db` | SQLite 数据库文件路径 |
| `ORCH_DB_AUTO_SCHEMA` | `false` | 启动时自动建表（生产环境建议设为 `false`） |
| `ORCH_CORS_ORIGINS` | `[]` | 跨域源（逗号分隔，如 `http://localhost:5173,http://localhost:3000`） |
| `ORCH_DEV_MODE` | `false` | 开发模式开关，开启后暴露内置 agent、前端 SPA、开发调试工具及 SSO 登录页 |
| `ORCH_LOG_LEVEL` | `INFO` | 日志级别（ConfigSchema 字段，但日志通过 `MH_LOG_LEVEL` 环境变量或自行配置 root logger 控制） |
| `ORCH_ENABLE_EVAL` | `true` | 是否暴露评测接口 |
| `ORCH_EVAL_RESULTS_DIR` | `./eval_results` | 评测结果存储目录 |
| `ORCH_VERIFY_AGENT_TOOL_SSL` | `false` | 调用远程 agent/tool 时是否验证 SSL 证书 |
| `ORCH_METRICS_ENABLED` | `false` | 启用指标采集（计数器/直方图/仪表盘）及 `/api/v1/metrics` 端点 |
| `ORCH_METRICS_PUSH_INTERVAL` | `60` | 指标推送间隔（秒），仅 `ORCH_METRICS_ENABLED=true` 时生效 |

### LLM Provider 配置

LLM 配置通过 `ORCH_PROVIDER_{NAME}__{KEY}` 环境变量设置，不再使用旧的 `ORCH_LLM_*` 变量：

```bash
# 配置 OpenAI
export ORCH_PROVIDER_OPENAI__API_KEY=sk-xxx
export ORCH_PROVIDER_OPENAI__BASE_URL=https://api.openai.com/v1

# 配置 Anthropic
export ORCH_PROVIDER_ANTHROPIC__API_KEY=sk-ant-xxx
```

内置 provider：`openai`、`anthropic`、`openai_viz`（openai 的克隆）。

Agent 元数据的 `provider` 和 `model` 字段控制 per-agent 的 provider 选择。默认使用 `openai`。

## 外部配置对接

当客户有自己的配置中心（Apollo/Nacos/Consul）和密钥系统（HashiCorp Vault/AWS Secrets Manager）时，可通过 `ConfigProvider` 协议对接。

### 解析优先级

配置值按以下优先级解析（高 → 低）：

1. **环境变量**（`ORCH_*`）— 最高优先级，运维可临时覆盖
2. **外接配置**（`ConfigProvider` 实例，敏感与非敏感通过不同实例区分）— 来自配置中心
3. **代码默认值** — 若以上均未设置，使用 `ConfigSchema` 中的默认值

### 远程 key 重映射

`ConfigManager.resolve()` 支持 `key_mapping` 参数，将内部字段名重映射为客户配置中心的 key：

```python
cfg = await config_mgr.resolve(
    MyConfig,
    prefix="my.registry",
    key_mapping={
        "db_path": "woa.orchestration.db.path",
    },
    sensitive_fields={"api_key"},
)
```

### 实现自定义 UserAuthProvider（对接企业 SSO）

`verify()` 收到的是完整的 FastAPI `Request`，可读 Cookie/Header/调外部 API：

```python
from typing import Any
from mh_gateway.auth import UserAuthProvider, UserIdentity

class CorpSSOVerifier(UserAuthProvider):
    async def verify(self, request: Any) -> UserIdentity | None:
        # 1. 从 Cookie 中提取会话标识
        session_id = request.cookies.get("sessionid")
        if not session_id:
            return None
        # 2. 调用企业认证 API（request 还可读其他 header/query）
        user_info = await self._call_auth_api(session_id)
        if not user_info:
            return None
        # 3. 返回标准身份（extra_data 保留完整信息）
        return UserIdentity(
            user_id=user_info["employee_id"],
            username=user_info["name"],
            roles=user_info.get("roles", []),
            extra_data=user_info,
        )
```

> HTTP Bearer token 由内置 `_DefaultAuthProvider` 从 `request.headers["authorization"]` 提取，客户使用 Cookie 时直接在 `verify()` 中读取 `request.cookies` 即可。

### 实现其他 Provider

```python
from mh_gateway import ConfigProvider

class ApolloConfigProvider(ConfigProvider):
    async def get(self, key: str) -> str | None:
        return await apollo_client.get_value(key)

class VaultSecretResolver(ConfigProvider):
    async def get(self, key: str) -> str | None:
        return await vault_client.read_secret(key)
```

## 内置 Agent 样例 & 开发模式

设置 `ORCH_DEV_MODE=true` 后，服务会暴露 3 个内置样例 agent 以及开发调试用工具端点：

| Agent | 英文名 | 中文名 | 说明 |
|-------|--------|--------|------|
| `triage` | General Assistant | 通用助手 | 理解用户需求并路由到专业 agent（code-reviewer / writer）。本地执行。 |
| `code-reviewer` | Code Reviewer | 代码审查 | 分析代码变更中的缺陷、风格、安全和性能问题。通过 M2M 端点执行。 |
| `writer` | Writing Assistant | 写作助手 | 辅助撰写文章、邮件、报告等。通过 M2M 端点执行。 |

内置 Tool 包括 `calculator`、`handoff`、`discover_agents`、`show_ui_meta`、`general_visualization`、`stop_agent`。

`triage` agent 在进程内本地执行（无 `endpoint_url`），`code-reviewer` 和 `writer` 通过 M2M 端点执行。内置 agent 的 system_prompt 支持中英文，根据前端传来的 `Accept-Language` 自动适配。

> **生产环境**请确保 `ORCH_DEV_MODE` 为 `false`（默认值），并通过 `management_provider` LifespanHook 注入企业自己的注册中心实现。

## 内置前端 UI（一站式部署）

设置 `ORCH_DEV_MODE=true` 后，FastAPI 会在 `/` 直接 serve 编译后的 SPA（单页应用），前提是 `static/` 目录存在。

```bash
# 构建前端（SPA + 组件 bundle → 复制到 static/）
bash scripts/build-frontend.sh

# 启动（前端在 http://localhost:8005）
ORCH_DEV_MODE=true uv run uvicorn mh_gateway.main:app --port 8005
```

> **注意**：前端静态文件需预先构建并放入 `static/` 目录。`ORCH_DEV_MODE=true` 时服务会自动挂载前端并处理 SPA fallback 路由。

## 本地开发

```bash
# 带前端（先构建前端 SPA + 复制到 static/）
bash scripts/dev-standalone.sh

# 或仅后端（前端由 Vite 开发服务器提供热更新）
uv run uvicorn mh_gateway.main:app --port 8005
cd web-frontend && npm run dev
```

或使用项目根目录的 `bash scripts/dev.sh` 一键启动所有服务。

## 构建分发

```bash
cd packages/mh-gateway
uv build
# 产出 dist/mh_gateway-*.whl
```

客户 pip install 后，编写自己的启动文件注入适配器即可。

## 测试

```bash
uv run pytest packages/mh-gateway/tests -v
```
