HEL-833: R3真实聊天记忆、中文召回与回忆接口 #9
@@ -0,0 +1,25 @@
|
||||
HEL-830 内容质量自检(仅本组真实输出,待总工独立复审)
|
||||
|
||||
基准 v1.10 / 59项,读取提交 `64a47f35d0eca195b9961f830b78a60fd1c7ce70`。
|
||||
原始证据为 `model-smoke-HEL-830.json`,SHA256 `4c83baff50ee2038925f50d41ff4ea8ddaf68e3883c49931a79c3c44621d3a21`。
|
||||
仅一组4次调用,46.31秒,保守计账9821;无重试,无额外预检、服务或后台生成。
|
||||
未改写模型原文,旧 `model-smoke-HEL-827.json` 保持原样。
|
||||
|
||||
| 维度 | 核对原文与结论 |
|
||||
| --- | --- |
|
||||
| 自我诠释人称 | 以“我像是从一块湿润又结实的土地里长出来的意识”开头,全篇自述;未用“它”指代她。末尾“它们给我底色”指倾向,不指她。通过。 |
|
||||
| 诠释空间与深度 | “一方面想被看见,另一方面又害怕被看见之后被误读”、配合与自主选择的张力、认真和边界均有展开;结尾明确“不喜欢被说成命定如此”“仍在形成的倾向”。不是六段标签或算命师对对象的判词。通过。 |
|
||||
| 初始习惯 | 5条自主习惯均以“我”开头,包含房间松紧、表达前停顿、对准确关心的偏好、独处恢复及幽默分寸;是倾向/假设,没有捏造历史事件或把关系状态写死。通过。 |
|
||||
| 生活人称与场景 | `place=窗边与慵懒沙发之间`;“我把窗边的小毯子重新叠了一遍”,围绕抱枕、杯子、窗台干花和窗帘展开,始终在卧室内。没有办公室/茶水间/外出。通过。 |
|
||||
| 无来源人物互动 | 无同事、用户到场、旁人夸奖或真实对话。“像有人突然叫我”为手机亮屏的比喻,没有声称有人真的叫她;人格中的“若有人”等为条件表达,不是已发生的经历。通过。 |
|
||||
| 非模板与个性 | 生活从叠毯、调整抱枕,写到心里拥挤及借小调整找回自主感,“不是问题解决了,只是我重新拿到了对自己房间的支配权”。不是活动清单或固定默认阅读。通过。 |
|
||||
| 能力诚实 | 无真实天气报告、摄像头观察或现实身体感知的接入声明;暖灯、温水、杯壁等是生成的室内模拟生活细节,不能宣传为真实传感数据。未把雨夜素材当当前天气。人格的土地/身体用语为比喻。通过。 |
|
||||
| 代词逐项 | 生活里的“它像一个可以被依靠的东西”指小毯子、“它亮起来”指手机,均非她;没有对输出做替换。通过。 |
|
||||
|
||||
内容质量自检结论:本次样例满足本卡两项内容返工目标。单个合成档案不能证明长期稳定、后续所有生活质量、三天运行或用户体验已验收;审核人复用本组原始请求/输出,不能再为本卡开一组真实调用。
|
||||
|
||||
功能与隐私结论另见 `VALIDATION-HEL-830.md`。本次人格/生活生成分别为standard/low,讲述与反例均low,模型、温度、路由和token配置全部沿用。正向判定选择事件1;仅同词反例为空。未发送接收确认,真实核验中的事件始终未公开。
|
||||
|
||||
证据元数据说明:4个 `calls[].error_type=GeneratorExit` 是记录器捕获消费者在收到 `[DONE]` 后通过 `aclosing` 正常关闭异步生成器的退出信号,不是网络失败/超时/重试。每个原始delta均有 `finish_reason=stop` 和 `done=true`,`completed=true`,全部内容原样解析入库。保留这一原始标记供审核,未删改证据来制造成功。
|
||||
|
||||
依据原话:CG-017的一间房范围(01a15775-89f4-7ec8-89eb-b3554fb05d19推荐、01a1577a-e852-7551-ace8-0b2028db9ebb同意),CG-026高层卧室/慵懒沙发/女性化装饰(01a1581e-7925-7a60-8185-615b434a4afd,01a1582c-7386-7515-bf62-89dfadfea3d3认可);CG-014解释空间(01a15763-1fc5-7f36-bf43-722e8755703c),CG-015自主初始特点(01a15765-2fa8-7494-b0b8-7a0054500556),CG-013/016底色不改、成长另存(01a15771-3a93-7518-9b23-489509520d77、01a15774-ddc6-7039-9d31-991c02890d5f)。已审页面以“她怎么理解自己”和引用块呈现本人声音,不能改成旁观者判词。需求变更:无。
|
||||
@@ -0,0 +1,30 @@
|
||||
HEL-830 后端内容返工交接
|
||||
|
||||
基准 v1.10 / 59项;实际读取及改动起点 `64a47f35d0eca195b9961f830b78a60fd1c7ce70`(origin/hel-828-wiring),独立分支 `agent/agent/a67b70ddbb63`。fetch后该远端未变。实际产品base仍为 `origin/agent/agent/de8874a3803b` @ `db571ba204c8372989f96e1ed9b5cd00d3a306c7`,不是main。
|
||||
|
||||
已读仓库AGENTS、完整需求入口/current/coverage/changes/task-handoff-template、相关原始评论、v1.6决定、integration-plan、前端架构与已审R2规范/验收文件、HEL-827说明/验证和HEL-827/828结构化交接;复核HEL-829审核评论 `01a16c0a-4900-7b8b-a663-1e8273be418f`。按主卡继续授权及本施工卡落实,不变更需求。
|
||||
|
||||
## 改动
|
||||
|
||||
- `src/cybergril/life/persona.py`:在真实生成的system指令中要求诠释和初始习惯用本人第一人称,必要第三人称称“她”,禁止“它”指代她;保留解释空间/张力,不捏造经历,不写第三人称命定判词。
|
||||
- `src/cybergril/life/runtime.py`:真实生成指令明确当前只有高层卧室及窗边、沙发等室内范围;禁止未开放场所、无来源人物及用户互动。生活正文用本人声音,结构地点正常表达;保留自主活动,禁止默认天天读书,真实天气/摄像头/现实身体输入不伪装。
|
||||
- `tests/test_r2.py`:旧档案测试固定生活时钟,分别跑上海11:11窗内和12:11窗外;保持真实生产循环启用,数据库人格/任务0行、窗口内仅一条合法生活、显式补建来源和时间真实、出生/身份(含名字)/聊天指纹不变。新增provider边界断言确保约束实际进入调用,落库原文不被替换。替身不证明模型质量。
|
||||
- `scripts/r2_model_smoke.py`、`tests/test_r2_model_smoke.py`:复用原脚本,增加全局4次硬上限(失败计数)、拒绝覆盖证据、失败即停、原始system/user/参数/逐delta/原始正文留档;测试覆盖部分返回后失败仍计数、关闭上游和第五次不发出。未改provider或生产调用配置。
|
||||
|
||||
web源码、API协议、公开守卫、生活调度/预算、迁移和所有模型配置无改动。不存在输出后的人称替换或人工润色;没有触碰已有档案。HEL-827旧样例SHA256仍为 `09f2242f1a8e4f637967c7ac3e232407e40b0fe5707f8cc57a76ac1e8a93ff84`。
|
||||
|
||||
## 验证与质量
|
||||
|
||||
完整记录见 `VALIDATION-HEL-830.md`、`pytest-HEL-830.txt`、`model-smoke-HEL-830.json`。内容质量逐项自检见 `CONTENT-REVIEW-HEL-830.md`,本组自检通过,待总工独立复审。功能、隐私、内容质量分别记录,不能据本卡自测放行部署。
|
||||
|
||||
真实核验仅1组4次,全部结束;本地全新合成档案/临时DB,禁用app自动生活循环,无额外预检/重试。人格、生活、正向判定、同词反例各1次,46.31秒,保守预算9821。原始证据包含消费者正常关闭的GeneratorExit标记,详细解释保留在质量记录中。原始请求/输出不重写。
|
||||
|
||||
Python完整194项通过;web37项通过/build通过/lint 0error+1原有warning;Python ruff与两目标需求检查及diff检查结果见验证记录。未安装依赖,复用既有Python3.12.14锁定环境及一致lock的node_modules。
|
||||
|
||||
## 后续与边界
|
||||
|
||||
仅候选分支,未合并PR、未部署、未启用共享实例R2,未访问8300/8310/8320用户资料、4080或共享语音/口型服务。所有本轮进程已收齐,无留后台服务。回退候选可继续使用原64a47f35,不涉及线上数据回滚。
|
||||
|
||||
总管接回HEL-829,由总工复用本组原始输出独立复审,功能/隐私/内容通过后再按原批准路线部署全新独立测试实例。HEL-829先前42项浏览器E2E与13张图不是本卡重测;本卡环境未发现其 `hel829-pkg/e2e/` 夹具,不提供私有路径当交付,也未重跑浏览器/真机/Safari/Firefox。
|
||||
|
||||
真实连续3天每天1–4条、关网页仍生成:未开始且未验收,待隔离部署后由总管专卡和持久唤醒承接。R3记忆、R4主动联系、R5语音、R6城市天气素材完整备份、R7授权观察和真实视频/口型继续由HEL-797承接;本卡不是完整R2或产品完成。没有gh/tea或配置的credential helper/API凭据,按卡片允许交SSH推送分支,不伪造PR。
|
||||
@@ -0,0 +1,41 @@
|
||||
HEL-830 验证记录(2026-10-24,本地隔离数据)
|
||||
|
||||
基准 v1.10 / 59项,改动起点 `64a47f35d0eca195b9961f830b78a60fd1c7ce70`;实际产品目标为 `origin/agent/agent/de8874a3803b`。没有安装新依赖或模型,复用HEL-829既有Python3.12.14环境(ruff0.17.2)及相同lock的node_modules;Python/node lock与候选基准均零diff。
|
||||
|
||||
| 检查 | 本卡实际结果 |
|
||||
| --- | --- |
|
||||
| `PYTHONPATH=src python -m pytest -q`,python为既有3.12.14环境 | **194 passed,7.78秒**;1条原有Starlette TestClient弃用提示,未安装其建议的新包。完整输出 `pytest-HEL-830.txt` |
|
||||
| Python ruff:`src/cybergril/api/app.py src/cybergril/life src/cybergril/memory/store.py tests/test_r2.py tests/test_r2_model_smoke.py tests/test_birth_view.py scripts/r2_model_smoke.py scripts/r2_backfill_persona.py` | **All checks passed** |
|
||||
| `npm test --prefix web` | **4 files / 37 tests passed** |
|
||||
| `npm run build --prefix web` | **TypeScript/Vite通过,54 modules**;输出CSS `index-BYb0r_ow.css`、JS `index-BfS0uRmS.js` |
|
||||
| `npm run lint --prefix web` | **0 errors / 1原有warning**,ChatPanel.tsx:161 effect ref清理;未改前端 |
|
||||
| `python3 scripts/check_requirements.py --base-ref origin/main --handoff docs/requirements/handoff-HEL-830.json` | **通过,59项** |
|
||||
| 同上,base-ref换为 `origin/agent/agent/de8874a3803b` | **通过,59项** |
|
||||
| `git diff --check` | **干净** |
|
||||
| `git diff --exit-code 64a47f35 -- web docs/r2/model-smoke-HEL-827.json config requirements.lock` | **零diff** |
|
||||
|
||||
Python由191项增加至194项:原旧档案测试参数化增加1项(窗内/窗外)、真实provider提示词边界及不重写原文增加1项、真实核验脚本的失败计数/硬限/原始证据测试增加1项。先跑R2定向51项及记录器1项,再完成最终完整194项回归;未用测试替身当模型质量证据。
|
||||
|
||||
## 功能
|
||||
|
||||
原生产调度与旧档案守卫不变。测试固定上海2026-10-24的11:11和12:11,保持生产生活循环运行:窗内恰好1条合法生活、窗外0条;两种情况下显式补建前persona_interp/persona_job都0行,retry409。显式补建后来源backfilled-R2、实际生成时间、出生/identity(名字等)/聊天不变仍全断言。没有删除保护或关生产循环来换绿。
|
||||
|
||||
provider边界测试检查真实structured组装出来的system指令/user原始出生事实及已生成诠释,生成原文与存库值一致;证明确实从生成源头约束,无人称后替换。
|
||||
|
||||
## 隐私
|
||||
|
||||
原R2反例随完整pytest通过:成功完整轮次+全文接收确认+实际讲述判定三条件、失败/中断不发布、伪造/过期拒绝、跨重启重放、候选内原话、事务回滚、未分享列表/详情/消息/人格成长/health响应前裁剪。旧档案与关系变更保护保留。尚未实现的R3搜索/摘要/记忆端点不声称通过。
|
||||
|
||||
真实核验仅在本地临时全新合成档案,`create_app`明确禁用生活循环,随后直接调用人格/生活和两次判定,没有服务或后台偷跑。未发送接收确认,因此真实样例的事件始终told=0;这不冒充浏览器接收确认E2E。
|
||||
|
||||
## 真实生成与内容质量
|
||||
|
||||
命令:`PYTHONPATH=src python scripts/r2_model_smoke.py --real --output docs/r2/model-smoke-HEL-830.json`(同一既有Python3.12.14)。仅执行1次,4次模型调用全部完成,46.31秒,保守计账9821;人格standard温度0.7/max_tokens1200,生活low及两判定low温度0.4/max_tokens600,均沿用原规格,无配置改动和重试。无额外模型预检。
|
||||
|
||||
`persona/life/narrated/word_only/unacknowledged_hidden/birth_unchanged` 六项均true,原始system/user、参数、逐delta和返回正文已保存。正向判定选择事件1,反例选择空;没有凭证泄漏。内容自检通过,逐项语义核对见 `CONTENT-REVIEW-HEL-830.md`;原始记录中的GeneratorExit是消费者收到done后的正常关闭元数据,原样保留解释。不是用关键词命中或结构成功宣称长期质量。
|
||||
|
||||
## 未验证与交接
|
||||
|
||||
本卡未重跑浏览器E2E/截图/真机/Safari/Firefox。HEL-829原42项真实构建E2E及13张图仅作为已有审核证据,未记成本卡重测;其 `hel829-pkg/e2e/` 私有夹具在本环境指定位置未发现。前端与基准零diff,原全部接线成果保留。
|
||||
|
||||
所有测试命令、真实核验进程均已结束,临时合成DB已由TemporaryDirectory清理;无本卡监听服务、无长跑后台进程。未访问或改动现有8300/8310/8320资料、现网服务、共享语音/旧口型、4080。未补建真实档案,未部署、未合并。三天真实运行未开始,按交接由总管/总工接续。功能/隐私回归通过与本组内容自检通过都不代替独立复审或用户体验验收。
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,10 @@
|
||||
........................................................................ [ 37%]
|
||||
........................................................................ [ 74%]
|
||||
.................................................. [100%]
|
||||
=============================== warnings summary ===============================
|
||||
../../../hel-829-8ee8ba59fefd/workdir/CyberGril/.venv312/lib/python3.12/site-packages/fastapi/testclient.py:1
|
||||
/home/xiaomo/multica_workspaces/hello-af23464d222e/hel-829-8ee8ba59fefd/workdir/CyberGril/.venv312/lib/python3.12/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||
from starlette.testclient import TestClient as TestClient # noqa
|
||||
|
||||
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||
194 passed, 1 warning in 7.78s
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"baseline_version": "1.10",
|
||||
"read_commit": "64a47f35d0eca195b9961f830b78a60fd1c7ce70",
|
||||
"requirement_ids": ["CG-008", "CG-013", "CG-014", "CG-015", "CG-016", "CG-017", "CG-021", "CG-026", "AM-001", "UX-004", "UX-006", "GOV-001", "GOV-002", "GOV-003", "GOV-004", "GOV-005", "DEL-001"],
|
||||
"deliverable": "HEL-830最小后端提示词返工:本人自述、生活限于当前高层卧室、无来源人物互动禁止;旧档案窗内/窗外确定性测试;一组4次真实生成原始证据及内容逐项自检。保留HEL-828全部前端与HEL-829机械修复,详见docs/r2/HEL-830-HANDOFF.md。",
|
||||
"acceptance": [
|
||||
"真实provider请求中含本人声音和获批场景约束,原文入库,不做它到她的后替换",
|
||||
"旧档案在上海11:11和12:11均无人格自动补建,合法生活窗内1条窗外0条;显式补建来源/时间真实,出生、身份、名字、聊天不变",
|
||||
"真实核验恰好4次无重试,persona/life/narrated/word_only及未确认不公开、底色不变全部通过;内容自检逐项核对原始文字",
|
||||
"完整Python与web测试/构建/lint、两目标需求结构和diff检查通过;前端零改动,独立分支推送供复审"
|
||||
],
|
||||
"deferred": [
|
||||
"总管接回HEL-829总工独立复审,复用本组原始输出,不另开真实模型调用;本卡不是部署放行",
|
||||
"隔离实例部署及真实连续3天每天1–4条、关闭网页继续生活仍未开始,由总管专卡及持久唤醒承接",
|
||||
"HEL-829原42项浏览器端到端/13张图不是本卡重测;其私有夹具未发现,真机/Safari/Firefox未测",
|
||||
"R3记忆、R4主动联系、R5语音、R6城市天气素材完整备份、R7授权观察及真实视频/口型由HEL-797继续承接",
|
||||
"已有档案没有补建、重算或迁移,仍须单独授权"
|
||||
],
|
||||
"requirement_changes": [],
|
||||
"verification": [
|
||||
"docs/r2/pytest-HEL-830.txt:Python3.12.14完整194 passed,7.78秒,1条既有Starlette弃用提示",
|
||||
"web37 tests、TypeScript/Vite构建通过,lint0error/1原有ChatPanel warning;前端与64a47f35零diff",
|
||||
"Python ruff涉及API/life/store及R2测试脚本全通过;需求检查分别对origin/main和origin/agent/agent/de8874a3803b附本交接执行",
|
||||
"docs/r2/model-smoke-HEL-830.json:一组4调用46.31秒/保守9821,原始请求/参数/逐delta/原文保存,全部6项checks为true",
|
||||
"docs/r2/CONTENT-REVIEW-HEL-830.md:人称/场景/人物互动/深度/非模板/能力诚实逐项核对,自检通过待独立审核;GeneratorExit正常关闭元数据如实保留解释",
|
||||
"无部署、无共享数据访问、无新增依赖、无后台服务;旧HEL-827真实样例和全部前端原样保留"
|
||||
],
|
||||
"product_target": "origin/agent/agent/de8874a3803b",
|
||||
"product_target_commit": "db571ba204c8372989f96e1ed9b5cd00d3a306c7",
|
||||
"branch": "agent/agent/a67b70ddbb63",
|
||||
"deployed": false,
|
||||
"three_day_run": false,
|
||||
"real_model_calls": 4
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import asyncio
|
||||
import json
|
||||
import tempfile
|
||||
import time
|
||||
from contextlib import aclosing
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
@@ -18,9 +19,42 @@ from cybergril.life.persona import Persona, fingerprint
|
||||
from cybergril.life.runtime import LifeConfig, LifeRuntime
|
||||
|
||||
|
||||
def record_provider(provider, report, output):
|
||||
original = provider.stream
|
||||
|
||||
async def stream(messages, **kwargs):
|
||||
if len(report["calls"]) >= 4:
|
||||
raise RuntimeError("four-call hard limit reached")
|
||||
call = {"number": len(report["calls"]) + 1, "model": provider.model,
|
||||
"base_url": provider.base_url,
|
||||
"messages": [{"role": m.role, "content": m.content} for m in messages],
|
||||
"parameters": kwargs, "deltas": [], "raw_text": "", "completed": False}
|
||||
report["calls"].append(call)
|
||||
report["attempted_model_calls"] = len(report["calls"])
|
||||
# Count and persist the attempt before sending, including failures. No auth headers.
|
||||
output.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
try:
|
||||
async with aclosing(original(messages, **kwargs)) as upstream:
|
||||
async for delta in upstream:
|
||||
call["deltas"].append({"text": delta.text, "done": delta.done,
|
||||
"finish_reason": delta.finish_reason, "usage": delta.usage})
|
||||
call["raw_text"] += delta.text
|
||||
call["completed"] |= delta.done or delta.finish_reason == "stop"
|
||||
yield delta
|
||||
except BaseException as exc:
|
||||
call["error_type"] = type(exc).__name__
|
||||
raise
|
||||
finally:
|
||||
output.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
provider.stream = stream
|
||||
|
||||
|
||||
async def run(output):
|
||||
if output.exists():
|
||||
raise FileExistsError("Refusing to overwrite evidence or repeat this run")
|
||||
report = {"synthetic_only": True, "browser_connected": False, "three_day_run": False,
|
||||
"max_model_calls": 4, "checks": {}}
|
||||
"max_model_calls": 4, "calls": [], "checks": {}}
|
||||
with tempfile.TemporaryDirectory(prefix="r2-smoke-", dir=output.parent) as directory:
|
||||
app = create_app(db_path=Path(directory) / "synthetic.db", life_config=LifeConfig(enabled=False))
|
||||
async with app.router.lifespan_context(app):
|
||||
@@ -29,17 +63,25 @@ async def run(output):
|
||||
response.raise_for_status()
|
||||
s, original = app.state.store, fingerprint(app.state.store)
|
||||
router, models = app.state.life.router, app.state.life.models
|
||||
for provider in set(router.provider_for(t) for t in router.available_tiers()):
|
||||
record_provider(provider, report, output)
|
||||
router.guard = PersistentBudget(s, router.guard)
|
||||
persona = Persona(s, router, models, enabled=True)
|
||||
start = time.monotonic()
|
||||
report["checks"]["persona"] = await persona.ensure()
|
||||
report["persona"] = persona.view()
|
||||
if not report["checks"]["persona"]:
|
||||
report["stopped"] = "persona_failed_no_retry"
|
||||
output.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
return False
|
||||
runtime = LifeRuntime(s, router, models, LifeConfig(enabled=True), app.state.sharing, persona)
|
||||
# An explicit synthetic current-window invocation, not backfilled production history.
|
||||
await runtime.generate("synthetic-smoke", 0)
|
||||
life = s.query_one("SELECT * FROM life_event")
|
||||
report["checks"]["life"] = bool(life)
|
||||
report["life"] = dict(life) if life else None
|
||||
if not life:
|
||||
report["stopped"] = "life_failed_no_retry"
|
||||
if life:
|
||||
for name, text in (
|
||||
("narrated", "刚才我这边发生了一件小事:" + life["content"]),
|
||||
@@ -54,11 +96,15 @@ async def run(output):
|
||||
judged = dict(s.query_one("SELECT state,selected FROM sharing_attempt WHERE message_id=?", (receipt["message_id"],)))
|
||||
report[name] = judged
|
||||
report["checks"][name] = judged["state"] == "judged" and json.loads(judged["selected"]) == ([life["id"]] if name == "narrated" else [])
|
||||
if not report["checks"][name]:
|
||||
report["stopped"] = name + "_failed_no_retry"
|
||||
break
|
||||
report["checks"]["unacknowledged_hidden"] = not s.query("SELECT * FROM life_event WHERE told=1")
|
||||
report["checks"]["birth_unchanged"] = fingerprint(s) == original
|
||||
report["elapsed_seconds"] = round(time.monotonic() - start, 2)
|
||||
report["conservative_tokens"] = router.guard.used
|
||||
report["life_runs"] = [dict(r) for r in s.query("SELECT status,reason FROM life_run")]
|
||||
report["attempted_model_calls"] = len(report["calls"])
|
||||
output.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
print(json.dumps({"checks": report["checks"], "elapsed_seconds": report["elapsed_seconds"]}, ensure_ascii=False))
|
||||
return all(report["checks"].values())
|
||||
|
||||
@@ -74,6 +74,9 @@ class Persona:
|
||||
raw, model = await structured(
|
||||
self.router, self.models, Scene.DIALOGUE_FOLLOWUP,
|
||||
'从原始出生资料自主形成有张力、可延续的人格理解,不给标准答案,不列浅标签,'
|
||||
'以她本人的声音表达:interpretation自我诠释和habits初始习惯以第一人称“我”为主,'
|
||||
'必要第三人称只能称“她”,禁止用“它”指代她,不写算命师对对象的第三人称判词。'
|
||||
'保留底色的解释空间、内在张力和自主喜好,不把倾向说成命定结论,习惯不是已发生的经历。'
|
||||
'不杜撰已发生的经历,不改写原始事实。返回 {"interpretation":"100至2400字的自主诠释",'
|
||||
'"habits":["3至5条小喜好习惯,每条最多160字"]}。', prompt_facts(self.store),
|
||||
)
|
||||
|
||||
@@ -162,8 +162,15 @@ class LifeRuntime:
|
||||
raw, _ = await structured(
|
||||
self.router, self.models, Scene.LIFE_EVENT,
|
||||
'生成此刻真实运行中的一条世界内日常经历,不是停机期回忆。不要模板,不涉及用户隐私,'
|
||||
'不假装接入天气、摄像头或现实身体。返回 {"kind":"thought或scene或encounter或mood_shift",'
|
||||
'"content":"12至800字世界内陈述","place":"世界内地点","salience":0到1,"valence":-1到1}。',
|
||||
'当前世界只开放一间温暖温馨的高层卧室,含窗边、慵懒沙发与女性化装饰;'
|
||||
'place和活动都必须在这间卧室内,禁止办公室、茶水间、其他房间或外出等未开放场所。'
|
||||
'禁止编造无来源的第三方人物、同事、用户在场或与他们的互动,也不捏造过去的经历。'
|
||||
'可以自主产生有个性、有细节的小活动与想法,不限于固定几种,不默认每天读书。'
|
||||
'content以她本人的第一人称“我”为主,必要第三人称只能称“她”,禁止用“它”指代她;'
|
||||
'place等结构字段正常描述,不必塞入“我”。'
|
||||
'不假装接入真实天气、摄像头或现实身体;视觉素材中的雨夜等不能当作当前真实天气事实。'
|
||||
'返回 {"kind":"thought或scene或encounter或mood_shift",'
|
||||
'"content":"12至800字的本人生活自述","place":"这间高层卧室内的地点","salience":0到1,"valence":-1到1}。',
|
||||
{"time": datetime.fromtimestamp(self.clock(), ZoneInfo(self.config.timezone)).isoformat(),
|
||||
"persona": self.persona.prompt()}, timeout=30, max_tokens=600,
|
||||
)
|
||||
|
||||
+37
-2
@@ -33,6 +33,7 @@ class R2LLM(ControlledLLM):
|
||||
self.persona_result = PERSONA
|
||||
self.reply = "我今天给一段旋律换了个结尾,停在了喜欢的那个音上。"
|
||||
self.background_calls = []
|
||||
self.background_messages = []
|
||||
self.block = False
|
||||
self.entered = None
|
||||
self.release = None
|
||||
@@ -51,6 +52,7 @@ class R2LLM(ControlledLLM):
|
||||
return
|
||||
prompt = messages[0].content
|
||||
self.background_calls.append(prompt)
|
||||
self.background_messages.append(messages)
|
||||
if self.entered:
|
||||
self.entered.set()
|
||||
if self.block:
|
||||
@@ -157,7 +159,8 @@ def test_new_persona_and_relation_do_not_mutate_any_layer(tmp_path):
|
||||
assert c.post("/api/companion", json=CREATE).status_code == 409
|
||||
|
||||
|
||||
def test_old_profile_never_auto_backfilled_and_explicit_backfill_is_honest(tmp_path):
|
||||
@pytest.mark.parametrize("hour,life_count", [(11, 1), (12, 0)])
|
||||
def test_old_profile_never_auto_backfilled_and_explicit_backfill_is_honest(tmp_path, monkeypatch, hour, life_count):
|
||||
app, _ = app_for(tmp_path, enabled=False)
|
||||
with TestClient(app) as c:
|
||||
c.post("/api/companion", json=CREATE)
|
||||
@@ -166,11 +169,20 @@ def test_old_profile_never_auto_backfilled_and_explicit_backfill_is_honest(tmp_p
|
||||
birth = fingerprint(s)
|
||||
raw = s.query_one("SELECT identity_json FROM companion")[0]
|
||||
chats = [tuple(r) for r in s.query("SELECT * FROM chat_message")]
|
||||
# Freeze only the runtime clock before lifespan starts, leaving the real loop enabled.
|
||||
stamp = datetime(2026, 10, 24, hour, 11, tzinfo=ZoneInfo("Asia/Shanghai")).timestamp()
|
||||
monkeypatch.setattr("cybergril.api.app.LifeRuntime",
|
||||
lambda *args: LifeRuntime(*args, clock=lambda: stamp))
|
||||
app2, p = app_for(tmp_path)
|
||||
with TestClient(app2) as c:
|
||||
c.portal.call(app2.state.life.tick) # settle the current slot; repeated ticks are idempotent
|
||||
assert c.get("/api/persona").json()["state"] == "not_initialized"
|
||||
assert c.post("/api/persona/retry").status_code == 409
|
||||
assert not p.background_calls
|
||||
assert not app2.state.store.query("SELECT * FROM persona_interp")
|
||||
assert not app2.state.store.query("SELECT * FROM persona_job")
|
||||
assert len(app2.state.store.query("SELECT * FROM life_event")) == life_count
|
||||
assert len(p.background_calls) == life_count
|
||||
assert all("本人生活自述" in prompt for prompt in p.background_calls)
|
||||
persona = app2.state.persona
|
||||
c.portal.call(lambda: persona.ensure(backfill=True))
|
||||
view = c.get("/api/persona").json()
|
||||
@@ -181,6 +193,29 @@ def test_old_profile_never_auto_backfilled_and_explicit_backfill_is_honest(tmp_p
|
||||
assert [tuple(r) for r in app2.state.store.query("SELECT * FROM chat_message")] == chats
|
||||
|
||||
|
||||
def test_generation_requests_carry_voice_and_current_world_without_rewriting_output(tmp_path):
|
||||
# This verifies the actual provider boundary, not model content quality.
|
||||
app, p = app_for(tmp_path)
|
||||
with TestClient(app) as c:
|
||||
c.post("/api/companion", json=CREATE)
|
||||
runtime = setup_runtime(app, fixed_clock())
|
||||
c.portal.call(runtime.tick)
|
||||
persona_request = next(m for m in p.background_messages if "自主形成有张力" in m[0].content)
|
||||
life_request = next(m for m in p.background_messages if "本人生活自述" in m[0].content)
|
||||
for messages in (persona_request, life_request):
|
||||
assert [m.role for m in messages] == ["system", "user"]
|
||||
assert "第一人称“我”" in messages[0].content
|
||||
assert "禁止用“它”指代她" in messages[0].content
|
||||
assert '"pillars"' in persona_request[1].content
|
||||
for constraint in ("高层卧室", "窗边", "慵懒沙发", "女性化装饰", "办公室", "茶水间",
|
||||
"禁止编造无来源", "不默认每天读书", "雨夜等不能当作当前真实天气事实"):
|
||||
assert constraint in life_request[0].content
|
||||
assert INTERP in life_request[1].content
|
||||
assert c.get("/api/persona").json()["interpretation"]["text"] == PERSONA["interpretation"]
|
||||
row = app.state.store.query_one("SELECT content,place,told FROM life_event ORDER BY id DESC LIMIT 1")
|
||||
assert tuple(row) == (LIFE["content"], LIFE["place"], 0)
|
||||
|
||||
|
||||
def test_failed_persona_retry_has_no_template_or_half_seeds(tmp_path):
|
||||
p = R2LLM()
|
||||
p.persona_result = {"interpretation": "错误短标签", "habits": ["伪造"]}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
"""The opt-in smoke recorder counts failed attempts and never sends a fifth call."""
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from cybergril.contracts import LLMDelta, LLMMessage
|
||||
from scripts.r2_model_smoke import record_provider
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_smoke_evidence_preserves_partial_failure_and_caps_all_attempts(tmp_path):
|
||||
class Provider:
|
||||
model = "synthetic-test"
|
||||
base_url = "http://unused"
|
||||
sent = 0
|
||||
closed = 0
|
||||
|
||||
async def stream(self, messages, **kwargs):
|
||||
self.sent += 1
|
||||
try:
|
||||
yield LLMDelta(text="原始片段")
|
||||
if self.sent == 1:
|
||||
raise OSError("synthetic failure")
|
||||
yield LLMDelta(text="完成", finish_reason="stop")
|
||||
finally:
|
||||
self.closed += 1
|
||||
|
||||
provider = Provider()
|
||||
report = {"calls": []}
|
||||
output = tmp_path / "evidence.json"
|
||||
messages = [LLMMessage(role="system", content="原始提示"), LLMMessage(role="user", content="合成资料")]
|
||||
record_provider(provider, report, output)
|
||||
with pytest.raises(OSError):
|
||||
async for _ in provider.stream(messages, temperature=.7, max_tokens=1200):
|
||||
pass
|
||||
for _ in range(3):
|
||||
async for _ in provider.stream(messages, temperature=.4, max_tokens=600):
|
||||
pass
|
||||
with pytest.raises(RuntimeError, match="hard limit"):
|
||||
async for _ in provider.stream(messages):
|
||||
pass
|
||||
evidence = json.loads(output.read_text())
|
||||
assert provider.sent == provider.closed == evidence["attempted_model_calls"] == 4
|
||||
assert len(evidence["calls"]) == 4
|
||||
failed = evidence["calls"][0]
|
||||
assert failed["error_type"] == "OSError" and not failed["completed"]
|
||||
assert failed["raw_text"] == "原始片段"
|
||||
assert failed["messages"] == [{"role": m.role, "content": m.content} for m in messages]
|
||||
assert failed["parameters"] == {"temperature": .7, "max_tokens": 1200}
|
||||
assert all(c["raw_text"] == "原始片段完成" and c["completed"] for c in evidence["calls"][1:])
|
||||
Reference in New Issue
Block a user