core — Graph, Data & LLM Infrastructure

Graph, Data & LLM Infrastructure

The foundational plumbing that every agent depends on — LangGraph workflow orchestration, shared state Portfolio Management" href="../agents_doc/management/index.html">management, LLM provider abstraction, financial data API tools, and caching.

🎯 Why This Module Exists

Without core infrastructure, every agent would need to independently manage LLM connections, data fetching, state management, and CLI parsing. By centralizing these concerns, the project achieves consistency and extensibility. The graph/ submodule defines AgentState — a TypedDict with three annotated fields (messages, data, metadata) that serves as the universal contract all agents read from and write to. The llm/ submodule abstracts away provider differences through a ModelProvider enum and LLMModel Pydantic class, so switching from OpenAI to Groq or Anthropic is a one-line change in configuration. The tools/ submodule provides a single financial data gateway with automatic rate-limit handling and retry logic. The data/ submodule handles Pydantic models and an in-memory cache with intelligent merge-on-write deduplication. This separation means adding a new agent requires zero infrastructure changes — just import AgentState, call get_model(), and use the API tools.

💡 Real-world analogy

Core infrastructure is like the plumbing, electrical wiring, and HVAC in an office building. Individual offices (agents) don't need to dig wells for water (financial data), generate their own electricity (LLM inference), or install their own thermostats (state management). They just plug into the building's centralized systems. When the building upgrades from gas heating to heat pumps (OpenAI to Anthropic), every office benefits automatically without any renovation.

20
Files
2
Sub-Modules
~2.7k
Lines of Code
Python
Language

LangGraph StateGraph Multi-Provider LLM Financial Data API

Architecture

flowchart LR
  subgraph Graph["graph/"]
    AS["AgentState\n(TypedDict)"]
    SG["StateGraph\nWorkflow Builder"]
    SAR["show_agent_reasoning()"]
  end
  subgraph LLM["llm/"]
    MP["ModelProvider\n(14 providers)"]
    LM["LLMModel\n(Pydantic)"]
    GM["get_model()"]
  end
  subgraph Tools["tools/"]
    API["api.py\nget_prices / get_financials"]
    RL["_make_api_request()\nRate-limit + Retry"]
  end
  subgraph Data["data/"]
    Cache["Cache\n(In-memory)"]
    Models["Pydantic Models\nPrice, FinancialMetrics, ..."]
  end
  AS --> SG
  GM --> MP
  GM --> LM
  API --> RL
  API --> Cache
  API --> Models

Data Flow

flowchart TD
  Agent["Agent Function"] -->|"reads/writes"| State["AgentState\nmessages + data + metadata"]
  Agent -->|"calls"| GetModel["get_model()\nllm/models.py"]
  GetModel --> Provider["LLM Provider\n(OpenAI / Anthropic / Groq / ...)"]
  Agent -->|"calls"| APITools["get_prices() / get_financials()\ntools/api.py"]
  APITools --> CacheCheck{"Cache Hit?"}
  CacheCheck -->|"Yes"| CachedData["Return Cached Data"]
  CacheCheck -->|"No"| ExternalAPI["Financial Datasets API"]
  ExternalAPI --> RateLimit["_make_api_request()\nRetry on 429"]
  RateLimit --> StoreCache["Cache.set_*()\nMerge + Deduplicate"]
  StoreCache --> CachedData

🚶 Step-by-Step Narrated Walkthrough

Follow a single agent requesting AAPL price data and generating an LLM analysis as it flows through the core infrastructure:

  1. Graph Entry — The StateGraph dispatches the agent function with an AgentState containing messages, data (tickers, portfolio, date range), and metadata (model config).
  2. State Read — The agent reads state["data"]["tickers"] to discover it needs to analyze AAPL. The Annotated[dict, merge_dicts] reducer ensures parallel agents merge cleanly.
  3. Price Fetch — Agent calls get_prices("AAPL", start_date, end_date) in tools/api.py. The function builds a cache key: "AAPL_2025-01-01_2025-12-31".
  4. Cache Lookup — Cache.get_prices(cache_key) checks the in-memory _prices_cache dict. On a hit, it returns immediately — no HTTP call needed.
  5. API Request — On a cache miss, _make_api_request() sends a GET to api.financialdatasets.ai with the X-API-KEY header. If the API returns HTTP 429, it retries with linear backoff (60s, 90s, 120s).
  6. Response Parsing — The raw JSON is validated through the PriceResponse Pydantic model, producing a list of Price objects.
  7. Cache Write — Cache.set_prices() merges new data with any existing entries using the time field as a deduplication key — O(n) merge with O(1) key lookup.
  8. LLM Model Resolution — Agent calls get_model(model_name, model_provider). The function matches the ModelProvider enum and instantiates the correct LangChain chat class (e.g., ChatOpenAI).
  9. LLM Invocation — The agent sends a structured prompt to the resolved LLM. LLMModel.has_json_mode() determines whether to request JSON output directly or parse free-text.
  10. State Write — The agent writes its analysis to state["data"]["analyst_signals"]. The merge_dicts reducer merges this with signals from other parallel agents.
  11. Debug Output — show_agent_reasoning(output, agent_name) serializes the result to formatted JSON for console debugging, handling Pandas objects, custom classes, and raw strings.

