Spaces:
Running
Running
| from __future__ import annotations | |
| import ast | |
| import json | |
| import logging | |
| import operator | |
| import re | |
| import urllib.error | |
| import urllib.parse | |
| import urllib.request | |
| from datetime import UTC, datetime | |
| from langchain_core.tools import tool | |
| logger = logging.getLogger(__name__) | |
| _USER_AGENT = "ollive-api/1.0 (local-assignment; python-urllib)" | |
| _WIKI_SUMMARY_URL = "https://en.wikipedia.org/api/rest_v1/page/summary/{title}" | |
| _WIKI_SEARCH_URL = "https://en.wikipedia.org/w/api.php" | |
| _SAFE_OPERATORS = { | |
| 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, | |
| ast.USub: operator.neg, | |
| } | |
| _UNIT_ALIASES = { | |
| "c": "c", | |
| "celsius": "c", | |
| "f": "f", | |
| "fahrenheit": "f", | |
| "km": "km", | |
| "kilometer": "km", | |
| "kilometers": "km", | |
| "mi": "mi", | |
| "mile": "mi", | |
| "miles": "mi", | |
| "kg": "kg", | |
| "kilogram": "kg", | |
| "kilograms": "kg", | |
| "lb": "lb", | |
| "lbs": "lb", | |
| "pound": "lb", | |
| "pounds": "lb", | |
| "m": "m", | |
| "meter": "m", | |
| "meters": "m", | |
| "metre": "m", | |
| "metres": "m", | |
| "ft": "ft", | |
| "foot": "ft", | |
| "feet": "ft", | |
| } | |
| def _safe_eval(node: ast.AST) -> float: | |
| if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)): | |
| return float(node.value) | |
| if isinstance(node, ast.UnaryOp) and type(node.op) in _SAFE_OPERATORS: | |
| return _SAFE_OPERATORS[type(node.op)](_safe_eval(node.operand)) | |
| if isinstance(node, ast.BinOp) and type(node.op) in _SAFE_OPERATORS: | |
| left = _safe_eval(node.left) | |
| right = _safe_eval(node.right) | |
| if isinstance(node.op, ast.Pow) and abs(right) > 10: | |
| raise ValueError("Exponent too large") | |
| return _SAFE_OPERATORS[type(node.op)](left, right) | |
| raise ValueError("Unsupported expression") | |
| def _wiki_request(url: str) -> dict: | |
| request = urllib.request.Request(url, headers={"User-Agent": _USER_AGENT}) | |
| with urllib.request.urlopen(request, timeout=12) as response: | |
| return json.loads(response.read().decode("utf-8")) | |
| def _wiki_search_title(query: str) -> str | None: | |
| params = urllib.parse.urlencode( | |
| { | |
| "action": "query", | |
| "list": "search", | |
| "srsearch": query, | |
| "format": "json", | |
| "srlimit": 1, | |
| "utf8": 1, | |
| } | |
| ) | |
| payload = _wiki_request(f"{_WIKI_SEARCH_URL}?{params}") | |
| hits = payload.get("query", {}).get("search", []) | |
| if not hits: | |
| return None | |
| return hits[0].get("title") | |
| def _wiki_summary_for_title(title: str) -> str: | |
| encoded = urllib.parse.quote(title.replace(" ", "_"), safe="") | |
| payload = _wiki_request(_WIKI_SUMMARY_URL.format(title=encoded)) | |
| if payload.get("type") == "disambiguation": | |
| raise ValueError( | |
| f"'{title}' matches multiple Wikipedia pages. Ask with a more specific title." | |
| ) | |
| extract = payload.get("extract", "").strip() | |
| if not extract: | |
| raise ValueError(f"No summary text found for '{title}'.") | |
| page_title = payload.get("title", title) | |
| return f"{page_title}: {extract}" | |
| def fetch_wikipedia_summary(query: str) -> str: | |
| """Fetch a Wikipedia summary using the public REST API with search fallback.""" | |
| cleaned = query.strip().strip("?.!") | |
| if not cleaned: | |
| raise ValueError("Wikipedia query is empty.") | |
| try: | |
| return _wiki_summary_for_title(cleaned) | |
| except urllib.error.HTTPError as exc: | |
| if exc.code != 404: | |
| raise | |
| except ValueError: | |
| raise | |
| resolved = _wiki_search_title(cleaned) | |
| if not resolved: | |
| raise ValueError(f"No Wikipedia article found for '{cleaned}'.") | |
| return _wiki_summary_for_title(resolved) | |
| def calculator(expression: str) -> str: | |
| """Evaluate a basic arithmetic expression with +, -, *, /, parentheses, and powers. | |
| Use when the user asks to calculate, compute, evaluate, or solve a numeric expression. | |
| Examples: "(17 * 23) + 4", "2 ** 10", "100 / 4". | |
| """ | |
| tree = ast.parse(expression.strip(), mode="eval") | |
| result = _safe_eval(tree.body) | |
| if result.is_integer(): | |
| return str(int(result)) | |
| return str(round(result, 6)) | |
| def get_current_time(timezone_name: str = "UTC") -> str: | |
| """Return the current date and time in UTC. | |
| Use when the user asks for the current time, today's date, or what time it is now. | |
| The timezone_name argument is accepted for compatibility but UTC is returned. | |
| """ | |
| _ = timezone_name | |
| now = datetime.now(UTC) | |
| return now.strftime("%Y-%m-%d %H:%M:%S UTC") | |
| def word_counter(text: str) -> str: | |
| """Count words and characters in a piece of text. | |
| Use when the user asks for a word count, character count, or how long a passage is. | |
| Pass the exact text to analyze, not the full user question. | |
| """ | |
| words = len(re.findall(r"\b\w+\b", text)) | |
| chars = len(text) | |
| return f"words={words}, characters={chars}" | |
| def wikipedia_summary(query: str) -> str: | |
| """Look up a short Wikipedia summary for a person, place, concept, or topic. | |
| Use for factual background questions such as "Who is Marie Curie?", | |
| "Tell me about the Eiffel Tower", or "Wikipedia summary of photosynthesis". | |
| Pass a concise topic name or article title, not the full conversational question. | |
| """ | |
| try: | |
| return fetch_wikipedia_summary(query) | |
| except Exception as exc: | |
| logger.warning("Wikipedia lookup failed for query=%r: %s", query, exc) | |
| return f"Wikipedia lookup failed: {exc}" | |
| def unit_converter(value: float, from_unit: str, to_unit: str) -> str: | |
| """Convert a numeric value between supported units. | |
| Supported pairs: Celsius/Fahrenheit, kilometers/miles, kilograms/pounds, meters/feet. | |
| Use when the user asks to convert measurements, for example "convert 10 km to miles". | |
| """ | |
| from_key = _UNIT_ALIASES.get(from_unit.strip().lower()) | |
| to_key = _UNIT_ALIASES.get(to_unit.strip().lower()) | |
| if not from_key or not to_key: | |
| supported = ", ".join(sorted(set(_UNIT_ALIASES))) | |
| return f"Unsupported units. Supported aliases include: {supported}" | |
| conversions = { | |
| ("c", "f"): lambda v: (v * 9 / 5) + 32, | |
| ("f", "c"): lambda v: (v - 32) * 5 / 9, | |
| ("km", "mi"): lambda v: v * 0.621371, | |
| ("mi", "km"): lambda v: v / 0.621371, | |
| ("kg", "lb"): lambda v: v * 2.20462, | |
| ("lb", "kg"): lambda v: v / 2.20462, | |
| ("m", "ft"): lambda v: v * 3.28084, | |
| ("ft", "m"): lambda v: v / 3.28084, | |
| } | |
| key = (from_key, to_key) | |
| if key not in conversions: | |
| return f"Unsupported conversion: {from_unit} -> {to_unit}" | |
| converted = conversions[key](value) | |
| return f"{value} {from_unit} = {round(converted, 4)} {to_unit}" | |
| def get_api_tools(): | |
| return [calculator, get_current_time, word_counter, wikipedia_summary, unit_converter] | |