Bind renewal dates to renewal clauses
This commit is contained in:
2
a2a.yaml
2
a2a.yaml
@@ -1,5 +1,5 @@
|
|||||||
name: contract-clock-studio-v1
|
name: contract-clock-studio-v1
|
||||||
version: 0.1.4
|
version: 0.1.5
|
||||||
entrypoint: agent:ContractClockStudioV1
|
entrypoint: agent:ContractClockStudioV1
|
||||||
expose:
|
expose:
|
||||||
public: false
|
public: false
|
||||||
|
|||||||
17
agent.py
17
agent.py
@@ -44,8 +44,8 @@ ALLOWED_BROWSER_MEDIA_TYPES = (
|
|||||||
)
|
)
|
||||||
DATE_RE = re.compile(r"\b(20\d{2}-\d{2}-\d{2})\b")
|
DATE_RE = re.compile(r"\b(20\d{2}-\d{2}-\d{2})\b")
|
||||||
RENEWAL_DATE_RE = re.compile(
|
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"\brenew(?:s|al|ed|ing)?\b[^.\n;]{0,160}?\b(?:on|as\s+of|effective(?:\s+on)?|"
|
||||||
r"(20\d{2}-\d{2}-\d{2})\b",
|
r"date(?:\s+is)?|until|through)\b\s*:?\s*(20\d{2}-\d{2}-\d{2})\b",
|
||||||
re.IGNORECASE,
|
re.IGNORECASE,
|
||||||
)
|
)
|
||||||
RENEWAL_RE = re.compile(r"\brenew(?:s|al|ed|ing)?\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 "
|
"contracts, saves user-scoped timelines, and reopens them in a packed "
|
||||||
"one-page studio."
|
"one-page studio."
|
||||||
)
|
)
|
||||||
version = "0.1.4"
|
version = "0.1.5"
|
||||||
|
|
||||||
config_model = ContractClockStudioV1Config
|
config_model = ContractClockStudioV1Config
|
||||||
auth_model = PlatformUserAuth
|
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}
|
return {"ok": False, "code": code, "contract_id": contract_id}
|
||||||
|
|
||||||
deadlines: list[dict[str, str]] = []
|
deadlines: list[dict[str, str]] = []
|
||||||
if RENEWAL_RE.search(clean_text):
|
renewal_match = RENEWAL_DATE_RE.search(clean_text)
|
||||||
renewal_date = explicit_dates[0]
|
if renewal_match:
|
||||||
|
renewal_date = date.fromisoformat(renewal_match.group(1))
|
||||||
deadlines.append({"kind": "renewal", "date": renewal_date.isoformat()})
|
deadlines.append({"kind": "renewal", "date": renewal_date.isoformat()})
|
||||||
notice_match = NOTICE_RE.search(clean_text)
|
notice_match = NOTICE_RE.search(clean_text)
|
||||||
if notice_match:
|
if notice_match:
|
||||||
@@ -287,6 +288,12 @@ def _analyze_text(*, contract_id: str, title: str, text: str) -> dict[str, Any]:
|
|||||||
deadlines.append(
|
deadlines.append(
|
||||||
{"kind": "notice", "date": (renewal_date - timedelta(days=notice_days)).isoformat()}
|
{"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:
|
else:
|
||||||
# Explicit dates outside a renewal clause are kept as dated obligations;
|
# Explicit dates outside a renewal clause are kept as dated obligations;
|
||||||
# relative invoice terms without a concrete anchor are intentionally not guessed.
|
# relative invoice terms without a concrete anchor are intentionally not guessed.
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ def test_full_stack_product_contract():
|
|||||||
frontend = (ROOT / "frontend" / "src" / "App.jsx").read_text(encoding="utf-8")
|
frontend = (ROOT / "frontend" / "src" / "App.jsx").read_text(encoding="utf-8")
|
||||||
browser_client = (ROOT / "frontend" / "src" / "a2a.js").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 "public: false" in manifest
|
||||||
assert "frontend:" in manifest and "mount: /app" in manifest
|
assert "frontend:" in manifest and "mount: /app" in manifest
|
||||||
assert "resources:" in manifest and "databases:" 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():
|
def test_browser_upload_validation():
|
||||||
doc = agent.BrowserDocument(
|
doc = agent.BrowserDocument(
|
||||||
filename="contract.txt",
|
filename="contract.txt",
|
||||||
|
|||||||
Reference in New Issue
Block a user