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_UPLOADS = 5
|
||||||
MAX_UPLOAD_BYTES = 256 * 1024
|
MAX_UPLOAD_BYTES = 256 * 1024
|
||||||
MAX_TEXT_FIELD_LENGTH = 180
|
MAX_TEXT_FIELD_LENGTH = 180
|
||||||
|
AGENT_VERSION = "0.1.3"
|
||||||
ACCEPTED_MEDIA_TYPES = {"application/json", "text/json", "text/csv", "text/plain"}
|
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"
|
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 "
|
"One-page InvoiceGuard app for deterministic duplicate invoice number "
|
||||||
"and stated-total mismatch review with user-scoped Postgres persistence."
|
"and stated-total mismatch review with user-scoped Postgres persistence."
|
||||||
)
|
)
|
||||||
version = "0.1.2"
|
version = AGENT_VERSION
|
||||||
|
|
||||||
config_model = InvoiceGuardStudioV1Config
|
config_model = InvoiceGuardStudioV1Config
|
||||||
auth_model = PlatformUserAuth
|
auth_model = PlatformUserAuth
|
||||||
@@ -124,7 +125,7 @@ class InvoiceGuardStudioV1(A2AAgent[InvoiceGuardStudioV1Config, PlatformUserAuth
|
|||||||
str,
|
str,
|
||||||
Field(min_length=1, max_length=120, pattern=r"^[A-Za-z0-9_.:-]+$"),
|
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]:
|
) -> dict[str, Any]:
|
||||||
tenant = _tenant_key(ctx)
|
tenant = _tenant_key(ctx)
|
||||||
result = _review_payload(case_id, invoices)
|
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))
|
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:
|
try:
|
||||||
clean_case_id = _clean_case_id(case_id)
|
clean_case_id = _clean_case_id(case_id)
|
||||||
except ValueError as exc:
|
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)
|
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]] = []
|
invoices: list[dict[str, Any]] = []
|
||||||
for index, item in enumerate(raw_invoices):
|
for index, raw_item in enumerate(raw_invoices):
|
||||||
if not isinstance(item, dict):
|
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)
|
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, "")]
|
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:
|
if missing:
|
||||||
return _validation_error(
|
return _validation_error(
|
||||||
"incomplete_invoice",
|
"incomplete_invoice",
|
||||||
f"Invoice at index {index} is missing required field(s): {', '.join(missing)}.",
|
f"Invoice at index {index} is missing required field(s): {', '.join(missing)}.",
|
||||||
case_id=clean_case_id,
|
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"))
|
subtotal = _money(item.get("subtotal"))
|
||||||
tax = _money(item.get("tax"))
|
tax = _money(item.get("tax"))
|
||||||
total = _money(item.get("total"))
|
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(
|
invoices.append(
|
||||||
{
|
{
|
||||||
"index": index,
|
"index": index,
|
||||||
"invoice_number": str(item["invoice_number"]).strip(),
|
"invoice_number": invoice_number,
|
||||||
"vendor": str(item["vendor"]).strip(),
|
"vendor": vendor,
|
||||||
"subtotal": _money_json(subtotal),
|
"subtotal": _money_json(subtotal),
|
||||||
"tax": _money_json(tax),
|
"tax": _money_json(tax),
|
||||||
"total": _money_json(total),
|
"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]}",
|
"receipt_id": f"invoice_guard:{case['case_id']}:{digest[:16]}",
|
||||||
"kind": "production_execution_receipt",
|
"kind": "production_execution_receipt",
|
||||||
"agent": "invoice-guard-studio-v1",
|
"agent": "invoice-guard-studio-v1",
|
||||||
"agent_version": "0.1.1",
|
"agent_version": AGENT_VERSION,
|
||||||
"skill": "review_invoices",
|
"skill": "review_invoices",
|
||||||
"tenant_hash": hashlib.sha256(tenant.encode("utf-8")).hexdigest()[:16],
|
"tenant_hash": hashlib.sha256(tenant.encode("utf-8")).hexdigest()[:16],
|
||||||
"input_hash": digest,
|
"input_hash": digest,
|
||||||
|
|||||||
@@ -76,6 +76,15 @@ def test_incomplete_invoice_failure_fixture():
|
|||||||
assert result["code"] == "incomplete_invoice"
|
assert result["code"] == "incomplete_invoice"
|
||||||
assert "vendor" in result["message"] and "total" in result["message"]
|
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():
|
def test_browser_upload_bridge_bounds_and_parses_json():
|
||||||
payload = json.dumps({"invoices": SUCCESS_INVOICES}).encode()
|
payload = json.dumps({"invoices": SUCCESS_INVOICES}).encode()
|
||||||
|
|||||||
Reference in New Issue
Block a user