Repair ContractClock persistence and receipt safety

This commit is contained in:
2026-07-18 05:27:54 -03:00
parent 78f8677e6f
commit 21284e8ab4
3 changed files with 88 additions and 23 deletions

View File

@@ -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}"