chore: update project files

This commit is contained in:
KIMATHI EMMANUEL 2026-03-23 04:47:48 +00:00
parent 1e81e2034e
commit 1cc2a3edde
18 changed files with 1626 additions and 1 deletions

View File

@ -1,6 +1,6 @@
# LLM Providers (set the one you use)
OPENAI_API_KEY=
GOOGLE_API_KEY=
GOOGLE_API_KEY=AIzaSyAunXkbkwv05l7BftH2GpmcDbHJZXSGzCg
ANTHROPIC_API_KEY=
XAI_API_KEY=
OPENROUTER_API_KEY=

4
.gitignore vendored
View File

@ -217,3 +217,7 @@ __marimo__/
# Cache
**/data_cache/
# Frontend
frontend/node_modules/
frontend/dist/

View File

@ -158,6 +158,36 @@ An interface will appear showing results as they load, letting you track the age
<img src="assets/cli/cli_transaction.png" width="100%" style="display: inline-block; margin: 0 2%;">
</p>
### Web API + React Frontend
The existing CLI remains unchanged. You can now also run TradingAgents through a lightweight HTTP API and a React frontend.
Start the API server:
```bash
uvicorn tradingagents.api.main:app --host 0.0.0.0 --port 8000 --reload
```
In another terminal, start the frontend:
```bash
cd frontend
npm install
npm run dev
```
Open `http://localhost:5173` in your browser.
API endpoints:
- `GET /api/health`
- `GET /api/options`
- `POST /api/analysis/jobs`
- `GET /api/analysis/jobs/{job_id}`
The frontend defaults to `http://127.0.0.1:8000` for API calls.
Override with:
```bash
VITE_API_BASE_URL=http://127.0.0.1:8000 npm run dev
```
## TradingAgents Package
### Implementation Details

18
frontend/index.html Normal file
View File

@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&family=IBM+Plex+Mono:wght@400;500&display=swap"
rel="stylesheet"
/>
<title>TradingAgents Web Console</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

22
frontend/package.json Normal file
View File

@ -0,0 +1,22 @@
{
"name": "tradingagents-frontend",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@types/react": "^18.3.19",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.2",
"typescript": "^5.6.2",
"vite": "^5.4.10"
}
}

345
frontend/src/App.tsx Normal file
View File

