code editor: Agent Studio improvement iteration 2 for invoice-guard-studio-v1. Goal
This commit is contained in:
30
agent.py
30
agent.py
@@ -44,6 +44,7 @@ MAX_INVOICES = 50
|
||||
MAX_UPLOADS = 5
|
||||
MAX_UPLOAD_BYTES = 256 * 1024
|
||||
MAX_TEXT_FIELD_LENGTH = 180
|
||||
AGENT_VERSION = "0.1.3"
|
||||
ACCEPTED_MEDIA_TYPES = {"application/json", "text/json", "text/csv", "text/plain"}
|
||||
DB_CONNECT_OPTIONS = "-c statement_timeout=5000 -c lock_timeout=3000 -c idle_in_transaction_session_timeout=10000"
|
||||
|
||||
@@ -81,7 +82,7 @@ class InvoiceGuardStudioV1(A2AAgent[InvoiceGuardStudioV1Config, PlatformUserAuth
|
||||
"One-page InvoiceGuard app for deterministic duplicate invoice number "
|
||||
"and stated-total mismatch review with user-scoped Postgres persistence."
|
||||
)
|
||||
version = "0.1.2"
|
||||
version = AGENT_VERSION
|
||||
|
||||
config_model = InvoiceGuardStudioV1Config
|
||||
auth_model = PlatformUserAuth
|
||||
@@ -124,7 +125,7 @@ class InvoiceGuardStudioV1(A2AAgent[InvoiceGuardStudioV1Config, PlatformUserAuth
|
||||
str,
|
||||
Field(min_length=1, max_length=120, pattern=r"^[A-Za-z0-9_.:-]+$"),
|
||||
],
|
||||
invoices: Annotated[list[dict[str, Any]], Field(min_length=1, max_length=MAX_INVOICES)],
|
||||
invoices: Annotated[list[InvoiceInput], Field(min_length=1, max_length=MAX_INVOICES)],
|
||||
) -> dict[str, Any]:
|
||||
tenant = _tenant_key(ctx)
|
||||
result = _review_payload(case_id, invoices)
|
||||
@@ -300,7 +301,7 @@ def _money_json(value: Decimal) -> float:
|
||||
return float(value.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP))
|
||||
|
||||
|
||||
def _review_payload(case_id: str, raw_invoices: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
def _review_payload(case_id: str, raw_invoices: list[InvoiceInput | dict[str, Any]]) -> dict[str, Any]:
|
||||
try:
|
||||
clean_case_id = _clean_case_id(case_id)
|
||||
except ValueError as exc:
|
||||
@@ -311,16 +312,29 @@ def _review_payload(case_id: str, raw_invoices: list[dict[str, Any]]) -> dict[st
|
||||
return _validation_error("too_many_invoices", f"At most {MAX_INVOICES} invoices are allowed per review.", case_id=clean_case_id)
|
||||
|
||||
invoices: list[dict[str, Any]] = []
|
||||
for index, item in enumerate(raw_invoices):
|
||||
if not isinstance(item, dict):
|
||||
for index, raw_item in enumerate(raw_invoices):
|
||||
if isinstance(raw_item, InvoiceInput):
|
||||
item = raw_item.model_dump()
|
||||
elif isinstance(raw_item, dict):
|
||||
try:
|
||||
item = InvoiceInput.model_validate(raw_item).model_dump()
|
||||
except ValidationError:
|
||||
item = raw_item
|
||||
else:
|
||||
return _validation_error("invalid_invoice", f"Invoice at index {index} must be an object.", case_id=clean_case_id)
|
||||
missing = [name for name in ("invoice_number", "vendor", "subtotal", "tax", "total") if item.get(name) in (None, "")]
|
||||
invoice_number = str(item.get("invoice_number", "")).strip()
|
||||
vendor = str(item.get("vendor", "")).strip()
|
||||
if not invoice_number or not vendor:
|
||||
missing.extend(name for name, value in (("invoice_number", invoice_number), ("vendor", vendor)) if not value and name not in missing)
|
||||
if missing:
|
||||
return _validation_error(
|
||||
"incomplete_invoice",
|
||||
f"Invoice at index {index} is missing required field(s): {', '.join(missing)}.",
|
||||
case_id=clean_case_id,
|
||||
)
|
||||
if len(invoice_number) > 120 or len(vendor) > MAX_TEXT_FIELD_LENGTH:
|
||||
return _validation_error("invalid_invoice", f"Invoice at index {index} exceeds text field length limits.", case_id=clean_case_id)
|
||||
subtotal = _money(item.get("subtotal"))
|
||||
tax = _money(item.get("tax"))
|
||||
total = _money(item.get("total"))
|
||||
@@ -329,8 +343,8 @@ def _review_payload(case_id: str, raw_invoices: list[dict[str, Any]]) -> dict[st
|
||||
invoices.append(
|
||||
{
|
||||
"index": index,
|
||||
"invoice_number": str(item["invoice_number"]).strip(),
|
||||
"vendor": str(item["vendor"]).strip(),
|
||||
"invoice_number": invoice_number,
|
||||
"vendor": vendor,
|
||||
"subtotal": _money_json(subtotal),
|
||||
"tax": _money_json(tax),
|
||||
"total": _money_json(total),
|
||||
@@ -473,7 +487,7 @@ def _receipt_for(tenant: str, case: dict[str, Any], source: str, saved_at: str)
|
||||
"receipt_id": f"invoice_guard:{case['case_id']}:{digest[:16]}",
|
||||
"kind": "production_execution_receipt",
|
||||
"agent": "invoice-guard-studio-v1",
|
||||
"agent_version": "0.1.1",
|
||||
"agent_version": AGENT_VERSION,
|
||||
"skill": "review_invoices",
|
||||
"tenant_hash": hashlib.sha256(tenant.encode("utf-8")).hexdigest()[:16],
|
||||
"input_hash": digest,
|
||||
|
||||
@@ -76,6 +76,15 @@ def test_incomplete_invoice_failure_fixture():
|
||||
assert result["code"] == "incomplete_invoice"
|
||||
assert "vendor" in result["message"] and "total" in result["message"]
|
||||
|
||||
blank = _review_payload("studio-invoice-invalid", [{"invoice_number": " ", "vendor": " ", "subtotal": 10, "tax": 1, "total": 11}])
|
||||
assert blank["ok"] is False
|
||||
assert blank["code"] == "incomplete_invoice"
|
||||
assert "invoice_number" in blank["message"] and "vendor" in blank["message"]
|
||||
|
||||
oversized = _review_payload("studio-invoice-invalid", [{"invoice_number": "I" * 121, "vendor": "Acme", "subtotal": 10, "tax": 1, "total": 11}])
|
||||
assert oversized["ok"] is False
|
||||
assert oversized["code"] == "invalid_invoice"
|
||||
|
||||
|
||||
def test_browser_upload_bridge_bounds_and_parses_json():
|
||||
payload = json.dumps({"invoices": SUCCESS_INVOICES}).encode()
|
||||
|
||||
Reference in New Issue
Block a user