Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
291 changes: 291 additions & 0 deletions docs/design/02-systemone-design.md

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions examples/systemone_model/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
TYPESAFE_API_KEY=
TYPESAFE_BASE_URL=
TYPESAFE_DEFAULT_MODEL=jev-1.14.0

LAYA_BASE_URL=http://127.0.0.1:8000
LAYA_API_KEY=
LAYA_MODEL=
147 changes: 147 additions & 0 deletions examples/systemone_model/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# System One TypeSafe 与 Laya 示例

本示例用同一组客服工单问题,分别调用官方 TypeSafe API 和本地 Laya HTTP 服务。问题类型覆盖 `Choice`、`Score` 和 `Noul`。Laya 路径还会打印 checkpoint 路由,并对两条工单调用 `system_one_batch`。

## 关键特性

- **同一组问题,两个客户端**:`SystemOneClient.typesafe` 调用官方 API,`SystemOneClient.laya` 调用 `laya-serve`
- **三种答案**:部门用 `Choice`,紧急程度用 `Score`,是否退款用 `Noul`,再用 `choice_answer`、`score_answer`、`noul_answer` 按问题名取出
- **Laya 可省略鉴权和模型**:不设置 `LAYA_API_KEY` 时不发送 `Authorization`;不设置 `LAYA_MODEL` 时由服务端选择 checkpoint
- **Laya 批量**:`system_one_batch` 一次提交两条工单。官方后端不提供该接口,本示例只在 Laya 路径调用它
- **缺配置即跳过**:`TYPESAFE_API_KEY` 为空时跳过 TypeSafe,`LAYA_BASE_URL` 为空时跳过 Laya,另一侧仍会运行

## 调用结构

```text
run.py
├── questions: department (Choice), urgency (Score), refund (Noul)
├── typesafe: SystemOneClient.typesafe -> system_one
└── laya: SystemOneClient.laya -> system_one, system_one_batch
└── response.routing
```

关键文件:

- [examples/systemone_model/config.py](./config.py):读取两个客户端的环境变量
- [examples/systemone_model/run.py](./run.py):构造问题、调用客户端并打印答案
- [examples/systemone_model/.env](./.env):环境变量占位

## 关键代码解释

### 1) 官方 TypeSafe 客户端(`run.py`)

- `SystemOneClient.typesafe(api_key=..., base_url=..., default_model=...)` 创建客户端
- `api_key` 来自 `TYPESAFE_API_KEY`。`base_url` 和 `default_model` 留空时,由官方 SDK 使用它自己的默认地址和模型 `jev-latest`
- 用 `async with` 保证客户端关闭
- 对一条工单调用 `system_one`

### 2) Laya 客户端(`run.py`)

- `SystemOneClient.laya(base_url, api_key=..., default_model=...)` 创建客户端
- `base_url` 是服务根地址,例如 `http://127.0.0.1:8000`
- 单条结果若带 `routing`,打印 checkpoint 名称和仓库
- 随后用同一组问题调用 `system_one_batch`

### 3) 读取答案(`run.py`)

- `choice_answer(response, "department").choice` 是选中的部门标签
- `score_answer(response, "urgency").score` 是紧急程度分数
- `noul_answer(response, "refund").noul` 是是否退款的 0/1 答案

## 环境要求

- Python3.10+,推荐 Python3.12
- TypeSafe 路径需要可访问官方 API 的 key
- Laya 路径需要已启动的 `laya-serve`。安装项目时已包含 `typesafe-sdk`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

问题: README 第 55 行称「安装项目时已包含 typesafe-sdk」,但 typesafe-sdk 只在 [typesafe] 和 [all] extra 中(pyproject.toml),普通 pip install trpc-agent-py 并装入 examples/systemone_model/ 运行 python3 run.py 时不会安装它;该表述只在走 ./build.sh(会装 requirements-test.txt)时成立。

