Download backend/app/agent/tools.py from fardeen16/finrag: direct link, hf CLI and curl.
- Browser
- Download file 6.91 kB
-
https://huggingface.co/spaces/fardeen16/finrag/resolve/main/backend/app/agent/tools.py
- Command line
-
hf download hf://spaces/fardeen16/finrag/backend/app/agent/tools.py
-
curl -L -o tools.py https://huggingface.co/spaces/fardeen16/finrag/resolve/main/backend/app/agent/tools.py
6.91 kB
| """The four specialist tools, ported to the local model. | |
| Behaviour matches `agent_tools.py`; only the model behind each call changed. | |
| Print statements are gone — node progress now travels over the event stream. | |
| """ | |
| from __future__ import annotations | |
| import asyncio | |
| import sqlite3 | |
| from typing import Any, Dict, List | |
| import pandas as pd | |
| from langchain_core.tools import tool | |
| from pydantic import BaseModel, Field | |
| from ..config import get_settings | |
| from .embeddings import assert_dimension_matches, get_embeddings | |
| from .llm import chat_model, structured_model | |
| from .resources import collection_dimension, get_qdrant, get_reranker, get_sql_database | |
| _QUERY_REWRITE = ( | |
| 'Rewrite the following query for a semantic search system. Respond with ONLY ' | |
| 'the rewritten query, no preamble.\nQuery: "{query}"\nRewritten:' | |
| ) | |
| async def optimize_query(query: str) -> str: | |
| """Rewrite a user query for retrieval. | |
| Falls back to the original query on failure: a bad rewrite should degrade | |
| retrieval, not fail the request. | |
| """ | |
| try: | |
| response = await chat_model(temperature=0.0, max_tokens=128).ainvoke( | |
| _QUERY_REWRITE.format(query=query) | |
| ) | |
| rewritten = (response.content or "").strip().strip('"') | |
| return rewritten or query | |
| except Exception: | |
| return query | |
| async def librarian_rag_tool(query: str) -> List[Dict[str, Any]]: | |
| """Retrieves deep, contextual information from Alphabet's financial filings.""" | |
| settings = get_settings() | |
| assert_dimension_matches(collection_dimension()) | |
| optimized = await optimize_query(query) | |
| embedding = await asyncio.to_thread(get_embeddings().embed_query, optimized) | |
| hits = await asyncio.to_thread( | |
| lambda: get_qdrant().query_points( | |
| collection_name=settings.collection_name, | |
| query=embedding, | |
| limit=settings.retrieval_candidates, | |
| with_payload=True, | |
| ).points | |
| ) | |
| if not hits: | |
| return [] | |
| # CrossEncoder is CPU-bound; keep it off the event loop. | |
| pairs = [[optimized, hit.payload.get("content", "")] for hit in hits] | |
| scores = await asyncio.to_thread(get_reranker().predict, pairs) | |
| ranked = sorted(zip(hits, scores), key=lambda pair: pair[1], reverse=True) | |
| return [ | |
| { | |
| "source": hit.payload.get("source"), | |
| "content": hit.payload.get("content"), | |
| "summary": hit.payload.get("summary"), | |
| "rerank_score": float(score), | |
| } | |
| for hit, score in ranked[: settings.retrieval_top_k] | |
| ] | |
| class _SqlQuery(BaseModel): | |
| sql: str = Field(description="A single read-only SQLite SELECT statement.") | |
| _SQL_PROMPT = """Write one SQLite SELECT query answering the question. | |
| Schema: | |
| {schema} | |
| Rules: | |
| - SELECT only. No INSERT, UPDATE, DELETE, DROP or PRAGMA. | |
| - Use only the columns shown above. | |
| - SQLite syntax: no YEAR(), no TOP. Use LIMIT. | |
| - Return the query only. | |
| Question: {question} | |
| """ | |
| # Only a bare SELECT/WITH is allowed through. The question text reaches the model | |
| # that writes this SQL, so the generated statement is untrusted input: a prompt | |
| # injection that talks the model into a DROP must not reach the database. | |
| _FORBIDDEN_SQL = ( | |
| "insert", "update", "delete", "drop", "alter", "create", | |
| "attach", "detach", "pragma", "replace", "vacuum", | |
| ) | |
| def _run_select(sql: str) -> str: | |
| settings = get_settings() | |
| stripped = sql.strip().rstrip(";").strip() | |
| lowered = stripped.lower() | |
| if not (lowered.startswith("select") or lowered.startswith("with")): | |
| return f"Refused: only SELECT queries are allowed, got: {stripped[:120]}" | |
| if any(word in lowered.split() for word in _FORBIDDEN_SQL): | |
| return f"Refused: statement contains a write operation: {stripped[:120]}" | |
| if ";" in stripped: | |
| return "Refused: multiple statements are not allowed." | |
| with sqlite3.connect(f"file:{settings.sqlite_path}?mode=ro", uri=True) as conn: | |
| df = pd.read_sql_query(stripped, conn) | |
| if df.empty: | |
| return f"Query returned no rows.\nSQL: {stripped}" | |
| return f"SQL: {stripped}\n\n{df.to_string(index=False, max_rows=40)}" | |
| async def analyst_sql_tool(query: str) -> str: | |
| """Answers specific questions about revenue, net income, etc., from a SQL database.""" | |
| # Replaces langchain's `create_sql_agent`, which was an agent loop costing | |
| # several LLM round trips and was the slowest node in the graph. One | |
| # constrained-decoding call plus a direct execution is faster, and it drops a | |
| # dependency on the legacy langchain-community agent stack. | |
| try: | |
| schema = await asyncio.to_thread(get_sql_database().get_table_info) | |
| except Exception as exc: | |
| return f"Could not read database schema: {type(exc).__name__}: {exc}" | |
| try: | |
| generated = await structured_model(_SqlQuery).ainvoke( | |
| _SQL_PROMPT.format(schema=schema, question=query) | |
| ) | |
| except Exception as exc: | |
| return f"Could not generate SQL: {type(exc).__name__}: {exc}" | |
| try: | |
| return await asyncio.to_thread(_run_select, generated.sql) | |
| except Exception as exc: | |
| return f"SQL failed: {type(exc).__name__}: {exc}\nSQL: {generated.sql}" | |
| def _trend_sync(_: str) -> str: | |
| settings = get_settings() | |
| with sqlite3.connect(settings.sqlite_path) as conn: | |
| df = pd.read_sql_query( | |
| f"SELECT * FROM {settings.table_name} ORDER BY year, quarter", conn | |
| ) | |
| if df.empty: | |
| return "No rows in the financial summary table." | |
| df["period"] = df["year"].astype(str) + "-" + df["quarter"].astype(str) | |
| df = df.set_index("period") | |
| metric = "revenue_usd_billions" | |
| qoq = df[metric].pct_change() | |
| yoy = df[metric].pct_change(4) | |
| return ( | |
| f"Analysis of {metric} from {df.index[0]} to {df.index[-1]}: " | |
| f"${df[metric].iloc[0]:.1f}B -> ${df[metric].iloc[-1]:.1f}B. " | |
| f"Latest QoQ {qoq.iloc[-1]:.1%}, latest YoY {yoy.iloc[-1]:.1%}." | |
| ) | |
| async def analyst_trend_tool(query: str) -> str: | |
| """Analyzes financial trends like QoQ or YoY growth from the SQL database.""" | |
| return await asyncio.to_thread(_trend_sync, query) | |
| async def scout_web_search_tool(query: str) -> str: | |
| """Searches the web for real-time information like stock prices or recent news.""" | |
| from langchain_community.tools import DuckDuckGoSearchRun | |
| try: | |
| return await DuckDuckGoSearchRun().arun(query) | |
| except Exception as exc: | |
| return f"Web search failed: {type(exc).__name__}: {exc}" | |
| ALL_TOOLS = [ | |
| librarian_rag_tool, | |
| analyst_sql_tool, | |
| analyst_trend_tool, | |
| scout_web_search_tool, | |
| ] | |
| TOOL_MAP = {t.name: t for t in ALL_TOOLS} | |