🏗️ Design Decisions

DecisionChoice MadeAlternatives ConsideredWhy This Approach
State management TypedDict with Annotated reducers Pydantic model, plain dict, dataclass LangGraph requires TypedDict for state; the Annotated reducers (operator.add for messages, merge_dicts for data) enable safe parallel agent writes without locks
LLM abstraction ModelProvider enum + JSON config file Hardcoded provider map, environment-based auto-detection Enum provides compile-time safety; JSON config allows adding models without code changes; the LLMModel Pydantic class handles provider-specific quirks (has_json_mode, is_deepseek)
Caching strategy In-memory dict with merge-on-write deduplication Redis, SQLite, file-based cache No external dependencies; merge deduplication prevents data bloat when overlapping date ranges are fetched; sufficient for single-run CLI workflows
API retry strategy Linear backoff (60s + 30s * attempt) Exponential backoff, circuit breaker Financial API rate limits are time-based quotas, not load-based; linear backoff with long delays (60-120s) matches typical quota reset windows
Data validation Pydantic models for API responses Raw dicts, TypedDict, manual validation Pydantic provides runtime type checking, serialization, and documentation; models like Price, FinancialMetrics, InsiderTrade are reused across agents and tools

🔬 Algorithm Spotlight

Cache Merge-on-Write Deduplication

The Problem: Multiple agents may request overlapping date ranges for the same ticker. If AAPL data for Jan-Jun is cached and an agent requests Mar-Sep, the new fetch returns Mar-Sep data that partially overlaps with the cache. Without deduplication, downstream agents would see duplicate data points that distort moving averages, volatility calculations, and other time-series analyses.

The Approach: Cache._merge_data() builds a set of existing keys (O(n) construction, O(1) lookup per item), then appends only items whose key field is not already present. The key field varies by data type: time for prices, report_period for financials, filing_date for insider trades, and date for company news. The method is generic — it accepts a key_field parameter, so adding a new data type requires no changes to the merge logic.

Why This Algorithm: A naive append would cause duplicate entries that skew analysis. A full replacement would lose earlier data outside the new range. The merge approach preserves all unique data points with O(n+m) time complexity where n is existing items and m is new items.

flowchart TD
    A["New API data arrives\n(m items)"] --> B["Build existing_keys set\nfrom cached data (n items)"]
    B --> C{"For each new item:\nkey in existing_keys?"}
    C -->|"Yes"| D["Skip\n(already cached)"]
    C -->|"No"| E["Append to merged list"]
    D --> F["Return merged list\n(no duplicates)"]
    E --> F
Trace through an example

Given: Cache has AAPL prices for dates [Jan-01, Jan-02, Jan-03]. New fetch returns [Jan-02, Jan-03, Jan-04, Jan-05].

Step 1: Build existing_keys = {"Jan-01", "Jan-02", "Jan-03"} from cached data.

Step 2: Iterate new data: Jan-02 is in set (skip), Jan-03 is in set (skip), Jan-04 is not (append), Jan-05 is not (append).

Result: Merged cache contains [Jan-01, Jan-02, Jan-03, Jan-04, Jan-05] — five unique entries. A naive append would have produced 7 entries with duplicates that would distort moving averages.

⚠️ What Could Go Wrong

