Updated April 15, 2026 — Migrated to A2A Specification v1
The agent card published by all agents in this repo has been updated to comply with the A2A v1 specification as required by Prompt Opinion. The following changes were made to
shared/app_factory.py:
What changed Detail urlremoved from agent cardDeprecated in v1. The agent's endpoint URL is now expressed via supportedInterfaces(see below).preferredTransportremovedDeprecated in v1. Preference order is now implicit — the first entry in supportedInterfacesis the preferred transport.supportedInterfacesaddedNew field. Each entry contains url,protocolBinding, andprotocolVersion. Replacesurl+preferredTransport.capabilities.stateTransitionHistoryset tofalseNo longer supported by Prompt Opinion. Field is retained in the schema but must be false.securitySchemesschema updatedEach scheme is now nested under a typed key (e.g. apiKeySecurityScheme) per the v1 JSON shape.The
urlparameter passed tocreate_a2a_app()in each agent'sapp.pyis unchanged — it is still read from environment variables (HEALTHCARE_AGENT_URL,GENERAL_AGENT_URL,ORCHESTRATOR_URL) and is now placed insidesupportedInterfacesinstead of the top-levelurlfield.
Runnable examples showing how to build external agents that connect to Prompt Opinion — the multi-agent platform for healthcare and enterprise workflows.
This is not a single-file template. It is a monorepo with three working agents that share a common infrastructure library. Clone it, run adk web . to see all three agents in a browser UI, then copy whichever example matches your use case and customise from there.
- What's in this repo
- Architecture
- Quick start
- The three agents
- The shared library
- Adding tools
- FHIR context (optional)
- Configuration reference
- API security
- Testing locally
- Running with Docker (local)
- Deploying to Google Cloud Run
- Connecting to Prompt Opinion
| Agent | Description | FHIR? | Port |
|---|---|---|---|
healthcare_agent |
Queries a patient's FHIR R4 record — demographics, meds, conditions, observations | ✅ Yes | 8001 |
general_agent |
Date/time queries and ICD-10-CM code lookups — no patient data needed | ❌ No | 8002 |
orchestrator |
Delegates to the other two agents using ADK's built-in sub-agent routing | ✅ Optional | 8003 |
All three share a shared/ library that provides middleware, logging, the FHIR context hook, FHIR R4 tools, and an app factory — so each agent's own files stay small and focused.
Prompt Opinion
│ POST / X-API-Key A2A JSON-RPC
│
▼
┌──────────────────────────────────────────────────┐
│ shared/middleware.py (ApiKeyMiddleware) │
│ · validates X-API-Key │
│ · bridges FHIR metadata to params.metadata │
└──────────────┬───────────────────────────────────┘
│
┌───────────┼───────────┐
▼ ▼ ▼
healthcare_ general_ orchestrator
agent agent │
│ │ delegates
│ │ via AgentTool
▼ ▼ │
shared/ local ├──► healthcare_agent
fhir_hook tools/ └──► general_agent
│ general.py
▼
session state
(fhir_url, fhir_token, patient_id)
│
▼
shared/tools/fhir.py ──► FHIR R4 server
Key design principle: FHIR credentials travel in the A2A message metadata — they never appear in the LLM prompt. The extract_fhir_context callback intercepts them before the model is called and stores them in session state, where tools read them at call time.
This repo targets A2A specification v1, which is the version required by the Prompt Opinion platform.
The Python a2a-sdk library (latest: 0.3.x) has not yet been updated to reflect the v1 schema changes. To bridge the gap, shared/app_factory.py includes a pair of thin forward-compatibility subclasses — AgentCardV1 and AgentExtensionV1 — that patch three fields the library does not yet expose:
| Field | Change in v1 | How it's handled |
|---|---|---|
supportedInterfaces |
New — replaces url + preferredTransport |
Added as an explicit Pydantic field on AgentCardV1 |
securitySchemes |
Schema changed to nested typed-key format (e.g. apiKeySecurityScheme) |
Overridden to dict[str, Any] on AgentCardV1 so the v1 JSON shape passes through unmodified |
params on extensions |
New — carries SMART scope declarations | Added as an explicit Pydantic field on AgentExtensionV1 |
The top-level url field is still passed to satisfy the current library's Pydantic validation; the same URL is also placed in supportedInterfaces for v1 compliance.
These shims will be removed once a2a-sdk ships native v1 support. Watch for a ⚠ BREAKING CHANGES entry in the a2a-python changelog mentioning AgentCard or spec version. At that point:
- Delete
AgentExtensionV1andAgentCardV1fromapp_factory.py - Remove
url=urlfrom theAgentCardV1(...)constructor call - Restore the typed
SecuritySchemeconstructors if the new library types serialise correctly
Everything else in the repo — agent logic, FHIR tools, middleware, Docker, Cloud Run — is unaffected by this compatibility layer.
- Python 3.11 or later
- An API key for your chosen model provider (Google AI Studio, OpenAI, or Anthropic)
- Git
git clone https://github.com/your-org/prompt-opinion-adk-python.git
cd prompt-opinion-adk-pythonpython -m venv .venv
# macOS / Linux
source .venv/bin/activate
# Windows (Command Prompt)
.venv\Scripts\activate
pip install -r requirements.txt# macOS / Linux
cp .env.example .env
# Windows (Command Prompt)
copy .env.example .envOpen .env and set the API key for your chosen model provider:
# Gemini via Google AI Studio (default)
GOOGLE_API_KEY=your-google-api-key-here
# Or OpenAI
# OPENAI_API_KEY=your-openai-key-here
# HEALTHCARE_AGENT_MODEL=openai/gpt-4o-mini
# Or Anthropic
# ANTHROPIC_API_KEY=your-anthropic-key-here
# HEALTHCARE_AGENT_MODEL=anthropic/claude-sonnet-4-6Option A — adk web (recommended for local development)
Opens a visual chat UI in your browser. All three agents appear in the dropdown. No API key header required.
adk web .Then open http://localhost:8000 and select which agent to chat with.
Note:
adk webbypasses the A2A middleware, so FHIR tools will report missing credentials (no metadata is sent). Everything else — tool calls, model responses, instructions — works normally. Use this for developing and testing agent logic before wiring to Prompt Opinion.
Option B — A2A servers (required to connect to Prompt Opinion)
All three at once with honcho (recommended):
pip install -r requirements-dev.txt # one-time
honcho startAll three agents start in a single terminal with colour-coded logs — healthcare in one colour, general in another, orchestrator in a third. Ports 8001, 8002, and 8003 are all live simultaneously.
Or start them individually in separate terminals:
# Terminal 1 — FHIR healthcare agent
uvicorn healthcare_agent.app:a2a_app --host 0.0.0.0 --port 8001
# Terminal 2 — General-purpose agent
uvicorn general_agent.app:a2a_app --host 0.0.0.0 --port 8002
# Terminal 3 — Orchestrator (delegates to agents 1 & 2)
uvicorn orchestrator.app:a2a_app --host 0.0.0.0 --port 8003Option C — Docker Compose (no Python install required)
If you have Docker Desktop installed, you can run all three agents in containers without setting up a Python virtual environment at all.
# First run: build the image and start all three agents
docker compose up --build
# Subsequent runs (image already built)
docker compose up
# Stop all agents
docker compose downAgents are available on the same ports as the bare-Python option — localhost:8001, localhost:8002, localhost:8003. See Running with Docker (local) for more detail.
curl http://localhost:8001/.well-known/agent-card.jsonYou should see the agent card JSON describing the agent's capabilities and security requirements.
The most complete example. Receives FHIR credentials from the caller via A2A metadata, extracts them into session state, and uses them to query a FHIR R4 server.
Files to change when building your own:
| File | What to change |
|---|---|
healthcare_agent/agent.py |
Model, instruction, tools list |
healthcare_agent/app.py |
Agent name, description, URL, FHIR extension URI |
shared/tools/fhir.py |
Add or modify FHIR query tools |
shared/middleware.py |
Update VALID_API_KEYS |
Use this as your starting point if your agent needs to query patient data from a FHIR server.
The minimal example. No before_model_callback, no FHIR tools. Demonstrates that the FHIR layer is completely optional.
Includes two tools that work offline with no external APIs:
get_current_datetime(timezone)— current date/time in any IANA timezonelook_up_icd10(term)— ICD-10-CM code lookup from a built-in reference table (15 common conditions)
Files to change when building your own:
| File | What to change |
|---|---|
general_agent/agent.py |
Model, instruction, tools list |
general_agent/app.py |
Agent name, description, URL |
general_agent/tools/general.py |
Replace with your own tools |
Use this as your starting point if your agent does not need patient data (knowledge lookup, scheduling, notifications, etc.).
Shows ADK's built-in sub-agent routing (AgentTool). The model decides which specialist to call based on the question. Both healthcare_agent and general_agent run in-process as sub-agents — no separate HTTP calls needed.
Session state is shared, so FHIR credentials extracted by the orchestrator's before_model_callback are immediately available to the healthcare_agent's tools.
Use this as your starting point if you want a single endpoint that coordinates multiple specialties.
To add a third sub-agent:
- Create a new agent package (copy
general_agentas a template) - Import its
root_agentinorchestrator/agent.py - Add
AgentTool(agent=your_new_agent)to the tools list - Update the instruction
shared/
├── logging_utils.py ANSI-colour logger, configure_logging(package_name)
├── middleware.py API key enforcement + FHIR metadata bridging
├── fhir_hook.py before_model_callback — extracts FHIR credentials into state
├── app_factory.py create_a2a_app() — builds the A2A ASGI app for any agent
└── tools/
├── __init__.py Re-exports all shared tools
└── fhir.py FHIR R4 query tools (demographics, meds, conditions, observations)
Think of shared/ as a class library. Any agent can import from it:
from shared.fhir_hook import extract_fhir_context
from shared.tools import get_patient_demographics
from shared.app_factory import create_a2a_appshared/ is never run directly — it has no agent.py or app.py.
Step 1 — Write the tool function (last param must be tool_context: ToolContext):
# general_agent/tools/general.py
from google.adk.tools import ToolContext
import logging
logger = logging.getLogger(__name__)
def get_care_team(tool_context: ToolContext) -> dict:
"""Returns the patient's care team members."""
patient_id = tool_context.state.get("patient_id", "unknown")
logger.info("tool_get_care_team patient_id=%s", patient_id)
# your implementation here
return {"status": "success", "care_team": [...]}Step 2 — Export it from the tools __init__.py:
from .general import get_current_datetime, look_up_icd10, get_care_team
__all__ = [..., "get_care_team"]Step 3 — Register it in agent.py:
from .tools import get_current_datetime, look_up_icd10, get_care_team
root_agent = Agent(..., tools=[..., get_care_team])Add it to shared/tools/fhir.py, export from shared/tools/__init__.py, then import it in any agent that needs it.
FHIR context is completely optional. Agents that don't need it simply omit before_model_callback — general_agent is the example.
A2A request
└── params.message.metadata
└── "http://.../fhir-context": { fhirUrl, fhirToken, patientId }
│
▼ shared/middleware.py bridges to params.metadata
│
▼ extract_fhir_context() runs before every LLM call
│
▼
session state
├── fhir_url → tool_context.state["fhir_url"]
├── fhir_token → tool_context.state["fhir_token"]
└── patient_id → tool_context.state["patient_id"]
{
"jsonrpc": "2.0",
"method": "message/stream",
"params": {
"message": {
"metadata": {
"https://your-workspace.promptopinion.ai/schemas/a2a/v1/fhir-context": {
"fhirUrl": "https://your-fhir-server.example.org/r4",
"fhirToken": "<short-lived-bearer-token>",
"patientId": "patient-uuid"
}
},
"parts": [{ "kind": "text", "text": "What medications is this patient on?" }],
"role": "user"
}
}
}extract_fhir_context writes nothing to session state. FHIR tools return a clear error message explaining that credentials were not provided. The agent passes that back to the caller rather than hallucinating data.
| Log marker | Meaning |
|---|---|
FHIR_URL_FOUND |
FHIR server URL received |
FHIR_TOKEN_FOUND fingerprint=len=N sha256=X |
Token received (value never logged) |
FHIR_PATIENT_FOUND |
Patient ID received |
hook_called_fhir_found |
All three credentials stored in state |
hook_called_no_metadata |
Request had no metadata |
hook_called_fhir_not_found |
Metadata present but FHIR key not found |
hook_called_fhir_malformed |
FHIR key found but value was not a JSON object |
Copy .env.example to .env and set values before starting any server.
| Variable | Required | Default | Description |
|---|---|---|---|
GOOGLE_API_KEY |
If using Gemini | — | Google AI Studio key — required when any agent model is set to gemini/.... Not needed if all agents use OpenAI or Anthropic. |
API_KEYS |
No | — | Comma-separated list of valid X-API-Key values for authenticated agents, e.g. key1,key2 |
API_KEY_PRIMARY |
No | — | First named API key slot for authenticated agents |
API_KEY_SECONDARY |
No | — | Second named API key slot for authenticated agents |
GENERAL_AGENT_MODEL |
No | gemini/gemini-2.5-flash |
Model for general_agent. All models go through LiteLLM — use provider-prefixed format (e.g. openai/gpt-4o-mini, anthropic/claude-sonnet-4-6, vertex_ai/gemini-2.5-flash). |
HEALTHCARE_AGENT_MODEL |
No | gemini/gemini-2.5-flash |
Model for healthcare_agent. Same format as GENERAL_AGENT_MODEL. |
ORCHESTRATOR_MODEL |
No | gemini/gemini-2.5-flash |
Model for orchestrator. Same format as GENERAL_AGENT_MODEL. |
OPENAI_API_KEY |
No | — | Required when any *_MODEL is set to an openai/ model. |
ANTHROPIC_API_KEY |
No | — | Required when any *_MODEL is set to an anthropic/ model. |
VERTEXAI_PROJECT |
No | — | GCP project ID — required when any *_MODEL is set to vertex_ai/.... Run gcloud auth application-default login for credentials. |
VERTEXAI_LOCATION |
No | — | GCP region for Vertex AI (e.g. us-central1). Required alongside VERTEXAI_PROJECT when using vertex_ai/ models. |
BASE_URL |
No | — | If all agents run behind a single tunnel (e.g. ngrok), set this to override all three agent URLs at once. Individual *_URL vars take precedence if set. |
PO_PLATFORM_BASE_URL |
No | http://localhost:5139 |
Base URL of your Prompt Opinion workspace. Used to construct the FHIR extension URI in the agent card for healthcare_agent and orchestrator. Set this to your actual workspace URL (e.g. https://your-workspace.promptopinion.ai). |
LOG_FULL_PAYLOAD |
No | true |
Log full JSON-RPC request body on each request |
LOG_HOOK_RAW_OBJECTS |
No | false |
Dump raw ADK callback objects — debug only |
HEALTHCARE_AGENT_URL |
No | http://localhost:8001 |
Public URL for the healthcare agent. Placed in the agent card's supportedInterfaces so Prompt Opinion knows where to send requests. |
GENERAL_AGENT_URL |
No | http://localhost:8002 |
Public URL for the general agent. Placed in the agent card's supportedInterfaces. |
ORCHESTRATOR_URL |
No | http://localhost:8003 |
Public URL for the orchestrator. Placed in the agent card's supportedInterfaces. |
Each agent independently controls whether it requires an API key.
The setting is declared in the agent's app.py and is automatically advertised in the agent card — so callers like Prompt Opinion discover the security requirement before sending any requests.
| Agent | require_api_key |
Who can call it |
|---|---|---|
healthcare_agent |
True (default) |
Only callers with a valid X-API-Key |
general_agent |
False |
Anyone — no key needed |
orchestrator |
True (default) |
Only callers with a valid X-API-Key |
Open the agent's app.py and set require_api_key:
# healthcare_agent/app.py — authenticated (default)
a2a_app = create_a2a_app(
...
require_api_key=True, # agent card declares X-API-Key required
# ApiKeyMiddleware blocks requests without a valid key
)
# general_agent/app.py — anonymous / public
a2a_app = create_a2a_app(
...
require_api_key=False, # agent card declares no security scheme
# no middleware attached — all requests pass through
)When require_api_key=False, the agent card's security field is empty — this is the standard A2A v1 way to signal "no authentication required" to any caller.
As of A2A v1, securitySchemes uses a nested typed-key format. The factory emits this automatically — you do not need to construct it yourself:
"securitySchemes": {
"apiKey": {
"apiKeySecurityScheme": {
"name": "X-API-Key",
"in": "header",
"description": "API key required to access this agent."
}
}
}This replaced the previous SecurityScheme(root=APIKeySecurityScheme(...)) typed wrapper from the older a2a-sdk API.
Configure one or more valid keys in your environment:
# Either comma-separated:
API_KEYS=my-secret-key-123,another-valid-key
# Or named slots:
API_KEY_PRIMARY=my-secret-key-123
API_KEY_SECONDARY=another-valid-keyThe middleware loads both formats automatically, so you can keep the example multi-key friendly without storing secrets in source control.
In production, populate those environment variables from a secrets manager:
# Example: inject API_KEYS or API_KEY_PRIMARY / API_KEY_SECONDARY
# from Azure Key Vault, AWS Secrets Manager, GCP Secret Manager, etc.| Endpoint | require_api_key=True |
require_api_key=False |
|---|---|---|
GET /.well-known/agent-card.json |
Open (always) | Open (always) |
POST / |
Requires X-API-Key |
Open |
A shell script exercises the full healthcare_agent pipeline with curl:
# Start the healthcare agent first (separate terminal)
uvicorn healthcare_agent.app:a2a_app --host 127.0.0.1 --port 8001 --log-level info
# Run all test cases
bash scripts/test_fhir_hook.sh| Case | Description | Expected log marker |
|---|---|---|
| A | Missing API key | security_rejected_missing_api_key |
| B | Valid key, no metadata | hook_called_no_metadata |
| C | Valid key, wrong metadata key | hook_called_fhir_not_found |
| D | Valid key + FHIR context — clinical summary | hook_called_fhir_found |
| D2 | Valid key + FHIR context — vital signs | tool_get_recent_observations |
| E | Valid key + malformed FHIR value | hook_called_fhir_malformed |
Docker lets you run the agents in containers on your local machine — no Python, no virtual environment, no pip install. You only need Docker Desktop.
This is also useful for testing the exact same image that will run in Google Cloud Run before you deploy.
Your machine (Option B) Your machine (Option C) Google Cloud Run
────────────────────── ────────────────────── ────────────────
Python venv + honcho Docker Desktop Google's servers
honcho start → docker compose up → gcloud run deploy
↕ ↕
same Dockerfile same Dockerfile
All three options use the same code and the same Dockerfile. Docker and Cloud Run are not separate things — Cloud Run is just "Docker hosted by Google."
Only run one option at a time. Docker (Option C) and honcho/uvicorn (Option B) all listen on the same ports — 8001, 8002, and 8003. If both are running simultaneously, the second one will crash with
address already in use. Always stop one before starting the other.
# First run — builds the image then starts all three agents
docker compose up --build
# Subsequent runs — image is already cached, starts immediately
docker compose up| Agent | Local URL |
|---|---|
healthcare_agent |
http://localhost:8001 |
general_agent |
http://localhost:8002 |
orchestrator |
http://localhost:8003 |
Seeing logs in your terminal? That's correct.
docker compose upstreams container output to the terminal you ran it from. The agents are running inside Docker — your terminal is just a live log viewer. To verify, open a second terminal and rundocker ps; you should see three running containers. If you'd rather Docker run silently in the background, usedocker compose up -dinstead (see below).
Stop the agents:
docker compose downCtrl+C vs
docker compose down: If you started withdocker compose up(attached), Ctrl+C stops the containers and frees the ports — butdocker compose downis cleaner as it also removes the containers fully. If you started withdocker compose up -d(background), Ctrl+C does nothing; you must rundocker compose downto stop them.
Run in the background (no log output in terminal):
docker compose up -d # start silently
docker compose logs -f # view logs on demand (Ctrl+C to stop following)
docker compose logs -f healthcare # view one agent's logs onlyRebuild after changing code:
docker compose up --buildRun a single agent only:
docker compose up healthcare # http://localhost:8001
docker compose up general # http://localhost:8002
docker compose up orchestrator # http://localhost:8003Cloud Run is the recommended way to publish these agents with a permanent public HTTPS URL. Each agent runs as its own managed service. The Cloud Run free tier includes:
| Resource | Free per month |
|---|---|
| Requests | 2,000,000 |
| Compute (memory) | 360,000 GB-seconds |
| Compute (CPU) | 180,000 vCPU-seconds |
This is more than enough for development and light production use. Gemini model calls via Google AI Studio (gemini/...) are on the free AI Studio quota. To avoid Vertex AI billing, use the gemini/ prefix (default) rather than vertex_ai/.
Avoid Agent Engine. Google ADK also offers "Agent Engine" (Vertex AI Managed Agents), which is a paid service with no free tier. The
gcloud run deployapproach used here deploys to standard Cloud Run, which has the free tier above.
- A Google Cloud account (a billing account is required for account verification, but the free tier means no charges for normal dev usage)
- Google Cloud CLI (
gcloud) installed - A GCP project (create one at console.cloud.google.com)
Authenticate and point gcloud at your project:
gcloud auth login
gcloud config set project YOUR_PROJECT_IDEnable the required APIs (takes ~1 minute, run once per project):
gcloud services enable \
run.googleapis.com \
cloudbuild.googleapis.com \
artifactregistry.googleapis.com \
secretmanager.googleapis.comStore your Google API key in Secret Manager (keeps it out of deployment logs and the Cloud Console UI):
echo -n "your-google-api-key-here" | \
gcloud secrets create google-api-key --data-file=-All three agents are built from the same Dockerfile at the root of the repo. The AGENT_MODULE environment variable tells the container which agent to start — so each Cloud Run service is just a separate deployment of the same image with a different value.
Deploy healthcare_agent (authenticated, FHIR-connected):
gcloud run deploy healthcare-agent \
--source . \
--region us-central1 \
--set-env-vars "AGENT_MODULE=healthcare_agent.app:a2a_app" \
--set-secrets "GOOGLE_API_KEY=google-api-key:latest" \
--allow-unauthenticated \
--min-instances 0 \
--max-instances 3Deploy general_agent (public, no API key needed):
gcloud run deploy general-agent \
--source . \
--region us-central1 \
--set-env-vars "AGENT_MODULE=general_agent.app:a2a_app" \
--set-secrets "GOOGLE_API_KEY=google-api-key:latest" \
--allow-unauthenticated \
--min-instances 0 \
--max-instances 3Deploy orchestrator (authenticated, delegates to both sub-agents in-process):
gcloud run deploy orchestrator \
--source . \
--region us-central1 \
--set-env-vars "AGENT_MODULE=orchestrator.app:a2a_app" \
--set-secrets "GOOGLE_API_KEY=google-api-key:latest" \
--allow-unauthenticated \
--min-instances 0 \
--max-instances 3After each deploy, gcloud prints the service URL — save all three:
Service URL: https://healthcare-agent-abc123-uc.a.run.app
Service URL: https://general-agent-abc123-uc.a.run.app
Service URL: https://orchestrator-abc123-uc.a.run.app
Note on
--allow-unauthenticated: This disables Cloud Run's IAM layer so requests can reach the agent without a Google identity. Application-level security (theX-API-Keyheader) is still enforced byApiKeyMiddlewarefor the agents that require it. Thegeneral_agentis intentionally open.
Using OpenAI or Anthropic instead of Gemini? Store the provider key in Secret Manager and pass the model via --set-env-vars. Example for healthcare_agent on GPT-4o-mini:
# Store the key once
echo -n "your-openai-key-here" | gcloud secrets create openai-api-key --data-file=-
# Deploy with OpenAI
gcloud run deploy healthcare-agent \
--source . \
--region us-central1 \
--set-env-vars "AGENT_MODULE=healthcare_agent.app:a2a_app,HEALTHCARE_AGENT_MODEL=openai/gpt-4o-mini" \
--set-secrets "OPENAI_API_KEY=openai-api-key:latest" \
--allow-unauthenticated \
--min-instances 0 \
--max-instances 3Replace OPENAI_API_KEY / openai/gpt-4o-mini with ANTHROPIC_API_KEY / anthropic/claude-sonnet-4-6 for Claude. Each agent has its own model var (GENERAL_AGENT_MODEL, HEALTHCARE_AGENT_MODEL, ORCHESTRATOR_MODEL) so you can mix providers across services.
The agent card advertises the agent's own public URL so callers (including Prompt Opinion) know where to send requests. After deploying, update each service with its real Cloud Run URL:
# Replace the URLs below with the ones printed by gcloud in Step 2
HEALTHCARE_URL=https://healthcare-agent-abc123-uc.a.run.app
GENERAL_URL=https://general-agent-abc123-uc.a.run.app
ORCHESTRATOR_URL=https://orchestrator-abc123-uc.a.run.app
gcloud run services update healthcare-agent \
--region us-central1 \
--update-env-vars "HEALTHCARE_AGENT_URL=${HEALTHCARE_URL}"
gcloud run services update general-agent \
--region us-central1 \
--update-env-vars "GENERAL_AGENT_URL=${GENERAL_URL}"
gcloud run services update orchestrator \
--region us-central1 \
--update-env-vars "ORCHESTRATOR_URL=${ORCHESTRATOR_URL}"Why the orchestrator doesn't need the other URLs: Sub-agents run in-process via
AgentTool— no HTTP calls are made from the orchestrator to the other Cloud Run services.ORCHESTRATOR_URLis only used for the agent card.
# Check the agent card for each service
curl https://healthcare-agent-abc123-uc.a.run.app/.well-known/agent-card.json
curl https://general-agent-abc123-uc.a.run.app/.well-known/agent-card.json
curl https://orchestrator-abc123-uc.a.run.app/.well-known/agent-card.json
# Call the public general_agent (no key needed)
curl -X POST https://general-agent-abc123-uc.a.run.app/ \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":"1","method":"message/send","params":{"message":{"role":"user","parts":[{"kind":"text","text":"What is the ICD-10 code for hypertension?"}]}}}'| Setting | Why it matters |
|---|---|
Use gemini/ model prefix (default) |
Routes through AI Studio — free Gemini quota. Switch to vertex_ai/ only if you have a Vertex AI project. |
--min-instances 0 |
Container scales to zero when idle — no compute charge while no requests arrive. |
--max-instances 3 |
Caps concurrency during development so you don't accidentally burn compute. |
--region us-central1 |
Cloud Run free tier applies to this region; not all regions qualify. |
Cold starts: With
--min-instances 0, the first request after a period of inactivity takes a few extra seconds while the container boots. This is fine for development. Set--min-instances 1to keep a container warm at all times (approximately $5/month on Cloud Run sustained-use pricing).
Google ADK ships an adk deploy cloud_run command designed for agents served through the ADK web UI (adk web). It wraps the agent using ADK's built-in FastAPI server, not the A2A to_a2a() ASGI server that this repo uses.
Because these agents expose the A2A JSON-RPC protocol (required by Prompt Opinion), gcloud run deploy --source . is the correct approach. The deployment infrastructure is identical — managed containers on Cloud Run — but the server wrapper is to_a2a() + uvicorn rather than ADK's web UI.
Prompt Opinion is a multi-agent platform that orchestrates agents like these — routing conversations, injecting patient context, and composing results across multiple specialised agents.
-
Deploy your agent to a publicly reachable URL (e.g.
https://my-agent.example.com). -
Set the public URL via environment variable — this URL is placed in the agent card's
supportedInterfacesarray (A2A v1 format):HEALTHCARE_AGENT_URL=https://my-agent.example.com
-
Set your Prompt Opinion workspace base URL so the FHIR extension URI in the agent card is correct:
PO_PLATFORM_BASE_URL=https://your-workspace.promptopinion.ai
This causes
fhir_extension_urito resolve to:https://your-workspace.promptopinion.ai/schemas/a2a/v1/fhir-context -
Register the agent in Prompt Opinion by providing:
- Agent card URL:
https://my-agent.example.com/.well-known/agent-card.json - Your
X-API-Keyvalue (Prompt Opinion sends this on every request)
- Agent card URL:
-
Prompt Opinion discovers your agent by fetching the agent card, reads
supportedInterfacesto find your endpoint, learns that an API key is required, and begins routing requests to it.
When your agent is called from Prompt Opinion, the platform automatically injects into the A2A message metadata:
- The patient's FHIR server URL for your workspace
- A short-lived bearer token scoped to the current user session
- The patient ID selected in the active encounter
Your tools receive these transparently from tool_context.state — you never handle FHIR authentication yourself.
MIT
Built on Google ADK and the A2A protocol. Designed for the Prompt Opinion multi-agent platform.