@tencent-rtc/trtc-agent-skills 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/CLAUDE.md +1 -1
- package/CODEBUDDY.md +1 -1
- package/README.md +6 -73
- package/README.zh.md +6 -69
- package/bin/cli.js +9 -7
- package/package.json +1 -1
- package/skills/trtc/SKILL.md +55 -4
- package/skills/trtc-ai-oral-coach/README.ja.md +131 -0
- package/skills/trtc-ai-oral-coach/README.md +132 -0
- package/skills/trtc-ai-oral-coach/README.zh-CN.md +131 -0
- package/skills/trtc-ai-oral-coach/SKILL.md +395 -0
- package/skills/trtc-ai-oral-coach/auto_adapters/integration_templates/custom-learning-kb-sample.md +88 -0
- package/skills/trtc-ai-oral-coach/auto_adapters/integration_templates/generic-integration.md +104 -0
- package/skills/trtc-ai-oral-coach/auto_adapters/manifest.yaml +27 -0
- package/skills/trtc-ai-oral-coach/auto_adapters/python/README.md +49 -0
- package/skills/trtc-ai-oral-coach/auto_adapters/python/coach_client.py.tpl +185 -0
- package/skills/trtc-ai-oral-coach/auto_adapters/web/README.md +46 -0
- package/skills/trtc-ai-oral-coach/auto_adapters/web/oral-coach-client.js.tpl +210 -0
- package/skills/trtc-ai-oral-coach/capabilities/ability-report/manifest.yaml +52 -0
- package/skills/trtc-ai-oral-coach/capabilities/ability-report/src/__init__.py +2 -0
- package/skills/trtc-ai-oral-coach/capabilities/ability-report/src/adapters/__init__.py +2 -0
- package/skills/trtc-ai-oral-coach/capabilities/ability-report/src/adapters/default.py +177 -0
- package/skills/trtc-ai-oral-coach/capabilities/ability-report/src/handler.py +29 -0
- package/skills/trtc-ai-oral-coach/capabilities/ability-report/src/router.py +23 -0
- package/skills/trtc-ai-oral-coach/capabilities/conversation-core/.env.example +59 -0
- package/skills/trtc-ai-oral-coach/capabilities/conversation-core/manifest.yaml +46 -0
- package/skills/trtc-ai-oral-coach/capabilities/conversation-core/requirements.txt +6 -0
- package/skills/trtc-ai-oral-coach/capabilities/conversation-core/src/TLSSigAPIv2.py +275 -0
- package/skills/trtc-ai-oral-coach/capabilities/conversation-core/src/__init__.py +2 -0
- package/skills/trtc-ai-oral-coach/capabilities/conversation-core/src/_capability_loader.py +227 -0
- package/skills/trtc-ai-oral-coach/capabilities/conversation-core/src/agent.py +197 -0
- package/skills/trtc-ai-oral-coach/capabilities/conversation-core/src/config.py +149 -0
- package/skills/trtc-ai-oral-coach/capabilities/conversation-core/src/core/__init__.py +2 -0
- package/skills/trtc-ai-oral-coach/capabilities/conversation-core/src/core/report_llm.py +129 -0
- package/skills/trtc-ai-oral-coach/capabilities/conversation-core/src/health.py +36 -0
- package/skills/trtc-ai-oral-coach/capabilities/conversation-core/src/server.py +205 -0
- package/skills/trtc-ai-oral-coach/capabilities/conversation-core/src/trtc_client.py +86 -0
- package/skills/trtc-ai-oral-coach/capabilities/conversation-core/src/usersig.py +33 -0
- package/skills/trtc-ai-oral-coach/capabilities/custom-learning-kb/manifest.yaml +65 -0
- package/skills/trtc-ai-oral-coach/capabilities/custom-learning-kb/src/__init__.py +2 -0
- package/skills/trtc-ai-oral-coach/capabilities/custom-learning-kb/src/adapters/__init__.py +2 -0
- package/skills/trtc-ai-oral-coach/capabilities/custom-learning-kb/src/adapters/clients.py +126 -0
- package/skills/trtc-ai-oral-coach/capabilities/custom-learning-kb/src/ports.py +46 -0
- package/skills/trtc-ai-oral-coach/capabilities/custom-learning-kb/src/router.py +27 -0
- package/skills/trtc-ai-oral-coach/capabilities/quick-correct/manifest.yaml +48 -0
- package/skills/trtc-ai-oral-coach/capabilities/quick-correct/src/__init__.py +2 -0
- package/skills/trtc-ai-oral-coach/capabilities/quick-correct/src/adapters/__init__.py +2 -0
- package/skills/trtc-ai-oral-coach/capabilities/quick-correct/src/adapters/default.py +106 -0
- package/skills/trtc-ai-oral-coach/capabilities/quick-correct/src/handler.py +33 -0
- package/skills/trtc-ai-oral-coach/capabilities/quick-correct/src/router.py +23 -0
- package/skills/trtc-ai-oral-coach/capabilities/reply-suggestion/manifest.yaml +49 -0
- package/skills/trtc-ai-oral-coach/capabilities/reply-suggestion/src/__init__.py +2 -0
- package/skills/trtc-ai-oral-coach/capabilities/reply-suggestion/src/adapters/__init__.py +2 -0
- package/skills/trtc-ai-oral-coach/capabilities/reply-suggestion/src/adapters/default.py +86 -0
- package/skills/trtc-ai-oral-coach/capabilities/reply-suggestion/src/handler.py +29 -0
- package/skills/trtc-ai-oral-coach/capabilities/reply-suggestion/src/router.py +23 -0
- package/skills/trtc-ai-oral-coach/capabilities/scenario-roleplay/data/practice-scenarios.json +500 -0
- package/skills/trtc-ai-oral-coach/capabilities/scenario-roleplay/manifest.yaml +54 -0
- package/skills/trtc-ai-oral-coach/capabilities/scenario-roleplay/src/__init__.py +2 -0
- package/skills/trtc-ai-oral-coach/capabilities/scenario-roleplay/src/adapters/__init__.py +2 -0
- package/skills/trtc-ai-oral-coach/capabilities/scenario-roleplay/src/adapters/default.py +113 -0
- package/skills/trtc-ai-oral-coach/capabilities/scenario-roleplay/src/compose.py +143 -0
- package/skills/trtc-ai-oral-coach/capabilities/scenario-roleplay/src/defaults.py +140 -0
- package/skills/trtc-ai-oral-coach/capabilities/scenario-roleplay/src/handler.py +29 -0
- package/skills/trtc-ai-oral-coach/capabilities/scenario-roleplay/src/router.py +22 -0
- package/skills/trtc-ai-oral-coach/capabilities/scenario-roleplay/src/scenario_source.py +179 -0
- package/skills/trtc-ai-oral-coach/references/design-specs.md +36 -0
- package/skills/trtc-ai-oral-coach/references/evaluator-port.md +75 -0
- package/skills/trtc-ai-oral-coach/scenarios/speaking-coach/recipe.yaml +86 -0
- package/skills/trtc-ai-oral-coach/scenarios/speaking-coach/ui/avatars/friend.png +0 -0
- package/skills/trtc-ai-oral-coach/scenarios/speaking-coach/ui/avatars/listener.png +0 -0
- package/skills/trtc-ai-oral-coach/scenarios/speaking-coach/ui/avatars/local.png +0 -0
- package/skills/trtc-ai-oral-coach/scenarios/speaking-coach/ui/coach.html +1986 -0
- package/skills/trtc-ai-oral-coach/scenarios/speaking-coach/ui/i18n.js +323 -0
- package/skills/trtc-ai-oral-coach/scenarios/speaking-coach/ui/practice-scenarios.json +500 -0
- package/skills/trtc-ai-oral-coach/scenarios/speaking-coach/ui/tokens.css +100 -0
- package/skills/trtc-ai-oral-coach/scripts/add-capability.py +256 -0
- package/skills/trtc-ai-oral-coach/scripts/verify-credentials.py +96 -0
- package/skills/trtc-ai-oral-coach/start.sh +59 -0
- package/skills/trtc-ai-oral-coach/triggers.yaml +32 -0
- package/skills/trtc-ai-service/README.ja.md +15 -32
- package/skills/trtc-ai-service/README.md +15 -32
- package/skills/trtc-ai-service/README.zh-CN.md +15 -32
- package/skills/trtc-ai-service/SKILL.md +188 -120
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""共享评估基座(F1)—— 4 个评估能力(scenario-roleplay / quick-correct /
|
|
3
|
+
reply-suggestion / ability-report)复用的 LLM 客户端。
|
|
4
|
+
|
|
5
|
+
设计要点(沿用定稿 Demo coach_evaluator 的成熟策略):
|
|
6
|
+
- OpenAI 兼容 /chat/completions,response_format=json_object 强约束
|
|
7
|
+
- 不同任务用不同 temperature / timeout(见 KIND_PROFILES)
|
|
8
|
+
- 防 Prompt Injection:调用方拼 prompt 时用 json.dumps() 序列化用户内容
|
|
9
|
+
- core 对**实时对话零 LLM**(那是 TRTC 云端);本模块仅是给能力用的共享 utility
|
|
10
|
+
|
|
11
|
+
用法(能力侧 default adapter):
|
|
12
|
+
from ..._shared import get_evaluator # 见各能力的加载封装
|
|
13
|
+
ev = get_evaluator()
|
|
14
|
+
raw = ev.call(prompt, kind="quick") # 返回 LLM 文本(JSON 串)
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
from typing import Dict, Tuple
|
|
21
|
+
|
|
22
|
+
import requests
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger("report_llm")
|
|
25
|
+
|
|
26
|
+
# kind → (temperature 调整策略, read_timeout 上限, system_msg)
|
|
27
|
+
_CONNECT_TIMEOUT = 10.0
|
|
28
|
+
KIND_PROFILES = {
|
|
29
|
+
"report": ("base", None, "You are a strict JSON-only English conversation coach producing learning reports."),
|
|
30
|
+
"quick": ("low", 15.0, "You are a strict JSON-only English-conversation coach producing Speak-style inline corrections."),
|
|
31
|
+
"hints": ("high", 15.0, "You are a strict JSON-only English-conversation coach producing short natural reply suggestions."),
|
|
32
|
+
"scene": ("higher",12.0, "You are a strict JSON-only assistant generating short vivid English-practice scene snippets."),
|
|
33
|
+
"opening": ("mid", 12.0, "You are a strict JSON-only assistant writing one in-character English opening line."),
|
|
34
|
+
# KB 场景素材批量提炼:要忠实原素材,创造力要求不高,timeout 稍长(一次处理多条)
|
|
35
|
+
"kb_scene": ("low", 20.0, "You are a strict JSON-only assistant extracting structured English-practice "
|
|
36
|
+
"scene setups from a customer's own knowledge-base snippets."),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ReportLLM:
|
|
41
|
+
"""共享 LLM 客户端。由 core 依据 .env REPORT_LLM_* 实例化后交给各能力。"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, api_key: str, api_url: str, model: str,
|
|
44
|
+
temperature: float = 0.2, timeout: float = 120.0) -> None:
|
|
45
|
+
self.api_key = (api_key or "").strip()
|
|
46
|
+
self.api_url = (api_url or "https://api.openai.com/v1/chat/completions").strip()
|
|
47
|
+
self.model = (model or "gpt-4o-mini").strip()
|
|
48
|
+
self.temperature = float(temperature)
|
|
49
|
+
self.timeout = float(timeout)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def configured(self) -> bool:
|
|
53
|
+
return bool(self.api_key)
|
|
54
|
+
|
|
55
|
+
def _temp_for(self, strategy: str) -> float:
|
|
56
|
+
t = self.temperature
|
|
57
|
+
return {
|
|
58
|
+
"low": min(0.2, t),
|
|
59
|
+
"base": t,
|
|
60
|
+
"mid": max(0.5, min(0.8, t + 0.3)),
|
|
61
|
+
"high": max(0.5, min(0.8, t + 0.4)),
|
|
62
|
+
"higher": max(0.6, min(0.9, t + 0.5)),
|
|
63
|
+
}.get(strategy, t)
|
|
64
|
+
|
|
65
|
+
def call(self, prompt: str, kind: str = "report") -> str:
|
|
66
|
+
"""调用 LLM,返回 message.content(应为 JSON 串)。失败抛异常,由能力侧兜底。"""
|
|
67
|
+
if not self.configured:
|
|
68
|
+
raise RuntimeError("REPORT_LLM api_key is not configured")
|
|
69
|
+
strategy, read_timeout, system_msg = KIND_PROFILES.get(kind, KIND_PROFILES["report"])
|
|
70
|
+
timeout: Tuple[float, float] = (
|
|
71
|
+
_CONNECT_TIMEOUT,
|
|
72
|
+
min(read_timeout, self.timeout) if read_timeout else self.timeout,
|
|
73
|
+
)
|
|
74
|
+
payload = {
|
|
75
|
+
"model": self.model,
|
|
76
|
+
"messages": [
|
|
77
|
+
{"role": "system", "content": system_msg},
|
|
78
|
+
{"role": "user", "content": prompt},
|
|
79
|
+
],
|
|
80
|
+
"temperature": self._temp_for(strategy),
|
|
81
|
+
"response_format": {"type": "json_object"},
|
|
82
|
+
}
|
|
83
|
+
headers = {
|
|
84
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
85
|
+
"Content-Type": "application/json",
|
|
86
|
+
}
|
|
87
|
+
resp = requests.post(self.api_url, headers=headers, json=payload, timeout=timeout)
|
|
88
|
+
resp.raise_for_status()
|
|
89
|
+
data = resp.json()
|
|
90
|
+
try:
|
|
91
|
+
return data["choices"][0]["message"]["content"]
|
|
92
|
+
except (KeyError, IndexError, TypeError) as e:
|
|
93
|
+
raise ValueError(f"unexpected LLM response shape: {e}; body={data}")
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def loads(raw: str) -> Dict:
|
|
97
|
+
"""JSON 解析辅助(能力侧统一用)。"""
|
|
98
|
+
data = json.loads(raw)
|
|
99
|
+
if not isinstance(data, dict):
|
|
100
|
+
raise ValueError("top-level is not an object")
|
|
101
|
+
return data
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ---- 单例:core 在 server 启动时用 .env 配置初始化后,能力通过 get_shared() 取用 ----
|
|
105
|
+
_shared: ReportLLM | None = None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def init_shared(api_key: str, api_url: str, model: str,
|
|
109
|
+
temperature: float = 0.2, timeout: float = 120.0) -> ReportLLM:
|
|
110
|
+
global _shared
|
|
111
|
+
_shared = ReportLLM(api_key, api_url, model, temperature, timeout)
|
|
112
|
+
return _shared
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def get_shared() -> ReportLLM:
|
|
116
|
+
"""能力侧调用;若 core 尚未初始化则返回一个未配置实例(configured=False,能力自动降级)。"""
|
|
117
|
+
global _shared
|
|
118
|
+
if _shared is None:
|
|
119
|
+
_shared = ReportLLM("", "", "gpt-4o-mini")
|
|
120
|
+
return _shared
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
# 全局模块别名:让通过 _capability_loader 加载的各能力可直接 `import coach_report_llm`
|
|
125
|
+
# 取到 server.py 初始化后的**同一**单例(避免跨包 dual-import 导致的配置丢失)。
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
import sys as _sys
|
|
128
|
+
_sys.modules.setdefault("coach_report_llm", _sys.modules[__name__])
|
|
129
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""健康自检:三 LED(tencent_cloud / trtc / llm)。供前端顶栏状态灯 + Path B e2e 用。
|
|
3
|
+
|
|
4
|
+
轻量策略:不做深度联网探测(避免慢/误报),只校验凭据是否齐全 + 格式合理。
|
|
5
|
+
需要深度校验时走 scripts/verify-credentials.py(对齐客服)。
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from typing import Dict
|
|
11
|
+
|
|
12
|
+
from .config import CoreConfig
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _led(ok: bool, msg: str) -> Dict[str, object]:
|
|
16
|
+
return {"ok": ok, "message": msg}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def check_all(cfg: CoreConfig) -> Dict[str, Dict[str, object]]:
|
|
20
|
+
# TRTC
|
|
21
|
+
if not cfg.trtc.configured:
|
|
22
|
+
trtc = _led(False, "TRTC 应用凭据未配置")
|
|
23
|
+
elif not re.fullmatch(r"[0-9a-fA-F]{64}", cfg.trtc.sdk_secret_key or ""):
|
|
24
|
+
trtc = _led(False, "SDKSecretKey 格式异常(应为 64 位十六进制)")
|
|
25
|
+
else:
|
|
26
|
+
trtc = _led(True, "TRTC 凭据就绪")
|
|
27
|
+
|
|
28
|
+
# Tencent Cloud
|
|
29
|
+
tc = _led(cfg.tencent.configured,
|
|
30
|
+
"腾讯云 API 凭据就绪" if cfg.tencent.configured else "腾讯云 API 凭据未配置")
|
|
31
|
+
|
|
32
|
+
# LLM
|
|
33
|
+
llm = _led(cfg.llm.configured,
|
|
34
|
+
"LLM 密钥就绪" if cfg.llm.configured else "LLM 密钥未配置")
|
|
35
|
+
|
|
36
|
+
return {"tencent_cloud": tc, "trtc": trtc, "llm": llm}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""FastAPI 入口:REST 路由(Path B)+ /action 兼容 facade(Path A 定稿 UI)+ 能力子路由挂载。
|
|
3
|
+
|
|
4
|
+
双轨(F2):
|
|
5
|
+
- REST:/api/v1/config|agent/start|stop|farewell|invoke|health(给 Path B 集成者)
|
|
6
|
+
- facade:POST /action + Action 头(定稿 Demo 的 app.js 原样调用,零改动)
|
|
7
|
+
两者都走同一套 Agent 方法。
|
|
8
|
+
|
|
9
|
+
能力路由:用 try_load_capability 预接线(未安装的能力优雅跳过)。
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Dict, Optional
|
|
17
|
+
|
|
18
|
+
from fastapi import FastAPI, Request, APIRouter, HTTPException
|
|
19
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
20
|
+
from fastapi.responses import FileResponse, JSONResponse
|
|
21
|
+
from fastapi.staticfiles import StaticFiles
|
|
22
|
+
|
|
23
|
+
from .config import load_config
|
|
24
|
+
from .health import check_all
|
|
25
|
+
from .agent import Agent
|
|
26
|
+
from .core.report_llm import init_shared
|
|
27
|
+
from ._capability_loader import try_load_capability
|
|
28
|
+
|
|
29
|
+
logging.basicConfig(level=os.getenv("LOG_LEVEL", "INFO"),
|
|
30
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
|
|
31
|
+
logger = logging.getLogger("server")
|
|
32
|
+
|
|
33
|
+
_cfg = load_config()
|
|
34
|
+
# 初始化共享评估基座(F1)—— 供 4 个评估能力复用
|
|
35
|
+
init_shared(_cfg.report_llm.api_key, _cfg.report_llm.api_url, _cfg.report_llm.model,
|
|
36
|
+
_cfg.report_llm_temperature, _cfg.report_llm_timeout)
|
|
37
|
+
|
|
38
|
+
_agent: Optional[Agent] = None
|
|
39
|
+
_init_error: Optional[str] = None
|
|
40
|
+
try:
|
|
41
|
+
if _cfg.trtc.configured and _cfg.tencent.configured:
|
|
42
|
+
_agent = Agent(_cfg)
|
|
43
|
+
logger.info("Agent initialized")
|
|
44
|
+
else:
|
|
45
|
+
_init_error = f"credentials missing: {_cfg.missing()}"
|
|
46
|
+
logger.warning(_init_error)
|
|
47
|
+
except Exception as exc: # noqa: BLE001
|
|
48
|
+
_init_error = str(exc)
|
|
49
|
+
logger.warning("Agent not initialized: %s", _init_error)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
app = FastAPI(title="ai-oral-coach / conversation-core", version="1.0.0")
|
|
53
|
+
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True,
|
|
54
|
+
allow_methods=["*"], allow_headers=["*"])
|
|
55
|
+
|
|
56
|
+
api = APIRouter(prefix="/api/v1")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _require_agent() -> Agent:
|
|
60
|
+
if _agent is None:
|
|
61
|
+
raise HTTPException(status_code=503, detail={
|
|
62
|
+
"code": "credentials_missing",
|
|
63
|
+
"message": _init_error or "credentials not configured",
|
|
64
|
+
"hint": "配置三把钥匙后重启(见 SKILL.md §5)",
|
|
65
|
+
})
|
|
66
|
+
return _agent
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# REST(Path B)
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
@api.get("/health")
|
|
73
|
+
def health() -> Dict[str, Any]:
|
|
74
|
+
checks = check_all(_cfg)
|
|
75
|
+
overall = "ok" if all(c["ok"] for c in checks.values()) else "partial_failure"
|
|
76
|
+
return {"status": overall, "checks": checks,
|
|
77
|
+
"configured": _cfg.fully_configured, "missing": _cfg.missing()}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@api.post("/config")
|
|
81
|
+
def rest_config(payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
82
|
+
return _require_agent().issue_config(payload.get("userid") or payload.get("user_id"))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@api.post("/agent/start")
|
|
86
|
+
def rest_start(payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
87
|
+
return _require_agent().start(payload)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@api.post("/agent/stop")
|
|
91
|
+
def rest_stop(payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
92
|
+
return _require_agent().stop(payload)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@api.post("/agent/farewell")
|
|
96
|
+
def rest_farewell(payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
97
|
+
return _require_agent().farewell(payload)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@api.post("/agent/invoke")
|
|
101
|
+
def rest_invoke(payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
102
|
+
return _require_agent().invoke(payload)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
app.include_router(api)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
# 能力子路由挂载(预接线;未安装能力 try_load 返回 None 自动跳过)
|
|
110
|
+
# ---------------------------------------------------------------------------
|
|
111
|
+
_CAP_ROUTES = [
|
|
112
|
+
("scenario-roleplay", "/api/v1/scene"),
|
|
113
|
+
("quick-correct", "/api/v1/correct"),
|
|
114
|
+
("reply-suggestion", "/api/v1/suggest"),
|
|
115
|
+
("ability-report", "/api/v1/report"),
|
|
116
|
+
("custom-learning-kb", "/api/v1/kb"),
|
|
117
|
+
]
|
|
118
|
+
for _cap, _prefix in _CAP_ROUTES:
|
|
119
|
+
_mod = try_load_capability(_cap, "src/router.py")
|
|
120
|
+
if _mod is not None and hasattr(_mod, "router"):
|
|
121
|
+
app.include_router(_mod.router, prefix=_prefix, tags=[_cap])
|
|
122
|
+
logger.info("mounted capability router: %s -> %s", _cap, _prefix)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
# /action 兼容 facade(Path A 定稿 UI)—— Action 头 → Agent 方法 / 能力
|
|
127
|
+
# 复刻定稿 Demo 的 POST /action 契约,让 app.js 零改动即可跑。
|
|
128
|
+
# ---------------------------------------------------------------------------
|
|
129
|
+
_FACADE_ACTIONS = {
|
|
130
|
+
"join", "StartAIConversation", "StopAIConversation", "FarewellAndStop",
|
|
131
|
+
"InvokeLLM", "GenerateReport", "QuickCorrect", "SuggestReplies", "GetSceneCandidates",
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _err(msg: str) -> JSONResponse:
|
|
136
|
+
return JSONResponse({"Response": {"Error": {"Code": "InvalidParameter", "Message": msg}}})
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@app.post("/action")
|
|
140
|
+
async def action(request: Request):
|
|
141
|
+
act = request.headers.get("Action", "")
|
|
142
|
+
if act not in _FACADE_ACTIONS:
|
|
143
|
+
return _err(f"action {act!r} invalid")
|
|
144
|
+
body = await request.json() if request.headers.get("content-type", "").startswith("application/json") else {}
|
|
145
|
+
body = body or {}
|
|
146
|
+
try:
|
|
147
|
+
# 核心 action 需要 Agent(依赖 TRTC/腾讯云);评估类只需 LLM,不经 Agent。
|
|
148
|
+
if act == "join":
|
|
149
|
+
return _require_agent().issue_config(body.get("userid"))
|
|
150
|
+
if act == "StartAIConversation":
|
|
151
|
+
return _require_agent().start(body)
|
|
152
|
+
if act == "StopAIConversation":
|
|
153
|
+
return _require_agent().stop(body)
|
|
154
|
+
if act == "FarewellAndStop":
|
|
155
|
+
return _require_agent().farewell(body)
|
|
156
|
+
if act == "InvokeLLM":
|
|
157
|
+
return _require_agent().invoke(body)
|
|
158
|
+
# 评估类 → 转发到对应能力的 handler(不依赖 Agent/TRTC)
|
|
159
|
+
if act == "GenerateReport":
|
|
160
|
+
return _dispatch_capability("ability-report", "generate", body)
|
|
161
|
+
if act == "QuickCorrect":
|
|
162
|
+
return _dispatch_capability("quick-correct", "generate", body)
|
|
163
|
+
if act == "SuggestReplies":
|
|
164
|
+
return _dispatch_capability("reply-suggestion", "generate", body)
|
|
165
|
+
if act == "GetSceneCandidates":
|
|
166
|
+
return _dispatch_capability("scenario-roleplay", "get_scene_candidates", body)
|
|
167
|
+
except HTTPException:
|
|
168
|
+
raise
|
|
169
|
+
except Exception as exc: # noqa: BLE001
|
|
170
|
+
logger.error("action %s failed: %s", act, exc)
|
|
171
|
+
return _err(str(exc))
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _dispatch_capability(cap: str, func: str, body: Dict[str, Any]):
|
|
175
|
+
"""调用能力 src/handler.py 里的 facade 函数(复刻 Demo 的响应形状)。"""
|
|
176
|
+
mod = try_load_capability(cap, "src/handler.py")
|
|
177
|
+
if mod is None or not hasattr(mod, func):
|
|
178
|
+
return _err(f"capability {cap!r} not installed")
|
|
179
|
+
return getattr(mod, func)(body)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ---------------------------------------------------------------------------
|
|
183
|
+
# 静态 Web Demo(Path A)—— 由 WEB_DEMO_DIR 指向三屏 SPA
|
|
184
|
+
# ---------------------------------------------------------------------------
|
|
185
|
+
_DEMO_DIR = Path(os.getenv("WEB_DEMO_DIR", "")) if os.getenv("WEB_DEMO_DIR") else None
|
|
186
|
+
if _DEMO_DIR and _DEMO_DIR.exists():
|
|
187
|
+
app.mount("/static", StaticFiles(directory=str(_DEMO_DIR), html=True), name="static")
|
|
188
|
+
|
|
189
|
+
@app.get("/")
|
|
190
|
+
def index() -> FileResponse:
|
|
191
|
+
# 优先 coach.html(定稿 Demo 入口),否则 index.html
|
|
192
|
+
for name in ("coach.html", "index.html"):
|
|
193
|
+
p = _DEMO_DIR / name
|
|
194
|
+
if p.exists():
|
|
195
|
+
return FileResponse(str(p))
|
|
196
|
+
raise HTTPException(status_code=404, detail="demo index not found")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def main() -> None:
|
|
200
|
+
import uvicorn
|
|
201
|
+
uvicorn.run(app, host=os.getenv("HOST", "0.0.0.0"), port=int(os.getenv("PORT", "8000")))
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
if __name__ == "__main__":
|
|
205
|
+
main()
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""TRTC OpenAPI 客户端封装:Start / Stop / Control AI Conversation。
|
|
3
|
+
|
|
4
|
+
只做协议编排,不含任何业务 prompt(业务在 scenario-roleplay 能力里组装好后传入)。
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
|
|
11
|
+
from tencentcloud.common import credential
|
|
12
|
+
from tencentcloud.common.profile.client_profile import ClientProfile
|
|
13
|
+
from tencentcloud.common.profile.http_profile import HttpProfile
|
|
14
|
+
from tencentcloud.trtc.v20190722 import trtc_client, models
|
|
15
|
+
|
|
16
|
+
from .config import CoreConfig
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TRTCClient:
|
|
20
|
+
def __init__(self, cfg: CoreConfig) -> None:
|
|
21
|
+
self._cfg = cfg
|
|
22
|
+
cred = credential.Credential(cfg.tencent.secret_id, cfg.tencent.secret_key)
|
|
23
|
+
http = HttpProfile()
|
|
24
|
+
http.endpoint = cfg.trtc.endpoint
|
|
25
|
+
cp = ClientProfile()
|
|
26
|
+
cp.httpProfile = http
|
|
27
|
+
self._api = trtc_client.TrtcClient(cred, cfg.trtc.api_region, cp)
|
|
28
|
+
|
|
29
|
+
def start(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
30
|
+
req = models.StartAIConversationRequest()
|
|
31
|
+
req.from_json_string(json.dumps(params, ensure_ascii=False))
|
|
32
|
+
resp = self._api.StartAIConversation(req)
|
|
33
|
+
return json.loads(resp.to_json_string())
|
|
34
|
+
|
|
35
|
+
def stop(self, task_id: str) -> Dict[str, Any]:
|
|
36
|
+
req = models.StopAIConversationRequest()
|
|
37
|
+
req.from_json_string(json.dumps({"TaskId": task_id}))
|
|
38
|
+
try:
|
|
39
|
+
resp = self._api.StopAIConversation(req)
|
|
40
|
+
return json.loads(resp.to_json_string())
|
|
41
|
+
except Exception as e: # noqa: BLE001
|
|
42
|
+
if "TaskNotExist" in str(e):
|
|
43
|
+
return {"RequestId": "N/A", "message": "Task already stopped"}
|
|
44
|
+
raise
|
|
45
|
+
|
|
46
|
+
def control(self, task_id: str, command: str, text: str,
|
|
47
|
+
interrupt: bool = True, stop_after_play: bool = False,
|
|
48
|
+
add_history: bool = True, priority: int = 0) -> Dict[str, Any]:
|
|
49
|
+
"""
|
|
50
|
+
!!! 关键修复 !!!
|
|
51
|
+
ControlAIConversation 的 Command 与子对象是按 Command 值互斥的:
|
|
52
|
+
Command="ServerPushText" → 必须配 "ServerPushText": {Text, Interrupt, AddHistory, Priority, StopAfterPlay}
|
|
53
|
+
Command="InvokeLLM" → 必须配 "InvokeLLM": {Content, Interrupt, ExperimentalParams}
|
|
54
|
+
之前这里不管 command 是什么,payload 永远塞进 "ServerPushText"(且用 "Text" 而不是
|
|
55
|
+
"Content")——Command=InvokeLLM 时 TRTC 服务端根本不会去读 ServerPushText 这个键,
|
|
56
|
+
等于 push-to-talk 场景下用户的原话从来没有真正传给大模型,AI 只能在没拿到这轮新发
|
|
57
|
+
言的情况下瞎凑一句延续对话("答非所问/编造内容"的根本原因)。
|
|
58
|
+
"""
|
|
59
|
+
if command == "InvokeLLM":
|
|
60
|
+
params = {
|
|
61
|
+
"TaskId": task_id,
|
|
62
|
+
"Command": "InvokeLLM",
|
|
63
|
+
"InvokeLLM": {
|
|
64
|
+
"Content": text,
|
|
65
|
+
"Interrupt": interrupt,
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
else:
|
|
69
|
+
push = {
|
|
70
|
+
"Text": text,
|
|
71
|
+
"Interrupt": interrupt,
|
|
72
|
+
"AddHistory": add_history,
|
|
73
|
+
"Priority": priority,
|
|
74
|
+
}
|
|
75
|
+
if stop_after_play:
|
|
76
|
+
push["StopAfterPlay"] = True
|
|
77
|
+
params = {"TaskId": task_id, "Command": command, "ServerPushText": push}
|
|
78
|
+
req = models.ControlAIConversationRequest()
|
|
79
|
+
req.from_json_string(json.dumps(params, ensure_ascii=False))
|
|
80
|
+
try:
|
|
81
|
+
resp = self._api.ControlAIConversation(req)
|
|
82
|
+
return json.loads(resp.to_json_string())
|
|
83
|
+
except Exception as e: # noqa: BLE001
|
|
84
|
+
if "TaskNotExist" in str(e):
|
|
85
|
+
return {"RequestId": "N/A", "message": "Task already ended"}
|
|
86
|
+
raise
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""UserSig 三套签发(用户 / 机器人 / 数字人)—— 复用腾讯云官方 TLSSigAPIv2。"""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from . import TLSSigAPIv2 # noqa: F401 —— 同目录官方库
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _api(sdk_app_id: int, secret: str):
|
|
9
|
+
return TLSSigAPIv2.TLSSigAPIv2(sdk_app_id, secret)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def gen_user_sig(sdk_app_id: int, sdk_secret_key: str, user_id: str,
|
|
13
|
+
expire_seconds: int = 86400) -> str:
|
|
14
|
+
if not user_id or len(user_id) > 32:
|
|
15
|
+
raise ValueError(f"invalid userId: {user_id!r}")
|
|
16
|
+
return _api(sdk_app_id, sdk_secret_key).genUserSig(user_id, expire_seconds)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def sign_trio(sdk_app_id: int, sdk_secret_key: str, user_id: str,
|
|
20
|
+
expire_seconds: int = 86400) -> dict:
|
|
21
|
+
"""一次签三套:真人用户 / AI 机器人 / 数字人。"""
|
|
22
|
+
robot_id = f"{user_id}_robot"
|
|
23
|
+
avatar_id = f"{user_id}_avatar"
|
|
24
|
+
api = _api(sdk_app_id, sdk_secret_key)
|
|
25
|
+
return {
|
|
26
|
+
"sdkappid": sdk_app_id,
|
|
27
|
+
"user_id": user_id,
|
|
28
|
+
"user_sig": api.genUserSig(user_id, expire_seconds),
|
|
29
|
+
"robot_user_id": robot_id,
|
|
30
|
+
"robot_user_sig": api.genUserSig(robot_id, expire_seconds),
|
|
31
|
+
"avatar_user_id": avatar_id,
|
|
32
|
+
"avatar_user_sig": api.genUserSig(avatar_id, expire_seconds),
|
|
33
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# custom-learning-kb —— 接入自有教研知识库(Path A / Path B 均可选装)
|
|
2
|
+
name: custom-learning-kb
|
|
3
|
+
version: "1.0.0"
|
|
4
|
+
type: capability
|
|
5
|
+
description: "把用户自有教研库(默认 Dify/Coze)接进来,驱动 Setup 屏的场景候选(由 scenario-roleplay 消费),也为报告的 betterExpressions 提供素材。唯一走『连外部系统』的能力。"
|
|
6
|
+
|
|
7
|
+
path_a: true # Path A / Path B 均可选装;不配置任何 KB_* 时行为等同不装(用内置题库)
|
|
8
|
+
|
|
9
|
+
dependencies:
|
|
10
|
+
- { name: conversation-core, version: ">=1.0.0,<2.0.0" }
|
|
11
|
+
|
|
12
|
+
extensions:
|
|
13
|
+
- inject_at: server.router_extension
|
|
14
|
+
inline_code: |
|
|
15
|
+
from ._capability_loader import try_load_capability as _try
|
|
16
|
+
_m = _try("custom-learning-kb", "src/router.py")
|
|
17
|
+
if _m is not None and hasattr(_m, "router"):
|
|
18
|
+
app.include_router(_m.router, prefix="/api/v1/kb", tags=["custom-learning-kb"])
|
|
19
|
+
|
|
20
|
+
config:
|
|
21
|
+
adapter: { default: "dify", env: "KB_ADAPTER" } # dify | coze | user_custom
|
|
22
|
+
top_k: { default: 3, env: "KB_TOP_K" }
|
|
23
|
+
|
|
24
|
+
endpoints:
|
|
25
|
+
- { method: POST, path: /api/v1/kb/retrieve, description: "检索教研片段(供内部能力调用,也可前端直调)" }
|
|
26
|
+
|
|
27
|
+
# 经典「连外部系统」契约 —— 用 KBClient Port(见 references/evaluator-port.md#2)
|
|
28
|
+
business_contract:
|
|
29
|
+
port_class: "src.ports.kb_client.KBClient"
|
|
30
|
+
default_adapter: "src.adapters.dify.DifyKBClient"
|
|
31
|
+
customization_sop: "references/evaluator-port.md#2-kbclient-port"
|
|
32
|
+
external_apis:
|
|
33
|
+
- name: kb.retrieve
|
|
34
|
+
direction: outbound # ← 关键:去拉用户的 KB(非 inbound)
|
|
35
|
+
method: POST
|
|
36
|
+
path: "{KB_DIFY_API_URL}/datasets/retrieve | Coze /open_api/knowledge/recall"
|
|
37
|
+
description: "检索用户知识库,返回相关教研片段"
|
|
38
|
+
request_schema: { query: string, top_k: int }
|
|
39
|
+
response_schema: { records: "[{text, source, score}]" }
|
|
40
|
+
auth: { type: bearer, location: header, name: Authorization }
|
|
41
|
+
timeout_ms: 8000
|
|
42
|
+
|
|
43
|
+
integration:
|
|
44
|
+
auto_adapters:
|
|
45
|
+
- { tech_stack: ["dify"], adapter: dify }
|
|
46
|
+
- { tech_stack: ["coze"], adapter: coze }
|
|
47
|
+
fallback:
|
|
48
|
+
manual_api: { rest_endpoint: "/api/v1/kb" }
|
|
49
|
+
# scenario-roleplay 是当前唯一的内部消费者:装了本能力后,Setup 屏场景候选会自动
|
|
50
|
+
# 从内置题库切到本能力检索到的内容(契约格式快解析 / 自由文本 LLM 提炼),检索/
|
|
51
|
+
# 提炼失败静默降级回内置题库,不需要额外接线。
|
|
52
|
+
consumers:
|
|
53
|
+
- { capability: scenario-roleplay, via: "src/scenario_source.py::get_kb_candidates" }
|
|
54
|
+
|
|
55
|
+
security:
|
|
56
|
+
network:
|
|
57
|
+
enforce_https: true # 非 localhost 强制 HTTPS
|
|
58
|
+
deny_private_network: true # 禁止访问内网地址(SSRF 防护)
|
|
59
|
+
|
|
60
|
+
acceptance:
|
|
61
|
+
- "配置 Dify/Coze key 后 /api/v1/kb/retrieve 能返回片段"
|
|
62
|
+
- "切换 KB_ADAPTER (dify|coze|user_custom) 无需改业务代码"
|
|
63
|
+
- "outbound 拒绝内网地址(localhost 除外)"
|
|
64
|
+
- "装了本能力后,GetSceneCandidates 自动改用 KB 内容;不装或检索失败静默降级回内置题库"
|
|
65
|
+
|