a2a-source-edit: write agent.py

This commit is contained in:
a2a-cloud
2026-07-18 07:14:23 +00:00
parent 7d5c62a07e
commit 76d8506d38

View File

@@ -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)):