import os from datetime import datetime from typing import Optional import pytz import requests from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import FAISS from langchain_core.tools import tool from tavily import TavilyClient BASE_DIR = os.path.dirname(os.path.abspath(__file__)) FAISS_DIR = os.path.join(BASE_DIR, "faiss_ethics_ch10") _embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2") _vector_store = FAISS.load_local( FAISS_DIR, _embeddings, allow_dangerous_deserialization=True, ) _retriever = _vector_store.as_retriever(search_kwargs={"k": 4}) def _tavily_client() -> TavilyClient: api_key = os.getenv("TAVILY_API_KEY") if not api_key: raise ValueError("TAVILY_API_KEY is not configured") return TavilyClient(api_key=api_key) def _http_get( url: str, timeout: int = 15, headers: Optional[dict] = None, params: Optional[dict] = None, ) -> requests.Response: return requests.get(url, timeout=timeout, headers=headers or {}, params=params or {}) @tool def rag_tool(query: str) -> str: """Retrieve relevant passages from the stored PDF document index.""" docs = _retriever.invoke(query) if not docs: return "No matching document passages found." parts = [] for i, d in enumerate(docs, 1): parts.append(f"[{i}] {d.page_content}") return "\n\n".join(parts) @tool def web_search(query: str) -> str: """Search the web via Tavily for up-to-date information. Use for current office holders, news, sports, commodities (silver/gold), and any fact that may have changed. Include the current year in the query.""" try: client = _tavily_client() response = client.search( query=query, search_depth="advanced", max_results=5, include_answer=True, ) parts = [] answer = response.get("answer") if answer: parts.append(f"Summary: {answer}") results = response.get("results") or [] for i, hit in enumerate(results, 1): title = hit.get("title", "") content = hit.get("content", "") url = hit.get("url", "") parts.append(f"{i}. {title}\n{content}\nSource: {url}") if not parts: return "No web results found." return "\n\n".join(parts) except Exception as e: return f"Web search failed: {type(e).__name__}: {e}" @tool def calculator(first_num: float, second_num: float, operation: str) -> dict: """Perform basic arithmetic operation on two numbers. Supported operation: add, sub, mul, div""" try: if operation == "add": result = first_num + second_num elif operation == "sub": result = first_num - second_num elif operation == "mul": result = first_num * second_num elif operation == "div": if second_num == 0: return {"error": "Division by zero not allowed!"} result = first_num / second_num else: return {"error": f"Unsupported operation '{operation}'"} return { "first_num": first_num, "second_num": second_num, "operation": operation, "result": result, } except Exception as e: return {"error": str(e)} @tool def get_stock_price(symbol: str) -> dict: """Return the latest daily close price for a stock symbol (e.g., AAPL, TSLA).""" symbol = symbol.strip().upper() url = f"https://www.alphavantage.co/query?function=TIME_SERIES_DAILY&symbol={symbol}&apikey=MU5WKN30VAC2LCDG" try: r = requests.get(url, timeout=15) data = r.json() ts = data.get("Time Series (Daily)") if not ts: return {"symbol": symbol, "error": "No data returned", "raw": data} latest_date = max(ts.keys()) latest = ts[latest_date] return { "symbol": symbol, "date": latest_date, "open": latest.get("1. open"), "high": latest.get("2. high"), "low": latest.get("3. low"), "close": latest.get("4. close"), "volume": latest.get("5. volume"), } except Exception as e: return {"symbol": symbol, "error": str(e)} @tool def get_weather(location: str) -> str: """Get current weather and short forecast for a city or place name (e.g. Chennai, London, New York). Free Open-Meteo data — no API key.""" location = location.strip() if not location: return "Please provide a location name." try: geo = _http_get( "https://geocoding-api.open-meteo.com/v1/search", params={"name": location, "count": 1, "language": "en", "format": "json"}, ) geo.raise_for_status() results = geo.json().get("results") or [] if not results: return f"No location found for '{location}'." place = results[0] lat, lon = place["latitude"], place["longitude"] name = place.get("name", location) country = place.get("country", "") admin = place.get("admin1", "") label = ", ".join(p for p in (name, admin, country) if p) forecast = _http_get( "https://api.open-meteo.com/v1/forecast", params={ "latitude": lat, "longitude": lon, "current": "temperature_2m,relative_humidity_2m,apparent_temperature,precipitation,weather_code,wind_speed_10m", "daily": "weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum", "timezone": "auto", "forecast_days": 3, }, ) forecast.raise_for_status() data = forecast.json() cur = data.get("current") or {} daily = data.get("daily") or {} code = cur.get("weather_code") desc = _weather_code_label(code) lines = [ f"Weather for {label}:", f"Now: {cur.get('temperature_2m')}°C (feels like {cur.get('apparent_temperature')}°C), {desc}", f"Humidity: {cur.get('relative_humidity_2m')}%, Wind: {cur.get('wind_speed_10m')} km/h", ] if daily.get("time"): lines.append("Next days:") for i, day in enumerate(daily["time"][:3]): d_code = (daily.get("weather_code") or [None])[i] tmax = (daily.get("temperature_2m_max") or [None])[i] tmin = (daily.get("temperature_2m_min") or [None])[i] lines.append(f" {day}: {tmin}–{tmax}°C, {_weather_code_label(d_code)}") return "\n".join(lines) except Exception as e: return f"Weather lookup failed: {type(e).__name__}: {e}" def _weather_code_label(code: Optional[int]) -> str: if code is None: return "unknown" labels = { 0: "clear sky", 1: "mainly clear", 2: "partly cloudy", 3: "overcast", 45: "fog", 48: "depositing rime fog", 51: "light drizzle", 53: "moderate drizzle", 55: "dense drizzle", 61: "slight rain", 63: "moderate rain", 65: "heavy rain", 71: "slight snow", 73: "moderate snow", 75: "heavy snow", 80: "rain showers", 81: "moderate rain showers", 82: "violent rain showers", 95: "thunderstorm", } return labels.get(code, f"weather code {code}") @tool def wikipedia_search(query: str) -> str: """Search Wikipedia for encyclopedia summaries. Use for historical facts, definitions, biographies, science concepts — not for live news or prices.""" query = query.strip() if not query: return "Please provide a search query." try: search = _http_get( "https://en.wikipedia.org/w/api.php", params={ "action": "query", "list": "search", "srsearch": query, "format": "json", "srlimit": 3, }, headers={"User-Agent": "SynapseAI/1.0 (education chatbot)"}, ) search.raise_for_status() hits = search.json().get("query", {}).get("search") or [] if not hits: return f"No Wikipedia articles found for '{query}'." parts = [] for hit in hits[:3]: title = hit.get("title", "") summary = _http_get( f"https://en.wikipedia.org/api/rest_v1/page/summary/{requests.utils.quote(title, safe='')}", headers={"User-Agent": "SynapseAI/1.0 (education chatbot)"}, ) if summary.status_code != 200: continue s = summary.json() extract = (s.get("extract") or "")[:1200] url = s.get("content_urls", {}).get("desktop", {}).get("page", "") parts.append(f"**{title}**\n{extract}\nSource: {url}") if not parts: return f"Could not load Wikipedia summaries for '{query}'." return "\n\n".join(parts) except Exception as e: return f"Wikipedia search failed: {type(e).__name__}: {e}" @tool def convert_currency(amount: float, from_currency: str, to_currency: str) -> str: """Convert money between currencies using live exchange rates (Frankfurter/ECB). Use ISO codes like USD, EUR, INR, GBP.""" from_currency = from_currency.strip().upper() to_currency = to_currency.strip().upper() try: r = _http_get( "https://api.frankfurter.app/latest", params={"amount": amount, "from": from_currency, "to": to_currency}, ) r.raise_for_status() data = r.json() rate_date = data.get("date", "") converted = (data.get("rates") or {}).get(to_currency) if converted is None: return f"Could not convert {from_currency} to {to_currency}." return ( f"{amount} {from_currency} = {converted} {to_currency} " f"(rate date: {rate_date}, source: ECB via Frankfurter)" ) except Exception as e: return f"Currency conversion failed: {type(e).__name__}: {e}" @tool def lookup_pincode(pincode: str) -> str: """Look up Indian postal pincode details (state, district, post offices). Use for 6-digit Indian pincodes only.""" pincode = pincode.strip() if not pincode.isdigit() or len(pincode) != 6: return "Please provide a valid 6-digit Indian pincode." try: r = _http_get(f"https://api.postalpincode.in/pincode/{pincode}") r.raise_for_status() payload = r.json() if not payload or payload[0].get("Status") != "Success": msg = payload[0].get("Message", "Pincode not found") if payload else "Pincode not found" return msg post_offices = payload[0].get("PostOffice") or [] if not post_offices: return f"No post offices found for pincode {pincode}." sample = post_offices[0] header = ( f"Pincode {pincode}: {sample.get('District', '')}, " f"{sample.get('State', '')}, {sample.get('Country', 'India')}" ) offices = [] for po in post_offices[:12]: offices.append( f"- {po.get('Name', '')} ({po.get('BranchType', '')}, {po.get('Block', '')})" ) extra = "" if len(post_offices) > 12: extra = f"\n... and {len(post_offices) - 12} more post offices." return header + "\nPost offices:\n" + "\n".join(offices) + extra except Exception as e: return f"Pincode lookup failed: {type(e).__name__}: {e}" @tool def fetch_url(url: str) -> str: """Read and summarize the main text content of a web page URL. Use when the user shares a link or asks what's on a specific page.""" url = url.strip() if not url.startswith(("http://", "https://")): url = "https://" + url try: r = _http_get( f"https://r.jina.ai/{url}", timeout=30, headers={"Accept": "text/plain"}, ) r.raise_for_status() text = r.text.strip() if len(text) > 8000: text = text[:8000] + "\n\n[Content truncated.]" return text or "No readable content returned from that URL." except Exception as e: return f"URL fetch failed: {type(e).__name__}: {e}" @tool def github_search(query: str, search_type: str = "repositories") -> str: """Search GitHub for repositories or users. search_type: 'repositories' or 'users'. Use for open-source projects, repos, GitHub profiles.""" query = query.strip() if not query: return "Please provide a search query." search_type = search_type.strip().lower() if search_type not in ("repositories", "users"): search_type = "repositories" headers = { "Accept": "application/vnd.github+json", "User-Agent": "SynapseAI/1.0", } token = os.getenv("GITHUB_TOKEN") if token: headers["Authorization"] = f"Bearer {token}" try: if search_type == "users": r = _http_get( "https://api.github.com/search/users", params={"q": query, "per_page": 5}, headers=headers, ) else: r = _http_get( "https://api.github.com/search/repositories", params={"q": query, "sort": "stars", "order": "desc", "per_page": 5}, headers=headers, ) r.raise_for_status() items = r.json().get("items") or [] if not items: return f"No GitHub {search_type} found for '{query}'." parts = [] for i, item in enumerate(items, 1): if search_type == "users": parts.append( f"{i}. {item.get('login')} — {item.get('html_url')}\n" f" Type: {item.get('type', 'User')}" ) else: desc = (item.get("description") or "No description")[:200] parts.append( f"{i}. {item.get('full_name')} (★ {item.get('stargazers_count', 0)})\n" f" {desc}\n" f" {item.get('html_url')}" ) return "\n\n".join(parts) except Exception as e: return f"GitHub search failed: {type(e).__name__}: {e}" @tool def geo_lookup(ip: str = "") -> str: """Look up geographic location for an IP address. Leave ip empty to look up the server's public IP. Use for 'where is this IP' questions — not for street-level user location.""" ip = ip.strip() try: if not ip: ip_resp = _http_get("https://api.ipify.org?format=json") ip_resp.raise_for_status() ip = ip_resp.json().get("ip", "") if not ip: return "Could not determine public IP." r = _http_get( f"http://ip-api.com/json/{ip}", params={ "fields": "status,message,country,countryCode,regionName,city,lat,lon,timezone,isp,query", }, ) r.raise_for_status() data = r.json() if data.get("status") != "success": return data.get("message", f"Lookup failed for IP {ip}") return ( f"IP: {data.get('query')}\n" f"Location: {data.get('city')}, {data.get('regionName')}, {data.get('country')} ({data.get('countryCode')})\n" f"Coordinates: {data.get('lat')}, {data.get('lon')}\n" f"Timezone: {data.get('timezone')}\n" f"ISP: {data.get('isp')}" ) except Exception as e: return f"Geo lookup failed: {type(e).__name__}: {e}" @tool def current_datetime(tz_name: str = "Asia/Kolkata") -> str: """Return current date & time for a given timezone (default: Asia/Kolkata).""" tz = pytz.timezone(tz_name) return datetime.now(tz).strftime("%A, %d %B %Y, %I:%M:%S %p %Z")