@ -0,0 +1,345 @@
import { useEffect, useMemo, useState } from "react";
type JobStatus = "queued" | "running" | "completed" | "failed";
type ProviderOption = {
id: string;
base_url: string;
models: string[];
};
type OptionsResponse = {
providers: ProviderOption[];
analysts: string[];
research_depths: number[];
};
type AnalysisJob = {
id: string;
status: JobStatus;
created_at: string;
started_at?: string;
completed_at?: string;
request: {
ticker: string;
analysis_date: string;
analysts: string[];
research_depth: number;
llm_provider: string;
quick_think_llm?: string;
deep_think_llm?: string;
backend_url?: string;
google_thinking_level?: string;
openai_reasoning_effort?: string;
};
result?: {
ticker: string;
analysis_date: string;
decision: string;
final_trade_decision: string;
investment_plan: string;
reports: Record<string, string | null>;
};
error?: string;
};
const API_BASE = import.meta.env.VITE_API_BASE_URL || "http://127.0.0.1:8000";
function todayIso(): string {
return new Date().toISOString().slice(0, 10);
}
export default function App() {
const [options, setOptions] = useState<OptionsResponse | null>(null);
const [loadingOptions, setLoadingOptions] = useState(true);
const [ticker, setTicker] = useState("SPY");
const [analysisDate, setAnalysisDate] = useState(todayIso());
const [researchDepth, setResearchDepth] = useState(1);
const [provider, setProvider] = useState("google");
const [selectedAnalysts, setSelectedAnalysts] = useState<string[]>([
"market",
"social",
"news",
"fundamentals",
]);
const [quickThink, setQuickThink] = useState("");
const [deepThink, setDeepThink] = useState("");
const [googleThinkingLevel, setGoogleThinkingLevel] = useState("high");
const [job, setJob] = useState<AnalysisJob | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const providers = options?.providers ?? [];
const providerMeta = useMemo(
() => providers.find((item) => item.id === provider),
[provider, providers]
);
useEffect(() => {
let mounted = true;
(async () => {
try {
const response = await fetch(`${API_BASE}/api/options`);
const data: OptionsResponse = await response.json();
if (!mounted) {
return;
}
setOptions(data);
const google = data.providers.find((item) => item.id === "google");
if (google && google.models.length > 0) {
setQuickThink(google.models[0]);
setDeepThink(google.models[0]);
}
} finally {
if (mounted) {
setLoadingOptions(false);
}
}
})();
return () => {
mounted = false;
};
}, []);
useEffect(() => {
if (!providerMeta?.models?.length) {
return;
}
if (!providerMeta.models.includes(quickThink)) {
setQuickThink(providerMeta.models[0]);
}
if (!providerMeta.models.includes(deepThink)) {
setDeepThink(providerMeta.models[0]);
}
}, [providerMeta, quickThink, deepThink]);
useEffect(() => {
if (!job || (job.status !== "queued" && job.status !== "running")) {
return;
}
const timer = setTimeout(async () => {
const response = await fetch(`${API_BASE}/api/analysis/jobs/${job.id}`);
const updated = (await response.json()) as AnalysisJob;
setJob(updated);
}, 2500);
return () => clearTimeout(timer);
}, [job]);
const toggleAnalyst = (value: string) => {
setSelectedAnalysts((current) => {
if (current.includes(value)) {
const next = current.filter((item) => item !== value);
return next.length ? next : current;
}
return [...current, value];
});
};
const submitJob = async () => {
setIsSubmitting(true);
setJob(null);
try {
const payload = {
ticker,
analysis_date: analysisDate,
analysts: selectedAnalysts,
research_depth: researchDepth,
llm_provider: provider,
backend_url: providerMeta?.base_url,
quick_think_llm: quickThink,
deep_think_llm: deepThink,
google_thinking_level: provider === "google" ? googleThinkingLevel : null,
};
const response = await fetch(`${API_BASE}/api/analysis/jobs`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`Request failed (${response.status})`);
}
const created = (await response.json()) as AnalysisJob;
setJob(created);
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
setJob({
id: "n/a",
status: "failed",
created_at: new Date().toISOString(),
request: {
ticker,
analysis_date: analysisDate,
analysts: selectedAnalysts,
research_depth: researchDepth,
llm_provider: provider,
},
error: message,
});
} finally {
setIsSubmitting(false);
}
};
return (
<main className="shell">
<header className="hero">
<div>
<h1>TradingAgents Web Console</h1>
<p>
Keep the current backend logic intact and orchestrate runs through HTTP APIs.
Submit a job, monitor state transitions, and inspect the final decision package.
</p>
</div>
<span className="badge">React + FastAPI</span>
</header>
<section className="grid">
<aside className="panel stack">
<h2>Run Setup</h2>
<label>
Ticker
<input value={ticker} onChange={(e) => setTicker(e.target.value.toUpperCase())} />
</label>
<label>
Analysis Date
<input
type="date"
value={analysisDate}
onChange={(e) => setAnalysisDate(e.target.value)}
/>
</label>
<label>
Provider
<select value={provider} onChange={(e) => setProvider(e.target.value)}>
{providers.map((item) => (
<option value={item.id} key={item.id}>
{item.id}
</option>
))}
</select>
</label>
<label>
Quick-Thinking Model
<select value={quickThink} onChange={(e) => setQuickThink(e.target.value)}>
{providerMeta?.models?.map((model) => (
<option value={model} key={model}>
{model}
</option>
))}
</select>
</label>
<label>
Deep-Thinking Model
<select value={deepThink} onChange={(e) => setDeepThink(e.target.value)}>
{providerMeta?.models?.map((model) => (
<option value={model} key={model}>
{model}
</option>
))}
</select>
</label>
{provider === "google" && (
<label>
Gemini Thinking Mode
<select
value={googleThinkingLevel}
onChange={(e) => setGoogleThinkingLevel(e.target.value)}
>
<option value="high">high</option>
<option value="minimal">minimal</option>
</select>
</label>
)}
<label>
Research Depth
<div className="depth">
{[1, 3, 5].map((depth) => (
<button
key={depth}
type="button"
className={researchDepth === depth ? "active" : ""}
onClick={() => setResearchDepth(depth)}
>
{depth === 1 ? "Shallow" : depth === 3 ? "Medium" : "Deep"}
</button>
))}
</div>
</label>
<div className="stack">
<span className="mono">Analyst Team</span>
<div className="checks">
{(options?.analysts || []).map((analyst) => (
<label key={analyst}>
<input
type="checkbox"
checked={selectedAnalysts.includes(analyst)}
onChange={() => toggleAnalyst(analyst)}
/>
{analyst}
</label>
))}
</div>
</div>
<button className="run" type="button" onClick={submitJob} disabled={isSubmitting || loadingOptions}>
{isSubmitting ? "Submitting..." : "Start Analysis Job"}
</button>
</aside>
<article className="panel">
<h3>Execution</h3>
<p className="mono">API: {API_BASE}</p>
{job ? (
<>
<div className="status">
<span className={`dot ${job.status}`} />
{job.status}
</div>
<p className="mono">Job ID: {job.id}</p>
<p>
{job.request.ticker} on {job.request.analysis_date} with {job.request.llm_provider}
</p>
{job.status === "failed" && (
<div className="card error pre">{job.error || "Job failed"}</div>
)}
{job.result && (
<div className="result">
<div className="card">
<h4>Decision</h4>
<div className="pre">{job.result.decision}</div>
</div>
<div className="card">
<h4>Final Trade Decision</h4>
<div className="pre">{String(job.result.final_trade_decision || "")}</div>
</div>
<div className="card">
<h4>Investment Plan</h4>
<div className="pre">{String(job.result.investment_plan || "")}</div>
</div>
{Object.entries(job.result.reports || {}).map(([key, value]) => (
<div className="card" key={key}>
<h4>{key}</h4>
<div className="pre">{String(value || "")}</div>
</div>
))}
</div>
)}
</>
) : (
<p>Submit a run to see live job status and result artifacts here.</p>
)}
</article>
</section>
</main>
);
}