Failure ModeTriggerDetectionRecovery / Mitigation
API rate limiting (HTTP 429) Too many requests within the API quota window response.status_code == 429 check in _make_api_request() Linear backoff retry (60s, 90s, 120s) up to max_retries. After exhausting retries, returns the 429 response and the caller receives an empty list
Missing API key FINANCIAL_DATASETS_API_KEY env var not set and no key passed as argument Request proceeds without X-API-KEY header API returns 401/403; get_prices() returns an empty list. Some endpoints may work without a key for limited data
Unsupported LLM provider Model name or provider string not in ModelProvider enum Pydantic validation error when constructing LLMModel Raises ValueError at startup, preventing silent misconfiguration. The 14-provider enum covers all supported backends
State merge conflict Two parallel agents write to the same data key Not detected — last-write-wins via merge_dicts By convention, each agent writes to its own namespaced key (e.g., analyst_signals[agent_name]). The merge_dicts reducer is shallow, so agents must not share sub-keys
Cache memory growth Long-running session fetching many tickers across wide date ranges No built-in detection; relies on process lifetime Cache is in-memory with no eviction policy. Mitigated by the fact that CLI runs are short-lived. The web app creates fresh graph instances per request

⚡ Performance Profile

Hot Paths

The most performance-sensitive code paths in this module:

Resource Usage

Sub-Modules

Orchestration" class="card" href="graph/index.html">

graph/

LangGraph workflow orchestration and AgentState definition.

data-tools/

Data models, caching, financial API tools, LLM provider abstraction, CLI, and utilities.

  • In-memory cache with merge deduplication
  • 14-provider LLM abstraction layer
  • Financial Datasets API client with retry

Key Interfaces

AgentState Contract

Every agent in the system reads from and writes to the same AgentState TypedDict. This is the universal contract that enables plug-and-play agent development:

class AgentState(TypedDict):
    messages: Annotated[Sequence[BaseMessage], operator.add]
    data: Annotated[dict[str, any], merge_dicts]
    metadata: Annotated[dict[str, any], merge_dicts]

The Annotated type hints attach reducer functions that LangGraph calls when merging parallel agent outputs. operator.add concatenates message lists; merge_dicts performs shallow dict merging ({**a, **b}). This design means agents never need to coordinate directly — they simply write to their own namespace in data and the framework handles the rest.

ModelProvider Abstraction

The ModelProvider enum defines all 14 supported LLM backends. Combined with the LLMModel Pydantic class, it provides a type-safe way to specify models:

class ModelProvider(str, Enum):
    OPENAI = "OpenAI"
    ANTHROPIC = "Anthropic"
    GROQ = "Groq"
    DEEPSEEK = "DeepSeek"
    # ... 10 more providers

class LLMModel(BaseModel):
    display_name: str
    model_name: str
    provider: ModelProvider

The has_json_mode() method is particularly important — it encapsulates provider-specific knowledge about structured output support, preventing runtime errors when agents request JSON responses from models that don't support it.

Directory Map

Source tree for core
src/
├── graph/                         ← Workflow orchestration
│   ├── state.py                   ← AgentState TypedDict + merge_dicts reducer
│   └── __init__.py
├── llm/                           ← LLM provider abstraction
│   ├── models.py                  ← ModelProvider enum, LLMModel, get_model()
│   └── models.json                ← Model catalog (names, providers, display names)
├── tools/                         ← Financial data gateway
│   └── api.py                     ← get_prices(), get_financials(), _make_api_request()
├── data/                          ← Data layer
│   ├── cache.py                   ← Cache class with merge-on-write deduplication
│   └── models.py                  ← Pydantic models (Price, FinancialMetrics, ...)
├── utils/                         ← Shared utilities
│   ├── analysts.py                ← ANALYST_CONFIG registry
│   └── progress.py                ← Progress tracking for SSE streaming
└── main.py                        ← CLI entry point and graph builder

Glossary

TermDefinition
AgentStateA TypedDict with three annotated fields that serves as the shared state contract for all agents in the LangGraph workflow
StateGraphLangGraph's workflow builder class that compiles agent functions and edges into an executable graph
ModelProviderEnum of 14 supported LLM backends (OpenAI, Anthropic, Groq, DeepSeek, etc.)
merge_dictsCustom reducer function that performs shallow dict merging, enabling parallel agents to write to different keys without conflicts
_make_api_requestInternal function wrapping HTTP requests with linear backoff retry logic for 429 rate-limit responses
Cache._merge_dataGeneric deduplication method that merges new API responses with existing cached data using a configurable key field