a2a-source-edit: write agent.py
This commit is contained in:
43
agent.py
43
agent.py
@@ -26,13 +26,15 @@ from a2a_pack import (
|
|||||||
AgentDatabaseEnv,
|
AgentDatabaseEnv,
|
||||||
AgentDatabaseMigrations,
|
AgentDatabaseMigrations,
|
||||||
AgentPlatformResources,
|
AgentPlatformResources,
|
||||||
|
FileUpload,
|
||||||
PlatformUserAuth,
|
PlatformUserAuth,
|
||||||
Pricing,
|
Pricing,
|
||||||
Resources,
|
Resources,
|
||||||
RunContext,
|
RunContext,
|
||||||
State,
|
State,
|
||||||
UploadedFile,
|
UploadedFile,
|
||||||
FileUpload,
|
WorkspaceAccess,
|
||||||
|
WorkspaceMode,
|
||||||
)
|
)
|
||||||
|
|
||||||
MAX_TEXT_CHARS = 200_000
|
MAX_TEXT_CHARS = 200_000
|
||||||
@@ -133,7 +135,7 @@ class ContractClockStudioV1(A2AAgent[ContractClockStudioV1Config, PlatformUserAu
|
|||||||
config_model = ContractClockStudioV1Config
|
config_model = ContractClockStudioV1Config
|
||||||
auth_model = PlatformUserAuth
|
auth_model = PlatformUserAuth
|
||||||
|
|
||||||
# Deterministic local logic only: no LLM credential is required.
|
# Deterministic local logic only: no LLM credential is read or required.
|
||||||
pricing = Pricing(
|
pricing = Pricing(
|
||||||
price_per_call_usd=0.0,
|
price_per_call_usd=0.0,
|
||||||
caller_pays_llm=False,
|
caller_pays_llm=False,
|
||||||
@@ -142,6 +144,12 @@ class ContractClockStudioV1(A2AAgent[ContractClockStudioV1Config, PlatformUserAu
|
|||||||
resources = Resources(cpu="500m", memory="512Mi", max_runtime_seconds=120)
|
resources = Resources(cpu="500m", memory="512Mi", max_runtime_seconds=120)
|
||||||
state = State.DURABLE
|
state = State.DURABLE
|
||||||
tools_used = ("postgres", "mcp")
|
tools_used = ("postgres", "mcp")
|
||||||
|
workspace_access = WorkspaceAccess.dynamic(
|
||||||
|
max_files=4,
|
||||||
|
allowed_modes=(WorkspaceMode.READ_ONLY,),
|
||||||
|
require_reason=False,
|
||||||
|
max_total_size_bytes=MAX_UPLOAD_BYTES,
|
||||||
|
)
|
||||||
platform_resources = AgentPlatformResources(
|
platform_resources = AgentPlatformResources(
|
||||||
databases=(
|
databases=(
|
||||||
AgentDatabase(
|
AgentDatabase(
|
||||||
@@ -194,7 +202,6 @@ class ContractClockStudioV1(A2AAgent[ContractClockStudioV1Config, PlatformUserAu
|
|||||||
persist_contract(tenant, clean_id, clean_title, payload, receipt_id)
|
persist_contract(tenant, clean_id, clean_title, payload, receipt_id)
|
||||||
persist_receipt(tenant, receipt_id, "analyze_contract", clean_id, payload, "ok")
|
persist_receipt(tenant, receipt_id, "analyze_contract", clean_id, payload, "ok")
|
||||||
except Exception:
|
except Exception:
|
||||||
# Fail closed: analysis without durable storage is not a success for this product.
|
|
||||||
return AnalyzeResult(
|
return AnalyzeResult(
|
||||||
ok=False,
|
ok=False,
|
||||||
contract_id=clean_id,
|
contract_id=clean_id,
|
||||||
@@ -262,20 +269,16 @@ class ContractClockStudioV1(A2AAgent[ContractClockStudioV1Config, PlatformUserAu
|
|||||||
raise ValidationFailure("file_too_large", "Upload is larger than 256 KiB.")
|
raise ValidationFailure("file_too_large", "Upload is larger than 256 KiB.")
|
||||||
if document.media_type not in ALLOWED_MEDIA_TYPES:
|
if document.media_type not in ALLOWED_MEDIA_TYPES:
|
||||||
raise ValidationFailure("unsupported_media_type", "Only plain text uploads are supported.")
|
raise ValidationFailure("unsupported_media_type", "Only plain text uploads are supported.")
|
||||||
grant = await ctx.workspace.request_access(
|
await ctx.workspace.request_access(
|
||||||
files=[document.path],
|
files=[document.path],
|
||||||
mode=a2a.WorkspaceMode.READ_ONLY,
|
mode=WorkspaceMode.READ_ONLY,
|
||||||
reason="Read the caller-uploaded contract text for deadline extraction.",
|
reason="Read the caller-uploaded contract text for deadline extraction.",
|
||||||
purpose="contract upload analysis",
|
purpose="contract upload analysis",
|
||||||
)
|
)
|
||||||
view = await ctx.workspace.open_view(
|
read_bytes = getattr(ctx.workspace, "read_bytes", None)
|
||||||
purpose="contract upload analysis",
|
if read_bytes is None:
|
||||||
hints=[document.path],
|
raise RuntimeError("workspace backend does not expose direct file reads")
|
||||||
max_files=1,
|
raw = read_bytes(document.path)
|
||||||
mode=a2a.WorkspaceMode.READ_ONLY,
|
|
||||||
reason=f"Read uploaded file under grant {grant.grant_id}.",
|
|
||||||
)
|
|
||||||
raw = await view.read(document.path)
|
|
||||||
if len(raw) > MAX_UPLOAD_BYTES:
|
if len(raw) > MAX_UPLOAD_BYTES:
|
||||||
raise ValidationFailure("file_too_large", "Upload is larger than 256 KiB.")
|
raise ValidationFailure("file_too_large", "Upload is larger than 256 KiB.")
|
||||||
text = raw.decode("utf-8")
|
text = raw.decode("utf-8")
|
||||||
@@ -406,15 +409,14 @@ def extract_timeline(text: str) -> ExtractedTimeline:
|
|||||||
warnings: list[str] = []
|
warnings: list[str] = []
|
||||||
renewal_dates = []
|
renewal_dates = []
|
||||||
for parsed, excerpt in dates:
|
for parsed, excerpt in dates:
|
||||||
if RENEWAL_RE.search(excerpt) or RENEWAL_RE.search(text[max(0, text.find(parsed.isoformat()) - 160): text.find(parsed.isoformat()) + 160]):
|
date_token = parsed.isoformat()
|
||||||
|
date_index = text.find(date_token)
|
||||||
|
local_context = text[max(0, date_index - 160): date_index + 160] if date_index >= 0 else excerpt
|
||||||
|
if RENEWAL_RE.search(excerpt) or RENEWAL_RE.search(local_context):
|
||||||
renewal_dates.append(parsed)
|
renewal_dates.append(parsed)
|
||||||
deadlines.append(
|
deadlines.append(Deadline(kind="renewal", date=date_token, summary="Automatic renewal date explicitly stated in contract.", source_text=excerpt))
|
||||||
Deadline(kind="renewal", date=parsed.isoformat(), summary="Automatic renewal date explicitly stated in contract.", source_text=excerpt)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
deadlines.append(
|
deadlines.append(Deadline(kind="obligation", date=date_token, summary="Explicit dated obligation stated in contract.", source_text=excerpt))
|
||||||
Deadline(kind="obligation", date=parsed.isoformat(), summary="Explicit dated obligation stated in contract.", source_text=excerpt)
|
|
||||||
)
|
|
||||||
|
|
||||||
notice_match = NOTICE_RE.search(text)
|
notice_match = NOTICE_RE.search(text)
|
||||||
if notice_match:
|
if notice_match:
|
||||||
@@ -434,7 +436,6 @@ def extract_timeline(text: str) -> ExtractedTimeline:
|
|||||||
elif RENEWAL_RE.search(text):
|
elif RENEWAL_RE.search(text):
|
||||||
warnings.append("No explicit notice-window duration was found for the renewal date.")
|
warnings.append("No explicit notice-window duration was found for the renewal date.")
|
||||||
|
|
||||||
# Acceptance and UI prefer renewal before notice before other dated obligations.
|
|
||||||
order = {"renewal": 0, "notice": 1, "obligation": 2}
|
order = {"renewal": 0, "notice": 1, "obligation": 2}
|
||||||
deduped: dict[tuple[str, str], Deadline] = {}
|
deduped: dict[tuple[str, str], Deadline] = {}
|
||||||
for deadline in sorted(deadlines, key=lambda item: (order[item.kind], item.date, item.summary)):
|
for deadline in sorted(deadlines, key=lambda item: (order[item.kind], item.date, item.summary)):
|
||||||
|
|||||||
Reference in New Issue
Block a user