Spaces:
Runtime error
Runtime error
| """Assistant tools. | |
| Two tools, exposed via LangChain's @tool decorator so either assistant can call | |
| them through the standard tool-calling interface: | |
| - calculator: safe arithmetic. Evaluated by walking a parsed AST (NOT Python's | |
| eval), so it can never execute arbitrary code. | |
| - web_search: Tavily web search, returning a short text digest of top hits. | |
| `TOOLS` is the list bound to each model. `run_tool_call` executes a single | |
| tool call (by name + args) and returns its string result. | |
| """ | |
| from __future__ import annotations | |
| import ast | |
| import operator | |
| from langchain_core.tools import tool | |
| from src.config import settings | |
| from src.observability import annotate_span, observe | |
| # --- Calculator ----------------------------------------------------------- | |
| # Only these AST node types / operators are allowed. Anything else (names, | |
| # function calls, attribute access, etc.) is rejected, so the calculator can | |
| # only ever do arithmetic on literal numbers. | |
| _ALLOWED_BINOPS = { | |
| ast.Add: operator.add, | |
| ast.Sub: operator.sub, | |
| ast.Mult: operator.mul, | |
| ast.Div: operator.truediv, | |
| ast.FloorDiv: operator.floordiv, | |
| ast.Mod: operator.mod, | |
| ast.Pow: operator.pow, | |
| } | |
| _ALLOWED_UNARYOPS = { | |
| ast.UAdd: operator.pos, | |
| ast.USub: operator.neg, | |
| } | |
| def _eval_node(node: ast.AST) -> float: | |
| """Recursively evaluate a whitelisted arithmetic AST node.""" | |
| if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)): | |
| return node.value | |
| if isinstance(node, ast.BinOp) and type(node.op) in _ALLOWED_BINOPS: | |
| return _ALLOWED_BINOPS[type(node.op)]( | |
| _eval_node(node.left), _eval_node(node.right) | |
| ) | |
| if isinstance(node, ast.UnaryOp) and type(node.op) in _ALLOWED_UNARYOPS: | |
| return _ALLOWED_UNARYOPS[type(node.op)](_eval_node(node.operand)) | |
| raise ValueError("Only basic arithmetic (+ - * / // % **) is allowed.") | |
| def calculator(expression: str) -> str: | |
| """Evaluate a basic arithmetic expression and return the result. | |
| Supports + - * / // % ** and parentheses on numbers only. Use this for any | |
| math instead of computing it yourself. | |
| """ | |
| try: | |
| tree = ast.parse(expression, mode="eval") | |
| result = _eval_node(tree.body) | |
| return str(result) | |
| except Exception as exc: # noqa: BLE001 - surface a clean message to the model | |
| return f"Calculator error: {exc}" | |
| # --- Web search ----------------------------------------------------------- | |
| def web_search(query: str) -> str: | |
| """Search the web for current information and return a short text digest. | |
| Use this for facts that may be recent, niche, or beyond your training data. | |
| """ | |
| if not settings.tavily_api_key: | |
| return "Web search is unavailable (TAVILY_API_KEY is not configured)." | |
| # Imported lazily so the tool module is importable without the dependency | |
| # being exercised (and without network calls at import time). | |
| from tavily import TavilyClient | |
| try: | |
| client = TavilyClient(api_key=settings.tavily_api_key) | |
| resp = client.search(query=query, max_results=3) | |
| results = resp.get("results", []) | |
| if not results: | |
| return "No web results found." | |
| lines = [ | |
| f"- {r.get('title', 'untitled')}: {r.get('content', '').strip()}" | |
| for r in results | |
| ] | |
| return "\n".join(lines) | |
| except Exception as exc: # noqa: BLE001 | |
| return f"Web search error: {exc}" | |
| # The toolset bound to both assistants. | |
| TOOLS = [calculator, web_search] | |
| # Lookup table so the tool-calling loop can dispatch by name. | |
| TOOLS_BY_NAME = {t.name: t for t in TOOLS} | |
| def run_tool_call(name: str, args: dict) -> str: | |
| """Execute one tool call by name and return its result as a string.""" | |
| annotate_span(metadata={"tool": name, "args": args}) | |
| tool_obj = TOOLS_BY_NAME.get(name) | |
| if tool_obj is None: | |
| return f"Unknown tool: {name}" | |
| return str(tool_obj.invoke(args)) | |