"""Financial analyst agent. Interprets retrieved evidence — trends, liquidity, leverage, profitability, risk — and can call tools (ratio engine, calculator, filings search, currency conversion) via a bounded tool-calling loop. Default model: GPT (strong numerical reasoning + tool use). """ from __future__ import annotations from langchain_core.messages import HumanMessage, SystemMessage, ToolMessage from src.agents.qa_agent import format_evidence from src.llm import get_llm from src.retrieval.hybrid import RetrievedChunk from src.tools.langchain_tools import ANALYST_TOOLS SYSTEM = """You are a senior financial analyst. Interpret the evidence excerpts to answer the question: explain trends, liquidity, leverage, profitability, and risks. Rules: - Ground every quantitative claim in the evidence and cite it, e.g. [balance_sheet_2024 p.12]. - Use the calculate_ratio / calculator tools for ALL arithmetic — never compute in your head. - Make your reasoning explicit as a short chain: observation -> ratio/figure -> implication. - Distinguish clearly between what the documents state and your inference. - If evidence is insufficient for a firm conclusion, say what additional data you would need.""" MAX_TOOL_ROUNDS = 6 def answer(question: str, retrieved: list[RetrievedChunk], history: str = "") -> tuple[str, list[dict]]: """Returns (answer_text, tool_trace). tool_trace records each tool call for the explainability panel in the UI.""" llm = get_llm("analyst").bind_tools(ANALYST_TOOLS) tools_by_name = {t.name: t for t in ANALYST_TOOLS} context = format_evidence(retrieved) if retrieved else "(no evidence retrieved)" prompt = "" if history: prompt += f"Conversation so far:\n{history}\n\n" prompt += f"Evidence excerpts:\n\n{context}\n\nQuestion: {question}" messages = [SystemMessage(content=SYSTEM), HumanMessage(content=prompt)] tool_trace: list[dict] = [] for _ in range(MAX_TOOL_ROUNDS): resp = llm.invoke(messages) messages.append(resp) if not getattr(resp, "tool_calls", None): return resp.content, tool_trace for call in resp.tool_calls: tool = tools_by_name.get(call["name"]) try: result = tool.invoke(call["args"]) if tool else f"Unknown tool {call['name']}" except Exception as e: result = f"Tool error: {e}" tool_trace.append({"tool": call["name"], "args": call["args"], "result": str(result)}) messages.append(ToolMessage(content=str(result), tool_call_id=call["id"])) # ran out of tool rounds — ask for a final synthesis without tools final = get_llm("analyst").invoke( messages + [HumanMessage(content="Provide your final answer now without further tool calls.")] ) return final.content, tool_trace