feat: add chat replay verification

This commit is contained in:
thanhnv
2026-07-08 22:45:12 +09:00
parent 49d0363c6a
commit 36af576f15
15 changed files with 350 additions and 14 deletions
@@ -69,6 +69,14 @@ def sha(text: str) -> str:
return hashlib.sha256(text.encode("utf-8")).hexdigest()
def sha_file(path: str) -> str:
h = hashlib.sha256()
with open(path, "rb") as fh:
for chunk in iter(lambda: fh.read(65536), b""):
h.update(chunk)
return h.hexdigest()
def git_commit() -> str:
try:
r = subprocess.run(["git", "rev-parse", "HEAD"], cwd=ROOT, capture_output=True, text=True, timeout=5)
@@ -204,6 +212,12 @@ def record_metrics(trace_id: str, message: str, answer: str, status: str, latenc
def finish(started, trace_id, chat_id, turn_id, tenant_id, actor, message, router, decision, answer,
action=None, action_gate=None, artifact_path="", safe_message=""):
elapsed = int((datetime.now(timezone.utc) - started).total_seconds() * 1000)
loop_run = {}
if os.environ.get("CASAN_CHAT_LOOP_RUN_JSON"):
try:
loop_run = json.loads(os.environ["CASAN_CHAT_LOOP_RUN_JSON"])
except ValueError:
loop_run = {"success": False, "decision": "DENIED", "reason": "loop_run_json_invalid"}
source = []
if artifact_path:
source.append({
@@ -211,6 +225,7 @@ def finish(started, trace_id, chat_id, turn_id, tenant_id, actor, message, route
"line": 1,
"excerpt": answer[:260],
"score": 10 if decision == "ACTION_COMPLETED" else 1,
"hash": sha_file(artifact_path),
"envelope": provenance("chat-operator-artifact", artifact_path, decision == "ACTION_COMPLETED"),
})
rec = record_turn({
@@ -229,7 +244,8 @@ def finish(started, trace_id, chat_id, turn_id, tenant_id, actor, message, route
"user_msg_ref": sha(message),
"safe_preview": (safe_message or "")[:180],
"answer_ref": sha(answer),
"sources": [{"path": s.get("path"), "line": s.get("line")} for s in source],
"sources": [{"path": s.get("path"), "line": s.get("line"), "hash": s.get("hash")} for s in source],
"loop_run": loop_run,
})
record_metrics(trace_id, safe_message or message, answer, "success" if decision == "ACTION_COMPLETED" else "failed", elapsed, (action or {}).get("id", "none"))
return {
@@ -247,6 +263,7 @@ def finish(started, trace_id, chat_id, turn_id, tenant_id, actor, message, route
"router": router,
"action": {k: action.get(k) for k in ("id", "label", "description")} if action else None,
"action_gate": action_gate or {},
"loop_run": loop_run,
}