Releases: Agentic-Analyst/api-runner
v1.0.0 — Async Orchestration Layer with Docker-in-Docker Execution
v1.0.0 — Async Orchestration Layer with Docker-in-Docker Execution
The central control plane for VYNN AI. This service does not run financial analysis in-process — it orchestrates the full lifecycle of AI-driven analysis workflows: receiving requests, spawning isolated Docker containers for each pipeline run, streaming real-time progress back to clients via SSE, managing dual WebSocket channels for live market data, and delivering structured outputs (PDF reports, Excel financial models, markdown summaries).
10,598 lines of Python across 30 source files. Deployed in production on Hetzner Cloud.
Docker-in-Docker Execution
The defining architectural decision:
- User submits analysis request via
POST /run - API Runner spawns an ephemeral Docker container (
fuzanwenn/stock-analyst:latest, ~975 MB) via the Docker SDK, mounting the host Docker socket (/var/run/docker.sock) - Container runs the full agent pipeline in isolation, sharing only the persistent
stockdatavolume for I/O - Logs stream back to the client via Server-Sent Events with typed event taxonomy
- Results persist to MongoDB; artifacts written to the shared volume
- Container is automatically removed on completion, timeout, or graceful stop (SIGTERM with 10s timeout, then force-kill)
Why DinD: Complete process isolation. A crashed or hung analysis container does not affect the API server or other users' requests. Each job is sandboxed with its own environment. Horizontal scaling is achieved by running multiple analysis containers concurrently.
6 pipeline types: comprehensive · financial-only · model-only · news-only · model-to-price · news-to-price
Real-Time Communication
Server-Sent Events (SSE) — /jobs/{job_id}/logs/stream
Per-job event streams with typed events:
| Event | Description |
|---|---|
connection |
Initial connection established |
log |
Log line with type field: NL (LLM natural language output) or LOG (system log) |
status |
Progress status update |
completed |
Analysis finished (includes session_id for chat pipelines) |
error |
Error occurred |
Automatic heartbeats keep connections alive through reverse proxies. Final drain logic ensures no events are lost on completion. Session ID extraction for chat jobs enables multi-turn conversation continuity.
WebSocket Hub — 2 distinct channels
- Market Prices (
/api/realtime/ws) — yfinance polling every 10s with subscriber-based architecture, intelligent caching, and per-symbol subscription management - News Feed (
/api/news/ws) — per-ticker subscriptions with article deduplication (by ID, URL hash, or URL), 30-second ping/pong keep-alive, and background auto-updater with smart rate limiting for SerpAPI quotas
Both channels support subscribe, unsubscribe, ping, and health check messages.
REST API
- Analysis job CRUD: create (
/run), status polling (/jobs/{id}/status/detailed), stop (/jobs/{id}/stopwith graceful SIGTERM), log snapshots (/jobs/{id}/logs) - Chat pipeline:
POST /chatwith natural language ticker identification and session continuity - File downloads: financial models (
.xlsx), professional reports (.pdf), financial/news summaries, complete tar archives - Historical OHLCV data with configurable timeframes (1D · 1W · 1M · 3M · 1Y · ALL)
Authentication
Three methods, all under /auth:
- Google OAuth 2.0 — Authlib OIDC flow
- GitHub OAuth — Authlib OAuth2 flow
- Email verification codes — passwordless login with HMAC-SHA256 token generation
Session management via Starlette middleware with HTTP-only cookies. Development mode supports hardcoded login codes for local testing.
Daily Intelligence Reports
- Pre-market scheduler at 8:30 AM ET (Mon–Fri) with automatic weekend skip
- Company-level and sector-level reports generated as batch Docker container jobs
- Markdown-to-PDF conversion via ReportLab with custom styling, table of contents, and internal links
- Batch status tracking: individual job status + aggregated batch status endpoints
Job Lifecycle Management
| Status | Description |
|---|---|
pending |
Job queued, waiting to start |
running |
Analysis actively executing in container |
completed |
Finished with all results available |
failed |
Analysis failed — check error field |
llm_timeout |
LLM provider overloaded — retryable |
stopping |
Stop request received, shutting down container |
stopped |
Successfully stopped by user — retryable |
Health Monitoring
Multi-level health checks across Docker connectivity, real-time price service, WebSocket server, news feed, backend image availability, data volume, and API key configuration. Kubernetes-style liveness probe at /healthz.
Data Architecture
Per-user, per-ticker, per-timestamp directory structure on a persistent Docker volume:
/data/{email}/{TICKER}/{timestamp}/
├── info.log # Pipeline execution log
├── models/
│ ├── {TICKER}_financial_model.xlsx # DCF Excel workbook
│ └── {TICKER}_price_adjustment_explanation.md
├── reports/
│ └── {TICKER}_Professional_Analysis_Report.md
├── summaries/
│ ├── {TICKER}_financial_summary.md
│ └── {TICKER}_news_summary.md
├── searched/ # Raw scraped articles
└── filtered/ # NLP-filtered articles + index.csv
News articles stored in MongoDB via vynn_core in per-ticker collections.
Known Limitations
- Session store is cookie-based (Starlette middleware), not backed by a distributed store — adequate for current scale but would need Redis-backed sessions for multi-instance deployments
- WebSocket channels use in-memory connection state; not horizontally scalable without a pub/sub broker
- News auto-updater rate limiting is keyed to SerpAPI quotas, not per-user
Tech Stack
Python 3.11 · FastAPI 0.104 · Uvicorn · Docker SDK 7.1 · MongoDB (Motor 3.5 + PyMongo 4.5) · Authlib · Starlette · ReportLab · yfinance · SerpAPI · SSE · WebSocket