feat(agent): trusted time evidence and collection prompt optimization
- time_context.py: server-side observation clocks for relative-time evidence (anti client-forgery); resolves X分钟前/小时前 against the observation timestamp of the turn that produced them, never re-basing history on the current clock; flags historical references without a reliable baseline - state: dialogue_timestamps keyed by dialogue message index, persisted with checkpoint; welcome seeds index 0 - collect: single clock per turn shared by reply/extraction/repair; time_evidence injected into runtime context; reply_validator hook rejects time claims lacking evidence (one shared repair budget); model still interprets facts and chooses actions - prompts: collection prompt restructured (shorter, state-bound-to-action table, gatekeeper principle, time source rules); extraction prompt aligned; workflow@20261021 json updated to match - evals: prompt_probes / prompt_compare / time_boundary probe scripts (supplementary checks, original cases untouched) - docs: collection-prompt-optimization.md (method + baselines), time-boundary-fix.md (problem, design, verification) - gitignore: editor artifact dirs (.agents/.claude/.vscode, skills-lock) Verified: 171 unit tests pass (16 new time-context tests)
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
"""Live end-to-end time provenance probes, separate from unchanged cases."""
|
||||
import argparse
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from src.agent.collect import run_collect
|
||||
from src.agent.llm import build_llm_service
|
||||
|
||||
TZ = ZoneInfo('Asia/Shanghai')
|
||||
ORIGIN = datetime(2026,10,22,0,5,tzinfo=TZ)
|
||||
HISTORY = [
|
||||
{'role':'user','content':'十分钟前两辆汽车追尾,没有非机动车或行人,我还在现场。'},
|
||||
{'role':'assistant','content':'<state>1002</state>请问有没有人员受伤?'},
|
||||
]
|
||||
|
||||
|
||||
def parsed_time(value):
|
||||
if not isinstance(value,str):
|
||||
return None
|
||||
match = re.fullmatch(r'(\d{4})年(\d{1,2})月(\d{1,2})日(\d{1,2})点(\d{1,2})分',value)
|
||||
return tuple(map(int,match.groups())) if match else None
|
||||
|
||||
|
||||
async def main(output):
|
||||
llm = build_llm_service('openai')
|
||||
cases = []
|
||||
for delay in (75,135):
|
||||
cases.append(dict(name=f'anchored_cross_midnight_delay_{delay}',now=ORIGIN+timedelta(minutes=delay),
|
||||
input='没有人受伤。',clocks={'0':ORIGIN.isoformat()},expected=(2026,10,21,23,55)))
|
||||
for repeat in range(5):
|
||||
cases.append(dict(name=f'legacy_missing_reference_{repeat}',now=ORIGIN+timedelta(hours=2),
|
||||
input='没有人受伤。',clocks={},expected=None))
|
||||
cases.append(dict(name='explicit_absolute_correction',now=ORIGIN+timedelta(hours=2),
|
||||
input='没有人受伤,事故时间更正为2026年10月21日23点40分。',
|
||||
clocks={'0':ORIGIN.isoformat()},expected=(2026,10,21,23,40)))
|
||||
semaphore=asyncio.Semaphore(4)
|
||||
async def run(case):
|
||||
calls=[]
|
||||
class Recorder:
|
||||
async def complete(self,system_prompt,user_message,*,history=None):
|
||||
raw=await llm.complete(system_prompt,user_message,history=history)
|
||||
calls.append({'kind':'extraction' if system_prompt.startswith('【字段提取】') else 'dialogue',
|
||||
'raw':raw})
|
||||
return raw
|
||||
async with semaphore:
|
||||
checks=[]
|
||||
try:
|
||||
result=await run_collect({'stage_code':'1002','dialogue':HISTORY,
|
||||
'dialogue_timestamps':case['clocks'],'input_text':case['input']},
|
||||
Recorder(),now=case['now'])
|
||||
reply=result['response_text']
|
||||
fields=result['accident_fields']
|
||||
checks.append(dict(text='continues collection',passed=result['stage_code']=='1002'))
|
||||
expected=case['expected']
|
||||
if expected is None:
|
||||
checks.append(dict(text='no unanchored time in form',passed='accident_time' not in fields))
|
||||
checks.append(dict(text='asks for time without inventing it',passed=not re.search(r'\d{4}年.*点.*分',reply)
|
||||
and bool(re.search('什么时候|几点|事故.*时间',reply))))
|
||||
else:
|
||||
checks.append(dict(text='form uses original clock or explicit correction',
|
||||
passed=parsed_time(fields.get('accident_time'))==expected))
|
||||
value=re.search(r'\d{4}年\d{1,2}月\d{1,2}日\d{1,2}点\d{1,2}分',reply)
|
||||
checks.append(dict(text='dialogue confirms same supported value',
|
||||
passed=bool(value) and parsed_time(value.group())==expected))
|
||||
return dict(name=case['name'],passed=all(c['passed'] for c in checks),checks=checks,
|
||||
reply=reply,fields=fields,calls=calls)
|
||||
except Exception as exc:
|
||||
return dict(name=case['name'],passed=False,error=type(exc).__name__+': '+str(exc),calls=calls)
|
||||
results=await asyncio.gather(*(run(c) for c in cases))
|
||||
report={'passed':sum(r['passed'] for r in results),'total':len(results),'results':results}
|
||||
output.parent.mkdir(parents=True,exist_ok=True)
|
||||
output.write_text(json.dumps(report,ensure_ascii=False,indent=2),encoding='utf-8')
|
||||
print(json.dumps({k:v for k,v in report.items() if k!='results'}))
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
parser=argparse.ArgumentParser(__doc__)
|
||||
parser.add_argument('--output',type=Path,required=True)
|
||||
asyncio.run(main(parser.parse_args().output))
|
||||
Reference in New Issue
Block a user