Argent combines live market data, financial news, sentiment analysis, and private analyst reports into evidence-based company research. Users can follow the agent's plan, tool calls, and report synthesis as they happen.
Important
Argent produces informational research, not personalized financial advice. Verify model output and source data independently before making financial decisions.
Financial analysts work across fragmented sources: market feeds, price history, news, sentiment signals, and proprietary strategy reports. Gathering that evidence manually can take 4-6 hours per company, with as much as 60-70% of analyst time spent collecting data instead of interpreting it.
Argent turns that disconnected workflow into one traceable research process. It automates evidence collection and first-pass synthesis while keeping the human informed through source citations, visible tool activity, explicit research gaps, and confidence-qualified conclusions. The goal is not to replace analyst judgment; it is to give that judgment better inputs, faster.
| Layer | Responsibility |
|---|---|
| Next.js workspace | Accepts research questions and displays live agent progress, reports, and prior research |
| FastAPI service | Manages threads and runs, streams SSE events, exposes health checks and Prometheus metrics |
| LangGraph agent | Plans the research workflow, selects tools, handles recoverable failures, and synthesizes the report |
| Research tools | Retrieve market prices, historical performance, financial news, sentiment, and private analyst evidence |
| Data layer | Stores run history in PostgreSQL and vectorized private reports in ChromaDB |
| Observability | Uses Prometheus, Grafana, structured logging, and optional LangSmith tracing |
Researcher -> Next.js -> FastAPI -> LangGraph -> Financial and RAG tools
^ | |
| v v
+-- SSE -- PostgreSQL ChromaDB
|
Prometheus/Grafana
Users submit a company analysis or comparison through a browser workspace. A LangGraph agent decides which tools are needed, gathers evidence, handles recoverable tool failures, and writes a structured research report.
For a full company analysis, the agent can:
- Retrieve current price, volume, and market capitalization.
- Calculate historical performance over a requested period, typically three years.
- Search recent financial news.
- Measure sentiment from the retrieved material.
- Retrieve company AI initiatives from private PDF reports through RAG.
- Synthesize financial metrics, sentiment, opportunities, risks, research gaps, and a confidence-qualified research view.
- Autonomous tool orchestration with LangGraph conditional routing
- Five research tools for price, history, news, sentiment, and private RAG
- Grounded private-document answers with source citations
- Live execution visibility through Server-Sent Events (SSE)
- Persistent research history using PostgreSQL in production or SQLite locally
- Graceful degradation when individual tools or providers fail
- Structured research reports with risks, opportunities, gaps, and confidence
- Prompt profiles for traditional, basic, and full autonomous behavior
- Operational metrics for runs, duration, tool calls, and SSE connections
- LangSmith tracing through environment configuration
- Production containers for the API and Next.js frontend
- Prometheus and Grafana monitoring configuration
flowchart LR
U[Researcher] --> UI[Next.js workspace]
UI -->|REST| API[FastAPI]
API --> RUN[Run service]
RUN --> AGENT[LangGraph agent]
RUN --> POLICY{Routing policy}
POLICY -->|tier, tools, RAG| AGENT
AGENT --> ROUTER{Tool calls?}
ROUTER -->|Yes| TOOLS[Tool node]
TOOLS --> PRICE[Yahoo Finance price]
TOOLS --> HISTORY[Yahoo Finance history]
TOOLS --> NEWS[Tavily news search]
TOOLS --> SENTIMENT[OpenAI sentiment]
TOOLS --> RAG[Private RAG tool]
RAG --> CHROMA[(ChromaDB)]
CHROMA --> PDF[Analyst PDF reports]
TOOLS --> AGENT
ROUTER -->|No| REPORT[Research report]
REPORT --> RUN
RUN -->|SSE events| UI
RUN --> STORE[(PostgreSQL / SQLite history)]
API --> METRICS[Prometheus metrics]
METRICS --> GRAFANA[Grafana]
AGENT -. traces .-> LANGSMITH[LangSmith]
sequenceDiagram
participant User
participant UI as Web UI
participant API as FastAPI
participant Agent as LangGraph Agent
participant Tools
User->>UI: Submit research question
UI->>API: Create thread and run
API-->>UI: Run accepted
API->>Agent: Execute research
Agent-->>UI: Planning event
loop Until sufficient evidence is collected
Agent->>Tools: Select and invoke tools
Tools-->>Agent: Structured evidence or safe error
Agent-->>UI: Tool progress over SSE
end
Agent-->>API: Final structured report
API-->>UI: Completion event and report
Before invoking the agent, each run is classified by an LLM-based routing
policy (app/routing/policy.py) that decides:
- Model tier:
fast(e.g.gpt-4o-mini) for narrow factual lookups, orcapable(e.g.gpt-4o) for comparison/full-analysis queries. - Tool subset: a narrower tool set for simple queries (e.g. just
get_stock_pricefor a plain price lookup) versus the full tool set for research-style queries. - RAG engagement: whether the private-database tool is included, subject
to the caller's
with_ragflag as a hard off-switch.
The classifier itself always runs on the cheap fast-tier model with
structured output, regardless of which tier it ultimately routes the query
to, so the extra classification call stays low-cost. If classification fails
for any reason (timeout, provider error), routing falls back to a safe
maximal default -- capable tier, full tool set -- the same graceful-
degradation pattern used by the sentiment-analysis tool's keyword fallback.
The decision is persisted on the run record (model_tier, provider,
model_name, tool_subset, rag_engaged) and emitted as a routing.decided
SSE event -- including the classifier's one-sentence reasoning -- before any
tool activity, so it's auditable the same way tool calls are. Set
ROUTING_ENABLED=false to bypass the classifier entirely and restore the
legacy behavior (always the full tool set, single model, no extra LLM call).
Classification logic (structured-output parsing, fallback behavior) is unit
tested offline with an injectable fake model; a small live-marked test
suite (app/tests/test_routing_policy_live.py) verifies the real classifier
against realistic phrasing when RUN_LIVE_AGENT_TESTS=true.
Set REDIS_URL to move agent execution off the API process and onto a
separate worker process (app/worker.py, an arq task queue): POST .../runs enqueues a job instead of running it via FastAPI's in-process
BackgroundTasks, so a queued run survives an API restart and multiple API
replicas can safely share one pool of workers. SSE event delivery fans out
through Redis pub/sub (app/api/event_fanout.py) rather than an in-process
queue, so a client subscribed to one API replica still receives events for a
run executing on a different worker process -- the RunStore interface
(subscribe/unsubscribe/append_event) is unchanged; this is purely an
internal implementation swap. Leave REDIS_URL empty to keep the original
single-process behavior (execution via BackgroundTasks, SSE fan-out
in-process) with no code changes needed -- useful for local development
without a Redis instance running.
docker compose up -d redis worker apiThe LangGraph conversational checkpointer is not yet Redis-backed (still
MemorySaver, in-process) -- see Current Limitations and Roadmap.
Every API route except /health/live and /health/ready requires
Authorization: Bearer <key>. Scope is API-key based with a role baked into
each key (user or admin) rather than full JWT -- appropriate at this
project's scale, and the same key record reuses cleanly as JWT claims later
if needed. Keys are stored hashed (SHA-256, salted via AUTH_API_KEY_SALT);
the raw key is only ever shown once, at creation time.
userkeys can create threads/runs, and only see their own -- threads and runs created before auth existed (owner_key_idisnull) are visible toadminkeys only, a fail-closed default rather than treating unowned resources as public.adminkeys can see everything and manage API keys viaPOST/GET /api/v1/admin/api-keysandDELETE /api/v1/admin/api-keys/{id}.- Set
AUTH_BOOTSTRAP_ADMIN_KEYto seed an initial admin key on startup (idempotent -- safe to leave set across restarts), then create further keys through the admin endpoint and rotate/revoke the bootstrap key.
curl -X POST http://localhost/backend/api/v1/admin/api-keys \
-H "Authorization: Bearer $AUTH_BOOTSTRAP_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"label": "my-frontend", "role": "user"}'Private company reports are processed as follows:
PDF or ZIP archive
-> safe extraction
-> PDF page loading
-> token-aware chunking (1,000 tokens, 200 overlap by default)
-> OpenAI embeddings
-> persistent Chroma collection
-> semantic top-k retrieval
-> grounded answer with document citations
The current sample corpus covers Amazon, Alphabet, IBM, Microsoft, and NVIDIA. Source documents and generated indexes are intentionally excluded from Git.
| Area | Technologies |
|---|---|
| Agent and LLM | LangGraph, LangChain, OpenAI |
| Financial data | yfinance / Yahoo Finance |
| News | Tavily Search |
| Retrieval | ChromaDB, OpenAI embeddings, PyPDF, tiktoken |
| Backend | Python, FastAPI, Pydantic, SSE |
| Persistence | PostgreSQL in production, SQLite locally, ChromaDB for vectors |
| Task queue | arq, Redis (optional -- see Durable Worker Queue below) |
| Frontend | Next.js, React, TypeScript, React Markdown, Lucide |
| Observability | Prometheus, Grafana, LangSmith, structured logging |
| Deployment | Docker, Docker Compose, Nginx |
| Testing | pytest, HTTPX, ESLint, TypeScript |
| CI/CD | GitHub Actions, Trivy (dependency + container scanning), GHCR |
Agentic_RAG/
├── app/
│ ├── agent/ # LangGraph, prompts, state, and agent nodes
│ ├── api/ # FastAPI routes, schemas, run service, and stores
│ ├── eval/ # Golden dataset, scoring, and evaluation harness
│ ├── observability/ # LangSmith tracing setup
│ ├── providers/ # Multi-provider chat/embedding factory and pricing
│ ├── rag/ # Loading, splitting, embedding, indexing, retrieval
│ ├── routing/ # Policy-based model tier / tool subset / RAG routing
│ ├── tools/ # Market, news, sentiment, and private RAG tools
│ ├── tests/ # Unit, workflow, API, retrieval, and live tests
│ ├── config.py # Environment-backed configuration
│ ├── worker.py # arq worker entry point (durable job queue)
│ └── main.py # FastAPI application entry point
├── frontend/
│ ├── app/ # Next.js workspace and styling
│ └── lib/ # Typed API client and shared types
├── deploy/
│ ├── grafana/ # Provisioned dashboards and data source
│ ├── prometheus/ # Metrics scraping configuration
│ └── nginx.conf # Production reverse proxy
├── docs/ # Project documentation and local source archive
├── Dockerfile
├── compose.yml
├── requirements.txt
└── .env.example
- Python 3.12 or newer
- Node.js 20.9 or newer
- OpenAI API credentials
- Tavily API credentials for live news search
git clone https://github.com/sebtosca/Financial_Research_Agent.git
cd Financial_Research_Agentpython -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -r requirements.txtUsing an existing Python environment is also supported; the virtual environment is recommended to isolate dependencies.
cp .env.example .envAt minimum, configure:
OPENAI_API_KEY=your-openai-key
OPENAI_API_BASE=
OPENAI_MODEL=gpt-4o-mini
TAVILY_API_KEY=your-tavily-key
APP_CORS_ORIGINS=http://localhost:3000
RUN_STORE_PATH=./data/research_history.sqlite3
DATABASE_URL=
DOCS_PATH=./app/docs
ZIP_FILE=./docs/Companies-AI-Initiatives.zip
CHROMA_DB_DIR=./chroma_dbWhen DATABASE_URL is set, the API stores threads, runs, events, and
cancellation state in PostgreSQL. When it is empty, local development falls
back to SQLite at RUN_STORE_PATH.
LangSmith tracing is optional:
LANGCHAIN_TRACING_V2=true
LANGCHAIN_API_KEY=your-langsmith-key
LANGCHAIN_PROJECT=financial-research-agentPlace PDF files below DOCS_PATH, or provide the ZIP archive configured by
ZIP_FILE, then run:
python -m app.rag.indexThe command safely extracts the archive when necessary and persists the Chroma
index at CHROMA_DB_DIR. Rebuild the index whenever the source corpus changes.
uvicorn app.main:app --reload --host 127.0.0.1 --port 8000- API:
http://localhost:8000 - OpenAPI documentation:
http://localhost:8000/docs - Metrics:
http://localhost:8000/metrics - Health:
http://localhost:8000/api/v1/health/live
The frontend calls the same authenticated API as everything else (see
Authentication and Authorization below), so it needs its own key. Mint a
user-role key with the backend running and AUTH_BOOTSTRAP_ADMIN_KEY set,
then create frontend/.env.local (already covered by .gitignore) with it:
curl -X POST http://localhost:8000/api/v1/admin/api-keys \
-H "Authorization: Bearer $AUTH_BOOTSTRAP_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"label": "frontend-dev", "role": "user"}'
# -> copy the returned key into frontend/.env.local:
# NEXT_PUBLIC_API_KEY=<key>In another terminal:
cd frontend
npm ci
npm run devOpen http://localhost:3000.
Analyze NVIDIA's investment outlook and AI research initiatives.Compare Microsoft and Google across AI strategy and market sentiment.Assess Amazon's three-year performance and current AI opportunities.Which company has the most innovative AI research? Provide evidence.Rank MSFT, GOOGL, NVDA, AMZN, and IBM by financial strength and AI positioning.
The frontend uses the same public API available to other clients. All routes below require an API key (see Authentication and Authorization above):
# Create a research thread
curl -X POST http://localhost:8000/api/v1/threads \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"title":"NVIDIA research"}'
# Start a run using the returned thread ID
curl -X POST http://localhost:8000/api/v1/threads/THREAD_ID/runs \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"query":"Analyze NVIDIA and its AI initiatives","with_rag":true}'
# Stream progress using the returned run ID
curl -N http://localhost:8000/api/v1/runs/RUN_ID/events \
-H "Authorization: Bearer $API_KEY"Run deterministic tests without external provider calls:
pytest -q -m "not integration"Run the live integration suite only when API credentials and network access are available:
pytest -q -m integration.github/workflows/ci.yml runs on every pull request and push to main:
deterministic backend tests, frontend typecheck/lint/build, a Trivy filesystem
scan of dependencies, and a Docker image build + Trivy image scan (fails on
CRITICAL findings, exceptions tracked in .trivyignore). On main, both
images are pushed to ghcr.io. .github/workflows/nightly.yml runs the live
integration/slow suite on a schedule (never on pull requests, so real API
keys stay out of PR/fork reach). .github/workflows/eval.yml runs the
evaluation harness (python -m app.eval.run --full-agent --judge llm) weekly
and uploads the report as a build artifact -- kept out of default CI since it
costs real API money. Requiring these checks before merge is configured via
GitHub branch protection (repository settings, not tracked in this repo).
Validate the frontend:
cd frontend
npm run typecheck
npm run lint
npm run buildThe deterministic suite currently covers agent configuration, tool-error handling, API lifecycle behavior, SQLite persistence, RAG safety checks, golden retrieval queries, indexing, prompts, and multi-tool workflow synthesis.
A versioned golden dataset and evaluation pipeline live in app/eval/
(EVAL_DATASET_VERSION = "v1", ~10 cases spanning price/history/news/
sentiment lookups, private-RAG queries, and full research/comparison
requests). It measures:
- Tool-selection/trajectory accuracy -- precision/recall/F1 of actual vs. expected tool calls, and whether RAG engagement matched expectations.
- Groundedness/relevance -- a zero-cost heuristic (vocabulary overlap) runs by default; an opt-in LLM-as-judge path gives richer signal.
- Latency/cost -- captured per run via Prometheus and persisted on the run record (see Observability below).
Run it directly:
python -m app.eval.run # routing-only, cheap
python -m app.eval.run --full-agent --judge llm # full pipeline, real cost
python -m app.eval.run --report jsonThis is a live evaluation against real providers (requires
OPENAI_API_KEY, and TAVILY_API_KEY for news-touching cases) and is
intentionally separate from the pytest suite so it can be invoked directly
from a CI job later without redesign. The scoring math itself (precision/
recall, heuristic overlap, judge-response parsing) is unit tested offline
with synthetic inputs; a live-marked suite (test_eval_live.py) exercises
the real classifier/agent/judge end to end.
Chat and embedding models are built through a small provider factory
(app/providers/) instead of being hardcoded to OpenAI. Set CHAT_PROVIDER
(or a per-tool override such as SENTIMENT_PROVIDER/PRIVATE_DATABASE_PROVIDER)
and EMBEDDING_PROVIDER to openai, anthropic, or google. OpenAI is the
only provider exercised against a live API in this repo today; Anthropic and
Google adapters are implemented and covered by offline dispatch/config tests
only. To use them for real, install the optional adapters and set the
matching API key:
pip install -r requirements-optional.txtThe backend exposes these Prometheus metrics:
financial_agent_runs_total{status}financial_agent_active_runsfinancial_agent_run_duration_secondsfinancial_agent_tool_calls_total{tool,status}financial_agent_tool_call_duration_seconds{tool}financial_agent_sse_connectionsfinancial_agent_routing_decisions_total{model_tier}financial_agent_llm_tokens_total{model,direction}financial_agent_llm_cost_usd_total{model}(approximate -- seeapp/providers/pricing.py)
Per-run token counts and estimated cost are also persisted on the run record
(prompt_tokens, completion_tokens, estimated_cost_usd), captured from
each LLM response's usage_metadata -- the same field LangChain populates
consistently across OpenAI/Anthropic/Google, so cost tracking needs no
per-provider branching.
Grafana is provisioned with an agent overview dashboard. The frontend presents user-facing execution stages, while Grafana, Prometheus, and LangSmith remain operator tools and are not exposed directly in the browser UI.
When LANGCHAIN_TRACING_V2=true, every run is explicitly tagged and named
(not just auto-instrumented) via app/observability/tracing.py: each trace
carries model_tier:<tier>, provider:<provider>, and rag_engaged:<bool>
tags plus run_id/thread_id/matched_rules metadata, so traces are
filterable in the LangSmith UI by routing decision, not just an opaque blob
per run. The trace's root run id is captured synchronously (via an explicit
LangChainTracer callback, not just the global env-var auto-patch) and
persisted as RunRecord.langsmith_run_id, so a run can be linked directly to
its trace.
POST /api/v1/runs/{run_id}/feedback persists feedback locally regardless of
LangSmith (FeedbackRecord, stored alongside runs/events), and additionally
forwards it to the run's LangSmith trace via Client().create_feedback(...)
when tracing is enabled and the run has a captured trace id. LangSmith
submission failures are logged and swallowed -- local persistence is the
source of truth, LangSmith is best-effort. Set LANGSMITH_FEEDBACK_ENABLED=false
to disable the LangSmith forwarding while keeping local feedback persistence.
Create and configure .env. Keep the private source archive at
docs/Companies-AI-Initiatives.zip, then build the stack:
docker compose build
docker compose up -dNEXT_PUBLIC_API_KEY (frontend's own API key, see Authentication and
Authorization below) is baked into the frontend image at build time, which
means a two-step bootstrap the first time: bring up api with
AUTH_BOOTSTRAP_ADMIN_KEY set but NEXT_PUBLIC_API_KEY still empty, mint a
user-role key against the running API, set NEXT_PUBLIC_API_KEY in .env
to it, then docker compose build frontend && docker compose up -d frontend.
Rotating the frontend's key later requires the same rebuild step.
The one-shot rag-indexer service builds the shared Chroma volume before the
API starts. Existing indexes are reused unless RAG_REBUILD_INDEX=true is set.
The API automatically initializes its PostgreSQL tables during startup.
Services:
| Service | Address |
|---|---|
| Web workspace | http://localhost/ |
| API documentation through Nginx | http://localhost/backend/docs |
| Grafana | http://127.0.0.1:3001 |
| Prometheus | http://127.0.0.1:9090 |
Prometheus and Grafana bind to localhost by default. Use a secured reverse proxy or SSH tunnel for remote operator access.
- Research output depends on third-party data availability and model quality.
- Docker Compose uses PostgreSQL for durable run history. Local development
defaults to SQLite unless
DATABASE_URLis configured. - LangGraph conversational checkpoints still use in-process memory and do not survive a process restart mid-conversation (a known follow-up now that a durable worker exists -- see Architecture below); completed reports and run history always survive in the selected database regardless.
- The current RAG retriever uses semantic similarity without a reranker or hybrid lexical search, and has no document-level access control yet.
- The repository does not yet publish benchmark scores for answer accuracy, groundedness, retrieval recall, or production latency as a report (the evaluation harness that can produce them exists -- see Testing above).
- Move long-running agent execution to a durable worker queue
- Move the LangGraph conversational checkpointer to Redis (currently in-process
MemorySaver) - Add authentication and role-based access control
- Add a versioned RAG evaluation dataset and custom groundedness/trajectory evaluation
- Add hybrid retrieval, reranking, and document-level access controls
- Add token and model-cost metrics
- Add retrieval-quality metrics (precision/recall on retrieval itself)
- Add CI/CD security, test, and container scanning workflows
- Add SEC filings, earnings reports, and additional market-data providers
- Add portfolio-level comparisons and exportable reports
No license file has been added yet. Until a license is selected, the repository should be treated as all rights reserved by its owner.