触发条件: 按 README 步骤克隆、pip install trpc-agent-py(不带 extra)、配置好环境变量后直接 python3 run.py。

实际影响: 示例在 import 阶段因 ModuleNotFoundError: typesafe_sdk 崩溃且无指引(与 __init__.py 裸导入无提示叠加),用户无法从文档判断需要安装 [typesafe] extra,与 pyproject 把 typesafe-sdk 设计为可选依赖的意图矛盾。

修正方向: 将第 55 行改为明确「需要执行 pip install 'trpc-agent-py[typesafe]' 或通过 ./build.sh 安装测试依赖」,与 pyproject 的 extra 语义保持一致。


## 构建步骤

```bash
git clone https://github.com/trpc-group/trpc-agent-python.git
cd trpc-agent-python
./build.sh
source .venv/bin/activate
```

## 运行步骤

### 配置环境变量

在 [examples/systemone_model/.env](./.env) 中配置(或通过 `export`):

| 变量 | 必填 | 说明 |
|------|------|------|
| `TYPESAFE_API_KEY` | TypeSafe 必填 | 官方 API key。为空时跳过 TypeSafe |
| `TYPESAFE_BASE_URL` | 否 | 官方 API 地址。为空时使用 SDK 默认地址 |
| `TYPESAFE_DEFAULT_MODEL` | 否 | 默认模型。为空时使用 `jev-latest` |
| `LAYA_BASE_URL` | Laya 必填 | Laya 服务根地址,例如 `http://127.0.0.1:8000` |
| `LAYA_API_KEY` | 否 | 为空时不发送 `Authorization` |
| `LAYA_MODEL` | 否 | 为空时由 Laya 选择 checkpoint |

### 运行命令

```bash
cd examples/systemone_model
python3 run.py
```

## 运行结果(示例)

下面是一次成功调用时的输出形态。部门标签、分数和 `noul` 的具体取值由服务端模型决定。

```text
============================================================
Backend: typesafe
============================================================
state: We were billed twice. Please refund the duplicate.
department: billing
urgency: 1.4
refund: 1
model: jev-latest
usage: input_tokens=120 output_tokens=0
----------------------------------------
============================================================
Backend: laya
============================================================
state: We were billed twice. Please refund the duplicate.
department: billing
urgency: 1.7
refund: 1
model: laya-rl-agent
usage: input_tokens=300 output_tokens=0
routing: model=multilingual repo=convaiinnovations/laya-multilingual
--- batch ---
[1/2]
state: We were billed twice. Please refund the duplicate.
department: billing
urgency: 1.7
refund: 1
model: laya-rl-agent
usage: input_tokens=300 output_tokens=0
routing: model=multilingual repo=convaiinnovations/laya-multilingual
[2/2]
state: The dashboard has been down since this morning.
department: technical
urgency: 2.0
refund: 0
model: laya-rl-agent
usage: input_tokens=280 output_tokens=0
routing: model=english repo=convaiinnovations/laya-english
----------------------------------------
```

## 结果分析(是否符合要求)

跑通后可以核对这几件事:

- **两个客户端都能回答同一组问题**:TypeSafe 与 Laya 都打印 `department`、`urgency`、`refund`
- **答案类型正确**:部门是 `criteria` 里的标签,紧急程度是分数,退款是 `noul`
- **Laya 带回路由**:`routing` 给出 checkpoint 名称;服务端报告仓库时一并打印
- **批量条数一致**:`system_one_batch` 的两条结果与提交的两条工单一一对应
- **缺配置不会拖垮另一条路径**:只配置其中一个后端时,另一条打印 `[SKIP]`

## 适用场景建议

