feat(financial-advisor): make recipe deployable for ADC integration - #2562
zeroasterisk wants to merge 4 commits into
Conversation
Configure serving infrastructure and container definitions for contrib/python/financial-advisor to facilitate deployment to ADC. Generated via the make-python-recipe-deployable repo skill using prompt: `Make the recipe contrib/python/financial-advisor deployable`
happyhuman
left a comment
There was a problem hiding this comment.
I left a few comments. Please keep in mind the skill cannot guarantee that its updates to the recipe is sufficient for deployability, and a manual verification step is required at the end.
…nd ergonomics - Production container: run as non-root appuser (UID 1000), multi-stage layer caching, graceful shutdown (--timeout-graceful-shutdown 10), /healthz liveness probe, and PYTHONUNBUFFERED=1. - Multi-agent robustness: add semantic descriptions to subagents for valid Gemini tool declarations, inject ADK session state templates, add search failure fallbacks, and enforce out-of-scope domain boundaries. - A2A & serving: fix agent card interface duplication memory leak, extract public generate_agent_card helper, sanitize Reasoning Engine request parsing to HTTP 400, add root .well-known redirect, and harmonize ports to 8080. - Security & governance: scrub leaked corporate email, harden .dockerignore against credential leaks, add indirect prompt injection defenses, and document least-privilege IAM roles. - Ergonomics & onboarding: add 8-topic Troubleshooting & FAQ to README.md, gate cloud OTel to prevent local container startup hangs, and expand hermetic test_runnability.py coverage.
| attach_reasoning_engine_routes, | ||
| ) | ||
|
|
||
| load_dotenv() |
There was a problem hiding this comment.
Avoid calling load_dotenv() in non-__init__ modules. Environment variables should be loaded in the package __init__.py to ensure consistent initialization across all entry points.
| if __name__ == "__main__": | ||
| import uvicorn | ||
|
|
||
| port = int(os.environ.get("PORT", "8080")) |
There was a problem hiding this comment.
Avoid using hardcoded defaults on environment variable reads. These defaults should be moved to .env.example instead; this pattern is present in 6 instances across the recipe.
|
|
||
| @app.post("/api/stream_reasoning_engine") | ||
| async def stream_query(request: Request) -> responses.StreamingResponse: | ||
| try: |
There was a problem hiding this comment.
The request body loading and dictionary validation logic in stream_query (lines 85-101) is duplicated verbatim in the query route (lines 131-147). Consider extracting this validation logic into a shared private helper function to improve maintainability and simplify future schema updates.
| # works. The sync route below draws the same distinction for the | ||
| # `""` and `async` buckets via iscoroutinefunction. | ||
| try: | ||
| stream = method(**kwargs) |
There was a problem hiding this comment.
The stream generator calls 'method(**kwargs)' directly without checking if the resolved method is a coroutine function. If the method is an async coroutine, it returns a coroutine object instead of an iterable, crashing the stream; check inspect.iscoroutinefunction(method) and await it first as done in the query endpoint.
|
|
||
| # 0.3 uses method names that include a '/' like "message/send" | ||
| # 1.0 uses PascalCase like "SendMessage" | ||
| json_body = getattr(request, "_json", {}) or {} |
There was a problem hiding this comment.
Accessing the private '_json' attribute of 'request' is unreliable since it is only populated after 'await request.json()' has been called. If the body hasn't been parsed yet, it will return an empty dict and default the A2A-Version to 1.0 even for 0.3 requests; convert 'build' to an async method and await 'request.json()'.
There was a problem hiding this comment.
Automated Hygiene review — 0 finding(s).
Also, on lines this PR does not change:
contrib/python/financial-advisor/deployment/test_deployment.py:56— The variableproject_idwas deleted from the environment reads, but is still referenced in the subsequent condition. Please restore theproject_idlookup to avoid a NameError.
| agent_version: str | None = None, | ||
| ) -> AgentCard: | ||
| """Generate an A2A AgentCard for an agent conforming to the A2A specification.""" | ||
| resolved_agent_version = agent_version or os.getenv( |
There was a problem hiding this comment.
Avoid specifying hardcoded default values inside environment reads like os.getenv. The default should be defined in the .env.example file.
| body = await request.json() | ||
| except Exception as exc: | ||
| raise HTTPException( | ||
| status_code=400, detail="Invalid JSON in request body" |
There was a problem hiding this comment.
Avoid using bare numeric literals for HTTP status codes. Use appropriate named constants from fastapi.status or http.HTTPStatus instead.
| otel_to_cloud=_otel_to_cloud_enabled(), | ||
| lifespan=lifespan, | ||
| ) | ||
| app.title = "financial-advisor" |
There was a problem hiding this comment.
Avoid hardcoding application metadata like title and description strings. Read these values dynamically from package configuration instead.
| "google-genai (>=1.5.0,<2.0.0)", | ||
| "pydantic (>=2.10.6,<3.0.0)", | ||
| "absl-py (>=2.2.1,<3.0.0)", | ||
| "google-adk[gcp,otel-gcp] (>=1.0.0)", |
There was a problem hiding this comment.
Do not hardcode dependency names and versions in deploy.py. Parse the requirements dynamically from pyproject.toml to avoid duplication.
| RUN uv sync --frozen | ||
|
|
||
| # Grant write permissions for ADK browser runtime config to avoid PermissionError on startup | ||
| RUN chmod -R a+w /code/.venv/lib/python3.11/site-packages/google/adk/cli/browser/assets/config 2>/dev/null || true |
There was a problem hiding this comment.
Avoid granting world-writable permissions (chmod -R a+w) to the package configuration directory. This violates the principle of least privilege and introduces code-tampering or privilege escalation risks within the container.
happyhuman
left a comment
There was a problem hiding this comment.
Please review the comments left by the agent, and also fix the broken test.
Summary
Makes
contrib/python/financial-advisordeployable as a containerized service as a step on the journey toward deployment to Application Design Center (ADC).This was generated and verified using the repo skill
make-python-recipe-deployablevia prompt:Changes
Dockerfile&.dockerignore: Container packaging targeting port 8080 withuvfast_api_app.py: FastAPI server entrypoint wrapping ADKRunnerapp_utils/a2a.py: A2A protocol endpoint implementation (/.well-known/agent-card.jsonand JSON-RPC)app_utils/services.py: In-memory session and artifact service configurationapp_utils/reasoning_engine_adapter.py: Reasoning Engine adapteragents-cli-manifest.yaml: Manifest configuring Cloud Run deployment target inus-central1pyproject.toml&uv.lock: Added serving dependencies (a2a-sdk,aiohttp,gcsfs,opentelemetry-resourcedetector-gcp) andgoogle-adk[gcp,otel-gcp]extrasfinancial_advisor/agent.py: InstantiatedAppobjectmanifest.yaml: Setdeployable: true.env.example: Documented serving and deployment environment variablesVerification
uv lock --python 3.11updated dependencies cleanly.ruff formatandruff checkpassed with zero errors.uv run validate manifestanduv run validate structurepassed.pytest tests/ -q) passed (2/2).TestClient(app)confirmed:/list-apps-> 200