Upload 2 files
Browse files- app/graph/nodes.py +171 -73
- app/graph/state.py +1 -2
app/graph/nodes.py
CHANGED
|
@@ -25,20 +25,66 @@ Text:
|
|
| 25 |
|
| 26 |
AGREEMENT_THRESHOLD = 0.5 # min confidence gap tolerated before escalation
|
| 27 |
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
|
|
|
| 31 |
|
| 32 |
Claim: {claim}
|
| 33 |
-
Evidence: {evidence}
|
| 34 |
|
| 35 |
-
Assessment A (fine-tuned NLI model): {label_a}
|
| 36 |
-
Assessment B (zero-shot LLM verifier): {label_b}
|
| 37 |
Assessment B's reasoning: {reasoning_b}
|
| 38 |
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
|
| 44 |
def _llm():
|
|
@@ -46,12 +92,14 @@ def _llm():
|
|
| 46 |
api_key=os.environ.get("GROQ_API_KEY"))
|
| 47 |
|
| 48 |
|
| 49 |
-
def
|
| 50 |
-
# Deliberately a different model from the verifier
|
| 51 |
-
# gpt-oss-20b) —
|
| 52 |
-
#
|
| 53 |
-
|
| 54 |
-
|
|
|
|
|
|
|
| 55 |
|
| 56 |
|
| 57 |
async def extract_claims(state: GraphState) -> GraphState:
|
|
@@ -112,8 +160,7 @@ async def verify_dual(state: GraphState) -> GraphState:
|
|
| 112 |
"source": "llm"},
|
| 113 |
"agreement": agree,
|
| 114 |
"escalated": False,
|
| 115 |
-
"
|
| 116 |
-
"adjudicator_reasoning": None,
|
| 117 |
"final_verdict": deberta_verdict if agree else None,
|
| 118 |
"attribution": None,
|
| 119 |
"resolution_note": None,
|
|
@@ -143,8 +190,7 @@ async def handle_unresolved_citation(state: GraphState) -> GraphState:
|
|
| 143 |
"llm_verdict": None,
|
| 144 |
"agreement": None,
|
| 145 |
"escalated": False,
|
| 146 |
-
"
|
| 147 |
-
"adjudicator_reasoning": None,
|
| 148 |
"final_verdict": None,
|
| 149 |
"attribution": None,
|
| 150 |
"resolution_note": ("Citation could not be resolved to usable evidence — "
|
|
@@ -161,12 +207,13 @@ def route_after_verify(state: GraphState) -> str:
|
|
| 161 |
|
| 162 |
|
| 163 |
async def escalate(state: GraphState) -> GraphState:
|
| 164 |
-
"""On disagreement:
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
|
|
|
| 170 |
"""
|
| 171 |
idx = state["current_index"]
|
| 172 |
cc = state["claims"][idx]
|
|
@@ -174,55 +221,106 @@ async def escalate(state: GraphState) -> GraphState:
|
|
| 174 |
last = audits[-1]
|
| 175 |
last["escalated"] = True
|
| 176 |
|
| 177 |
-
|
| 178 |
-
s2_key = os.environ.get("S2_API_KEY")
|
| 179 |
-
retry_query = semantic_scholar.extract_query(cc["claim"])
|
| 180 |
-
retry_paper = await semantic_scholar.search_paper(retry_query, api_key=s2_key)
|
| 181 |
-
|
| 182 |
-
llm_res = None
|
| 183 |
-
if retry_paper and retry_paper.get("abstract"):
|
| 184 |
-
retry_evidence = retry_paper["abstract"]
|
| 185 |
-
deberta_res = await claim_client.analyze(cc["claim"], retry_evidence)
|
| 186 |
-
winner = deberta_res["winner"]
|
| 187 |
-
llm_res = await llm_verifier.verify(cc["claim"], retry_evidence)
|
| 188 |
-
|
| 189 |
-
if winner["label"] == llm_res.get("label"):
|
| 190 |
-
# Retry resolved the disagreement — adopt the new verdict values,
|
| 191 |
-
# but do NOT overwrite `agreement` — it must stay False, since it
|
| 192 |
-
# records the INITIAL disagreement that triggered escalation.
|
| 193 |
-
# build_report's initial_agreement_rate depends on this staying accurate.
|
| 194 |
-
last["winner_sentence"] = winner["sentence"]
|
| 195 |
-
last["attribution_available"] = deberta_res.get("attribution_available", False)
|
| 196 |
-
last["deberta_verdict"] = {"label": winner["label"],
|
| 197 |
-
"confidence": winner["confidence"], "source": "deberta"}
|
| 198 |
-
last["llm_verdict"] = {"label": llm_res.get("label"),
|
| 199 |
-
"confidence": llm_res.get("confidence", 0.0), "source": "llm"}
|
| 200 |
-
last["escalation_retry_succeeded"] = True
|
| 201 |
-
last["final_verdict"] = last["deberta_verdict"]
|
| 202 |
-
return {**state, "audits": audits}
|
| 203 |
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
prompt = ADJUDICATOR_PROMPT.format(
|
| 207 |
claim=cc["claim"],
|
| 208 |
-
evidence=
|
| 209 |
label_a=last["deberta_verdict"]["label"],
|
| 210 |
confidence_a=last["deberta_verdict"]["confidence"],
|
| 211 |
label_b=last["llm_verdict"]["label"],
|
| 212 |
confidence_b=last["llm_verdict"]["confidence"],
|
| 213 |
-
reasoning_b=
|
| 214 |
)
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
return {**state, "audits": audits}
|
| 227 |
|
| 228 |
|
|
@@ -232,8 +330,8 @@ async def explain(state: GraphState) -> GraphState:
|
|
| 232 |
audits = state["audits"]
|
| 233 |
last = audits[-1]
|
| 234 |
if last["final_verdict"] is None:
|
| 235 |
-
# Should not normally happen
|
| 236 |
-
# escalate sets it via
|
| 237 |
last["final_verdict"] = last["deberta_verdict"]
|
| 238 |
|
| 239 |
if last["attribution_available"]:
|
|
@@ -262,9 +360,10 @@ async def build_report(state: GraphState) -> GraphState:
|
|
| 262 |
verified = [a for a in audits if a["agreement"] is not None] # excludes unresolved citations
|
| 263 |
agreement_rate = (sum(a["agreement"] for a in verified) / len(verified)) if verified else 0.0
|
| 264 |
n_escalated = sum(a["escalated"] for a in audits)
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
|
|
|
| 268 |
)
|
| 269 |
n_unresolved = sum(1 for a in audits if a.get("resolution_note"))
|
| 270 |
report = {
|
|
@@ -272,8 +371,7 @@ async def build_report(state: GraphState) -> GraphState:
|
|
| 272 |
"n_unresolved_citations": n_unresolved,
|
| 273 |
"initial_agreement_rate": agreement_rate, # computed over resolved claims only
|
| 274 |
"n_escalated": n_escalated,
|
| 275 |
-
"
|
| 276 |
-
"n_resolved_by_adjudicator": n_adjudicated,
|
| 277 |
"claims": audits,
|
| 278 |
}
|
| 279 |
return {**state, "report": report}
|
|
|
|
| 25 |
|
| 26 |
AGREEMENT_THRESHOLD = 0.5 # min confidence gap tolerated before escalation
|
| 27 |
|
| 28 |
+
MAX_AGENT_ITERATIONS = 3 # ReAct-loop safety cap — avoids runaway free-tier usage
|
| 29 |
+
|
| 30 |
+
AGENT_SYSTEM_PROMPT = """You are resolving a disagreement between two independent \
|
| 31 |
+
methods that assessed whether a piece of evidence supports a scientific claim.
|
| 32 |
|
| 33 |
Claim: {claim}
|
| 34 |
+
Evidence currently available: {evidence}
|
| 35 |
|
| 36 |
+
Assessment A (fine-tuned NLI model): {label_a} (confidence {confidence_a})
|
| 37 |
+
Assessment B (zero-shot LLM verifier): {label_b} (confidence {confidence_b})
|
| 38 |
Assessment B's reasoning: {reasoning_b}
|
| 39 |
|
| 40 |
+
You have tools available to investigate further before concluding. Use them if \
|
| 41 |
+
they would genuinely help; call `conclude` as soon as you have a well-supported \
|
| 42 |
+
answer. Do not call more tools than necessary. If you cannot resolve the \
|
| 43 |
+
disagreement with more evidence, concluding NOT_ENOUGH_INFO is a legitimate, \
|
| 44 |
+
complete answer — it is not a failure, and is preferable to guessing."""
|
| 45 |
+
|
| 46 |
+
AGENT_TOOLS = [
|
| 47 |
+
{
|
| 48 |
+
"type": "function",
|
| 49 |
+
"function": {
|
| 50 |
+
"name": "retry_with_query",
|
| 51 |
+
"description": ("Search Semantic Scholar again with a different query, "
|
| 52 |
+
"in case the currently available evidence missed the "
|
| 53 |
+
"relevant passage. Write your own search query."),
|
| 54 |
+
"parameters": {
|
| 55 |
+
"type": "object",
|
| 56 |
+
"properties": {"query": {"type": "string", "description": "New search query"}},
|
| 57 |
+
"required": ["query"],
|
| 58 |
+
},
|
| 59 |
+
},
|
| 60 |
+
},
|
| 61 |
+
{
|
| 62 |
+
"type": "function",
|
| 63 |
+
"function": {
|
| 64 |
+
"name": "request_second_opinion",
|
| 65 |
+
"description": ("Get an independent, blind re-verification of the claim "
|
| 66 |
+
"against the current evidence (no prior verdicts shown "
|
| 67 |
+
"to it, to avoid anchoring bias)."),
|
| 68 |
+
"parameters": {"type": "object", "properties": {}},
|
| 69 |
+
},
|
| 70 |
+
},
|
| 71 |
+
{
|
| 72 |
+
"type": "function",
|
| 73 |
+
"function": {
|
| 74 |
+
"name": "conclude",
|
| 75 |
+
"description": "Give your final, complete answer. This ends the investigation.",
|
| 76 |
+
"parameters": {
|
| 77 |
+
"type": "object",
|
| 78 |
+
"properties": {
|
| 79 |
+
"label": {"type": "string", "enum": ["SUPPORT", "NOT_ENOUGH_INFO", "CONTRADICT"]},
|
| 80 |
+
"confidence": {"type": "number"},
|
| 81 |
+
"reasoning": {"type": "string"},
|
| 82 |
+
},
|
| 83 |
+
"required": ["label", "confidence", "reasoning"],
|
| 84 |
+
},
|
| 85 |
+
},
|
| 86 |
+
},
|
| 87 |
+
]
|
| 88 |
|
| 89 |
|
| 90 |
def _llm():
|
|
|
|
| 92 |
api_key=os.environ.get("GROQ_API_KEY"))
|
| 93 |
|
| 94 |
|
| 95 |
+
def _planner_llm():
|
| 96 |
+
# Deliberately a different, actively-supported model from the verifier
|
| 97 |
+
# (gpt-oss-20b) — a same-model critic tends to rubber-stamp its own kind
|
| 98 |
+
# of reasoning (Panickssery et al., 2024). llama-3.3-70b-versatile was
|
| 99 |
+
# deprecated by Groq (announced 2026-06-17); gpt-oss-120b is Groq's own
|
| 100 |
+
# recommended replacement and remains actively supported.
|
| 101 |
+
return ChatGroq(model="openai/gpt-oss-120b", temperature=0.0,
|
| 102 |
+
api_key=os.environ.get("GROQ_API_KEY")).bind_tools(AGENT_TOOLS)
|
| 103 |
|
| 104 |
|
| 105 |
async def extract_claims(state: GraphState) -> GraphState:
|
|
|
|
| 160 |
"source": "llm"},
|
| 161 |
"agreement": agree,
|
| 162 |
"escalated": False,
|
| 163 |
+
"escalation_trace": None,
|
|
|
|
| 164 |
"final_verdict": deberta_verdict if agree else None,
|
| 165 |
"attribution": None,
|
| 166 |
"resolution_note": None,
|
|
|
|
| 190 |
"llm_verdict": None,
|
| 191 |
"agreement": None,
|
| 192 |
"escalated": False,
|
| 193 |
+
"escalation_trace": None,
|
|
|
|
| 194 |
"final_verdict": None,
|
| 195 |
"attribution": None,
|
| 196 |
"resolution_note": ("Citation could not be resolved to usable evidence — "
|
|
|
|
| 207 |
|
| 208 |
|
| 209 |
async def escalate(state: GraphState) -> GraphState:
|
| 210 |
+
"""On disagreement: a planner model (gpt-oss-120b, distinct from the
|
| 211 |
+
gpt-oss-20b verifier) autonomously decides how to resolve it -- it can
|
| 212 |
+
retry retrieval with its own reformulated query, request a blind second
|
| 213 |
+
opinion, or conclude directly, in whatever order and however many times
|
| 214 |
+
(up to MAX_AGENT_ITERATIONS) it judges necessary. This is a ReAct-style
|
| 215 |
+
reason-act-observe loop: the control flow is decided by the model at
|
| 216 |
+
each step, not by fixed code. See docs/dev_log.md for design rationale.
|
| 217 |
"""
|
| 218 |
idx = state["current_index"]
|
| 219 |
cc = state["claims"][idx]
|
|
|
|
| 221 |
last = audits[-1]
|
| 222 |
last["escalated"] = True
|
| 223 |
|
| 224 |
+
from langchain_core.messages import SystemMessage, ToolMessage
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
|
| 226 |
+
current_evidence = cc["evidence_text"] or ""
|
| 227 |
+
system_prompt = AGENT_SYSTEM_PROMPT.format(
|
|
|
|
| 228 |
claim=cc["claim"],
|
| 229 |
+
evidence=current_evidence,
|
| 230 |
label_a=last["deberta_verdict"]["label"],
|
| 231 |
confidence_a=last["deberta_verdict"]["confidence"],
|
| 232 |
label_b=last["llm_verdict"]["label"],
|
| 233 |
confidence_b=last["llm_verdict"]["confidence"],
|
| 234 |
+
reasoning_b="", # llm_verifier.verify doesn't currently surface reasoning to verify_dual
|
| 235 |
)
|
| 236 |
+
messages = [SystemMessage(content=system_prompt)]
|
| 237 |
+
trace = []
|
| 238 |
+
planner = _planner_llm()
|
| 239 |
+
s2_key = os.environ.get("S2_API_KEY")
|
| 240 |
+
|
| 241 |
+
for _ in range(MAX_AGENT_ITERATIONS):
|
| 242 |
+
try:
|
| 243 |
+
response = await planner.ainvoke(messages)
|
| 244 |
+
except Exception as exc:
|
| 245 |
+
# Groq's tool-call parser can hard-fail (400) if the model blends
|
| 246 |
+
# free-text reasoning with a malformed tool call, rather than
|
| 247 |
+
# returning a plain response we could just re-prompt. Observed in
|
| 248 |
+
# practice after repeated tool failures (e.g. two 429s in a row)
|
| 249 |
+
# pushed the model toward verbose reasoning instead of a clean
|
| 250 |
+
# tool call. Treat this the same as "no usable answer yet" rather
|
| 251 |
+
# than crashing the whole graph.
|
| 252 |
+
trace.append({"action": "planner_error", "input": None, "observation": str(exc)})
|
| 253 |
+
break
|
| 254 |
+
|
| 255 |
+
messages.append(response)
|
| 256 |
+
|
| 257 |
+
if not response.tool_calls:
|
| 258 |
+
messages.append(SystemMessage(content="Please call the `conclude` tool with your final answer."))
|
| 259 |
+
continue
|
| 260 |
+
|
| 261 |
+
tool_call = response.tool_calls[0] # one action at a time, per ReAct
|
| 262 |
+
name, args = tool_call["name"], tool_call["args"]
|
| 263 |
+
|
| 264 |
+
if name == "conclude":
|
| 265 |
+
last["final_verdict"] = {"label": args["label"], "confidence": args.get("confidence", 0.0),
|
| 266 |
+
"source": "agent"}
|
| 267 |
+
trace.append({"action": "conclude", "input": args, "observation": None})
|
| 268 |
+
last["escalation_trace"] = trace
|
| 269 |
+
return {**state, "audits": audits}
|
| 270 |
|
| 271 |
+
elif name == "retry_with_query":
|
| 272 |
+
paper = await semantic_scholar.search_paper(args["query"], api_key=s2_key)
|
| 273 |
+
if paper and paper.get("abstract"):
|
| 274 |
+
current_evidence = paper["abstract"]
|
| 275 |
+
deberta_res = await claim_client.analyze(cc["claim"], current_evidence)
|
| 276 |
+
winner = deberta_res["winner"]
|
| 277 |
+
new_llm_res = await llm_verifier.verify(cc["claim"], current_evidence)
|
| 278 |
+
observation = (f"New evidence found: \"{current_evidence[:300]}\". "
|
| 279 |
+
f"Re-verified: NLI model says {winner['label']} "
|
| 280 |
+
f"({winner['confidence']:.2f}), LLM verifier says "
|
| 281 |
+
f"{new_llm_res.get('label')} ({new_llm_res.get('confidence', 0):.2f}).")
|
| 282 |
+
last["winner_sentence"] = winner["sentence"]
|
| 283 |
+
last["attribution_available"] = deberta_res.get("attribution_available", False)
|
| 284 |
+
last["deberta_verdict"] = {"label": winner["label"], "confidence": winner["confidence"], "source": "deberta"}
|
| 285 |
+
last["llm_verdict"] = {"label": new_llm_res.get("label"), "confidence": new_llm_res.get("confidence", 0.0), "source": "llm"}
|
| 286 |
+
else:
|
| 287 |
+
observation = "No usable evidence found for that query (no match or no abstract indexed)."
|
| 288 |
+
trace.append({"action": "retry_with_query", "input": args, "observation": observation})
|
| 289 |
+
messages.append(ToolMessage(content=observation, tool_call_id=tool_call["id"]))
|
| 290 |
+
|
| 291 |
+
elif name == "request_second_opinion":
|
| 292 |
+
second = await llm_verifier.verify(cc["claim"], current_evidence) # blind -- no prior verdicts in this call
|
| 293 |
+
observation = (f"Second opinion (blind, independent): {second.get('label')} "
|
| 294 |
+
f"(confidence {second.get('confidence', 0):.2f}). "
|
| 295 |
+
f"Reasoning: {second.get('reasoning', '')}")
|
| 296 |
+
trace.append({"action": "request_second_opinion", "input": {}, "observation": observation})
|
| 297 |
+
messages.append(ToolMessage(content=observation, tool_call_id=tool_call["id"]))
|
| 298 |
+
|
| 299 |
+
# Iteration cap hit (or planner errored above) without a `conclude` call --
|
| 300 |
+
# force a final answer rather than leaving the claim unresolved. Per
|
| 301 |
+
# ReAct/Reflexion literature, a calibrated NOT_ENOUGH_INFO is a legitimate
|
| 302 |
+
# complete answer, not a failure.
|
| 303 |
+
messages.append(SystemMessage(content="You must conclude now with your best answer, "
|
| 304 |
+
"using only the `conclude` tool."))
|
| 305 |
+
try:
|
| 306 |
+
final_response = await planner.ainvoke(messages)
|
| 307 |
+
except Exception as exc:
|
| 308 |
+
trace.append({"action": "planner_error_on_forced_conclude", "input": None, "observation": str(exc)})
|
| 309 |
+
final_response = None
|
| 310 |
+
|
| 311 |
+
if final_response and final_response.tool_calls and final_response.tool_calls[0]["name"] == "conclude":
|
| 312 |
+
args = final_response.tool_calls[0]["args"]
|
| 313 |
+
last["final_verdict"] = {"label": args["label"], "confidence": args.get("confidence", 0.0), "source": "agent"}
|
| 314 |
+
trace.append({"action": "conclude (forced at iteration cap)", "input": args, "observation": None})
|
| 315 |
+
else:
|
| 316 |
+
# Model never produced a valid conclude call, either it declined to
|
| 317 |
+
# or Groq's parser failed on it. Fall back to NOT_ENOUGH_INFO rather
|
| 318 |
+
# than guess, consistent with the "refusal over fabrication" principle,
|
| 319 |
+
# and rather than crash the request.
|
| 320 |
+
last["final_verdict"] = {"label": "NOT_ENOUGH_INFO", "confidence": 0.0, "source": "agent_fallback"}
|
| 321 |
+
trace.append({"action": "forced_fallback", "input": None, "observation": "Model did not call conclude within iteration cap."})
|
| 322 |
+
|
| 323 |
+
last["escalation_trace"] = trace
|
| 324 |
return {**state, "audits": audits}
|
| 325 |
|
| 326 |
|
|
|
|
| 330 |
audits = state["audits"]
|
| 331 |
last = audits[-1]
|
| 332 |
if last["final_verdict"] is None:
|
| 333 |
+
# Should not normally happen -- verify_dual sets it on agreement,
|
| 334 |
+
# escalate always sets it via conclude/forced-conclude/fallback.
|
| 335 |
last["final_verdict"] = last["deberta_verdict"]
|
| 336 |
|
| 337 |
if last["attribution_available"]:
|
|
|
|
| 360 |
verified = [a for a in audits if a["agreement"] is not None] # excludes unresolved citations
|
| 361 |
agreement_rate = (sum(a["agreement"] for a in verified) / len(verified)) if verified else 0.0
|
| 362 |
n_escalated = sum(a["escalated"] for a in audits)
|
| 363 |
+
escalated_audits = [a for a in audits if a["escalated"]]
|
| 364 |
+
avg_agent_iterations = (
|
| 365 |
+
sum(len(a.get("escalation_trace") or []) for a in escalated_audits) / len(escalated_audits)
|
| 366 |
+
if escalated_audits else 0.0
|
| 367 |
)
|
| 368 |
n_unresolved = sum(1 for a in audits if a.get("resolution_note"))
|
| 369 |
report = {
|
|
|
|
| 371 |
"n_unresolved_citations": n_unresolved,
|
| 372 |
"initial_agreement_rate": agreement_rate, # computed over resolved claims only
|
| 373 |
"n_escalated": n_escalated,
|
| 374 |
+
"avg_agent_iterations_when_escalated": avg_agent_iterations,
|
|
|
|
| 375 |
"claims": audits,
|
| 376 |
}
|
| 377 |
return {**state, "report": report}
|
app/graph/state.py
CHANGED
|
@@ -25,8 +25,7 @@ class ClaimAudit(TypedDict):
|
|
| 25 |
llm_verdict: Optional[Verdict]
|
| 26 |
agreement: Optional[bool]
|
| 27 |
escalated: bool
|
| 28 |
-
|
| 29 |
-
adjudicator_reasoning: Optional[str] # set only if adjudicator step ran
|
| 30 |
final_verdict: Optional[Verdict]
|
| 31 |
attribution: Optional[list] # [{"token": ..., "score": ...}, ...] or None
|
| 32 |
resolution_note: Optional[str] # set when citation could not be resolved to evidence
|
|
|
|
| 25 |
llm_verdict: Optional[Verdict]
|
| 26 |
agreement: Optional[bool]
|
| 27 |
escalated: bool
|
| 28 |
+
escalation_trace: Optional[list] # [{"action": ..., "input": ..., "observation": ...}, ...]
|
|
|
|
| 29 |
final_verdict: Optional[Verdict]
|
| 30 |
attribution: Optional[list] # [{"token": ..., "score": ...}, ...] or None
|
| 31 |
resolution_note: Optional[str] # set when citation could not be resolved to evidence
|