Doctor SEO · Radial Pulse

Agent Pipeline — Tech Stack Architecture v4

The AWS infrastructure underneath the 15-agent pipeline in Radial-Pulse-System-Architecture.html. High-level only — one page per decision, aligned end to end with End-to-End-Request-Path-AWS.html. For deep detail (guardrails, network topology, commercial prerequisites, operations) see v2. v4 adds an expandable per-agent tech stack breakdown for the Lane B fan-out.

10Stack layers
3Agent-build patterns
8Request steps
5Human gates
Decision summary Full stack Agent framework tiering Model runtime — Bedrock Data & storage Auth Webapp & backend layout End-to-end request path
ADR

Decision summary

One workflow owner — DBOS Transact

DBOS Transact owns the workflow: Lane A → B → C sequencing, parallel fan-out via durable queues, step-level retries, cancellation, scheduled re-audits (@DBOS.scheduled), and the five human-gate pause/resume points (G1–G5) via durable send/recv events. Every workflow and step checkpoints to the same RDS Postgres instance in § Data & storage before doing anything else — a crash mid-run or a 4-hour Gate 1 wait both cost nothing, since there's no separate state-machine service holding a slot.

LangGraph is a library, not the orchestrator

LangGraph runs inside a single DBOS step, and only for the three agents that must reason across multiple tool results: A1 (entity resolution), B5 (competitor shortlist) and C5 (evidence audit). It never owns cross-step workflow state — that boundary belongs to DBOS alone.

DSPy compiles what LangGraph runs. Every prompt inside A1/B5/C5's tool-calling nodes — and the structured-call agents in Lane A/B/C — is a DSPy Signature compiled against a gold set, not a hand-tuned string. LangGraph still owns control flow (which tool to call next); DSPy only owns the prompt text at each node. Recompiling a signature produces a new prompt version, recorded in prompt_versions exactly like a hand-written prompt would be.

Everything else needs no framework

A2–A3, B1, B2–B4/B6/B7, C3 and C4 are a single schema-constrained model call. (B1 Keyword, Catchment & Market is sequential in Lane A, not parallel in Lane B — every Lane B agent consumes its output before running.) C1/C2 are plain Python — scoring and opportunity math are deterministic by design. The renderer is a template plus headless Chromium, with no model at all.