10
frontend/src/main.tsx Normal file
View File

@ -0,0 +1,10 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./styles.css";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

254
frontend/src/styles.css Normal file
View File

@ -0,0 +1,254 @@
:root {
--bg-0: #061824;
--bg-1: #0d2b40;
--card: rgba(9, 22, 35, 0.74);
--card-line: rgba(127, 214, 192, 0.22);
--text: #e8f7f2;
--muted: #9ec8bc;
--accent: #7fd6c0;
--accent-2: #f5b971;
--danger: #f27575;
--ok: #7ee081;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
color: var(--text);
font-family: "Space Grotesk", sans-serif;
background:
radial-gradient(circle at 20% 15%, rgba(127, 214, 192, 0.15), transparent 40%),
radial-gradient(circle at 78% 12%, rgba(245, 185, 113, 0.22), transparent 42%),
linear-gradient(160deg, var(--bg-0) 0%, var(--bg-1) 60%, #112c22 100%);
}
.shell {
max-width: 1160px;
margin: 0 auto;
padding: 32px 18px 56px;
}
.hero {
display: flex;
justify-content: space-between;
align-items: end;
gap: 16px;
margin-bottom: 24px;
animation: rise 0.6s ease;
}
.hero h1 {
margin: 0;
font-size: clamp(1.8rem, 4.6vw, 3.1rem);
line-height: 1.02;
letter-spacing: -0.03em;
}
.hero p {
margin: 8px 0 0;
color: var(--muted);
max-width: 640px;
}
.badge {
font-family: "IBM Plex Mono", monospace;
color: #091623;
background: linear-gradient(90deg, var(--accent), var(--accent-2));
padding: 8px 12px;
border-radius: 12px;
font-size: 0.82rem;
white-space: nowrap;
}
.grid {
display: grid;
grid-template-columns: minmax(300px, 390px) 1fr;
gap: 18px;
}
.panel {
border: 1px solid var(--card-line);
background: var(--card);
backdrop-filter: blur(10px);
border-radius: 18px;
padding: 18px;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03), 0 22px 44px rgba(0, 0, 0, 0.25);
}
.panel h2,
.panel h3 {
margin: 0 0 12px;
}
.stack {
display: grid;
gap: 12px;
}
label {
display: grid;
gap: 6px;
color: var(--muted);
font-size: 0.88rem;
}
input,
select,
button,
textarea {
font: inherit;
}
input,
select {
border-radius: 10px;
border: 1px solid rgba(127, 214, 192, 0.28);
padding: 10px;
color: var(--text);
background: rgba(7, 18, 30, 0.7);
}
.depth {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.depth button,
.run {
border: 1px solid rgba(127, 214, 192, 0.45);
color: var(--text);
background: rgba(12, 30, 45, 0.85);
padding: 9px 10px;
border-radius: 10px;
cursor: pointer;
transition: transform 0.2s ease, border-color 0.2s ease;
}
.depth button.active {
background: linear-gradient(100deg, rgba(127, 214, 192, 0.24), rgba(245, 185, 113, 0.21));
border-color: rgba(245, 185, 113, 0.74);
}
.depth button:hover,
.run:hover {
transform: translateY(-1px);
}
.run {
margin-top: 8px;
font-weight: 700;
background: linear-gradient(100deg, rgba(127, 214, 192, 0.2), rgba(245, 185, 113, 0.14));
}
.run:disabled {
cursor: not-allowed;
opacity: 0.6;
}
.checks {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.checks label {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
border-radius: 10px;
border: 1px solid rgba(127, 214, 192, 0.17);
background: rgba(4, 16, 26, 0.5);
color: var(--text);
}
.mono {
font-family: "IBM Plex Mono", monospace;
}
.status {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-radius: 999px;
border: 1px solid rgba(127, 214, 192, 0.35);
background: rgba(7, 20, 30, 0.7);
text-transform: uppercase;
letter-spacing: 0.06em;
font-size: 0.72rem;
}
.status .dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.dot.queued,
.dot.running {
background: var(--accent-2);
}
.dot.completed {
background: var(--ok);
}
.dot.failed {
background: var(--danger);
}
.result {
margin-top: 14px;
display: grid;
gap: 10px;
}
.card {
border: 1px solid rgba(127, 214, 192, 0.18);
border-radius: 12px;
padding: 12px;
background: rgba(6, 18, 28, 0.66);
}
.card h4 {
margin: 0 0 8px;
}
.pre {
white-space: pre-wrap;
line-height: 1.45;
color: #dbf6ee;
}
.error {
color: #ffe4e4;
border-color: rgba(242, 117, 117, 0.46);
background: rgba(80, 22, 22, 0.55);
}
@keyframes rise {
from {
opacity: 0;
transform: translateY(6px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 920px) {
.grid {
grid-template-columns: 1fr;
}
.checks {
grid-template-columns: 1fr;
}
}

15
frontend/tsconfig.json Normal file
View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true
},
"include": ["src"]
}

10
frontend/vite.config.ts Normal file
View File

@ -0,0 +1,10 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
host: true,
},
});

