Repair ContractClock persistence and receipt safety
This commit is contained in:
73
agent.py
73
agent.py
@@ -43,6 +43,7 @@ MAX_BROWSER_UPLOAD_BYTES = 512_000
|
||||
ALLOWED_UPLOAD_MEDIA_TYPES = ("text/plain", "application/octet-stream")
|
||||
DB_STATEMENT_TIMEOUT_MS = 5_000
|
||||
DB_LOCK_TIMEOUT_MS = 3_000
|
||||
DB_IDLE_TRANSACTION_TIMEOUT_MS = 5_000
|
||||
|
||||
ContractId = Annotated[
|
||||
str,
|
||||
@@ -107,7 +108,7 @@ class ContractClockStudioV1(A2AAgent[ContractClockStudioV1Config, PlatformUserAu
|
||||
"from contracts, persists user-scoped timelines, and serves a packed "
|
||||
"one-page React product at /app."
|
||||
)
|
||||
version = "0.1.5"
|
||||
version = "0.1.6"
|
||||
|
||||
config_model = ContractClockStudioV1Config
|
||||
auth_model = PlatformUserAuth
|
||||
@@ -246,7 +247,7 @@ def _tenant_key(ctx: RunContext[PlatformUserAuth]) -> str | None:
|
||||
user_id = getattr(auth, "user_id", None)
|
||||
if user_id is not None:
|
||||
return f"user:{user_id}"
|
||||
subject = (getattr(auth, "sub", None) or getattr(auth, "email", None) or "").strip()
|
||||
subject = (getattr(auth, "sub", None) or "").strip()
|
||||
return f"user:{subject}" if subject else None
|
||||
|
||||
|
||||
@@ -306,10 +307,15 @@ def _extract_deadlines(text: str) -> list[ExtractedDeadline]:
|
||||
lower = clause.lower()
|
||||
if "renew" not in lower:
|
||||
continue
|
||||
dates = _explicit_iso_dates(clause)
|
||||
if not dates:
|
||||
renewal_offset = lower.find("renew")
|
||||
dates_after_renewal = [
|
||||
parsed
|
||||
for offset, parsed in _explicit_iso_dates_with_positions(clause)
|
||||
if offset > renewal_offset
|
||||
]
|
||||
if not dates_after_renewal:
|
||||
continue
|
||||
renewal_date = dates[0]
|
||||
renewal_date = dates_after_renewal[0]
|
||||
if ("renewal", renewal_date) not in seen:
|
||||
deadlines.append(
|
||||
ExtractedDeadline(
|
||||
@@ -344,7 +350,11 @@ def _deadline_clauses(text: str) -> list[str]:
|
||||
|
||||
|
||||
def _normalize_text(text: str) -> str:
|
||||
return re.sub(r"\s+", " ", text).strip()
|
||||
normalized_lines = [
|
||||
re.sub(r"[^\S\n]+", " ", line).strip()
|
||||
for line in text.splitlines()
|
||||
]
|
||||
return "\n".join(line for line in normalized_lines if line).strip()
|
||||
|
||||
|
||||
def _contains_explicit_iso_date(text: str) -> bool:
|
||||
@@ -357,10 +367,14 @@ def _mentions_deadline_domain(text: str) -> bool:
|
||||
|
||||
|
||||
def _explicit_iso_dates(text: str) -> list[date]:
|
||||
out: list[date] = []
|
||||
return [parsed for _, parsed in _explicit_iso_dates_with_positions(text)]
|
||||
|
||||
|
||||
def _explicit_iso_dates_with_positions(text: str) -> list[tuple[int, date]]:
|
||||
out: list[tuple[int, date]] = []
|
||||
for match in re.finditer(r"\b(20\d{2})-(0[1-9]|1[0-2])-([0-2]\d|3[01])\b", text):
|
||||
try:
|
||||
out.append(date.fromisoformat(match.group(0)))
|
||||
out.append((match.start(), date.fromisoformat(match.group(0))))
|
||||
except ValueError:
|
||||
continue
|
||||
return out
|
||||
@@ -486,18 +500,16 @@ async def _persist_contract_result(
|
||||
|
||||
receipt_id = _receipt_id()
|
||||
now = _now_iso()
|
||||
input_payload = {"contract_id": contract_id, "title": title, "text_sha256": _sha256(text)}
|
||||
input_payload = {"contract_id": contract_id, "text_sha256": _sha256(text)}
|
||||
result_payload = result.model_dump(mode="json")
|
||||
with psycopg.connect(
|
||||
os.environ["DATABASE_URL"],
|
||||
connect_timeout=3,
|
||||
options=f"-c statement_timeout={DB_STATEMENT_TIMEOUT_MS} -c lock_timeout={DB_LOCK_TIMEOUT_MS}",
|
||||
options=_database_options(),
|
||||
row_factory=dict_row,
|
||||
) as conn:
|
||||
with conn.transaction():
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SET LOCAL statement_timeout = %s", (DB_STATEMENT_TIMEOUT_MS,))
|
||||
cur.execute("SET LOCAL lock_timeout = %s", (DB_LOCK_TIMEOUT_MS,))
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO contracts (tenant_key, contract_id, title, source_text_hash, updated_at)
|
||||
@@ -544,7 +556,10 @@ async def _persist_contract_result(
|
||||
_hash_json(input_payload),
|
||||
_hash_json(result_payload),
|
||||
"ok",
|
||||
json.dumps({"input": input_payload, "result": result_payload}, sort_keys=True),
|
||||
json.dumps(
|
||||
{"input": input_payload, "result": _receipt_result_summary(result)},
|
||||
sort_keys=True,
|
||||
),
|
||||
),
|
||||
)
|
||||
return result.model_copy(update={"receipt_id": receipt_id, "saved_at": now})
|
||||
@@ -573,11 +588,10 @@ async def _persist_receipt_only(
|
||||
with psycopg.connect(
|
||||
os.environ["DATABASE_URL"],
|
||||
connect_timeout=3,
|
||||
options=f"-c statement_timeout={DB_STATEMENT_TIMEOUT_MS} -c lock_timeout={DB_LOCK_TIMEOUT_MS}",
|
||||
options=_database_options(),
|
||||
) as conn:
|
||||
with conn.transaction():
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SET LOCAL statement_timeout = %s", (DB_STATEMENT_TIMEOUT_MS,))
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO execution_receipts
|
||||
@@ -594,7 +608,10 @@ async def _persist_receipt_only(
|
||||
_hash_json(input_payload),
|
||||
_hash_json(result.model_dump(mode="json")),
|
||||
"error",
|
||||
json.dumps({"input": input_payload, "result": result.model_dump(mode="json")}, sort_keys=True),
|
||||
json.dumps(
|
||||
{"input": input_payload, "result": _receipt_result_summary(result)},
|
||||
sort_keys=True,
|
||||
),
|
||||
),
|
||||
)
|
||||
except Exception:
|
||||
@@ -620,11 +637,10 @@ async def _load_contract_result(
|
||||
with psycopg.connect(
|
||||
os.environ["DATABASE_URL"],
|
||||
connect_timeout=3,
|
||||
options=f"-c statement_timeout={DB_STATEMENT_TIMEOUT_MS} -c lock_timeout={DB_LOCK_TIMEOUT_MS}",
|
||||
options=_database_options(),
|
||||
row_factory=dict_row,
|
||||
) as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SET statement_timeout = %s", (DB_STATEMENT_TIMEOUT_MS,))
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, title, updated_at
|
||||
@@ -677,6 +693,27 @@ async def _load_contract_result(
|
||||
)
|
||||
|
||||
|
||||
def _database_options() -> str:
|
||||
return (
|
||||
f"-c statement_timeout={DB_STATEMENT_TIMEOUT_MS} "
|
||||
f"-c lock_timeout={DB_LOCK_TIMEOUT_MS} "
|
||||
f"-c idle_in_transaction_session_timeout={DB_IDLE_TRANSACTION_TIMEOUT_MS}"
|
||||
)
|
||||
|
||||
|
||||
def _receipt_result_summary(result: ContractResult) -> dict[str, Any]:
|
||||
return {
|
||||
"ok": result.ok,
|
||||
"code": result.code,
|
||||
"contract_id": result.contract_id,
|
||||
"deadline_count": len(result.deadlines),
|
||||
"deadlines": [
|
||||
{"kind": deadline.kind, "date": deadline.date}
|
||||
for deadline in result.deadlines
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _receipt_id() -> str:
|
||||
return f"ccr_{uuid.uuid4().hex}"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user