diff --git a/agent.py b/agent.py index 3feaf95..9afac87 100644 --- a/agent.py +++ b/agent.py @@ -134,19 +134,8 @@ class QuoteJudgeStudioV1(A2AAgent[QuoteJudgeStudioV1Config, PlatformUserAuth]): ) ) - @a2a.tool( - description="Compare two or more vendor quotes, persist the result for this platform user, and return the recommendation.", - timeout_seconds=30, - idempotent=True, - cost_class="deterministic", - ) - async def compare_quotes( - self, - ctx: RunContext[PlatformUserAuth], - comparison_id: str, - quotes: list[QuoteInput], - weights: QuoteWeights, - ) -> dict[str, Any]: + @a2a.tool(description="Compare two or more vendor quotes, persist the result for this platform user, and return the recommendation.", timeout_seconds=30, idempotent=True, cost_class="deterministic") + async def compare_quotes(self, ctx: RunContext[PlatformUserAuth], comparison_id: str, quotes: list[QuoteInput], weights: QuoteWeights) -> dict[str, Any]: validation = _validate_request(comparison_id, quotes, weights) if validation is not None: await _persist_receipt_best_effort(ctx, "compare_quotes", comparison_id, "error", validation) @@ -158,22 +147,13 @@ class QuoteJudgeStudioV1(A2AAgent[QuoteJudgeStudioV1Config, PlatformUserAuth]): await _persist_receipt_best_effort(ctx, "compare_quotes", comparison_id, "ok", result) except RuntimeError as exc: return _setup_required(comparison_id, str(exc)) - except Exception: # noqa: BLE001 + except Exception: return _temporary_db_error(comparison_id) await ctx.emit_progress(f"saved comparison {comparison_id}") return result - @a2a.tool( - description="Reload a saved quote comparison for the current signed-in platform user.", - timeout_seconds=20, - idempotent=True, - cost_class="deterministic", - ) - async def get_comparison( - self, - ctx: RunContext[PlatformUserAuth], - comparison_id: str, - ) -> dict[str, Any]: + @a2a.tool(description="Reload a saved quote comparison for the current signed-in platform user.", timeout_seconds=20, idempotent=True, cost_class="deterministic") + async def get_comparison(self, ctx: RunContext[PlatformUserAuth], comparison_id: str) -> dict[str, Any]: tenant = _tenant_key(ctx) clean_id = _clean_comparison_id(comparison_id) if clean_id is None: @@ -183,76 +163,42 @@ class QuoteJudgeStudioV1(A2AAgent[QuoteJudgeStudioV1Config, PlatformUserAuth]): await _persist_receipt_best_effort(ctx, "get_comparison", clean_id, "ok", record or {"ok": False}) except RuntimeError as exc: return _setup_required(clean_id, str(exc)) - except Exception: # noqa: BLE001 + except Exception: return _temporary_db_error(clean_id) if record is None: return {"ok": False, "code": "comparison_not_found", "comparison_id": clean_id, "message": "No saved comparison exists for this user and id."} return record - @a2a.tool( - description="Compare quotes from an external client FileUpload (CSV, JSON, or plain text) and persist the result.", - timeout_seconds=30, - idempotent=True, - cost_class="deterministic", - ) + @a2a.tool(description="Compare quotes from an external client FileUpload (CSV, JSON, or plain text) and persist the result.", timeout_seconds=30, idempotent=True, cost_class="deterministic") async def compare_quotes_file( self, ctx: RunContext[PlatformUserAuth], comparison_id: str, weights: QuoteWeights, - document: Annotated[ - UploadedFile, - FileUpload( - accept=ALLOWED_UPLOAD_MEDIA_TYPES, - max_bytes=MAX_UPLOAD_BYTES, - description="CSV/JSON/text quote file with vendor, unit_price, quantity, delivery_days, warranty_months.", - ), - ], + document: Annotated[UploadedFile, FileUpload(accept=ALLOWED_UPLOAD_MEDIA_TYPES, max_bytes=MAX_UPLOAD_BYTES, description="CSV/JSON/text quote file with vendor, unit_price, quantity, delivery_days, warranty_months.")], ) -> dict[str, Any]: try: payload = ctx.workspace.read_bytes(document.path) # type: ignore[attr-defined] - except Exception: # noqa: BLE001 + except Exception: return {"ok": False, "code": "upload_read_failed", "comparison_id": comparison_id, "message": "Could not read the uploaded file from the invocation workspace."} parsed = _parse_uploaded_quotes(document.filename, document.media_type, payload) if not parsed["ok"]: return {**parsed, "comparison_id": comparison_id} return await self.compare_quotes(ctx, comparison_id, parsed["quotes"], weights) - @a2a.tool( - description="Browser JSON bridge: accept one bounded base64 quote file, parse it, compare quotes, and persist the result.", - timeout_seconds=30, - idempotent=True, - cost_class="deterministic", - ) - async def compare_quotes_upload( - self, - ctx: RunContext[PlatformUserAuth], - comparison_id: str, - upload: BrowserQuoteDocument, - weights: QuoteWeights, - ) -> dict[str, Any]: + @a2a.tool(description="Browser JSON bridge: accept one bounded base64 quote file, parse it, compare quotes, and persist the result.", timeout_seconds=30, idempotent=True, cost_class="deterministic") + async def compare_quotes_upload(self, ctx: RunContext[PlatformUserAuth], comparison_id: str, upload: BrowserQuoteDocument, weights: QuoteWeights) -> dict[str, Any]: return await self.compare_quotes_browser_upload(ctx, comparison_id, weights, [upload]) - @a2a.tool( - description="Browser JSON bridge: accept bounded base64 quote files, parse them, compare quotes, and persist the result.", - timeout_seconds=30, - idempotent=True, - cost_class="deterministic", - ) - async def compare_quotes_browser_upload( - self, - ctx: RunContext[PlatformUserAuth], - comparison_id: str, - weights: QuoteWeights, - documents: list[BrowserQuoteDocument], - ) -> dict[str, Any]: + @a2a.tool(description="Browser JSON bridge: accept bounded base64 quote files, parse them, compare quotes, and persist the result.", timeout_seconds=30, idempotent=True, cost_class="deterministic") + async def compare_quotes_browser_upload(self, ctx: RunContext[PlatformUserAuth], comparison_id: str, weights: QuoteWeights, documents: list[BrowserQuoteDocument]) -> dict[str, Any]: if not documents or len(documents) > MAX_BROWSER_UPLOADS: return {"ok": False, "code": "invalid_upload_count", "comparison_id": comparison_id, "message": f"Upload 1 to {MAX_BROWSER_UPLOADS} files."} all_quotes: list[QuoteInput] = [] for document in documents: try: data = base64.b64decode(document.data_base64, validate=True) - except Exception: # noqa: BLE001 + except Exception: return {"ok": False, "code": "invalid_base64_upload", "comparison_id": comparison_id, "message": f"{document.filename} is not valid base64."} if len(data) > MAX_UPLOAD_BYTES: return {"ok": False, "code": "upload_too_large", "comparison_id": comparison_id, "message": f"{document.filename} exceeds {MAX_UPLOAD_BYTES} bytes."} @@ -308,7 +254,7 @@ def _score_quotes(comparison_id: str, quotes: list[QuoteInput], weights: QuoteWe "weighted_score": rounded_score, "score_breakdown": {"price": _round(price_score * 100), "delivery": _round(delivery_score * 100), "warranty": _round(warranty_score * 100)}, }) - ranked = sorted(scored, key=lambda row: (-row["score"], row["total_price"], row["delivery_days"], -row["warranty_months"], row["vendor"].lower())) + ranked = sorted(scored, key=_quote_sort_key) winner = ranked[0] result = { "ok": True, @@ -334,6 +280,10 @@ def _score_quotes(comparison_id: str, quotes: list[QuoteInput], weights: QuoteWe return result +def _quote_sort_key(row: dict[str, Any]) -> tuple[Any, ...]: + return (-row["score"], row["total_price"], row["delivery_days"], -row["warranty_months"], row["vendor"].lower()) + + def _money(value: Any) -> float: return float(Decimal(str(value)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)) @@ -364,7 +314,7 @@ def _parse_uploaded_quotes(filename: str, media_type: str, data: bytes) -> dict[ quotes = [QuoteInput.model_validate(row) for row in rows] else: quotes = _parse_csv_or_text(text) - except Exception as exc: # noqa: BLE001 + except Exception as exc: return {"ok": False, "code": "quote_parse_failed", "message": f"Could not parse quote upload: {str(exc)[:160]}"} return {"ok": True, "quotes": quotes} @@ -376,7 +326,7 @@ def _parse_csv_or_text(text: str) -> list[QuoteInput]: reader = csv.DictReader(io.StringIO(sample)) required = {"vendor", "unit_price", "quantity", "delivery_days", "warranty_months"} if reader.fieldnames and required.issubset({name.strip() for name in reader.fieldnames}): - return [QuoteInput.model_validate({k.strip(): v for k, v in row.items()}) for row in reader] + return [QuoteInput.model_validate({key.strip(): value for key, value in csv_row.items()}) for csv_row in reader] raise ValueError("CSV must include vendor, unit_price, quantity, delivery_days, warranty_months columns") @@ -399,12 +349,7 @@ def _connect(): import psycopg from psycopg.rows import dict_row - return psycopg.connect( - _database_url(), - connect_timeout=DB_CONNECT_TIMEOUT_SECONDS, - row_factory=dict_row, - options=f"-c statement_timeout={DB_STATEMENT_TIMEOUT_MS} -c lock_timeout={DB_LOCK_TIMEOUT_MS}", - ) + return psycopg.connect(_database_url(), connect_timeout=DB_CONNECT_TIMEOUT_SECONDS, row_factory=dict_row, options=f"-c statement_timeout={DB_STATEMENT_TIMEOUT_MS} -c lock_timeout={DB_LOCK_TIMEOUT_MS}") async def _save_comparison(tenant_key: str, result: dict[str, Any]) -> None: @@ -440,19 +385,11 @@ async def _load_comparison(tenant_key: str, comparison_id: str) -> dict[str, Any def _load_comparison_sync(tenant_key: str, comparison_id: str) -> dict[str, Any] | None: with _connect() as conn: with conn.cursor() as cur: - cur.execute( - """ - SELECT result_json - FROM quote_judge_comparisons - WHERE tenant_key = %s AND comparison_id = %s - LIMIT 1 - """, - (tenant_key, comparison_id), - ) - row = cur.fetchone() - if not row: + cur.execute("""SELECT result_json FROM quote_judge_comparisons WHERE tenant_key = %s AND comparison_id = %s LIMIT 1""", (tenant_key, comparison_id)) + db_row = cur.fetchone() + if not db_row: return None - payload = row["result_json"] + payload = db_row["result_json"] return json.loads(payload) if isinstance(payload, str) else dict(payload) @@ -462,7 +399,7 @@ async def _persist_receipt_best_effort(ctx: RunContext[PlatformUserAuth], skill_ tenant = _tenant_key(ctx) receipt = _receipt_payload(skill_name, comparison_id, payload) await asyncio.to_thread(_persist_receipt_sync, tenant, comparison_id, skill_name, status, getattr(ctx, "task_id", "") or "", receipt) - except Exception: # noqa: BLE001 + except Exception: return