diff --git a/agent.py b/agent.py index 64bd8ec..ff9e198 100644 --- a/agent.py +++ b/agent.py @@ -26,13 +26,15 @@ from a2a_pack import ( AgentDatabaseEnv, AgentDatabaseMigrations, AgentPlatformResources, + FileUpload, PlatformUserAuth, Pricing, Resources, RunContext, State, UploadedFile, - FileUpload, + WorkspaceAccess, + WorkspaceMode, ) MAX_TEXT_CHARS = 200_000 @@ -133,7 +135,7 @@ class ContractClockStudioV1(A2AAgent[ContractClockStudioV1Config, PlatformUserAu config_model = ContractClockStudioV1Config 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( price_per_call_usd=0.0, caller_pays_llm=False, @@ -142,6 +144,12 @@ class ContractClockStudioV1(A2AAgent[ContractClockStudioV1Config, PlatformUserAu resources = Resources(cpu="500m", memory="512Mi", max_runtime_seconds=120) state = State.DURABLE 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( databases=( AgentDatabase( @@ -194,7 +202,6 @@ class ContractClockStudioV1(A2AAgent[ContractClockStudioV1Config, PlatformUserAu persist_contract(tenant, clean_id, clean_title, payload, receipt_id) persist_receipt(tenant, receipt_id, "analyze_contract", clean_id, payload, "ok") except Exception: - # Fail closed: analysis without durable storage is not a success for this product. return AnalyzeResult( ok=False, contract_id=clean_id, @@ -262,20 +269,16 @@ class ContractClockStudioV1(A2AAgent[ContractClockStudioV1Config, PlatformUserAu raise ValidationFailure("file_too_large", "Upload is larger than 256 KiB.") if document.media_type not in ALLOWED_MEDIA_TYPES: 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], - mode=a2a.WorkspaceMode.READ_ONLY, + mode=WorkspaceMode.READ_ONLY, reason="Read the caller-uploaded contract text for deadline extraction.", purpose="contract upload analysis", ) - view = await ctx.workspace.open_view( - purpose="contract upload analysis", - hints=[document.path], - max_files=1, - mode=a2a.WorkspaceMode.READ_ONLY, - reason=f"Read uploaded file under grant {grant.grant_id}.", - ) - raw = await view.read(document.path) + read_bytes = getattr(ctx.workspace, "read_bytes", None) + if read_bytes is None: + raise RuntimeError("workspace backend does not expose direct file reads") + raw = read_bytes(document.path) if len(raw) > MAX_UPLOAD_BYTES: raise ValidationFailure("file_too_large", "Upload is larger than 256 KiB.") text = raw.decode("utf-8") @@ -406,15 +409,14 @@ def extract_timeline(text: str) -> ExtractedTimeline: warnings: list[str] = [] renewal_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) - deadlines.append( - Deadline(kind="renewal", date=parsed.isoformat(), summary="Automatic renewal date explicitly stated in contract.", source_text=excerpt) - ) + deadlines.append(Deadline(kind="renewal", date=date_token, summary="Automatic renewal date explicitly stated in contract.", source_text=excerpt)) else: - deadlines.append( - Deadline(kind="obligation", date=parsed.isoformat(), summary="Explicit dated obligation stated in contract.", source_text=excerpt) - ) + deadlines.append(Deadline(kind="obligation", date=date_token, summary="Explicit dated obligation stated in contract.", source_text=excerpt)) notice_match = NOTICE_RE.search(text) if notice_match: @@ -434,7 +436,6 @@ def extract_timeline(text: str) -> ExtractedTimeline: elif RENEWAL_RE.search(text): 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} deduped: dict[tuple[str, str], Deadline] = {} for deadline in sorted(deadlines, key=lambda item: (order[item.kind], item.date, item.summary)):