MCP is deferred; fixed integrations use typed Python adapters. DSPy is adopted for prompt compilation once a gold set exists for that agent — never for orchestration (that's DBOS alone), never to tune the score formula (that stays a fixed Python function).

01

Full stack, top to bottom

Ten layers, all AWS-managed. Adopted ships from day one · Reserved is designed for but not built until a measured need proves it.

L1
Ingress — Amazon API GatewayAdopted

The AWS-managed execute-api HTTPS endpoint routes every browser and service request to Lambda. No custom domain, external DNS, CDN or WAF is a day-one dependency; add ACM + DNS only if a public domain is introduced later.

API Gateway
L2
Webapp — React on LambdaAdopted

Audit intake, live progress, report and evidence viewer, and the review UI for G1–G5. Shows domain milestones only — never an agent's raw reasoning trace.

React · Vite · AWS Lambda
L3
Auth — Google LoginAdopted

Approved internal users sign in with Google Login (Sign in with Google, OAuth 2.0). The control plane validates every session server-side; all signed-in users can work through the review gates, with each decision attributed in the audit log. Detail in § Auth.

Google Login (OAuth 2.0 / OIDC) · Amazon Cognito · API Gateway authorizer
L4
Control plane — FastAPI on LambdaAdopted

Validates commands, registers idempotent audit runs, returns safe status, and routes every gate decision back to the workflow. It never crawls, scores, calls a model, or owns a long-running audit.

FastAPI · AWS Lambda
L5
Workflow — DBOS TransactAdopted

The single owner of workflow state, as a durable-execution library rather than a separate managed state machine. Durable queues fan out Lane B; step-level retry policy enforces provider failure handling; durable send/recv events hold G1–G5 waits without consuming compute; @DBOS.scheduled triggers re-audits. Every checkpoint lands in the same RDS Postgres instance as the rest of the data plane — no separate state-machine service to run or pay for. Fargate remains the fallback only if a single step's runtime outgrows a Lambda invocation's execution limit.

DBOS Transact · Amazon RDS PostgreSQL
L6
Agent execution — LangGraph + DSPy, in a DBOS stepAdopted

Each DBOS workflow step invokes one bounded Lambda. Only A1, B5 and C5 run a LangGraph tool loop inside that step; every other agent is a structured call or plain Python. Wherever a gold set exists for that agent, its prompt is a DSPy-compiled signature rather than a hand-written string — LangGraph still decides control flow, DSPy only owns the prompt text. Detail in § Agent framework tiering.

LangGraph · DSPy · Python · AWS Lambda
L7
Models — Amazon BedrockAdopted

Claude Haiku 4.5 / Sonnet 5 / Opus 5, routed per agent by task shape, called through the Anthropic-Messages-API proxy. Detail in § Model runtime.

Amazon Bedrock · Claude
L8
Tool access — typed provider adaptersAdopted

Every provider is a typed Python client imported by the Lambda that needs it: Places, DataForSEO, Google Business Profile, SE Ranking AI Search, Local Falcon, Practo/Justdial, Instagram, YouTube. Each declares timeout, retry class, quota, cache TTL and fallback, and has a fixture twin for replayable tests. MCP is deferred — introduce it only if model-driven tool discovery across runtimes becomes a real need, and never for scoring, persistence or rendering.

Typed Python adapters · Secrets Manager
L9
Data plane — RDS PostgreSQL + S3Adopted

RDS is the single source of truth for run state and decisions; S3 holds immutable raw evidence and rendered PDFs. RDS Proxy protects connections from Lambda concurrency. Detail in § Data & storage.

Amazon RDS · RDS Proxy · Amazon S3 · KMS
L10
Observability & evaluationAdopted

Traces every workflow step, agent, tool call and Bedrock call: latency, cost, and the pipeline's quality metrics — fact precision, competitor F1, GEO F1, unsupported-claim rate, and reviewer edit distance from Gate 4. Those metrics are the gold sets DSPy compiles each agent's prompt against — a metric moving the wrong way is the trigger to recompile, not to hand-edit the prompt. Every Lambda invocation's structured logs land in CloudWatch Logs, correlated by audit_run_id, for stepping through a single failed run outside the trace view; CloudWatch Alarms watch error rate and spend against budget.

OpenTelemetry · CloudWatch Logs & Alarms · LangSmith
02

Agent framework tiering

Same evaluate(input) → result contract for every agent; the implementation underneath is chosen per agent, not applied uniformly. Three patterns cover all 15.

AgentPatternWhy this pattern
A1 Entity ResolutionLangGraph tool loopDisambiguation needs reasoning across several tool results (Places, Business Data, Maps SERP), not one completion.
B5 Competitor ShortlistLangGraph tool loopSame tool-loop shape as A1, reused rather than re-derived.
C5 Evidence AuditorLangGraph tool loopAdversarial audit against a fixed evidence table, in a fresh context with no visibility into C3's reasoning trace — so it can't rubber-stamp the composer's blind spots.
A2, A3, B1Structured single callOne schema-constrained completion each in Lane A sequence. A2 profiles the entity; A3 estimates economics; B1 resolves the keyword universe and catchment — all before Lane B fans out (since every B2–B7 agent consumes B1's output).
B2–B4, B6, B7, C4Structured single callOne schema-constrained completion each — classification, extraction, or a sentence around a number Python already computed. No loop, no framework.
C3 Narrative ComposerStructured single call, one passNo tool calls — pure generation over the payload it's handed; every figure is passed in, none recalled from memory.
C1, C2Plain PythonPure functions with fixtures. Scoring and opportunity math must be reproducible; a model here would be a liability.
R RendererTemplate + headless ChromiumNo model at all — if it isn't in the Gate-4-approved payload, it can't appear on the page.

One execution owner per node

A node is either a LangGraph tool loop, a structured model call, or deterministic code — never two at once. Retries, budget and termination for a node belong to exactly one layer, so a failure has exactly one place to look.

03

Model runtime — Amazon Bedrock

Bedrock hosts every Claude call. How the pipeline talks to it matters more than the model choice.

Proxy shape, not native Converse

Call Bedrock through the Anthropic-Messages-API-compatible proxyx-api-key + anthropic-version headers, standard Messages request/response shape — not the native …/model/{id}/converse endpoint. A bedrock-api-key-… bearer token authenticates against the proxy and is rejected by native Converse with a 403. Verify the exact endpoint and model-id string against a working implementation before assuming AWS's general Converse docs apply.

TierModelUsed byWhy
Bulkclaude-haiku-4-5A2, A3, B1, B2, B3, B4, B7, C4Classification and schema-constrained extraction at volume — hundreds of reviews or keywords per run must stay cheap enough to process per-item.
Tool-callingclaude-sonnet-5A1, B5, C5Multi-step reasoning across tool results. A lighter tier than C3 keeps the C5 audit cheap without weakening it — it's a targeted check against known facts.
Narrativeclaude-opus-5C3One pass, no tools, highest-stakes prose in the report. Runs once per audit, not per evidence row, so quality outweighs cost.

Cost controls

Cache the system prompt and large static context (specialty taxonomy, benchmark tables) per agent. Pin one region for predictable latency and cost. Every node declares a maximum tool-call, token, time and spend ceiling — enforced by DBOS, never by the prompt.

04

Data & storage

StoreHoldsNotes
Amazon RDS PostgreSQLaudit_runs, seed_inputs, evidence_index (pointers, not payloads), review_decisions, prompt_versions, report_revisionsSingle source of truth for run state and every gate decision. Private subnets, reached through RDS Proxy. A re-audit writes a new revision — it never overwrites one.
Amazon S3Immutable raw provider responses (Source Artifacts), screenshots, rendered PDFsWritten before any extraction touches the data — a provenance rule, not an optimization. Versioning on; Object Lock where retention demands immutability.
Secrets Manager + KMSProvider API keys, the Bedrock proxy token, database credentialsEach Lambda execution role reads only the secrets it needs. Nothing lives in the repo.
pgvector · Redis · R2 ReservedSemantic evidence search · caching and rate-limit coordination · cheaper object egressAll three are designed for but unbuilt. Add one only when a concrete measured need appears — not preemptively.
05

Auth

Two separate domains — internal people signing off on gates, and services calling each other. They use different credentials and should not be conflated.

WhoAccessAudit record
Approved internal userShared access to the internal webapp, audit runs, evidence, reports and gates G1–G5.Every submission, approval, rejection and feedback action records the authenticated user and timestamp.

Chosen approach — Google Login

Google is the identity provider via Sign in with Google (OAuth 2.0 / OIDC) — no Google Workspace admin-console federation required. Amazon Cognito federates through OIDC and issues the application JWT; API Gateway validates that JWT before FastAPI receives the request. No gate-specific roles are required at this stage.

Service auth

Three separate, non-interchangeable credentials: Lambda execution roles for AWS resources, scoped provider keys from Secrets Manager for external APIs, and the Bedrock proxy token for model calls. Gate decisions derive the user's identity from the authenticated session — never from the client — and every decision writes to the audit log.

06

Webapp & backend layout

One monorepo. Apps deploy as Lambda functions; packages are shared libraries imported by them.

doctor-seo/
├── apps/
│   ├── web/                  # React — intake, progress, report/evidence viewer, gate UI
│   ├── control-plane/        # FastAPI — auth, commands, /reviews, /publish
│   └── workflow/             # DBOS Transact durable workflows + Lambda step handlers
├── packages/
│   ├── agents/               # LangGraph loops (A1, B5, C5) + structured-call agents
│   ├── adapters/             # typed provider clients: places, dataforseo, gbp,
│   │                         # ai-search, aggregators, social + fixture twins
│   ├── prompts/              # DSPy signatures + compiled programs (a1.py … c5.py),
│   │                         # versioned and PR-reviewed like any other prompt asset
│   ├── scoring/              # C1/C2 deterministic Python, no framework dependency
│   └── schemas/              # Pydantic contracts shared across all three apps
└── infra/                    # Terraform or CDK — API Gateway, Lambda,
                              # RDS, S3, KMS, Secrets Manager, CloudWatch Logs + alarms

Prompt discipline

System prompts are compiled artifacts, not hand-typed strings — a DSPy signature compiled against a gold set, checked into git like code, changed through a recompile + PR review, recorded per run in prompt_versions so scoring drift traces back to a specific compile. User/task prompts are data — assembled per invocation from typed state, never a template with user text spliced into the system role. The seed's free-text remark is the one open field in the pipeline; it steers tone and hero-procedure only, and is never a measured input.

07

End-to-end request path

One audit run, walked through every layer, from a doctor's name to a signed-off PDF. See End-to-End-Request-Path-AWS.html for the same path broken into 12 steps with per-step requirements.

1
Seed submitted

An approved internal user enters name, locality, specialty and remark in the React webapp, reached over HTTPS through API Gateway. Session auth has already been validated through Google Login.

2
Run registered

The FastAPI control plane verifies the caller, checks an idempotency key, and writes an audit_runs row to RDS Postgres before queuing any work — so a refresh can't create a duplicate audit.

3
Lane A resolves

DBOS persists the run state to Postgres, then invokes A1 as a LangGraph tool loop — its prompt a DSPy-compiled signature — calling the Places adapter against Bedrock (Sonnet 5) to resolve the canonical clinic. Then A2 enriches the profile, A3 estimates economics, and B1 resolves the keyword universe and catchment — all sequential.

4
Gates 1 & 2

Low match confidence pauses the run; an approved internal user approves or rejects from the webapp. After enrichment and the economic estimate, another review decision ratifies the ranges at Gate 2. Both waits are held by a durable DBOS event — nothing consumes compute while the workflow sleeps, and it resumes exactly where it left off on approval.

5
Lane B fans out

Six agents run in parallel as DBOS queued steps, each writing raw responses to S3 before extraction and indexed evidence rows to RDS. All consume B1's keyword and catchment outputs. A provider that can't answer marks the run partial — a model is never asked to fill the gap.

Per-agent tech stack — B2–B7 (click to expand)
B2SEO & Website Technical
Structured call · Haiku 4.5
Input
A1's website URL + B1's Money Keyword Set.
Output
Organic rank positions, on-page/technical score, Core Web Vitals, content & trust score.
Data source
DataForSEO (SERP + on-page); PageSpeed Insights / CrUX for Core Web Vitals; SE Ranking Website Audit API (/v1/site-audit/audits/…) as a consolidating alternative — covers on-page technical checks and Core Web Vitals on the same vendor B3 already calls.
B3GEO / AI Search
Structured call · Haiku 4.5
Input
A1's canonical identity + B1's Money Keyword Set, seeding the fixed AI-prompt set.
Output
Mentions across 8 AI engines; citation accuracy vs. the canonical record.
Data source
SE Ranking AI Search API.
B4Maps & Reviews
Structured call · Haiku 4.5
Input
A1's place ID + B1's catchment definition.
Output
GBP completeness, map-pack position/heatmap, review sentiment, owner response rate.
Data source
Google Places API + Google Business Profile API; Local Falcon for the geo-grid heatmap. Checked SE Ranking's Local Marketing tool (map-pack tracking, GBP data, reviews) as a consolidation candidate — it ships no public API, UI-only, so it can't feed this pipeline. DataForSEO Business Data API (My Business Info + Google Reviews, with owner responses) and its Maps SERP API (geo-coordinate targeting, up to 700 listings/point) do cover this ground on the same vendor already used by B2 — a genuine consolidation option in place of Google Places/GBP/Local Falcon, at the cost of building the geo-grid scan yourself instead of getting Local Falcon's pre-built heatmap.
B5Competitor
LangGraph tool loop · Sonnet 5
Input
A1's place ID + B1's catchment + B4's map-pack data.
Output
Exactly 3 rival identities; feeds every "Top Competitor" column on the report.
Data source
Same providers as B2–B4, called 3× more — cached by place_id.
B6Social
No model — metrics fetch
Input
A2's discovered social handles + B1's keyword universe.
Output
Active platforms, follower counts, posting cadence, engagement rate.
Data source
Instagram Business Discovery API, YouTube Data API.
B7Aggregator & NAP
Structured call · Haiku 4.5, human-assisted
Input
A1's canonical name, address & phone + B1's catchment definition.
Output
Aggregator profile completeness; NAP diff vs. the Google canonical.
Data source
Direct site check on Practo and Justdial — no API exists, so the report discloses this sourcing.
6
Gate 3

An approved internal user spot-checks the evidence table and its linked raw artifacts, then approves, refetches, or marks a row unavailable.

7
Lane C synthesizes

C1/C2 run as plain Python over the approved evidence; C3 (Opus 5) drafts the narrative from that payload alone; C5 (Sonnet 5, fresh context) audits every claim against the evidence and blocks anything unsupported.

8
Gate 4 → render & deliver

An approved internal user signs off — this gate always fires. Only then does a Playwright/Chromium Lambda render the approved payload to PDF, publish an immutable Report Revision to S3, and serve it through a short-lived pre-signed URL.