- prompts/collection.md: versioned collection system prompt extracted from workflow node yQaBxfrWjbRr with source/version/porting notes - llm.py: LLMService protocol; FakeLLMService (scripted, records calls); OpenAILLMService (async, wait_for timeout with cancellation, retry only on timeout/429/connection, errors normalized) - collect.py: run_collect reference impl - one LLM call produces state prefix + body, controlled repair (max 1 retry) for missing prefix or illegal stage proposal, whitelist guard, CollectReplyError without raw model content; dialogue history + confirmed fields injected into prompt so the model skips known info (作业4-5) - fields.py / clock.py: three-state field candidates (unknown/ candidate/confirmed), per-type validation (count not inferred from description, future time rejected), injectable Clock - graph.py: welcome node - first message returns fixed 1001 greeting without LLM (mirrors workflow is_first_message -> Assigned Reply#3), route empty stage to welcome - endpoints/delete_session: backend switch - langgraph clears graph checkpoint, fastgpt path unchanged - config: AGENT_LLM_PROVIDER fake|openai with validation - untrack .env (secrets), move template to root .env.example Verified: - pytest 103 passed; smoke turns 1/2/1 - Real LLM e2e: POST /chat multi-turn context works - Golden eval collection_skip_known_info: 12/12 cases pass - Golden eval no_false: 8/10 cases pass; 4 confirm/photo cases fail at final step - model routes on short reply '对' (prompt-adherence gap, photo/confirm stages are assignment 5 scope); recorded in evals/results/20261020-144856 Known limits: field extraction from model reply not wired (stub); photo/confirm nodes still scaffold; clarify counting is approximate.
77 lines
2.5 KiB
Python
77 lines
2.5 KiB
Python
"""可控时钟(作业 4-6)。
|
|
|
|
测试不依赖运行机器的当天时间:注入 ``FixedClock`` 即可固定
|
|
“当前时间”,校验相对时间、未来时间、边界。
|
|
"""
|
|
|
|
from datetime import datetime
|
|
from typing import Protocol
|
|
import re
|
|
|
|
|
|
class Clock(Protocol):
|
|
"""时钟协议:图节点与校验逻辑只依赖它获取当前时间。"""
|
|
|
|
def now(self) -> datetime:
|
|
...
|
|
|
|
|
|
class SystemClock:
|
|
"""真实时钟(生产环境)。"""
|
|
|
|
def now(self) -> datetime:
|
|
return datetime.now()
|
|
|
|
|
|
class FixedClock:
|
|
"""固定时钟(测试):now 永远返回构造时给定的时间。"""
|
|
|
|
def __init__(self, fixed: datetime):
|
|
self._fixed = fixed
|
|
|
|
def now(self) -> datetime:
|
|
return self._fixed
|
|
|
|
|
|
_FUTURE_MARKERS = ("明天", "后天", "未来", "等会", "稍后", "接下来")
|
|
|
|
_ABS_RE = re.compile(r"^\s*(\d{1,2})\s*点(?:\s*(\d{1,2})\s*分?)?\s*$")
|
|
_REL_MIN_RE = re.compile(r"(\d+)\s*分钟前")
|
|
_REL_HOUR_RE = re.compile(r"(?:半\s*小时|(\d+)\s*小时)前")
|
|
|
|
|
|
def validate_accident_time(value: str, now: datetime) -> bool:
|
|
"""校验事故时间字符串是否合法(可解析且不晚于 now)。
|
|
|
|
参考实现覆盖的常见口语形态(不依赖机器当天时间,now 由调用方注入):
|
|
- 相对时间:“刚才/刚刚”、“N 分钟前”、“N 小时前/半小时前”;
|
|
- 绝对时间:“11点”“11点45分”——按 now 的当天日期解析,
|
|
解析结果晚于 now(未来时间)判为非法;
|
|
- 含明显未来标记(“明天”“稍后”等)一律非法;
|
|
- 无法解析的形态返回 False,由调用方进入澄清,不得猜值。
|
|
|
|
已知限制:未覆盖“昨天上午”“上周五”等跨日表达,作业细化时补充。
|
|
"""
|
|
text = (value or "").strip()
|
|
if not text:
|
|
return False
|
|
if any(marker in text for marker in _FUTURE_MARKERS):
|
|
return False
|
|
if text in ("刚才", "刚刚"):
|
|
return True
|
|
m = _REL_MIN_RE.search(text)
|
|
if m:
|
|
return 0 <= int(m.group(1)) <= 24 * 60 # Sanity 上限,防荒谬输入
|
|
m = _REL_HOUR_RE.search(text)
|
|
if m:
|
|
hours = 1 if m.group(1) is None else int(m.group(1))
|
|
return 0 < hours <= 24
|
|
m = _ABS_RE.match(text)
|
|
if m:
|
|
hour, minute = int(m.group(1)), int(m.group(2) or 0)
|
|
if hour > 23 or minute > 59:
|
|
return False
|
|
candidate = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
|
|
return candidate <= now
|
|
return False
|