Spaces:
Sleeping
Sleeping
| """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 | |