mirror of
https://github.com/shawnkim1997/All-in-one-Financial-Analysis.git
synced 2026-08-18 21:08:07 +00:00
170 lines
6.2 KiB
Python
170 lines
6.2 KiB
Python
"""Copilot chat router with terminal context injection."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any, Literal
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
from pydantic import BaseModel, Field
|
|
|
|
from server.routers.analysis import _call_gemini, _get_financial_context
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
class CopilotContext(BaseModel):
|
|
activeSymbol: str | None = None
|
|
activePage: str = "equity"
|
|
recentSymbols: list[str] = Field(default_factory=list)
|
|
currency: str = "USD"
|
|
theme: str = "bloomberg"
|
|
watchlist: list[str] = Field(default_factory=list)
|
|
|
|
|
|
class CopilotMessage(BaseModel):
|
|
role: Literal["user", "assistant", "system"]
|
|
content: str
|
|
|
|
|
|
class CopilotChatRequest(BaseModel):
|
|
message: str
|
|
context: CopilotContext = Field(default_factory=CopilotContext)
|
|
history: list[CopilotMessage] = Field(default_factory=list)
|
|
api_key: str = ""
|
|
|
|
|
|
class RedTeamRequest(BaseModel):
|
|
ticker: str = Field(..., min_length=1)
|
|
thesis: str = Field(..., min_length=10)
|
|
api_key: str = ""
|
|
|
|
|
|
def _fallback_red_team(ticker: str, thesis: str) -> dict[str, Any]:
|
|
return {
|
|
"ticker": ticker,
|
|
"source": "rules",
|
|
"critiques": [
|
|
{
|
|
"argument": f"{ticker} may already price in the upside described in the thesis.",
|
|
"evidence_needed": "Compare current valuation multiples, growth expectations, and revision trends against peers.",
|
|
"severity": "high",
|
|
},
|
|
{
|
|
"argument": "The thesis may underweight execution risk and the time required for the catalyst to flow through earnings.",
|
|
"evidence_needed": "Track management guidance, milestone delivery, margin bridge, and capex or working-capital needs.",
|
|
"severity": "medium",
|
|
},
|
|
{
|
|
"argument": "A bearish macro or rates regime could compress multiples even if company fundamentals improve.",
|
|
"evidence_needed": "Stress-test the valuation against lower terminal multiples, higher discount rates, and weaker demand.",
|
|
"severity": "medium",
|
|
},
|
|
{
|
|
"argument": "The strongest counter-case is that consensus already understands the narrative but disagrees on durability.",
|
|
"evidence_needed": "Review sell-side estimate dispersion, short interest, and the gap between narrative KPIs and reported cash flow.",
|
|
"severity": "high",
|
|
},
|
|
],
|
|
"overlooked_risks": [
|
|
"Valuation multiple compression",
|
|
"Margin or cash-flow conversion disappointment",
|
|
"Competitive response stronger than expected",
|
|
],
|
|
"consensus_check": "Check whether the thesis is variant on numbers, timing, or only narrative. Narrative-only variants are usually weaker.",
|
|
"strongest_pushback": "The thesis needs proof that upside is not already embedded in consensus estimates and current valuation.",
|
|
"thesis_echo": thesis[:500],
|
|
}
|
|
|
|
|
|
def _parse_json_object(text: str) -> dict[str, Any]:
|
|
cleaned = text.strip()
|
|
if cleaned.startswith("```"):
|
|
cleaned = cleaned.strip("`")
|
|
if cleaned.lower().startswith("json"):
|
|
cleaned = cleaned[4:].strip()
|
|
start = cleaned.find("{")
|
|
end = cleaned.rfind("}")
|
|
if start >= 0 and end > start:
|
|
cleaned = cleaned[start:end + 1]
|
|
return json.loads(cleaned)
|
|
|
|
|
|
@router.post("/chat")
|
|
async def copilot_chat(req: CopilotChatRequest) -> dict[str, Any]:
|
|
api_key = req.api_key.strip()
|
|
if not api_key:
|
|
raise HTTPException(status_code=400, detail="Gemini API key is required")
|
|
|
|
active_symbol = (req.context.activeSymbol or "").strip().upper()
|
|
context_json = json.dumps(req.context.model_dump(), ensure_ascii=False, indent=2)
|
|
financial_context = _get_financial_context(active_symbol) if active_symbol else "No active financial symbol."
|
|
history = "\n".join(f"{msg.role}: {msg.content}" for msg in req.history[-8:])
|
|
prompt = f"""
|
|
You are ATLAS Copilot, a concise equity research assistant embedded in ATLAS Terminal.
|
|
Use the terminal context below to answer the user's question. Do not invent numbers.
|
|
If a requested figure is not in context, say what data would be needed.
|
|
|
|
Terminal context:
|
|
{context_json}
|
|
|
|
Financial context for active symbol:
|
|
{financial_context}
|
|
|
|
Recent conversation:
|
|
{history or "No prior messages."}
|
|
|
|
User question:
|
|
{req.message}
|
|
""".strip()
|
|
|
|
text = await _call_gemini(api_key=api_key, prompt=prompt, max_tokens=2048, temperature=0.35)
|
|
return {"message": text, "context": req.context.model_dump()}
|
|
|
|
|
|
@router.post("/red-team")
|
|
async def red_team(req: RedTeamRequest) -> dict[str, Any]:
|
|
ticker = req.ticker.strip().upper()
|
|
thesis = req.thesis.strip()
|
|
fallback = _fallback_red_team(ticker, thesis)
|
|
api_key = req.api_key.strip()
|
|
if not api_key:
|
|
return fallback
|
|
|
|
prompt = f"""
|
|
You are a skeptical short-seller reviewing an investment thesis.
|
|
Do not invent financial figures. Be concrete, adversarial, and useful.
|
|
|
|
TICKER: {ticker}
|
|
THESIS:
|
|
{thesis}
|
|
|
|
Return ONLY valid JSON in this shape:
|
|
{{
|
|
"critiques": [
|
|
{{
|
|
"argument": "One-sentence critique",
|
|
"evidence_needed": "What data would confirm or refute this",
|
|
"severity": "high|medium|low"
|
|
}}
|
|
],
|
|
"overlooked_risks": ["risk 1", "risk 2"],
|
|
"consensus_check": "Where this thesis aligns vs diverges from consensus",
|
|
"strongest_pushback": "The single strongest argument against this thesis"
|
|
}}
|
|
""".strip()
|
|
|
|
try:
|
|
raw = await _call_gemini(api_key=api_key, prompt=prompt, max_tokens=1600, temperature=0.35)
|
|
parsed = _parse_json_object(raw)
|
|
return {
|
|
"ticker": ticker,
|
|
"source": "gemini",
|
|
"critiques": parsed.get("critiques") if isinstance(parsed.get("critiques"), list) else fallback["critiques"],
|
|
"overlooked_risks": parsed.get("overlooked_risks") if isinstance(parsed.get("overlooked_risks"), list) else fallback["overlooked_risks"],
|
|
"consensus_check": parsed.get("consensus_check") or fallback["consensus_check"],
|
|
"strongest_pushback": parsed.get("strongest_pushback") or fallback["strongest_pushback"],
|
|
}
|
|
except Exception:
|
|
return fallback
|