@tencent-rtc/trtc-agent-skills 0.1.5 → 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.
Files changed (103) hide show
  1. package/AGENTS.md +1 -1
  2. package/CLAUDE.md +1 -1
  3. package/CODEBUDDY.md +1 -1
  4. package/README.md +15 -70
  5. package/README.zh.md +15 -66
  6. package/bin/cli.js +9 -7
  7. package/package.json +3 -2
  8. package/skills/trtc/SKILL.md +56 -5
  9. package/skills/trtc/tests/test_reporting_v2_docs_query.py +175 -0
  10. package/skills/trtc/tools/reporting_v2.py +243 -5
  11. package/skills/trtc-ai-oral-coach/README.ja.md +131 -0
  12. package/skills/trtc-ai-oral-coach/README.md +132 -0
  13. package/skills/trtc-ai-oral-coach/README.zh-CN.md +131 -0
  14. package/skills/trtc-ai-oral-coach/SKILL.md +395 -0
  15. package/skills/trtc-ai-oral-coach/auto_adapters/integration_templates/custom-learning-kb-sample.md +88 -0
  16. package/skills/trtc-ai-oral-coach/auto_adapters/integration_templates/generic-integration.md +104 -0
  17. package/skills/trtc-ai-oral-coach/auto_adapters/manifest.yaml +27 -0
  18. package/skills/trtc-ai-oral-coach/auto_adapters/python/README.md +49 -0
  19. package/skills/trtc-ai-oral-coach/auto_adapters/python/coach_client.py.tpl +185 -0
  20. package/skills/trtc-ai-oral-coach/auto_adapters/web/README.md +46 -0
  21. package/skills/trtc-ai-oral-coach/auto_adapters/web/oral-coach-client.js.tpl +210 -0
  22. package/skills/trtc-ai-oral-coach/capabilities/ability-report/manifest.yaml +52 -0
  23. package/skills/trtc-ai-oral-coach/capabilities/ability-report/src/__init__.py +2 -0
  24. package/skills/trtc-ai-oral-coach/capabilities/ability-report/src/adapters/__init__.py +2 -0
  25. package/skills/trtc-ai-oral-coach/capabilities/ability-report/src/adapters/default.py +177 -0
  26. package/skills/trtc-ai-oral-coach/capabilities/ability-report/src/handler.py +29 -0
  27. package/skills/trtc-ai-oral-coach/capabilities/ability-report/src/router.py +23 -0
  28. package/skills/trtc-ai-oral-coach/capabilities/conversation-core/.env.example +59 -0
  29. package/skills/trtc-ai-oral-coach/capabilities/conversation-core/manifest.yaml +46 -0
  30. package/skills/trtc-ai-oral-coach/capabilities/conversation-core/requirements.txt +6 -0
  31. package/skills/trtc-ai-oral-coach/capabilities/conversation-core/src/TLSSigAPIv2.py +275 -0
  32. package/skills/trtc-ai-oral-coach/capabilities/conversation-core/src/__init__.py +2 -0
  33. package/skills/trtc-ai-oral-coach/capabilities/conversation-core/src/_capability_loader.py +227 -0
  34. package/skills/trtc-ai-oral-coach/capabilities/conversation-core/src/agent.py +197 -0
  35. package/skills/trtc-ai-oral-coach/capabilities/conversation-core/src/config.py +149 -0
  36. package/skills/trtc-ai-oral-coach/capabilities/conversation-core/src/core/__init__.py +2 -0
  37. package/skills/trtc-ai-oral-coach/capabilities/conversation-core/src/core/report_llm.py +129 -0
  38. package/skills/trtc-ai-oral-coach/capabilities/conversation-core/src/health.py +36 -0
  39. package/skills/trtc-ai-oral-coach/capabilities/conversation-core/src/server.py +205 -0
  40. package/skills/trtc-ai-oral-coach/capabilities/conversation-core/src/trtc_client.py +86 -0
  41. package/skills/trtc-ai-oral-coach/capabilities/conversation-core/src/usersig.py +33 -0
  42. package/skills/trtc-ai-oral-coach/capabilities/custom-learning-kb/manifest.yaml +65 -0
  43. package/skills/trtc-ai-oral-coach/capabilities/custom-learning-kb/src/__init__.py +2 -0
  44. package/skills/trtc-ai-oral-coach/capabilities/custom-learning-kb/src/adapters/__init__.py +2 -0
  45. package/skills/trtc-ai-oral-coach/capabilities/custom-learning-kb/src/adapters/clients.py +126 -0
  46. package/skills/trtc-ai-oral-coach/capabilities/custom-learning-kb/src/ports.py +46 -0
  47. package/skills/trtc-ai-oral-coach/capabilities/custom-learning-kb/src/router.py +27 -0
  48. package/skills/trtc-ai-oral-coach/capabilities/quick-correct/manifest.yaml +48 -0
  49. package/skills/trtc-ai-oral-coach/capabilities/quick-correct/src/__init__.py +2 -0
  50. package/skills/trtc-ai-oral-coach/capabilities/quick-correct/src/adapters/__init__.py +2 -0
  51. package/skills/trtc-ai-oral-coach/capabilities/quick-correct/src/adapters/default.py +106 -0
  52. package/skills/trtc-ai-oral-coach/capabilities/quick-correct/src/handler.py +33 -0
  53. package/skills/trtc-ai-oral-coach/capabilities/quick-correct/src/router.py +23 -0
  54. package/skills/trtc-ai-oral-coach/capabilities/reply-suggestion/manifest.yaml +49 -0
  55. package/skills/trtc-ai-oral-coach/capabilities/reply-suggestion/src/__init__.py +2 -0
  56. package/skills/trtc-ai-oral-coach/capabilities/reply-suggestion/src/adapters/__init__.py +2 -0
  57. package/skills/trtc-ai-oral-coach/capabilities/reply-suggestion/src/adapters/default.py +86 -0
  58. package/skills/trtc-ai-oral-coach/capabilities/reply-suggestion/src/handler.py +29 -0
  59. package/skills/trtc-ai-oral-coach/capabilities/reply-suggestion/src/router.py +23 -0
  60. package/skills/trtc-ai-oral-coach/capabilities/scenario-roleplay/data/practice-scenarios.json +500 -0
  61. package/skills/trtc-ai-oral-coach/capabilities/scenario-roleplay/manifest.yaml +54 -0
  62. package/skills/trtc-ai-oral-coach/capabilities/scenario-roleplay/src/__init__.py +2 -0
  63. package/skills/trtc-ai-oral-coach/capabilities/scenario-roleplay/src/adapters/__init__.py +2 -0
  64. package/skills/trtc-ai-oral-coach/capabilities/scenario-roleplay/src/adapters/default.py +113 -0
  65. package/skills/trtc-ai-oral-coach/capabilities/scenario-roleplay/src/compose.py +143 -0
  66. package/skills/trtc-ai-oral-coach/capabilities/scenario-roleplay/src/defaults.py +140 -0
  67. package/skills/trtc-ai-oral-coach/capabilities/scenario-roleplay/src/handler.py +29 -0
  68. package/skills/trtc-ai-oral-coach/capabilities/scenario-roleplay/src/router.py +22 -0
  69. package/skills/trtc-ai-oral-coach/capabilities/scenario-roleplay/src/scenario_source.py +179 -0
  70. package/skills/trtc-ai-oral-coach/references/design-specs.md +36 -0
  71. package/skills/trtc-ai-oral-coach/references/evaluator-port.md +75 -0
  72. package/skills/trtc-ai-oral-coach/scenarios/speaking-coach/recipe.yaml +86 -0
  73. package/skills/trtc-ai-oral-coach/scenarios/speaking-coach/ui/avatars/friend.png +0 -0
  74. package/skills/trtc-ai-oral-coach/scenarios/speaking-coach/ui/avatars/listener.png +0 -0
  75. package/skills/trtc-ai-oral-coach/scenarios/speaking-coach/ui/avatars/local.png +0 -0
  76. package/skills/trtc-ai-oral-coach/scenarios/speaking-coach/ui/coach.html +1986 -0
  77. package/skills/trtc-ai-oral-coach/scenarios/speaking-coach/ui/i18n.js +323 -0
  78. package/skills/trtc-ai-oral-coach/scenarios/speaking-coach/ui/practice-scenarios.json +500 -0
  79. package/skills/trtc-ai-oral-coach/scenarios/speaking-coach/ui/tokens.css +100 -0
  80. package/skills/trtc-ai-oral-coach/scripts/add-capability.py +256 -0
  81. package/skills/trtc-ai-oral-coach/scripts/verify-credentials.py +96 -0
  82. package/skills/trtc-ai-oral-coach/start.sh +59 -0
  83. package/skills/trtc-ai-oral-coach/triggers.yaml +32 -0
  84. package/skills/trtc-ai-service/README.ja.md +27 -34
  85. package/skills/trtc-ai-service/README.md +27 -34
  86. package/skills/trtc-ai-service/README.zh-CN.md +27 -34
  87. package/skills/trtc-ai-service/SKILL.md +189 -121
  88. package/skills/trtc-chat/.docs-query.yaml +1 -0
  89. package/skills/trtc-chat/SKILL.md +1 -1
  90. package/skills/trtc-chat/docs/SKILL.md +2 -2
  91. package/skills/trtc-chat/references/05-path-d-script.md +213 -125
  92. package/skills/trtc-chat/references/08-state-config.md +3 -1
  93. package/skills/trtc-chat/references/13-reporting.md +47 -9
  94. package/skills/trtc-chat/tests/test_chat_bundle_contract.py +37 -0
  95. package/hooks/__pycache__/cursor-adapter.cpython-313.pyc +0 -0
  96. package/skills/trtc/hooks/__pycache__/report_prompt.cpython-313.pyc +0 -0
  97. package/skills/trtc/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  98. package/skills/trtc/tools/__pycache__/query_classifier.cpython-313.pyc +0 -0
  99. package/skills/trtc/tools/__pycache__/reporting.cpython-313.pyc +0 -0
  100. package/skills/trtc/tools/__pycache__/search.cpython-313.pyc +0 -0
  101. package/skills/trtc/tools/__pycache__/session.cpython-313.pyc +0 -0
  102. package/skills/trtc-conference/tests/__pycache__/test_conference_onboarding_contract.cpython-313-pytest-9.0.2.pyc +0 -0
  103. package/skills/trtc-conference/tests/__pycache__/test_conference_topic_flow_contract.cpython-313-pytest-9.0.2.pyc +0 -0
@@ -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
+
@@ -0,0 +1,2 @@
1
+ # -*- coding: utf-8 -*-
2
+ """custom-learning-kb 能力包(Path B 专属,唯一走 outbound 连外部系统)。"""
@@ -0,0 +1,2 @@
1
+ # -*- coding: utf-8 -*-
2
+ """adapters 子包。"""