This repository was archived by the owner on Jul 22, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathloop.py
More file actions
154 lines (135 loc) · 4.47 KB
/
Copy pathloop.py
File metadata and controls
154 lines (135 loc) · 4.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
"""The bounded, code-driven ReAct loop.
reason -> act -> observe over a fixed whitelist (parse, chunk, select, extract,
validate)
ending at a hard interrupt. The model is called only inside extract; every other
step is deterministic code. The loop cannot exceed STEP_CAP and cannot call
outside the whitelist — those invariants are what the eval harness asserts.
"""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass, field
from hipo_contracts import DegradationState, Rejection, ToolName, TraceStep
from hipo_api.agent.extract import EXTRACT_MODEL, ExtractParseError, extract_claims
from hipo_api.agent.normalize import normalize_claims
from hipo_api.agent.select import select_chunks
from hipo_api.build import build_candidate
from hipo_api.chunk import chunk_pages
from hipo_api.parse import parse_pdf
from hipo_api.validate import validate_factsheet
STEP_CAP = 8
@dataclass
class AgentResult:
steps: list[TraceStep] = field(default_factory=list)
state: DegradationState = DegradationState.REJECTED
payload: dict = field(default_factory=dict)
terminated_at_interrupt: bool = False
def run_agent(
pdf_bytes: bytes,
entry: dict,
client,
*,
model: str = EXTRACT_MODEL,
on_step: Callable[[TraceStep], None] | None = None,
on_token: Callable[[str], None] | None = None,
) -> AgentResult:
result = AgentResult()
n = 0
def record(thought, tool, args_digest, observation, model_name=None):
nonlocal n
step = TraceStep(
step=n,
thought=thought,
tool=tool,
args_digest=args_digest,
observation=observation,
model=model_name,
)
result.steps.append(step)
if on_step is not None:
on_step(step)
n += 1
doc = parse_pdf(pdf_bytes)
record(
"read the filing",
ToolName.PARSE,
{"pages": doc.page_count},
f"{doc.page_count} pages",
)
chunks = chunk_pages(doc)
record(
"select candidate pages",
ToolName.CHUNK,
{"chunks": len(chunks)},
f"{len(chunks)} non-empty chunks",
)
selected, dropped_pages = select_chunks(chunks)
record(
"narrow to the most relevant pages within budget",
ToolName.SELECT,
{"kept": len(selected), "dropped": dropped_pages},
f"selected {len(selected)} of {len(chunks)} pages",
)
try:
claims = extract_claims(selected, client, model=model, on_token=on_token)
except ExtractParseError as exc:
record(
"extract claims from the filing",
ToolName.EXTRACT,
{},
f"extraction failed: {exc}",
model_name=model,
)
rej = Rejection.single("extract", "model extraction failed")
result.payload = rej.model_dump()
result.state = DegradationState.REJECTED
record(
"stop — nothing to surface",
ToolName.INTERRUPT,
{},
"surfaced rejection",
)
result.terminated_at_interrupt = True
return result
n_claims = len(claims.get("financial_highlights", [])) + len(
claims.get("risks", [])
)
record(
"extract claims from the filing",
ToolName.EXTRACT,
{"claims": n_claims},
f"model returned {n_claims} claims",
model_name=model,
)
base = build_candidate(doc, entry)
candidate = normalize_claims(base, claims)
kept = len(candidate["financial_highlights"]) + len(candidate["risks"])
dropped = n_claims - kept
outcome = validate_factsheet(candidate, doc)
if isinstance(outcome, Rejection):
result.payload = outcome.model_dump()
result.state = DegradationState.REJECTED
record(
"validate schema + citations",
ToolName.VALIDATE,
{"dropped": dropped},
"validation rejected the candidate",
)
else:
result.payload = outcome.model_dump()
result.state = (
DegradationState.FULL if dropped == 0 else DegradationState.PARTIAL
)
record(
"validate schema + citations",
ToolName.VALIDATE,
{"dropped": dropped},
f"validated; {dropped} claim(s) dropped",
)
record(
"draft ready — never auto-act",
ToolName.INTERRUPT,
{},
"surfaced draft factsheet",
)
result.terminated_at_interrupt = True
return result