- 验证 System One 问题构造,以及 TypeSafe / Laya 两个客户端的单次调用:适合使用本示例
- 验证 Laya 的 checkpoint 路由和 `system_one_batch`:适合使用本示例
- 需要把分类结果接到 `LlmAgent` 工具循环里:本示例只覆盖客户端调用,可把 `run.py` 里的问题构造和答案读取搬进自己的工具函数
56 changes: 56 additions & 0 deletions examples/systemone_model/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
#
# Copyright (C) 2026 Tencent. All rights reserved.
#
# tRPC-Agent-Python is licensed under Apache-2.0.
"""Read System One client settings from the environment."""

import os
from dataclasses import dataclass


@dataclass(frozen=True)
class TypeSafeSettings:
"""Settings for ``SystemOneClient.typesafe``."""

api_key: str
base_url: str | None
default_model: str | None


@dataclass(frozen=True)
class LayaSettings:
"""Settings for ``SystemOneClient.laya``."""

base_url: str
api_key: str | None
default_model: str | None


def typesafe_settings() -> TypeSafeSettings | None:
"""Return TypeSafe settings, or ``None`` when ``TYPESAFE_API_KEY`` is empty."""
api_key = os.getenv("TYPESAFE_API_KEY", "").strip()
if not api_key:
return None
return TypeSafeSettings(
api_key=api_key,
base_url=_optional("TYPESAFE_BASE_URL"),
default_model=_optional("TYPESAFE_DEFAULT_MODEL"),
)


def laya_settings() -> LayaSettings | None:
"""Return Laya settings, or ``None`` when ``LAYA_BASE_URL`` is empty."""
base_url = os.getenv("LAYA_BASE_URL", "").strip()
if not base_url:
return None
return LayaSettings(
base_url=base_url,
api_key=_optional("LAYA_API_KEY"),
default_model=_optional("LAYA_MODEL"),
)


def _optional(name: str) -> str | None:
value = os.getenv(name, "").strip()
return value or None
146 changes: 146 additions & 0 deletions examples/systemone_model/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
#
# Copyright (C) 2026 Tencent. All rights reserved.
#
# tRPC-Agent-Python is licensed under Apache-2.0.
"""Classify the same support ticket with the TypeSafe API and with Laya."""

import asyncio

from dotenv import load_dotenv
from trpc_agent_sdk.models.systemone import Choice
from trpc_agent_sdk.models.systemone import LayaSystemOneResponse
from trpc_agent_sdk.models.systemone import Noul
from trpc_agent_sdk.models.systemone import Score
from trpc_agent_sdk.models.systemone import SystemOneClient
from trpc_agent_sdk.models.systemone import SystemOneResponse
from trpc_agent_sdk.models.systemone import choice_answer
from trpc_agent_sdk.models.systemone import noul_answer
from trpc_agent_sdk.models.systemone import score_answer

from config import LayaSettings
from config import TypeSafeSettings
from config import laya_settings
from config import typesafe_settings

load_dotenv()

QUESTIONS = {
"department": Choice(
instructions="Which team should handle this?",
criteria={
"billing": "invoices, payments, refunds",
"technical": "bugs, outages",
"other": "everything else",
},
),
"urgency": Score(
instructions="How urgent is this?",
criteria=["not urgent", "soon", "blocking"],
),
"refund": Noul(instructions="Does the user request a refund?"),
}

TICKET = {"body": "We were billed twice. Please refund the duplicate."}
BATCH_TICKETS = [
TICKET,
{"body": "The dashboard has been down since this morning."},
]


def print_answers(response: SystemOneResponse) -> None:
"""Print the three ticket answers plus model, usage, and Laya routing."""
print(f"department: {choice_answer(response, 'department').choice}")
print(f"urgency: {score_answer(response, 'urgency').score}")
print(f"refund: {noul_answer(response, 'refund').noul}")
print(f"model: {response.model}")
usage = response.usage
print(f"usage: input_tokens={usage.input_tokens} output_tokens={usage.output_tokens}")
if isinstance(response, LayaSystemOneResponse) and response.routing is not None:
routing = response.routing
repo = f" repo={routing.repo}" if routing.repo else ""
print(f"routing: model={routing.model}{repo}")


