From 11e097a1a5fbfa4f6838cdea9c44c58fc9180341 Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 18 Jul 2026 05:00:29 -0300 Subject: [PATCH] Bind renewal dates to renewal clauses --- a2a.yaml | 2 +- agent.py | 17 ++++++++++++----- tests/test_full_stack_contract.py | 20 +++++++++++++++++++- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/a2a.yaml b/a2a.yaml index a8fb2ee..be1d64f 100644 --- a/a2a.yaml +++ b/a2a.yaml @@ -1,5 +1,5 @@ name: contract-clock-studio-v1 -version: 0.1.4 +version: 0.1.5 entrypoint: agent:ContractClockStudioV1 expose: public: false diff --git a/agent.py b/agent.py index 53a04a6..19d1162 100644 --- a/agent.py +++ b/agent.py @@ -44,8 +44,8 @@ ALLOWED_BROWSER_MEDIA_TYPES = ( ) DATE_RE = re.compile(r"\b(20\d{2}-\d{2}-\d{2})\b") RENEWAL_DATE_RE = re.compile( - r"\brenew(?:s|al|ed|ing)?\b[^.\n;]{0,160}\b(?:on|as\s+of|effective|date|until|through)\s+" - r"(20\d{2}-\d{2}-\d{2})\b", + r"\brenew(?:s|al|ed|ing)?\b[^.\n;]{0,160}?\b(?:on|as\s+of|effective(?:\s+on)?|" + r"date(?:\s+is)?|until|through)\b\s*:?\s*(20\d{2}-\d{2}-\d{2})\b", re.IGNORECASE, ) RENEWAL_RE = re.compile(r"\brenew(?:s|al|ed|ing)?\b", re.IGNORECASE) @@ -76,7 +76,7 @@ class ContractClockStudioV1(A2AAgent[ContractClockStudioV1Config, PlatformUserAu "contracts, saves user-scoped timelines, and reopens them in a packed " "one-page studio." ) - version = "0.1.4" + version = "0.1.5" config_model = ContractClockStudioV1Config auth_model = PlatformUserAuth @@ -277,8 +277,9 @@ def _analyze_text(*, contract_id: str, title: str, text: str) -> dict[str, Any]: return {"ok": False, "code": code, "contract_id": contract_id} deadlines: list[dict[str, str]] = [] - if RENEWAL_RE.search(clean_text): - renewal_date = explicit_dates[0] + renewal_match = RENEWAL_DATE_RE.search(clean_text) + if renewal_match: + renewal_date = date.fromisoformat(renewal_match.group(1)) deadlines.append({"kind": "renewal", "date": renewal_date.isoformat()}) notice_match = NOTICE_RE.search(clean_text) if notice_match: @@ -287,6 +288,12 @@ def _analyze_text(*, contract_id: str, title: str, text: str) -> dict[str, Any]: deadlines.append( {"kind": "notice", "date": (renewal_date - timedelta(days=notice_days)).isoformat()} ) + elif RENEWAL_RE.search(clean_text): + return { + "ok": False, + "code": "ambiguous_renewal_date", + "contract_id": contract_id, + } else: # Explicit dates outside a renewal clause are kept as dated obligations; # relative invoice terms without a concrete anchor are intentionally not guessed. diff --git a/tests/test_full_stack_contract.py b/tests/test_full_stack_contract.py index 7ca365f..398dbbb 100644 --- a/tests/test_full_stack_contract.py +++ b/tests/test_full_stack_contract.py @@ -21,7 +21,7 @@ def test_full_stack_product_contract(): frontend = (ROOT / "frontend" / "src" / "App.jsx").read_text(encoding="utf-8") browser_client = (ROOT / "frontend" / "src" / "a2a.js").read_text(encoding="utf-8") - assert "version: 0.1.3" in manifest + assert "version: 0.1.5" in manifest assert "public: false" in manifest assert "frontend:" in manifest and "mount: /app" in manifest assert "resources:" in manifest and "databases:" in manifest @@ -84,6 +84,24 @@ def test_success_and_failure_extraction_are_explicit_date_only(): } +def test_renewal_date_is_bound_to_the_renewal_clause(): + result = agent._analyze_text( + contract_id="multi-date-contract", + title="Multi-date contract", + text=( + "This Agreement is effective on 2025-01-01. " + "It renews automatically on 2026-12-31 unless either party gives " + "at least 60 days written notice." + ), + ) + + assert result["ok"] is True + assert result["deadlines"] == [ + {"kind": "renewal", "date": "2026-12-31"}, + {"kind": "notice", "date": "2026-11-01"}, + ] + + def test_browser_upload_validation(): doc = agent.BrowserDocument( filename="contract.txt",