Make ContractClock persistence atomic
This commit is contained in:
62
agent.py
62
agent.py
@@ -43,6 +43,7 @@ ALLOWED_MEDIA_TYPES = ("text/plain", "text/markdown", "application/octet-stream"
|
||||
DATABASE_ENV = "DATABASE_URL"
|
||||
DB_STATEMENT_TIMEOUT_MS = 5_000
|
||||
DB_CONNECT_TIMEOUT_SECONDS = 3
|
||||
VERSION = "0.1.1"
|
||||
|
||||
ISO_DATE_RE = re.compile(r"\b(20\d{2}|19\d{2})-(0[1-9]|1[0-2])-([0-2]\d|3[01])\b")
|
||||
NOTICE_RE = re.compile(
|
||||
@@ -137,7 +138,7 @@ class ContractClockStudioV1(A2AAgent[ContractClockStudioV1Config, PlatformUserAu
|
||||
"extract explicit renewal and notice dates, persist the timeline in "
|
||||
"user-scoped managed Postgres, and reopen it later."
|
||||
)
|
||||
version = "0.1.0"
|
||||
version = VERSION
|
||||
|
||||
config_model = ContractClockStudioV1Config
|
||||
auth_model = PlatformUserAuth
|
||||
@@ -183,8 +184,8 @@ class ContractClockStudioV1(A2AAgent[ContractClockStudioV1Config, PlatformUserAu
|
||||
text: str,
|
||||
title: str,
|
||||
) -> AnalyzeResult:
|
||||
tenant = tenant_key(ctx)
|
||||
try:
|
||||
tenant = tenant_key(ctx)
|
||||
clean_id = validate_contract_id(contract_id)
|
||||
clean_title = validate_title(title)
|
||||
clean_text = validate_text(text)
|
||||
@@ -207,8 +208,15 @@ class ContractClockStudioV1(A2AAgent[ContractClockStudioV1Config, PlatformUserAu
|
||||
}
|
||||
receipt_id = production_receipt_id(tenant, "analyze_contract", payload)
|
||||
try:
|
||||
persist_contract(tenant, clean_id, clean_title, payload, receipt_id)
|
||||
persist_receipt(tenant, receipt_id, "analyze_contract", clean_id, payload, "ok")
|
||||
persist_contract_with_receipt(
|
||||
tenant,
|
||||
clean_id,
|
||||
clean_title,
|
||||
payload,
|
||||
receipt_id,
|
||||
"analyze_contract",
|
||||
"ok",
|
||||
)
|
||||
except Exception:
|
||||
return AnalyzeResult(
|
||||
ok=False,
|
||||
@@ -309,8 +317,8 @@ class ContractClockStudioV1(A2AAgent[ContractClockStudioV1Config, PlatformUserAu
|
||||
ctx: RunContext[PlatformUserAuth],
|
||||
contract_id: str,
|
||||
) -> ContractRecord:
|
||||
tenant = tenant_key(ctx)
|
||||
try:
|
||||
tenant = tenant_key(ctx)
|
||||
clean_id = validate_contract_id(contract_id)
|
||||
record = load_contract(tenant, clean_id)
|
||||
except ValidationFailure as exc:
|
||||
@@ -337,10 +345,12 @@ class ContractClockStudioV1(A2AAgent[ContractClockStudioV1Config, PlatformUserAu
|
||||
cost_class="deterministic",
|
||||
)
|
||||
async def list_contracts(self, ctx: RunContext[PlatformUserAuth], limit: int = 20) -> ListContractsResult:
|
||||
tenant = tenant_key(ctx)
|
||||
safe_limit = max(1, min(int(limit), 50))
|
||||
try:
|
||||
tenant = tenant_key(ctx)
|
||||
safe_limit = max(1, min(int(limit), 50))
|
||||
rows = list_contract_rows(tenant, safe_limit)
|
||||
except ValidationFailure as exc:
|
||||
return ListContractsResult(ok=False, code=exc.code, message=exc.message)
|
||||
except Exception:
|
||||
return ListContractsResult(ok=False, code="persistence_unavailable", message="ContractClock could not list saved timelines.")
|
||||
return ListContractsResult(ok=True, contracts=[ContractSummary.model_validate(row) for row in rows])
|
||||
@@ -481,28 +491,18 @@ def db_connection() -> Iterator[Any]:
|
||||
connect_timeout=DB_CONNECT_TIMEOUT_SECONDS,
|
||||
options=f"-c statement_timeout={DB_STATEMENT_TIMEOUT_MS} -c idle_in_transaction_session_timeout={DB_STATEMENT_TIMEOUT_MS}",
|
||||
) as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SET LOCAL statement_timeout = %s", (DB_STATEMENT_TIMEOUT_MS,))
|
||||
yield conn
|
||||
|
||||
|
||||
def persist_contract(tenant: str, contract_id: str, title: str, payload: dict[str, Any], receipt_id: str) -> None:
|
||||
with db_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO contract_timelines (tenant_key, contract_id, title, payload, receipt_id, updated_at)
|
||||
VALUES (%s, %s, %s, %s::jsonb, %s, NOW())
|
||||
ON CONFLICT (tenant_key, contract_id)
|
||||
DO UPDATE SET title = EXCLUDED.title, payload = EXCLUDED.payload,
|
||||
receipt_id = EXCLUDED.receipt_id, updated_at = NOW()
|
||||
""",
|
||||
(tenant, contract_id, title, json.dumps(payload), receipt_id),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def persist_receipt(tenant: str, receipt_id: str, skill_name: str, contract_id: str, payload: dict[str, Any], status: str) -> None:
|
||||
def persist_contract_with_receipt(
|
||||
tenant: str,
|
||||
contract_id: str,
|
||||
title: str,
|
||||
payload: dict[str, Any],
|
||||
receipt_id: str,
|
||||
skill_name: str,
|
||||
status: str,
|
||||
) -> None:
|
||||
receipt = {
|
||||
"receipt_id": receipt_id,
|
||||
"agent": ContractClockStudioV1.name,
|
||||
@@ -515,6 +515,16 @@ def persist_receipt(tenant: str, receipt_id: str, skill_name: str, contract_id:
|
||||
}
|
||||
with db_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO contract_timelines (tenant_key, contract_id, title, payload, receipt_id, updated_at)
|
||||
VALUES (%s, %s, %s, %s::jsonb, %s, NOW())
|
||||
ON CONFLICT (tenant_key, contract_id)
|
||||
DO UPDATE SET title = EXCLUDED.title, payload = EXCLUDED.payload,
|
||||
receipt_id = EXCLUDED.receipt_id, updated_at = NOW()
|
||||
""",
|
||||
(tenant, contract_id, title, json.dumps(payload), receipt_id),
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO execution_receipts (tenant_key, receipt_id, skill_name, contract_id, status, payload)
|
||||
|
||||
Reference in New Issue
Block a user