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.
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:
- Graph Entry — The
StateGraphdispatches the agent function with anAgentStatecontainingmessages,data(tickers, portfolio, date range), andmetadata(model config). - State Read — The agent reads
state["data"]["tickers"]to discover it needs to analyzeAAPL. TheAnnotated[dict, merge_dicts]reducer ensures parallel agents merge cleanly. - Price Fetch — Agent calls
get_prices("AAPL", start_date, end_date)intools/api.py. The function builds a cache key:"AAPL_2025-01-01_2025-12-31". - Cache Lookup —
Cache.get_prices(cache_key)checks the in-memory_prices_cachedict. On a hit, it returns immediately — no HTTP call needed. - API Request — On a cache miss,
_make_api_request()sends a GET toapi.financialdatasets.aiwith theX-API-KEYheader. If the API returns HTTP 429, it retries with linear backoff (60s, 90s, 120s). - Response Parsing — The raw JSON is validated through the
PriceResponsePydantic model, producing a list ofPriceobjects. - Cache Write —
Cache.set_prices()merges new data with any existing entries using thetimefield as a deduplication key — O(n) merge with O(1) key lookup. - LLM Model Resolution — Agent calls
get_model(model_name, model_provider). The function matches theModelProviderenum and instantiates the correct LangChain chat class (e.g.,ChatOpenAI). - 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. - State Write — The agent writes its analysis to
state["data"]["analyst_signals"]. Themerge_dictsreducer merges this with signals from other parallel agents. - 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
| Decision | Choice Made | Alternatives Considered | Why 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 Mode | Trigger | Detection | Recovery / 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:
- Cache lookup (
get_prices,get_financial_metrics): O(1) dict lookup by cache key. This is the fast path that avoids network I/O entirely. - Cache merge (
_merge_data): O(n+m) where n = existing items, m = new items. The set construction dominates; iteration is linear. Called once per API response. get_model()dispatch: O(1) provider enum match. Instantiates LangChain client objects which cache underlying HTTP sessions.
Resource Usage
- Memory: Cache stores raw dicts (not Pydantic objects) to minimize memory. A typical run with 5 tickers and 1 year of daily prices uses approximately 2-5 MB of cache memory.
- Network I/O: API calls to
financialdatasets.aiare the primary bottleneck (200-800ms per call). Cache hits eliminate this entirely. Rate-limited retries add 60-120s delays. - LLM Latency: Model instantiation is fast (~10ms); inference latency depends on the provider (1-30s per call). This is external to core infrastructure but dominates end-to-end time.
Sub-Modules
graph/
LangGraph workflow orchestration and AgentState definition.
AgentStateTypedDict with annotated reducersStateGraphworkflow builder and compiler- Agent reasoning debug output
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
| Term | Definition |
|---|---|
AgentState | A TypedDict with three annotated fields that serves as the shared state contract for all agents in the LangGraph workflow |
StateGraph | LangGraph's workflow builder class that compiles agent functions and edges into an executable graph |
ModelProvider | Enum of 14 supported LLM backends (OpenAI, Anthropic, Groq, DeepSeek, etc.) |
merge_dicts | Custom reducer function that performs shallow dict merging, enabling parallel agents to write to different keys without conflicts |
_make_api_request | Internal function wrapping HTTP requests with linear backoff retry logic for 429 rate-limit responses |
Cache._merge_data | Generic deduplication method that merges new API responses with existing cached data using a configurable key field |