View File

@ -12,6 +12,7 @@ dependencies = [
"langchain-core>=0.3.81",
"backtrader>=1.9.78.123",
"chainlit>=2.5.5",
"fastapi>=0.118.0",
"langchain-anthropic>=0.3.15",
"langchain-experimental>=0.3.4",
"langchain-google-genai>=2.1.5",
@ -30,6 +31,7 @@ dependencies = [
"stockstats>=0.6.5",
"tqdm>=4.67.1",
"typing-extensions>=4.14.0",
"uvicorn>=0.37.0",
"yfinance>=0.2.63",
]

View File

@ -20,3 +20,5 @@ typer
questionary
langchain_anthropic
langchain-google-genai
fastapi
uvicorn[standard]

View File

@ -0,0 +1 @@
I apologize, but "gold" is not a valid ticker symbol. Could you please provide the correct ticker symbol for the company you would like me to analyze?

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
"""HTTP API package for TradingAgents."""

223
tradingagents/api/main.py Normal file
View File

@ -0,0 +1,223 @@
from __future__ import annotations
import datetime as dt
import json
import threading
import traceback
import uuid
from concurrent.futures import ThreadPoolExecutor
from typing import Any, Dict, List, Literal, Optional
from dotenv import load_dotenv
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field, field_validator
from tradingagents.default_config import DEFAULT_CONFIG
from tradingagents.graph.trading_graph import TradingAgentsGraph
from tradingagents.llm_clients.validators import VALID_MODELS
load_dotenv()
APP_TITLE = "TradingAgents API"
ANALYST_ORDER = ["market", "social", "news", "fundamentals"]
PROVIDER_BASE_URLS = {
"openai": "https://api.openai.com/v1",
"google": "https://generativelanguage.googleapis.com/v1",
"anthropic": "https://api.anthropic.com/",
"xai": "https://api.x.ai/v1",
"openrouter": "https://openrouter.ai/api/v1",
"ollama": "http://localhost:11434/v1",
}
class AnalysisRequest(BaseModel):
ticker: str = Field(..., min_length=1, max_length=12)
analysis_date: str = Field(default_factory=lambda: dt.date.today().isoformat())
analysts: List[str] = Field(default_factory=lambda: ANALYST_ORDER.copy())
research_depth: Literal[1, 3, 5] = 1
llm_provider: str = "google"
quick_think_llm: Optional[str] = None
deep_think_llm: Optional[str] = None
backend_url: Optional[str] = None
google_thinking_level: Optional[str] = "high"
openai_reasoning_effort: Optional[str] = None
@field_validator("ticker")
@classmethod
def validate_ticker(cls, value: str) -> str:
ticker = value.strip().upper()
if not ticker:
raise ValueError("ticker cannot be empty")
return ticker
@field_validator("analysis_date")
@classmethod
def validate_date(cls, value: str) -> str:
try:
dt.date.fromisoformat(value)
except ValueError as exc:
raise ValueError("analysis_date must be YYYY-MM-DD") from exc
return value
@field_validator("analysts")
@classmethod
def validate_analysts(cls, value: List[str]) -> List[str]:
normalized = [item.strip().lower() for item in value if item.strip()]
if not normalized:
raise ValueError("analysts must include at least one analyst")
invalid = [item for item in normalized if item not in ANALYST_ORDER]
if invalid:
raise ValueError(f"invalid analysts: {', '.join(invalid)}")
return normalized
@field_validator("llm_provider")
@classmethod
def validate_provider(cls, value: str) -> str:
provider = value.strip().lower()
if provider not in PROVIDER_BASE_URLS:
raise ValueError(f"unsupported provider: {provider}")
return provider
class AnalysisJob(BaseModel):
id: str
status: Literal["queued", "running", "completed", "failed"]
created_at: str
started_at: Optional[str] = None
completed_at: Optional[str] = None
request: AnalysisRequest
result: Optional[Dict[str, Any]] = None
error: Optional[str] = None
app = FastAPI(title=APP_TITLE, version="0.1.0")
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
_jobs_lock = threading.Lock()
_jobs: Dict[str, AnalysisJob] = {}
_executor = ThreadPoolExecutor(max_workers=2)
def _json_safe(value: Any) -> Any:
return json.loads(json.dumps(value, default=str))
def _default_model(provider: str) -> str:
models = VALID_MODELS.get(provider)
if models:
return models[0]
if provider == "ollama":
return "qwen3:latest"
return "gpt-5-mini"
def _build_config(request: AnalysisRequest) -> Dict[str, Any]:
config = DEFAULT_CONFIG.copy()
config["max_debate_rounds"] = request.research_depth
config["max_risk_discuss_rounds"] = request.research_depth
config["llm_provider"] = request.llm_provider
config["backend_url"] = request.backend_url or PROVIDER_BASE_URLS[request.llm_provider]
config["quick_think_llm"] = request.quick_think_llm or _default_model(request.llm_provider)
config["deep_think_llm"] = request.deep_think_llm or _default_model(request.llm_provider)
config["google_thinking_level"] = request.google_thinking_level
config["openai_reasoning_effort"] = request.openai_reasoning_effort
return config
def _run_job(job_id: str) -> None:
with _jobs_lock:
job = _jobs[job_id]
job.status = "running"
job.started_at = dt.datetime.utcnow().isoformat() + "Z"
try:
config = _build_config(job.request)
selected_analysts = [a for a in ANALYST_ORDER if a in job.request.analysts]
graph = TradingAgentsGraph(
selected_analysts=selected_analysts,
debug=False,
config=config,
)
final_state, decision = graph.propagate(
job.request.ticker,
job.request.analysis_date,
)
result = {
"ticker": job.request.ticker,
"analysis_date": job.request.analysis_date,
"decision": decision,
"final_trade_decision": _json_safe(final_state.get("final_trade_decision")),
"investment_plan": _json_safe(final_state.get("investment_plan")),
"reports": {
"market": _json_safe(final_state.get("market_report")),
"sentiment": _json_safe(final_state.get("sentiment_report")),
"news": _json_safe(final_state.get("news_report")),
"fundamentals": _json_safe(final_state.get("fundamentals_report")),
"trader": _json_safe(final_state.get("trader_investment_plan")),
},
}
with _jobs_lock:
job.status = "completed"
job.completed_at = dt.datetime.utcnow().isoformat() + "Z"
job.result = result
job.error = None
except Exception:
with _jobs_lock:
job.status = "failed"
job.completed_at = dt.datetime.utcnow().isoformat() + "Z"
job.error = traceback.format_exc(limit=6)
@app.get("/api/health")
def health() -> Dict[str, str]:
return {"status": "ok", "service": APP_TITLE}
@app.get("/api/options")
def options() -> Dict[str, Any]:
return {
"providers": [
{
"id": provider,
"base_url": base_url,
"models": VALID_MODELS.get(provider, []),
}
for provider, base_url in PROVIDER_BASE_URLS.items()
],
"analysts": ANALYST_ORDER,
"research_depths": [1, 3, 5],
}
@app.post("/api/analysis/jobs", response_model=AnalysisJob)
def create_analysis_job(request: AnalysisRequest) -> AnalysisJob:
job_id = str(uuid.uuid4())
job = AnalysisJob(
id=job_id,
status="queued",
created_at=dt.datetime.utcnow().isoformat() + "Z",
request=request,
)
with _jobs_lock:
_jobs[job_id] = job
_executor.submit(_run_job, job_id)
return job
@app.get("/api/analysis/jobs/{job_id}", response_model=AnalysisJob)
def get_analysis_job(job_id: str) -> AnalysisJob:
with _jobs_lock:
job = _jobs.get(job_id)
if not job:
raise HTTPException(status_code=404, detail="Job not found")
return job