def print_state(state: dict[str, str]) -> None:
"""Print the ticket text that was sent as ``state``."""
print(f"state: {state['body']}")


async def run_typesafe(settings: TypeSafeSettings) -> None:
"""Evaluate one ticket on the official TypeSafe API."""
async with SystemOneClient.typesafe(
api_key=settings.api_key,
base_url=settings.base_url,
default_model=settings.default_model,
) as client:
response = await client.system_one(TICKET, QUESTIONS)
print_state(TICKET)
print_answers(response)


async def run_laya(settings: LayaSettings) -> None:
"""Evaluate one ticket, then a two-ticket batch, on a Laya server."""
async with SystemOneClient.laya(
settings.base_url,
api_key=settings.api_key,
default_model=settings.default_model,
) as client:
response = await client.system_one(TICKET, QUESTIONS)
batch = await client.system_one_batch(BATCH_TICKETS, QUESTIONS)
print_state(TICKET)
print_answers(response)
print("--- batch ---")
for index, (state, item) in enumerate(zip(BATCH_TICKETS, batch), start=1):
print(f"[{index}/{len(BATCH_TICKETS)}]")
print_state(state)
print_answers(item)


async def _run_typesafe() -> bool:
"""Run TypeSafe when ``TYPESAFE_API_KEY`` is set."""
settings = typesafe_settings()
if settings is None:
print("[SKIP] TYPESAFE_API_KEY is not set")
return False
await run_typesafe(settings)
return True


async def _run_laya() -> bool:
"""Run Laya when ``LAYA_BASE_URL`` is set."""
settings = laya_settings()
if settings is None:
print("[SKIP] LAYA_BASE_URL is not set")
return False
await run_laya(settings)
return True


async def main() -> None:
"""Run TypeSafe and Laya, skipping a client whose required setting is missing."""
attempted = 0
succeeded = 0
for label, runner in (("typesafe", _run_typesafe), ("laya", _run_laya)):
print("=" * 60)
print(f"Backend: {label}")
print("=" * 60)
try:
sent = await runner()
except Exception as exc:
print(f"[FAIL] {exc}")
attempted += 1
continue
if not sent:
continue
attempted += 1
succeeded += 1
print("-" * 40)
if attempted == 0:
raise SystemExit("No backend ran. Set TYPESAFE_API_KEY and/or LAYA_BASE_URL.")
if succeeded == 0:
raise SystemExit(1)


if __name__ == "__main__":
asyncio.run(main())
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ hy-memory = [
"hy-memory>=2.0.6,<3.0.0",
]

typesafe = [
"typesafe-sdk>=0.7.5",
Comment thread
Rook1ex marked this conversation as resolved.
Resolved
]

langchain_tool = [
"langchain_tavily",
"langchain-core>=0.3.27",
Expand Down Expand Up @@ -181,6 +185,7 @@ all = [
"e2b-code-interpreter>=2.0.0",
"gepa>=0.0.7",
"rich>=13.0.0",
"typesafe-sdk>=0.7.5",
]

[project.scripts]
Expand Down
3 changes: 3 additions & 0 deletions requirements-test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,6 @@ aiofiles
mem0ai>=1.0.3
fastapi
mempalace==3.3.4

# Test Typesafe(Jev)
typesafe-sdk>=0.7.5
6 changes: 6 additions & 0 deletions tests/models/systemone/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
#
# Copyright (C) 2026 Tencent. All rights reserved.
#
# tRPC-Agent-Python is licensed under Apache-2.0.
"""Unit tests for the System One clients."""
6 changes: 6 additions & 0 deletions tests/models/systemone/typesafe/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
#
# Copyright (C) 2026 Tencent. All rights reserved.
#
# tRPC-Agent-Python is licensed under Apache-2.0.
"""Unit tests for the TypeSafe and Laya System One clients."""
Loading
Loading