agentic-financial-analyst / src /agents /analyst_agent.py
finpy1789's picture
Agentic Financial Document Analyst: multi-agent RAG + MCP, agent-coloured UI
aa4269d verified
Raw
History Blame Contribute Delete
2.87 kB
"""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