Multi-agent research system that turns a company domain into an AWS sales-opportunity report.
Three specialized agents Β· one shared container image Β· agent-to-agent over HTTP
Give it a prompt like "Analyze atscale.com for AWS opportunities" and it produces a polished HTML customer-intelligence report β executive summary, company snapshot, AWS opportunities mapped to business challenges, and strategic recommendations β saved locally and optionally uploaded to S3.
Under the hood, a Deep Intel orchestrator fans out to two specialist research agents in parallel over the A2A protocol, validates their findings against typed contracts, and renders the report deterministically from a template. Every agent runs on Amazon Bedrock and is served with bedrock-agentcore's A2A runtime β the same binary runs locally under Docker Compose or in the cloud as AgentCore Runtimes.
flowchart TD
U([Client request]) -->|A2A JSON-RPC| DI
subgraph Agents["One shared image Β· selected by AGENT_ID"]
DI["π§ Deep Intel<br/><i>orchestrator</i>"]
AR["π AWS Research<br/><i>worker</i>"]
BI["π’ Business Intel<br/><i>worker</i>"]
end
DI -->|A2A, parallel| AR
DI -->|A2A, parallel| BI
AR -->|Tavily MCP| WEB[(Web research)]
BI -->|Tavily MCP| WEB
AR -->|"AwsFindings (structured output)"| DI
BI -->|"CompanyProfile (structured output)"| DI
DI -->|ReportContent β Jinja2| HTML[/"π HTML report<br/>local + S3"/]
- π³ One image, three agents β every container runs the same code; the
AGENT_IDenv var (deep_intel|aws_research|business_intel) decides which agent a process becomes at boot. - π A2A-native β each agent is an independent A2A service with an agent card, served via
bedrock_agentcore.runtime.serve_a2a(AgentCore contract: port 9000 at/,/pinghealth, card at/.well-known/agent-card.json). - β‘ Guaranteed parallelism β the orchestrator fans out to both workers with
asyncio.gatherinside a single tool, not by hoping the model emits concurrent tool calls. - π§Ύ Schema-enforced output β workers produce their typed contracts (
AwsFindings,CompanyProfile) via Strands structured output: enforced by the provider at decode time, never parsed out of prose. - π¨οΈ Deterministic HTML β the LLM composes only typed
ReportContent; a Jinja2 template owns all markup/CSS. Reports are always valid, consistent, and injection-safe (autoescaped). - π‘ Streaming where it matters β the orchestrator streams its response over A2A SSE; workers return a single structured-JSON artifact by design.
- ποΈ Registry-driven capability β every agent's skills, output schema, discovery env vars, and MCP servers are declared in one place (
agents/registry.py). Adding a capability is a data change, not a new service class. - π Skills as Markdown β research methodology lives in
agents/skills/*/SKILL.md, loaded on demand via progressive disclosure. Edit worker behavior without touching Python. - βοΈ IaC included β CDK stacks deploy the three agents as AgentCore Runtimes with per-agent IAM roles and worker-ARN injection.
- Python 3.10+ (containers use 3.12) and
uv - AWS credentials with Bedrock access
- A Tavily MCP endpoint URL (web research for the workers)
git clone <this-repo> && cd jarvis-strands-github-mcp
uv syncCreate a .env file:
# Web research (required β workers call Tavily via MCP)
TAVILY_MCP_URL=https://your-tavily-mcp-endpoint
TAVILY_MCP_TOKEN=your_token # optional, if the endpoint needs auth
# Bedrock model (required β no default). If your org gates Bedrock behind
# application inference profiles, use the profile ARN here.
MODEL=us.anthropic.claude-sonnet-5
# AWS credentials (or use a profile / instance role)
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
AWS_REGION=us-east-1
# Reporting (optional)
S3_BUCKET=your-reports-bucketπ Full environment variable reference
| Variable | Required | Default | Description |
|---|---|---|---|
AGENT_ID |
β | β | Agent this process serves: deep_intel | aws_research | business_intel |
TAVILY_MCP_URL |
β | β | Tavily MCP server URL |
MODEL |
β | β | Bedrock model id, inference-profile ARN, or application-inference-profile ARN |
TAVILY_MCP_TOKEN |
β | β | Auth token for the Tavily MCP endpoint |
TAVILY_MCP_AUTH_HEADER |
β | Authorization |
Header name for the MCP token |
TAVILY_MCP_AUTH_PREFIX |
β | Bearer |
Prefix prepended to the MCP token |
TAVILY_MCP_TRANSPORT |
β | streamable_http |
MCP transport |
AWS_REGION |
β | us-east-1 |
AWS region for Bedrock / S3 |
MAX_TOKENS |
β | 8192 |
Max tokens per model call |
BEDROCK_READ_TIMEOUT |
β | 300 |
Bedrock read timeout (s) |
BEDROCK_CONNECT_TIMEOUT |
β | 30 |
Bedrock connect timeout (s) |
BEDROCK_MAX_ATTEMPTS |
β | 3 |
Bedrock retry attempts |
REPORT_OUTPUT_DIR |
β | reports |
Local output directory |
S3_BUCKET |
β | β | Enables S3 upload when set |
S3_PREFIX |
β | aws-intel-reports |
S3 key prefix |
ORCHESTRATOR_LOG_LEVEL |
β | INFO |
DEBUG surfaces tool inputs/results |
ORCHESTRATOR_LOG_FILE |
β | orchestrator.log |
Log file path |
PORT |
β | 9000 |
Local port override |
AGENT_BASE_URL |
http://localhost:<PORT> |
URL other agents use to reach this one β advertised in the agent card. Set per-service (compose does this); never set globally | |
DEEP_INTEL_URL / AWS_RESEARCH_URL / BUSINESS_INTEL_URL |
β | β | Explicit peer URLs (local/docker discovery) |
AWS_RESEARCH_AGENT_ARN / BUSINESS_INTEL_AGENT_ARN |
β | β | AgentCore runtime ARNs (deployed discovery fallback) |
docker compose up --build| Service | Host port | AGENT_ID |
|---|---|---|
| Deep Intel (orchestrator) | 9001 |
deep_intel |
| AWS Research | 9002 |
aws_research |
| Business Intel | 9003 |
business_intel |
Any A2A/JSON-RPC client works β no SDK required:
curl -s -X POST http://localhost:9001/ \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "1",
"method": "message/send",
"params": {
"message": {
"kind": "message",
"messageId": "'"$(uuidgen)"'",
"role": "user",
"parts": [{"kind": "text", "text": "Analyze stripe.com for AWS opportunities"}]
}
}
}'The response is a brief findings summary with the report location; the full HTML lands in reports/<Company>_<timestamp>.html (and S3 when configured).
Run a single agent without Docker
AGENT_ID=aws_research PORT=9002 uv run python -m agents.serverHealth: GET /ping Β· Agent card: GET /.well-known/agent-card.json
sequenceDiagram
autonumber
participant C as Client
participant DI as Deep Intel
participant AR as AWS Research
participant BI as Business Intel
C->>DI: message/send (A2A JSON-RPC)
DI->>DI: gather_research tool
par parallel fan-out (asyncio.gather)
DI->>AR: A2A message
AR->>AR: Skill + Tavily MCP research
AR-->>DI: AwsFindings (structured JSON)
and
DI->>BI: A2A message
BI->>BI: Skill + Tavily MCP research
BI-->>DI: CompanyProfile (structured JSON)
end
DI->>DI: validate (Pydantic) + cross-check sources
DI->>DI: save_report β Jinja2 render β disk + S3
DI-->>C: summary + report location (streamed over SSE)
- Deep Intel receives the prompt and calls its
gather_researchtool, which fans out to both workers in parallel and validates each response against its Pydantic contract. - Workers research using the Tavily MCP tools, guided by their Skill; their final answer is produced via structured output, so the returned artifact is schema-valid JSON by construction.
- Deep Intel cross-checks the sources, composes typed
ReportContent(content only β no markup), and itssave_reporttool renders the HTML through the Jinja2 template and persists it.
- A2A clients send messages to
card.url, not to the endpoint you configure (that's only used to fetch the card). Each agent therefore advertises the address peers can reach it at (AGENT_BASE_URL; AgentCore overwrites it with the real runtime URL when deployed). - Workers don't stream. In A2A-compliant streaming mode, mid-loop narration would stream into the artifact and displace the final structured JSON β so workers use single-artifact mode, while the orchestrator streams to the client.
- Per-conversation isolation.
StrandsA2AExecutorbuilds a fresh agent per A2Acontext_id(LRU-cached); the expensive Tavily MCP client is started once per process and shared.
Expand
Dockerfile # Single image for all agents
docker-compose.yml # Three agent services (host ports 9001-9003)
agents/
βββ server.py # Entrypoint: AGENT_ID β registry spec β serve_a2a (port 9000)
βββ registry.py # β Source of truth: agent specs, schemas, skills, MCP servers, discovery
βββ a2a_client.py # Thin A2A client for parallel worker calls (+ deferred auth seam)
βββ deep_intel/
β βββ orchestrator.py # Orchestrator agent: gather_research + save_report tools
β βββ reporting.py # Jinja2 rendering, disk storage, optional S3 upload
βββ skills/ # Worker methodologies (progressive-disclosure Skills)
β βββ aws-opportunity-research/SKILL.md
β βββ company-intelligence/SKILL.md
βββ shared/
βββ research_agent.py # Unified worker engine (skill + schema + role line)
βββ schemas.py # Typed contracts: AwsFindings, CompanyProfile, ReportContent
βββ bedrock.py # BedrockModel + timeout/retry construction
βββ templates/report.html.j2 # Report layout/CSS (the LLM never writes markup)
βββ mcp/ # MCP client builders (Tavily via streamable HTTP)
βββ core/ # Config (fail-fast), logging, exceptions
cdk/ # AgentCore Runtime deployment (see cdk/README.md)
test/ # Contract + renderer + tool-path tests
reports/ # Generated HTML reports (bind-mounted)
The cdk/ app deploys the three agents as AgentCore Runtimes on the A2A protocol β one shared ECR image, one runtime per AGENT_ID:
cdk deploy JarvisAgentsBase # ECR repo + reports S3 bucket
# CI pushes the image (.github/workflows/ci-ecr.yml)
cdk deploy JarvisAgentsRuntime -c imageTag=<git-sha> -c model=<model-or-profile-arn> -c tavilyMcpUrl=<url>Workers deploy first; their runtime ARNs are injected into Deep Intel's environment and granted bedrock-agentcore:InvokeAgentRuntime. Rolling out new code is decoupled from CDK: CI pushes a new image, then update-agent-runtime (the ECS force-new-deployment analog). Inbound auth supports a custom JWT authorizer (e.g. Microsoft Entra) via CDK context, falling back to IAM SigV4. See cdk/README.md.
uv run pytestCovers the workerβorchestrator typed contract (_coerce), the report renderer (valid HTML, autoescaping, optional sections), and save_report through the real Strands tool-invocation path.
- Inter-agent auth for deployed runtimes β machine OAuth (client-credentials) in
a2a_client.py:_auth_token; deployed orchestratorβworker calls 403 until wired - Secrets hygiene β move
TAVILY_MCP_TOKEN/ OAuth client secret to AWS Secrets Manager - Tool-error observability β WARNING-level hook for failed tool calls
- Persistent task store β tasks are in-memory today and don't survive restarts
Built on the Strands Agents SDK, the A2A protocol, Amazon Bedrock AgentCore, and Tavily web research via MCP.
License to be determined.