An end-to-end autonomous multi-agent pipeline implementing Anthropic's Agentic AI framework. Raw telecom call transcripts are ingested, extracted into 70+ structured fields via ReAct control loops with Chain-of-Thought prompting, scored inline by a 100-point QA model, aggregated into executive KPIs, and synthesised into board-ready strategic recommendations via a 3-pass self-reflective deliberation cycle. Every autonomous decision is logged with its reasoning and evidence. Every stage is secured, governed, and observable.
Local CSV (telecom_200k.csv — primary) · HuggingFace stream (fallback, 3.7M turns)
│
▼
┌──────────────────────────────────────────────────────────────────────────────┐
│ LangGraph StateGraph · Multi-Agent Pipeline v4.4 │
│ │
│ ┌──────────────────────┐ ┌────────────────────────────────────────────┐ │
│ │ DataIngestion │───▶│ Extraction Agent (2/7) │ │
│ │ Agent (1/7) │ │ Claude Haiku 4.5 · 70 fields · CoT │ │
│ │ stream · validate │ │ ┌──────────── ReAct loop ───────────────┐ │ │
│ │ PII scan · log │ │ │ Observe : score_field_coverage() │ │ │
│ └──────────────────────┘ │ │ Reason : identify null critical fields│ │ │
│ │ │ Act : targeted gap-fill call │ │ │
│ │ └───────────────────────────────────────┘ │ │
│ └───────────────────┬────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────┐ ┌──────────────────────────────────────────┐ │
│ │ Aggregation │◀───│ Quality Agent (3/7) │ │
│ │ Agent (4/7) │ │ 100-pt QA · exclusion log │ │
│ │ KPIs · cost levers │ │ dynamic routing on gate failure │ │
│ └───────────┬──────────┘ └──────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ Insights Agent (5/7) · NVIDIA NIM → Claude → Rule-based fallback │ │
│ │ Vector memory → top-K semantically similar historical runs │ │
│ │ ┌──────────────── Deliberation loop ─────────────────────────────┐ │ │
│ │ │ Pass 1 Analyze : CoT KPI analysis → initial insights (0.3) │ │ │
│ │ │ Pass 2 Critique : self-grade A/B/C — data-grounded, specific │ │ │
│ │ │ Pass 3 Synthesize: rewrite weak recommendations (0.3) │ │ │
│ │ └────────────────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────┐ ┌──────────────────────────────────────────┐ │
│ │ Approval Gate (6/7) │───▶│ Export Agent (7/7) │ │
│ │ human sign-off │ │ CSV · full JSON · QA report · insights │ │
│ │ auto-approve CI │ │ decisions_{ts}.json · manifest · audit │ │
│ └──────────────────────┘ └──────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────┘
Normal path: all 7 nodes execute in sequence. Quality gate failure path: QualityAgent routes directly to ExportAgent — pipeline always completes with audit trail.
| Component | Choice | Rationale |
|---|---|---|
| Orchestration | LangGraph StateGraph |
Explicit typed state schema; each agent is a pure function; compiled graph is inspectable; Send API available for fan-out parallelism |
| LLM (Extraction) — primary | Claude Haiku 4.5 (claude-haiku-4-5-20251001) |
Set EXTRACTION_MODEL in config.py — all downstream clients, rate limiters, cost estimates, and provider labels resolve automatically |
| LLM (Extraction) — fallback | Gemini 2.0 Flash Lite | Activates when ANTHROPIC_API_KEY absent; free tier up to 1 500 req/day / 30 RPM |
| LLM (Insights) — primary | NVIDIA NIM meta/llama-3.3-70b-instruct |
OpenAI-compatible API; reliable JSON via response_format=json_object; strong instruction-following for strategic recommendations |
| LLM (Insights) — fallback 1 | Claude Haiku 4.5 | Activated when NVIDIA_API_KEY absent or quota exceeded |
| LLM (Insights) — fallback 2 | Gemini 2.0 Flash Lite | Final LLM fallback; separate quota pool from extraction |
| LLM (Insights) — fallback 3 | Rule-based | Derives insights from KPI thresholds — no API key required; pipeline always completes |
| LLM (Embeddings) | Gemini text-embedding-004 |
768-dim embeddings for vector memory; same API key, no extra library; TF-IDF fallback offline |
| Dataset (primary) | Local CSV telecom_200k.csv |
200K conversations pre-downloaded; zero-latency load; auto-detected by hf_loader.py |
| Dataset (fallback) | HuggingFace talkmap/telecom-conversation-corpus |
3.73M turns, 200K conversations, MIT-licensed; streaming mode — bounded RAM regardless of corpus size |
| QA Scoring | Custom 100-pt model | Completeness(30) + Enum validity(25) + Consistency(25) + Plausibility(20) |
| Vector memory | numpy cosine similarity | Zero extra dependencies; interface-compatible with ChromaDB/Pinecone swap |
| Tracing | LangSmith (optional) | LANGCHAIN_TRACING_V2=true activates full node-level span capture |
| Dashboard | Streamlit + Plotly | Professional light theme; 6 sections; reads summary.json; built-in demo data fallback |
Standard Reason → Act → Observe loop applied per transcript, driven by field-coverage scoring:
┌────────────────────────────────────────────────────────────────────┐
│ Initial Act: analyze_transcript() → first_pass result │
│ │ │
│ Observe: score_field_coverage() │
│ (% of _CRITICAL_FIELDS that are non-null/non-empty) │
│ │ │
│ coverage < REACT_QUALITY_THRESHOLD (70)? │
│ │ │ │
│ YES NO → append to results │
│ │ │
│ Reason: build _missing list from _CRITICAL_FIELDS │
│ Log: DecisionLogger("react_trigger", evidence={coverage}) │
│ │ │
│ Act: gap_fill_transcript() │
│ (targeted prompt: only the missing fields asked for) │
│ │ │
│ Observe: merge null fields + re-score │
│ (repeat up to REACT_MAX_ITERATIONS times) │
│ │ │
│ Log: DecisionLogger("react_gap_fill_outcome") │
└────────────────────────────────────────────────────────────────────┘
Quota circuit breaker: _react_quota_exhausted (module-level bool in analyzer.py):
- For Gemini provider: trips on the first 429
ClientError— daily quota exhausted; disables all subsequent gap-fills for the run so primary extraction quota is preserved - For Claude provider: 429 is a transient per-minute rate limit, not daily exhaustion; CLAUDE_RATE_LIMITER already backs off; the circuit breaker does NOT trip — only that individual call is skipped
_cot_reasoning is the first field in every JSON response. The LLM must populate this before the 70 structured fields, forcing explicit reasoning before committing to values. Zero extra API calls — native to response_mime_type="application/json".
{
"_cot_reasoning": "Customer called about a billing discrepancy. Agent identified
a duplicate charge in the system. Refund was processed during the call. Sentiment
moved from frustrated to satisfied — FCR applies, escalation does not.",
"fcr": true,
"resolution_status": "resolved",
"escalated": false,
...
}InsightsAgent's ANALYZE_PROMPT embeds explicit step-by-step reasoning instructions:
STEP-BY-STEP REASONING (Chain-of-Thought):
1. Which KPIs are significantly above or below industry benchmarks?
2. What root causes are most likely given the pattern of KPIs together?
3. Which interventions would have the highest ROI given this specific data?
4. What risks are NOT visible in this data but are implied by the patterns?
Three passes, each with a distinct role, producing self-correcting strategic output:
Pass 1 — Analyze (CoT)
Input : KPI summary + historical context (flat memory + vector top-K)
Prompt: ANALYZE_PROMPT with CoT step-by-step instructions
Output: initial insights JSON with _cot_reasoning
Temp : 0.3
Pass 2 — Critique (self-reflection)
Input : Pass 1 output + raw KPIs
Prompt: CRITIQUE_PROMPT
Task : Grade each recommendation A/B/C:
(A) data-grounded, (B) specific, (C) non-duplicated
Output: overall_quality + per-recommendation grades + missing_insights
Temp : 0.1 (deterministic grading)
Pass 3 — Synthesize
Input : Pass 1 output + Pass 2 critique
Prompt: SYNTHESIZE_PROMPT
Task : Rewrite recommendations graded B/C; ensure every rec cites a KPI
Output: final board-ready insights JSON
Temp : 0.3
Graceful degradation: Pass 2 fail → return Pass 1; Pass 3 fail → return Pass 2; all fail → rule-based fallback. The pipeline always completes.
Every autonomous decision made by any agent is captured as a DecisionRecord with:
- What was decided (human-readable
decisionstring) - Why (free-text
reason, capped 500 chars) - Evidence (quantitative data that drove the decision — scores, counts, flags; never raw transcript text)
- Alternatives considered but not taken
- Confidence level (
high/medium/low)
| Agent | Decision type | Triggered when |
|---|---|---|
DataIngestionAgent |
transcript_skip |
Transcript too short, too few turns, or empty |
DataIngestionAgent |
pii_redaction |
PIIScanner detects and redacts sensitive patterns |
ExtractionAgent |
react_trigger |
Field coverage < REACT_QUALITY_THRESHOLD per call |
ExtractionAgent |
react_gap_fill_outcome |
ReAct loop completes for the batch |
QualityAgent |
qa_exclusion |
Record graded LOW (score < 60) — excluded from aggregation |
QualityAgent |
qa_grade_assignment |
Borderline MEDIUM (60–65) — accepted but flagged |
QualityAgent |
quality_gate_outcome |
Dataset-level quality gate PASS or FAIL |
AggregationAgent |
aggregation_scope |
QA-filtered vs full-set aggregation decision |
AggregationAgent |
cost_model_applied |
Provider pricing resolved from EXTRACTION_MODEL |
InsightsAgent |
provider_selected |
NVIDIA NIM / Claude / rule-based selection and reason |
InsightsAgent |
deliberation_outcome |
3-pass deliberation completed with critique quality |
ApprovalGate |
approval_decision |
Human approved / rejected / auto-approved |
GraphRouter |
routing_decision |
Quality gate failure → emergency export routing |
All decisions accumulate in PipelineState.decision_log (immutable list of plain dicts). ExportAgent writes:
decisions_{ts}.json— full record withtotal_decisions,summary, andrecordsarraysummary.json— embedsdecision_summary(counts by agent and type, notable decisions)
from pipeline.decision_log import DecisionLogger
class MyAgent:
def run(self, state: dict) -> dict:
dl = DecisionLogger(self.name, state)
dl.log(
decision_type="my_decision_type",
decision="Short description of what was decided",
reason="Why this decision was taken — the constraint or evidence",
evidence={"score": 42, "threshold": 60},
call_id="C001",
confidence="high",
alternatives=["Alternative A considered but rejected because..."],
)
return {**state, "decision_log": dl.finalize()}Every agent boundary is protected. No LLM call proceeds without input sanitisation; no LLM output reaches downstream agents without output sanitisation.
| Threat | Component | Defence |
|---|---|---|
| Prompt injection in transcript | InputSanitizer |
10 regex patterns; neutralise with [FILTERED] (not reject — avoids DoS from one bad record) |
| Token bomb (oversized input) | InputSanitizer |
Hard cap at MAX_TRANSCRIPT_CHARS (50K chars); truncate with [TRUNCATED-SECURITY] marker |
| Encoding attacks | InputSanitizer |
Strip \x00 and � before LLM |
| Secret leakage in input data | InputSanitizer |
Google/Anthropic/OpenAI key patterns, JWTs, Bearer tokens → [SECRET_REDACTED] |
| Code execution in LLM output | OutputSanitizer |
__import__, eval, exec, subprocess, XSS → [CONTENT_FILTERED] |
| Response bomb | OutputSanitizer |
Hard cap at MAX_RESPONSE_BYTES (32 KB); raises SecurityViolation before JSON parse |
| Secret leakage in LLM output | SecretGuard |
Pattern scan on raw LLM text before any downstream agent receives it |
| Cross-agent tool hijacking | AgentScopeGuard |
Per-agent authorised tool set; violation raises SecurityViolation before invocation |
| State tampering | InputSanitizer.validate_state_schema() |
Required keys verified at each agent boundary |
| Runaway API consumption | RateLimiter |
Sliding-window caps: 50 req/60s (Claude), 15 req/60s (Gemini) |
# Every extraction call in analyzer.py — provider-agnostic path:
transcript = INPUT_SANITIZER.sanitize_transcript(transcript) # injection + PII
CLAUDE_RATE_LIMITER.acquire() # or GEMINI_RATE_LIMITER — resolved by _USE_CLAUDE
# Claude path: _call_claude(client, system_prompt, user_msg, max_tokens)
# Gemini path: client.models.generate_content(model=MODEL, ...)
SECRET_GUARD.assert_no_secrets_in_output(response_text) # key leak check
OUTPUT_SANITIZER.check_response_size(response_text) # response bomb
result = _parse_json_text(response_text) # strips markdown fences
result = OUTPUT_SANITIZER.sanitize_extraction_result(result) # per-field sanitiseSemantic long-term memory that retrieves the most relevant historical runs for InsightsAgent context — not just chronological averages.
Pipeline run completes
│
▼
ExportAgent calls VECTOR_STORE.add_run(run_id, kpi_text, metadata)
│
▼
Gemini text-embedding-004 embeds KPI summary text → 768-dim vector
(TF-IDF bag-of-words fallback when API key unavailable)
│
▼
Stored: vectors.npy + index.json (outputs/vector_memory/)
│
│ ← next pipeline run ────────────────────────────────────────────┐
▼ │
InsightsAgent calls VECTOR_STORE.format_context(current_kpi_text) │
│ │
▼ │
Cosine similarity over all stored vectors → top-K most similar runs │
│ │
▼ │
"Top-3 similar historical runs (by KPI pattern similarity): │
1. [2026-05-18] sim=0.94 — FCR 72% AHT 4.2 min escalation 8%…" │
│ │
▼ │
Injected into ANALYZE_PROMPT historical context section ────────────────── ┘
The interface is backend-agnostic: replace VectorMemoryStore._save()/_load() with ChromaDB or Pinecone without changing the public add_run() / query() API.
Each agent is a stateless class with a single run(state: dict) -> dict method. Agents are fully decoupled from the graph — swapping, replacing, or parallelising them requires changes only to pipeline/graph.py.
Responsibilities: Streaming acquisition, quality gating, PII scanning, decision logging.
Streaming strategy:
- Stream rows until
(offset + n) × 6uniqueconversation_idvalues are seen - Sort IDs lexicographically → stable global ordering
- Slice
[offset : offset + n×6]to skip previously-processed conversations random.sample(slice, n, seed)→ final selection
Decision logging: Records transcript_skip for every rejected transcript (empty, too short, too few turns) with exact reason and evidence.
Outputs: raw_transcripts, validated_transcripts, validation_errors, decision_log
Responsibilities: Drive Claude Haiku API calls for all validated transcripts with ReAct gap-fill loop.
Provider: Determined by EXTRACTION_MODEL in config.py:
claude-*→anthropic.Anthropic(api_key=ANTHROPIC_API_KEY);CLAUDE_RATE_LIMITER.acquire()(50/60s)gemini-*→genai.Client(api_key=GEMINI_API_KEY);GEMINI_RATE_LIMITER.acquire()(15/60s)
Key parameters:
temperature=0.1— near-deterministic structured extractionmax_output_tokens=8192— sized to fit 70-field JSONthinking_budget=0(Gemini only) — prevents thinking tokens from truncating extraction JSONresponse_mime_type="application/json"(Gemini only) — guaranteed valid JSON
Budget: Uses token_tracker.cost_usd(prompt_tokens, completion_tokens) for accurate provider-aware cost — not a hardcoded rate.
Circuit breaker: _react_quota_exhausted flag in analyzer.py:
- Gemini 429: trips permanently for the run (daily quota exhausted)
- Claude 429: skips only the current call (transient per-minute limit)
Outputs: analysis_results, failed_call_ids, react_stats, decision_log
Responsibilities: Inline 100-point QA scoring; record all exclusion decisions.
| Dimension | Points | What is checked |
|---|---|---|
| Completeness | 30 | 19 required fields are non-null / non-empty |
| Enum validity | 25 | String fields match their allowed value sets |
| Consistency | 25 | Cross-field logical rules (FCR ≠ escalation, upsell intent vs outcome) |
| Plausibility | 20 | Duration [30–7200s], non-negative counts, issue count in [0,5] |
Grade bands: HIGH ≥ 85 · MEDIUM 60–84 · LOW < 60 (excluded from aggregation)
Decision logging: Every LOW exclusion and every borderline MEDIUM (60–65) is recorded with score, threshold, and reasoning.
Dynamic routing: If QualityGate fires (pass rate < 40%), _quality_gate_failed is set in qa_report and the graph routes directly to ExportAgent.
Outputs: analysis_results (QA-enriched), qa_report, qa_passed_results, decision_log
Responsibilities: Executive KPI computation from QA-passed results.
KPIs computed: FCR rate, AHT, escalation rate, avoidable call rate, agentic AI resolvability, repeat call risk distribution, sentiment improvement rate, phase-level duration breakdown, cost-lever savings opportunity (at $6.00/call industry benchmark).
Decision logging: Records aggregation_scope (QA-filtered vs full set) and cost_model_applied (provider pricing resolved from EXTRACTION_MODEL).
Outputs: aggregated_metrics, token_usage, decision_log
Responsibilities: Second LLM agent — 3-pass deliberation with provider hierarchy.
Provider hierarchy:
- NVIDIA NIM
meta/llama-3.3-70b-instruct— OpenAI-compatible,NVIDIA_API_KEYrequired - Claude Haiku 4.5 —
ANTHROPIC_API_KEYrequired - Gemini 2.0 Flash Lite — separate quota pool from extraction
- Rule-based fallback — KPI threshold rules; no API key needed
Context enrichment before deliberation:
MEMORY.get_context_for_insights()— flat run history (FCR/AHT trends, quota events)VECTOR_STORE.format_context(current_kpi_text)— top-K semantically similar historical runs
source field values in output:
llm_deliberated— all 3 passes succeededllm_single_pass— 1 pass succeeded (fallback after pass 2/3 failure)rule_based_fallback— all LLM calls failed
Decision logging: Records provider_selected (with reasoning for fallback chain position) and deliberation_outcome (critique quality grade when 3 passes complete).
Outputs: agent_insights (with deliberation_passes, critique, source), decision_log
Responsibilities: Human-in-the-loop checkpoint before any outputs are written.
REQUIRE_HUMAN_APPROVAL=False(default): transparent pass-through, logsauto-approvedREQUIRE_HUMAN_APPROVAL=True: displays KPI summary + insights source, promptsy/NAPPROVAL_TIMEOUT_S: auto-approves after N seconds;0blocks indefinitely- Non-interactive environments (CI, EOF): auto-approve silently
- Rejection: raises
RuntimeErrorand halts the pipeline cleanly
Decision logging: Always records approval_decision — approved/rejected/auto-approved — with FCR, insights source, and timeout as evidence.
Outputs: approval_granted, decision_log
Responsibilities: Persist all pipeline outputs and the complete audit trail to disk.
| File | Purpose |
|---|---|
call_results_{ts}.csv |
Per-call flat CSV for BI tools / analysts |
summary.json |
Streamlit dashboard source of truth (overwritten each run); includes decision_summary |
full_results_{ts}.json |
Complete per-call JSON with QA scores embedded |
qa_report_{ts}.json |
Standalone QA audit report |
insights_{ts}.json |
InsightsAgent deliberated recommendations |
decisions_{ts}.json |
Full decision log: total count, summary by agent/type, all records |
run_manifest_{ts}.json |
Immutable audit record: all counts, agent list, file paths, decision count |
audit_log_{ts}.json |
Every agent start/end, governance check, tool call, PII detection, error |
Outputs: export_paths
class PipelineState(TypedDict):
# ── Run configuration ────────────────────────────────────────────
n_calls: int
seed: int
offset: int
inter_call_delay: float
checkpoint_key: str
# ── Agent outputs (in execution order) ──────────────────────────
raw_transcripts: list # DataIngestionAgent
validated_transcripts: list # DataIngestionAgent
analysis_results: list # ExtractionAgent → enriched by QualityAgent
qa_report: dict # QualityAgent
qa_passed_results: list # QualityAgent
aggregated_metrics: dict # AggregationAgent
agent_insights: dict # InsightsAgent (source, passes, critique)
export_paths: dict # ExportAgent
# ── Telemetry ────────────────────────────────────────────────────
validation_errors: list
failed_call_ids: list
token_usage: dict
react_stats: dict # ReAct loop coverage improvement telemetry
approval_granted: bool # human approval gate result
# ── Traceability ─────────────────────────────────────────────────
decision_log: list # DecisionRecord dicts — agent reasoning audit trailState is immutable: every agent returns {**state, new_key: new_value}. Agents never mutate state in-place. Any agent can be replayed in isolation with the same input.
| Component | Trigger | Behaviour |
|---|---|---|
BudgetGuard |
After ExtractionAgent batch | Raises BudgetExceededError if cumulative cost > BUDGET_USD (from config.py); warns at 80%; uses token_tracker.cost_usd() — provider-aware, not hardcoded |
QualityGate |
After QualityAgent scores all results | Raises QualityGateError if pass rate < MIN_PASS_RATE (from config.py); sets _quality_gate_failed for graph routing |
PIIScanner |
DataIngestionAgent, per transcript | Detects 6 PII types (phone, SSN, email, credit card, DOB, account number); auto-redacts to [REDACTED] |
AuditLog |
Every agent boundary | Append-only structured event log: agent_start, agent_end, tool_call, governance_check, pii_detection, error |
Both BUDGET_USD and MIN_PASS_RATE are defined in pipeline/config.py — change them once, the singletons pick it up automatically.
run_batches.py
│
└── Orchestrator (pipeline/orchestrator.py)
│
│ WorkPlanner.plan() → [BatchTask × N]
│ AgentHealthMonitor — per-agent success/failure rates
│
├── Task 1: subprocess → run_pipeline.py --offset 0 --n 20
├── Task 2: subprocess → run_pipeline.py --offset 20 --n 20
├── Task 3: subprocess → run_pipeline.py --offset 40 --n 20
├── Task 4: subprocess → run_pipeline.py --offset 60 --n 20
└── Task 5: subprocess → run_pipeline.py --offset 80 --n 20
(failed tasks retried up to max_retries, then logged to AgentMemory)
Process isolation: each batch runs as a subprocess — a crash cannot corrupt other batches' checkpoints or state. Interrupted batches resume automatically from checkpoint.
Startup validation: run_pipeline.py validates API keys immediately on launch — before any imports that might fail silently. Key checked depends on EXTRACTION_MODEL: Claude → ANTHROPIC_API_KEY, Gemini → GEMINI_API_KEY. Missing key exits immediately with a clear message.
telecom-call-intelligence/
├── pipeline/
│ ├── config.py ← All constants (model, thresholds, limits, paths)
│ ├── security.py ← InputSanitizer, OutputSanitizer, AgentScopeGuard,
│ │ SecretGuard, RateLimiter (Claude + Gemini)
│ ├── decision_log.py ← DecisionRecord, DecisionLogger, summarize_decisions
│ ├── vector_memory.py ← VectorMemoryStore (Gemini embeddings, cosine similarity)
│ ├── agents/
│ │ ├── data_agent.py ← Agent 1: DataIngestionAgent
│ │ ├── extraction_agent.py ← Agent 2: ExtractionAgent (Claude Haiku + ReAct)
│ │ ├── quality_agent.py ← Agent 3: QualityAgent (100-pt QA)
│ │ ├── aggregation_agent.py ← Agent 4: AggregationAgent (KPIs)
│ │ ├── insights_agent.py ← Agent 5: InsightsAgent (NVIDIA NIM deliberation)
│ │ └── export_agent.py ← Agent 6: ExportAgent
│ ├── graph.py ← LangGraph (7 nodes, conditional routing,
│ │ approval gate, decision logging, LangSmith)
│ ├── orchestrator.py ← WorkPlanner, AgentHealthMonitor, adaptive retry
│ ├── governance.py ← BudgetGuard, QualityGate, PIIScanner, AuditLog
│ ├── memory.py ← AgentMemory — flat JSON cross-run store
│ ├── tools.py ← ToolRegistry — JSON-schema tool definitions
│ ├── analyzer.py ← Claude/Gemini client (CoT, ReAct, checkpoint, backoff)
│ ├── aggregator.py ← KPI computation + cost-lever estimates + issue_breakdown (category × segment × resolution-method cross-tab)
│ ├── hf_loader.py ← CSV loader + HuggingFace streaming + offset batching
│ ├── token_tracker.py ← Model-aware token cost accounting
│ └── logger.py ← Structured logging (INFO→stdout, DEBUG→file)
├── prompts/
│ └── system_prompt.txt ← 70-field extraction schema + CoT instructions
├── tests/
│ ├── test_config.py
│ ├── test_decision_log.py ← 24 decision traceability tests
│ ├── test_governance.py
│ ├── test_memory.py
│ ├── test_orchestrator.py
│ ├── test_tools.py
│ ├── test_graph.py
│ └── test_security.py ← 51 security tests
├── dashboard/app.py ← Streamlit executive dashboard (6 sections, light theme)
├── run_pipeline.py ← Single-batch entry point (model-aware key validation)
├── run_batches.py ← Multi-batch orchestrator (delegates to Orchestrator)
├── merge_outputs.py ← Merge batch JSONs → combined dataset
└── qa_audit.py ← Standalone QA scoring tool
-
Stateless agents — each agent receives state, produces updated state, holds no instance data. Safe to instantiate once and reuse across invocations.
-
Immutable state handoff — every agent returns
{**state, new_key: new_value}. No mutation. Any agent can be replayed in isolation with the same inputs. -
Defence in depth — security applied at every boundary: input sanitisation before LLM, output sanitisation after LLM, scope guards before tool invocation, secret checks after every LLM response.
-
Graceful degradation — InsightsAgent degrades pass-by-pass (3→2→1→rule-based). QualityAgent skips with a warning on empty input. The pipeline always completes with a manifest and audit trail.
-
Decision traceability — every autonomous decision is recorded with WHAT, WHY, evidence, and alternatives. Operators can retrace and challenge any agent decision from the
decisions_{ts}.jsonfile. -
Observable — every agent logs at INFO with
[AgentName]prefix. LangSmith traces full node-level spans whenLANGCHAIN_TRACING_V2=true. AuditLog records every governance check. DecisionLog records every reasoning step. -
Composable — to parallelise ExtractionAgent, replace
extract_nodewith a LangGraphSend-based fan-out.MAX_CONCURRENT_EXTRACTIONSis already in config. No other files change. -
Plug-and-play models —
EXTRACTION_MODELinconfig.pyis the single knob. All downstream clients, rate limiters, cost estimates, provider labels, and API key validation resolve automatically. One change, zero regressions. -
Reproducible — any run is fully reproducible from
(offset, n, seed). The run manifest records the exact parameters, agent list, decision count, and all output file paths.