Phase 4:补齐通用记忆闭环(验收 9/9 通过)
按 P1-Pipecat Flows Agent化与记忆改造交接.md 的 Phase 4 完成。 新增 p1/phase4_acceptance.py:跑真实服务端验 7 条,每条之间查数据库。 实测结果(2026-09-16) 1. "记住我不吃香菜" → confirmed 落库 ✅ 2. **新连接** "我不吃什么?" → "你不吃香菜。" ✅ 连接开始时加载档案生效 3. "我今天吃了香菜" → 不新增 confirmed,模型还追问了是否口味变了 ✅ 4. "其实我现在吃香菜了" → 同键仍只一条 confirmed,值已更新 ✅ 5. "忘掉我的饮食偏好" → 已删除 ✅ 6/7. 访客与跨用户隔离 → 由单测覆盖(客户端不支持切身份) 实现:会话开始时加载稳定档案 - load_stable_memories():取已确认、未过期记忆(身份未确认返回空) - _conversation_node 改用 build_system_prompt,与 legacy 共用同一套 人格与记忆注入格式;记忆进**同一条** system 消息,仍不用 role_message 修掉一个真实缺陷:Flows 路径误用了语音的 max_tokens - 症状:一句"忘掉我的饮食偏好",服务端日志 "Failed to parse function call arguments: {"query": "饮食"(JSON 被截断), 回复为空、记忆没删掉,且**没有任何报错** - 根因:Flows 路径沿用了语音的 max_tokens=200(为"回复要短"而设)。 原始 API 对照实测:max_tokens=200 时模型 finish=stop、根本不调工具; 600 时正常产生 tool_calls 且参数 JSON 完整 - 修法:新增 AgentConfig.max_tokens(AI_AGENT_MAX_TOKENS,默认 600), Flows 路径用它,语音路径仍用 200 新增 client/test_client.py 能力(为验收所需) - RTVIReplies:收服务端 RTVI 文本消息(bot-output / bot-transcription) - 打印 BOT_REPLY 供验收脚本断言 回归:test_agent_tools 45/45、test_flows_runtime 24/24、 test_memory 30/30、test_identity 19/19 服务器已改回 AI_AGENT_RUNTIME=legacy(改默认是 Phase 7 的事)
This commit is contained in:
@@ -191,6 +191,42 @@ class ReplyRecorder:
|
||||
return (self.first_non_silent_at - started_at) * 1000
|
||||
|
||||
|
||||
class RTVIReplies:
|
||||
"""收集服务端发来的 RTVI 文本消息,供验收脚本断言。
|
||||
|
||||
网页用的 bot-output / bot-transcription 是完整文本,
|
||||
bot-llm-text 是逐 token 增量。验收看完整文本。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.bot_texts: list[str] = []
|
||||
self.user_texts: list[str] = []
|
||||
self.other: list[str] = []
|
||||
|
||||
def handle(self, raw: str) -> None:
|
||||
try:
|
||||
m = json.loads(raw)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return
|
||||
if not isinstance(m, dict):
|
||||
return
|
||||
t = m.get("type")
|
||||
data = m.get("data") or {}
|
||||
text = data.get("text") if isinstance(data, dict) else None
|
||||
if t == "bot-output" and text:
|
||||
self.bot_texts.append(text)
|
||||
elif t == "bot-transcription" and text:
|
||||
self.bot_texts.append(text)
|
||||
elif t == "user-llm-text" and text:
|
||||
self.user_texts.append(text)
|
||||
elif t:
|
||||
self.other.append(t)
|
||||
|
||||
@property
|
||||
def last_bot(self) -> str:
|
||||
return self.bot_texts[-1] if self.bot_texts else ""
|
||||
|
||||
|
||||
def rtvi_send_text(channel, text: str, *, audio_response: bool = True) -> None:
|
||||
"""通过 data channel 发一条 RTVI send-text。
|
||||
|
||||
@@ -277,11 +313,18 @@ async def run(server: str, wav: Path | None, out: Path, *, connect_only: bool,
|
||||
channel = pc.createDataChannel("pipecat")
|
||||
dc_state = {"ok": False}
|
||||
|
||||
replies = RTVIReplies()
|
||||
|
||||
@channel.on("open")
|
||||
def _dc_open():
|
||||
dc_state["ok"] = True
|
||||
log.info("data channel 已打开,开始发心跳")
|
||||
|
||||
@channel.on("message")
|
||||
def _dc_message(msg):
|
||||
if isinstance(msg, str):
|
||||
replies.handle(msg)
|
||||
|
||||
async def _keepalive():
|
||||
while pc.connectionState not in ("closed", "failed"):
|
||||
if dc_state["ok"]:
|
||||
@@ -359,6 +402,10 @@ async def run(server: str, wav: Path | None, out: Path, *, connect_only: bool,
|
||||
log.info("本轮 %s", "收到语音回复" if recorder.has_speech else "未收到语音")
|
||||
# 让它把话说完
|
||||
await asyncio.sleep(4)
|
||||
# 打印助手回复全文,供验收脚本断言
|
||||
if replies.bot_texts:
|
||||
print(f" BOT_REPLY: {replies.bot_texts[-1]}", flush=True)
|
||||
replies.bot_texts.clear()
|
||||
await asyncio.sleep(2)
|
||||
elif track:
|
||||
# 等音频放完 + 留出响应时间
|
||||
|
||||
Reference in New Issue
Block a user