# tools/tool_handler.py # # Central handler for all tool execution # - Routes tool calls to appropriate tool class # - Returns formatted results for chat display import sys import os import logging import re from typing import Dict, Any, Optional import time from datetime import datetime, timezone, timedelta # Local DB handle mirroring services/webui_service.DATABASE (no cross-module import to avoid circular deps) _IA_DATABASE = "/home/workspace/Projects/NeuralAI/data/neuralai.db" def _domain_of(url: str) -> str: """Return a short readable host (e.g. bbc.com) from a URL.""" try: from urllib.parse import urlparse net = urlparse(url).netloc return net[4:] if net.startswith("www.") else (net or "source") except Exception: return "source" def get_db(): import sqlite3 from pathlib import Path Path("/home/workspace/Projects/NeuralAI/data").mkdir(parents=True, exist_ok=True) return sqlite3.connect(_IA_DATABASE) logger = logging.getLogger("NeuralAI.ToolHandler") import logging logger = logging.getLogger(__name__) # Add tools to path sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from tools.code_sandbox import CodeSandbox, execute from tools.file_manager import FileManager from tools.web_fetcher import WebFetcher from tools.web_browser import get_manager, close_session from tools.web_search import WebSearch from tools.db_connector import DatabaseConnector from tools.summarize import summarize_sources from tools.git_assistant import GitAssistant from tools.image_generator import image_generator from tools.media_generator import ( generate_text, generate_video, generate_audio, generate_embeddings, realtime_voice_url, ) class MediaGenerator: """Thin wrapper restoring the legacy class API over the refactored module-level media functions in tools.media_generator.""" def text(self, prompt: str, model: str = "openai") -> dict: return generate_text(prompt) def video(self, prompt: str) -> dict: res = generate_video(prompt) if res.get("success") and "video_url" in res: res["url"] = res["video_url"] return res def audio(self, text: str, voice: str = "alloy") -> dict: res = generate_audio(text, voice) if res.get("success") and "audio_url" in res: res["url"] = res["audio_url"] return res def realtime_voice(self) -> dict: url = realtime_voice_url() return {"success": True, "url": url, "model": "pollinations-realtime"} def embed(self, text: str, model: str = "openai-3-small") -> dict: return generate_embeddings(text, model) media = MediaGenerator() from tools.voice_transcriber import voice_transcriber from tools.knowledge_graph import save_memory, search_memory, get_graph, recall, extract_and_store from tools.agentic import run as agentic_run # --- reminder helpers (used by /remind) --- def _parse_remind_time(text: str) -> Optional[float]: """Parse a natural-ish time into a unix epoch (UTC). Returns None if unparseable.""" import re as _re low = text.lower().strip() now = time.time() m = _re.match(r"^(\d+)\s*(s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days)$", low) if m: n = int(m.group(1)); unit = m.group(2)[0] mult = {"s": 1, "m": 60, "h": 3600, "d": 86400}[unit] return now + n * mult # also accept " " like "30 seconds" / "in 30 seconds" mw = _re.match(r"(?:in\s+)?(\d+)\s*(second|minute|hour|day|sec|min|hr)s?\b", low) if mw: n = int(mw.group(1)); unit = mw.group(2)[0] mult = {"s": 1, "m": 60, "h": 3600, "d": 86400}[unit] return now + n * mult mt = _re.match(r"tomorrow\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?", low) if mt: hh = int(mt.group(1)); mm = int(mt.group(2) or 0) if mt.group(3) == "pm" and hh < 12: hh += 12 if mt.group(3) == "am" and hh == 12: hh = 0 t = (datetime.now() + timedelta(days=1)).replace(hour=hh, minute=mm, second=0, microsecond=0) return t.timestamp() ma = _re.match(r"(\d{4}-\d{2}-\d{2})[ T](\d{1,2}):(\d{2})", text.strip()) if ma: try: return datetime.strptime(f"{ma.group(1)} {ma.group(2)}:{ma.group(3)}", "%Y-%m-%d %H:%M").timestamp() except Exception: return None return None def _remind_msg(text: str) -> str: """Extract the message portion after a leading time expression.""" import re as _re cleaned = _re.sub(r"^\s*(\d+\s*(s|m|h|d|sec|min|hr|hour|day|mins|hrs|hours|days|seconds)\b)", "", text, flags=_re.I).strip() cleaned = _re.sub(r"^\s*tomorrow\s+\d{1,2}(?::\d{2})?\s*(am|pm)?\b", "", cleaned, flags=_re.I).strip() cleaned = _re.sub(r"^\s*\d{4}-\d{2}-\d{2}[ T]\d{1,2}:\d{2}\b", "", cleaned).strip() cleaned = _re.sub(r"^\s*in\s+\d+\s*(second|minute|hour|day|sec|min|hr)s?\b", "", cleaned, flags=_re.I).strip() return cleaned or text try: from tools.refine import refine_text as _refine_text except Exception: _refine_text = None # Tools whose output should be passed through refine_text inside execute() _REFINE_KINDS = { "news": "news", "web_search": "web_search", "web_fetcher": "web_fetcher", "research": "research", } class ToolHandler: """Central handler for tool execution.""" def __init__(self, workspace: str = "/home/workspace"): self.workspace = workspace self.code_sandbox = CodeSandbox() self.file_manager = FileManager(base_dir=workspace) self.web_fetcher = WebFetcher() self.web_search = WebSearch() self.db_connector = DatabaseConnector() self.git_assistant = GitAssistant(repo_path=workspace) self.media = media self.knowledge_graph = True # module-level functions, no instance state needed def execute(self, tool: str, params: Dict[str, Any]) -> Dict[str, Any]: """ Execute a tool with the given parameters. Args: tool: Tool name (terminal, code_exec, file_manager, web_fetcher, database, git) params: Tool-specific parameters Returns: { "success": bool, "output": str, "error": str, "data": dict (tool-specific) } """ handlers = { "terminal": self._handle_terminal, "code_exec": self._handle_code_exec, "file_manager": self._handle_file_manager, "web_fetcher": self._handle_web_fetcher, "web_browser": self._handle_web_browser, "web_search": self._handle_web_search, "research": self._handle_research, "database": self._handle_database, "git": self._handle_git, "image": self._handle_image, "speak": self._handle_speak, "summarize": self._handle_summarize, "translate": self._handle_translate, "news": self._handle_news, "youtube": self._handle_youtube, "text": self._handle_text, "video": self._handle_video, "audio": self._handle_audio, "voice": self._handle_voice, "embed": self._handle_embed, "remember": self._handle_remember, "recall": self._handle_recall, "graph": self._handle_graph, "agent": self._handle_agent, "autosave": self._handle_autosave, "calc": self._handle_calc, "doc": self._handle_doc, "remember": self._handle_remember, "recall": self._handle_recall, "vision": self._handle_vision, "remind": self._handle_remind, } handler = handlers.get(tool) if handler is None: return {"success": False, "output": "", "error": f"Unknown tool: {tool}", "data": {}} # Normalize params: callers (e.g. the news automation) may pass a bare # string as the query. Every handler expects a dict, so coerce strings # into {"query": } to avoid "'str' object has no attribute 'get'". if isinstance(params, str): params = {"query": params} try: result = handler(params) except Exception as e: logger.error(f"[tool_handler] {tool} failed: {e}") return {"success": False, "output": "", "error": str(e), "data": {}} # Refine display output for web tools so /api/tool returns clean markdown # regardless of whether the call came from the NL router or a slash command. if _refine_text is not None and tool in _REFINE_KINDS and isinstance(result, dict) and result.get("success"): raw = result.get("output", "") if raw: try: result["output"] = _refine_text(raw, _REFINE_KINDS[tool]) except Exception: pass return result def _handle_remember(self, params: Dict[str, Any]) -> Dict[str, Any]: content = params.get("content", "") if not content and isinstance(params.get("text"), str): content = params["text"] if not content: return {"success": False, "error": "content required", "output": "", "data": {}} res = save_memory(content, tags=params.get("tags", ""), relation_to=params.get("relation_to", ""), relation=params.get("relation", "")) return {"success": res.get("success", False), "output": f"๐Ÿง  Remembered (id={res.get('id','?')}, remote={res.get('remote', False)})", "error": "", "data": res} def _handle_recall(self, params: Dict[str, Any]) -> Dict[str, Any]: query = params.get("query", params.get("q", "")) if not query: return {"success": False, "error": "query required", "output": "", "data": {}} out = recall(query, limit=int(params.get("limit", 5))) return {"success": True, "output": out, "error": "", "data": {}} def _handle_graph(self, params: Dict[str, Any]) -> Dict[str, Any]: node_id = params.get("id", "") if not node_id: return {"success": False, "error": "id required", "output": "", "data": {}} g = get_graph(node_id) return {"success": True, "output": json.dumps(g), "error": "", "data": g} def _handle_agent(self, params: Dict[str, Any]) -> Dict[str, Any]: task = params.get("task", params.get("query", "")) if not task: return {"success": False, "error": "task required", "output": "", "data": {}} res = agentic_run(task, tool_handler=self) return {"success": res.get("success", False), "output": f"๐Ÿค– Agent completed {res.get('steps',0)} steps:\n\n{res.get('brief','')}", "error": "", "data": res} def _handle_autosave(self, params: Dict[str, Any]) -> Dict[str, Any]: text = params.get("text", params.get("content", "")) if not text: return {"success": False, "error": "text required", "output": "", "data": {}} res = extract_and_store(text) return {"success": res.get("success", False), "output": f"๐Ÿง  Conversation ingested for auto-memory (remote={res.get('remote', False)})", "error": "", "data": res} handler = handlers.get(tool) if not handler: return { "success": False, "output": "", "error": f"Unknown tool: {tool}", "data": {} } try: return handler(params) except Exception as e: return { "success": False, "output": "", "error": f"Tool execution error: {str(e)}", "data": {} } def _handle_terminal(self, params: Dict[str, Any]) -> Dict[str, Any]: """Handle terminal/shell commands.""" command = params.get("command", "") if not command: return { "success": False, "output": "", "error": "No command provided", "data": {} } result = self.code_sandbox.run_bash(command) output = result["output"] if result["error"]: output += f"\n[stderr]\n{result['error']}" return { "success": result["success"], "output": output, "error": "", "data": { "exit_code": result["exit_code"], "execution_time": result["execution_time"] } } def _handle_code_exec(self, params: Dict[str, Any]) -> Dict[str, Any]: """Handle code execution requests.""" code = params.get("code", "") language = params.get("language", "python") if not code: return { "success": False, "output": "", "error": "No code provided", "data": {} } # If code doesn't look like actual code, it might be a message # asking to run something mentioned elsewhere if not any(kw in code for kw in ["def ", "function ", "print(", "console.log", "import "]): return { "success": False, "output": "", "error": "No executable code detected. Provide code to run.", "data": {} } result = execute(code, language=language) output = result["output"] if result["error"]: output += f"\n[error]\n{result['error']}" return { "success": result["success"], "output": output, "error": "", "data": { "exit_code": result["exit_code"], "execution_time": result["execution_time"], "language": language } } def _handle_file_manager(self, params: Dict[str, Any]) -> Dict[str, Any]: """Handle file operations.""" query = params.get("query", "").lower() # Determine operation from query if "list" in query or "show files" in query or "what files" in query: path = params.get("path", ".") result = self.file_manager.list_dir(path) if result["success"]: output = f"Directory: {result['path']}\n\n" output += f"Directories ({result['total_dirs']}):\n" for d in result["directories"]: output += f" ๐Ÿ“ {d['name']}\n" output += f"\nFiles ({result['total_files']}):\n" for f in result["files"]: size = self._format_size(f["size"]) output += f" ๐Ÿ“„ {f['name']} ({size})\n" return { "success": True, "output": output, "error": "", "data": result } return { "success": False, "output": "", "error": result.get("error", "Failed to list directory"), "data": result } elif "read" in query: # Extract path from query path = params.get("path", "") if not path: # Try to extract from query import re match = re.search(r"read (?:file )?['\"]?([^\s'\"]+)['\"]?", query) if match: path = match.group(1) result = self.file_manager.read_file(path) if result["success"]: output = f"File: {result['path']}\n" output += f"Size: {result['size']} bytes | Lines: {result['lines']}\n\n" output += f"```\n{result['content']}\n```" return { "success": True, "output": output, "error": "", "data": result } return { "success": False, "output": "", "error": result.get("error", "Failed to read file"), "data": result } elif "search" in query or "find" in query: # Extract search pattern pattern = params.get("pattern", "") if not pattern: # Try to extract from query import re match = re.search(r"(?:search|find) (?:for )?['\"]?([^\s'\"]+)['\"]?", query) if match: pattern = match.group(1) else: pattern = query.split()[-1] # Use last word as fallback search_content = "content" in query or "in files" in query result = self.file_manager.search(pattern, search_content=search_content) if result["success"]: output = f"Search for '{pattern}': {result['total']} results\n\n" for r in result["results"][:20]: if r.get("line"): output += f"๐Ÿ“„ {r['path']}:{r['line']}\n {r['match']}\n\n" else: output += f"๐Ÿ“„ {r['path']}\n" return { "success": True, "output": output, "error": "", "data": result } return { "success": False, "output": "", "error": result.get("error", "Search failed"), "data": result } # Default: list current directory result = self.file_manager.list_dir() if result["success"]: output = f"Directory: {result['path']}\n\n" output += f"Directories ({result['total_dirs']}):\n" for d in result["directories"][:10]: output += f" ๐Ÿ“ {d['name']}\n" output += f"\nFiles ({result['total_files']}):\n" for f in result["files"][:10]: size = self._format_size(f["size"]) output += f" ๐Ÿ“„ {f['name']} ({size})\n" return { "success": True, "output": output, "error": "", "data": result } return { "success": False, "output": "", "error": result.get("error", "Failed to list directory"), "data": result } def _handle_web_fetcher(self, params: Dict[str, Any]) -> Dict[str, Any]: """Handle web fetching operations.""" url = params.get("url", "") if not url: return { "success": False, "output": "", "error": "No URL provided", "data": {} } # Fetch and parse the URL result = self.web_fetcher.fetch(url) if result["success"]: output = f"URL: {result['url']}\n" output += f"Title: {result['title']}\n" output += f"Status: {result['status']}\n\n" output += f"Content Preview:\n{result['text'][:1500]}...\n\n" output += f"Links: {len(result['links'])} found\n" output += f"Images: {len(result['images'])} found" return { "success": True, "output": output, "error": "", "data": result } return { "success": False, "output": "", "error": result.get("error", "Failed to fetch URL"), "data": result } def _handle_database(self, params: Dict[str, Any]) -> Dict[str, Any]: """Handle database operations.""" query = params.get("query", "").lower() sql = params.get("sql", "") # Default to NeuralAI's database if not connected if not self.db_connector.active_db: db_path = os.path.join(self.workspace, "Projects/NeuralAI/from-scratch/web_ui/neuralai.db") if os.path.exists(db_path): self.db_connector.connect_sqlite(db_path, "neuralai") else: # Create in-memory DB for testing self.db_connector.connect_sqlite(":memory:", "memory") if "show tables" in query or "list tables" in query: result = self.db_connector.tables() if result["success"]: output = f"Tables ({result['count']}):\n" for t in result["tables"]: output += f" ๐Ÿ“Š {t}\n" return { "success": True, "output": output, "error": "", "data": result } return { "success": False, "output": "", "error": result.get("error", "Failed to list tables"), "data": result } elif "schema" in query: result = self.db_connector.schema() if result["success"]: output = "Database Schema:\n\n" for table in result["tables"]: output += f"๐Ÿ“Š {table['name']}:\n" for col in table["columns"]: pk = " ๐Ÿ”‘" if col["primary_key"] else "" output += f" - {col['name']}: {col['type']}{pk}\n" output += "\n" return { "success": True, "output": output, "error": "", "data": result } return { "success": False, "output": "", "error": result.get("error", "Failed to get schema"), "data": result } elif sql: result = self.db_connector.query(sql) if result["success"]: output = f"Query: {sql}\n\n" if result["rows"]: output += f"Results ({result['row_count']} rows):\n" # Format as table if result["columns"]: output += "| " + " | ".join(result["columns"]) + " |\n" output += "|" + "|".join(["---" for _ in result["columns"]]) + "|\n" for row in result["rows"][:20]: values = [str(v) for v in row.values()] output += "| " + " | ".join(values) + " |\n" else: output += f"Affected {result['row_count']} rows" return { "success": True, "output": output, "error": "", "data": result } return { "success": False, "output": "", "error": result.get("error", "Query failed"), "data": result } # Default: show tables result = self.db_connector.tables() output = f"Connected to: {self.db_connector.active_db}\n\n" output += f"Tables ({result.get('count', 0)}):\n" for t in result.get("tables", []): output += f" ๐Ÿ“Š {t}\n" return { "success": True, "output": output, "error": "", "data": result } def _handle_git(self, params: Dict[str, Any]) -> Dict[str, Any]: """Handle git operations.""" action = params.get("action", "status").lower() # Check if in a git repo if not self.git_assistant.is_repo()["is_repo"]: return { "success": False, "output": "", "error": "Not a git repository", "data": {} } if "status" in action: result = self.git_assistant.status() if result["success"]: output = f"Branch: {result['branch']}\n" output += f"Ahead: {result['ahead']} | Behind: {result['behind']}\n\n" if result["staged"]: output += "Staged:\n" for f in result["staged"]: output += f" โœ… {f}\n" if result["modified"]: output += "Modified:\n" for f in result["modified"]: output += f" ๐Ÿ“ {f}\n" if result["untracked"]: output += "Untracked:\n" for f in result["untracked"]: output += f" โ“ {f}\n" if not any([result["staged"], result["modified"], result["untracked"]]): output += "Working directory clean โœจ" return { "success": True, "output": output, "error": "", "data": result } elif "log" in action: result = self.git_assistant.log(count=10) if result["success"]: output = f"Recent commits ({result['count']}):\n\n" for c in result["commits"]: output += f"๐Ÿ“ {c['hash']} - {c['message']}\n" output += f" {c['author']} โ€ข {c['date']}\n\n" return { "success": True, "output": output, "error": "", "data": result } elif "branch" in action: result = self.git_assistant.branch(list_all=True) if result["success"]: output = f"Current: {result['current']}\n\n" output += "Branches:\n" for b in result["branches"]: marker = "โ†’ " if b == result["current"] else " " output += f"{marker}{b}\n" return { "success": True, "output": output, "error": "", "data": result } elif "diff" in action: result = self.git_assistant.diff() if result["success"]: output = "Git Diff:\n\n" output += f"```diff\n{result['diff']}\n```" return { "success": True, "output": output, "error": "", "data": result } elif "remote" in action: result = self.git_assistant.remote() if result["success"]: output = "Remotes:\n" for name, url in result["remotes"].items(): output += f" {name}: {url}\n" return { "success": True, "output": output, "error": "", "data": result } # Default: show status result = self.git_assistant.status() if result["success"]: output = f"Branch: {result['branch']}\n" output += f"Ahead: {result['ahead']} | Behind: {result['behind']}\n\n" if result["staged"]: output += "Staged:\n" for f in result["staged"]: output += f" โœ… {f}\n" if result["modified"]: output += "Modified:\n" for f in result["modified"]: output += f" ๐Ÿ“ {f}\n" if result["untracked"]: output += "Untracked:\n" for f in result["untracked"]: output += f" โ“ {f}\n" if not any([result["staged"], result["modified"], result["untracked"]]): output += "Working directory clean โœจ" return { "success": True, "output": output, "error": "", "data": result } return { "success": False, "output": "", "error": "Git operation failed", "data": {} } def _handle_web_browser(self, params: Dict[str, Any]) -> Dict[str, Any]: """Agentic browsing with Playwright (real web surfing).""" url = params.get("url", "") steps = params.get("steps", []) session_id = params.get("session_id", "default") if not url: return {"success": False, "output": "", "error": "No URL provided", "data": {}} try: sess = get_manager(session_id) result = sess.run(url, steps or []) return {"success": True, "output": result, "error": "", "data": {}} except Exception as e: return {"success": False, "output": "", "error": f"Browse error: {e}", "data": {}} finally: if session_id and not params.get("keep_session"): close_session(session_id) def _handle_web_search(self, params: Dict[str, Any]) -> Dict[str, Any]: """Web search via provider or built-in fetcher: /web .""" query = (params.get("query") or "").strip() if not query: return {"success": False, "output": "", "error": "No query provided", "data": {}} try: results = self.web_search.search(query, top_k=int(params.get("top_k", 5))) if isinstance(results, str): return {"success": True, "output": results, "error": "", "data": {}} if not results: return {"success": False, "output": "", "error": f"No results for '{query}'", "data": {}} out = ["**Search results**\n"] seen = set() for i, r in enumerate(results, 1): url = r.get("url", "") if url in seen: continue seen.add(url) title = r.get("title", "").strip() snippet = (r.get("snippet", "") or "").strip() src = r.get("source", "") or "" line = f"{i}. [{title}]({url})" if url else f"{i}. {title}" if src and src != title: line += f" โ€” {src}" if snippet and snippet != src: line += f"\n {snippet[:220]}" out.append(line) return {"success": True, "output": "\n\n".join(out), "error": "", "data": {"results": results}} except Exception as e: return {"success": False, "output": "", "error": f"Search error: {e}", "data": {}} def _handle_image(self, params: Dict[str, Any]) -> Dict[str, Any]: """AI image generation: /img .""" prompt = (params.get("prompt") or "").strip() if not prompt: return {"success": False, "error": "Usage: /img "} try: res = image_generator.generate(prompt) if not isinstance(res, dict) or not res.get("success", False): err = res.get("error", "Image generation failed") if isinstance(res, dict) else str(res) return {"success": False, "error": err, "prompt": prompt} url = res.get("image_url") or res.get("url") path = res.get("image_path") or res.get("path") summary = f"Image generated: {url}" if url else f"Image saved to {path}" return { "success": True, "output": summary, "url": url, "path": path, "prompt": prompt, } except Exception as e: return {"success": False, "error": f"Image generation failed: {e}"} def _handle_speak(self, params: Dict[str, Any]) -> Dict[str, Any]: """Text-to-speech: /speak .""" text = (params.get("text") or "").strip() if not text: return {"success": False, "error": "Usage: /speak "} try: from tools.tts import text_to_speech audio_url = text_to_speech(text) return {"success": True, "audio_url": audio_url, "text": text} except Exception as e: return {"success": False, "error": f"TTS failed: {e}"} def _handle_summarize(self, params: Dict[str, Any]) -> Dict[str, Any]: """Summarize a URL or raw text: /summarize .""" text = (params.get("text") or "").strip() if not text: return {"success": False, "error": "Usage: /summarize "} try: if text.startswith("http://") or text.startswith("https://"): fetched = WebFetcher().fetch(text) content = fetched.get("text") or fetched.get("content") or "" src_label = text else: content = text src_label = "provided text" brief = summarize_sources([{"title": src_label, "text": content}], query="summary") return {"success": True, "summary": brief, "source": src_label} except Exception as e: return {"success": False, "error": f"Summarize failed: {e}"} def _handle_translate(self, params: Dict[str, Any]) -> Dict[str, Any]: """Translate text: /translate (default lang=es). Accepts either explicit {text, lang/target} or a legacy {query} blob (e.g. "good morning to french") from the keyword NL router. """ text = (params.get("text") or "").strip() target = (params.get("target") or params.get("lang") or "es").strip() if not text and params.get("query"): # legacy blob: " to " or " " blob = params["query"].strip() m = re.match(r"(?i)^(.*?)\s+to\s+([a-z]{2,}|[a-z]+)\s*$", blob) if m: text, target = m.group(1).strip(), m.group(2).strip() else: # assume first token is the language parts = blob.split(None, 1) if len(parts) == 2: target, text = parts[0].strip(), parts[1].strip() else: text = blob if not text: return {"success": False, "error": "Usage: /translate "} try: from tools.translate import translate_text out = translate_text(text, target) return {"success": True, "output": out, "translation": out, "target": target, "source": text} except Exception as e: return {"success": False, "error": f"Translate failed: {e}"} def _handle_news(self, params: Dict[str, Any]) -> Dict[str, Any]: """News search: /news .""" topic = (params.get("query") or params.get("topic") or "").strip() if not topic: return {"success": False, "output": "", "error": "No topic provided", "data": {}} try: t = re.sub(r"^(the\s+)?(latest|recent|current|today'?s?|newest|top|breaking|headlines?)\s+", "", topic, flags=re.I).strip() if not t or t.lower() in ("news", "headlines", "latest"): t = "top news today" q = t if t.lower().endswith("news") else t + " news" results = self.web_search.search(q, top_k=int(params.get("top_k", 6))) if isinstance(results, str): return {"success": True, "output": results, "error": "", "data": {}} if not results: return {"success": False, "output": "", "error": f"No news for '{topic}'", "data": {}} out = ["**Latest headlines**\n"] seen = set() for i, r in enumerate(results, 1): url = r.get("url", "") if url in seen: continue seen.add(url) title = r.get("title", "").strip() src = r.get("source", "") or "" snippet = (r.get("snippet", "") or "").strip() line = f"{i}. **{title}**" if src: line += f"\n {src}" if snippet and snippet != src: line += f"\n {snippet}" # Emit a clickable link. If the resolver returned a real publisher # URL, use it directly. If it's still a Google News tracking blob, # strip the ?oc= tracking param so the link is short + stable # (clickable to Google's reader) instead of a 300-char blob. if url: if "news.google.com" in url: clean = url.split("?")[0] label = (src + " ยท Read on Google News") if src else "Read on Google News" else: clean = url label = src if src else _domain_of(url) line += f"\n ๐Ÿ”— [{label}]({clean})" out.append(line) return {"success": True, "output": "\n\n".join(out), "error": "", "data": {"results": results}} except Exception as e: return {"success": False, "output": "", "error": f"News error: {e}", "data": {}} def _handle_youtube(self, params: Dict[str, Any]) -> Dict[str, Any]: """YouTube transcript + summary: /yt .""" url = (params.get("url") or params.get("query") or "").strip() if not url or "youtube.com" not in url and "youtu.be" not in url: return {"success": False, "output": "", "error": "Provide a YouTube URL", "data": {}} try: import urllib.request, json, re api = "https://noembed.com/embed?url=" + urllib.parse.quote(url) req = urllib.request.Request(api, headers={"User-Agent": "Mozilla/5.0"}) with urllib.request.urlopen(req, timeout=15) as r: meta = json.loads(r.read().decode("utf-8", errors="ignore")) title = meta.get("title", url) author = meta.get("author_name", "") # Use summary chain on the video page description if available summary = summarize_sources([{"title": title, "url": url, "text": meta.get("title", "")}], query=title, max_sentences=4) out = f"โ–ถ๏ธ YouTube: {title}\n๐Ÿ‘ค {author}\n\n{summary}\n\nWatch: {url}" return {"success": True, "output": out, "error": "", "data": {"title": title, "author": author}} except Exception as e: return {"success": False, "output": "", "error": f"YouTube error: {e}", "data": {}} def _handle_research(self, params: Dict[str, Any]) -> Dict[str, Any]: """Research chain: /research -> web_search -> fetch top N -> summarize -> one brief.""" topic = (params.get("query") or params.get("topic") or "").strip() if not topic: return {"success": False, "output": "", "error": "No topic provided", "data": {}} top_n = int(params.get("top_n", params.get("top_k", 4))) try: results = self.web_search.search(topic, top_k=top_n) if isinstance(results, str): return {"success": True, "output": results, "error": "", "data": {}} if not results: return {"success": False, "output": "", "error": f"No results for '{topic}'", "data": {}} sources = [] for r in results[:top_n]: url = r.get("url", "") if not url: continue try: fetched = self.web_fetcher.extract_text(url) text = fetched.get("text", "") or fetched.get("content", "") except Exception: text = "" sources.append({"title": r.get("title", url), "url": url, "text": text}) brief = summarize_sources(sources, query=topic, max_sentences=5) out = f"๐Ÿ“š Research: {topic}\n\n{brief}" src_list = "\n".join(f"- {s['url']}" for s in sources if s.get("url")) if src_list: out += f"\n\nSources:\n{src_list}" return {"success": True, "output": out, "error": "", "data": {"sources": [s.get("url") for s in sources]}} except Exception as e: return {"success": False, "output": "", "error": f"Research error: {e}", "data": {}} # ---- Pollinations unified media tools (text/video/audio/voice/embed) ---- def _handle_text(self, params: Dict[str, Any]) -> Dict[str, Any]: """Unified LLM text/chat completion: /text .""" prompt = (params.get("prompt") or "").strip() model = (params.get("model") or "openai").strip() if not prompt: return {"success": False, "error": "Usage: /text "} try: res = self.media.text(prompt=prompt, model=model) if not res.get("success"): return {"success": False, "error": res.get("error", "text gen failed")} return {"success": True, "output": res["text"], "error": "", "data": {"model": res.get("model"), "provider": res.get("provider")}} except Exception as e: return {"success": False, "error": f"Text error: {e}"} def _handle_video(self, params: Dict[str, Any]) -> Dict[str, Any]: """Text-to-video generation: /video .""" prompt = (params.get("prompt") or "").strip() if not prompt: return {"success": False, "error": "Usage: /video "} try: res = self.media.video(prompt=prompt) if not res.get("success"): return {"success": False, "error": res.get("error", "video gen failed")} return {"success": True, "output": f"๐ŸŽฌ Video ready: {res['url']}", "error": "", "data": {"url": res["url"], "provider": res.get("provider")}} except Exception as e: return {"success": False, "error": f"Video error: {e}"} def _handle_audio(self, params: Dict[str, Any]) -> Dict[str, Any]: """NeuralAI Pollinations TTS: /audio .""" text = (params.get("text") or "").strip() voice = (params.get("voice") or "alloy").strip() if not text: return {"success": False, "error": "Usage: /audio "} try: res = self.media.audio(text=text, voice=voice) if not res.get("success"): return {"success": False, "error": res.get("error", "audio gen failed")} return {"success": True, "output": f"๐Ÿ”Š Audio ready: {res['url']}", "error": "", "data": {"url": res["url"], "provider": res.get("provider")}} except Exception as e: return {"success": False, "error": f"Audio error: {e}"} def _handle_voice(self, params: Dict[str, Any]) -> Dict[str, Any]: """Realtime voice session info: /voice.""" try: info = self.media.realtime_voice() if not info.get("success"): return {"success": False, "error": info.get("error", "realtime voice unavailable")} return {"success": True, "output": f"๐ŸŽ™๏ธ Realtime voice WebSocket ready:\n{info['url']}\n\nConnect from the browser/client to stream voice. Model: {info.get('model')}", "error": "", "data": info} except Exception as e: return {"success": False, "error": f"Voice error: {e}"} def _handle_embed(self, params: Dict[str, Any]) -> Dict[str, Any]: """Text embeddings: /embed .""" text = (params.get("text") or "").strip() model = (params.get("model") or "openai-3-small").strip() if not text: return {"success": False, "error": "Usage: /embed "} try: res = self.media.embed(text=text, model=model) if not res.get("success"): return {"success": False, "error": res.get("error", "embed failed")} vec = res.get("embedding", []) return {"success": True, "output": f"๐Ÿงฎ Embedding ready โ€” model {res.get('model')}, dims {len(vec)}. (stored; first 5: {vec[:5]})", "error": "", "data": res} except Exception as e: return {"success": False, "error": f"Embed error: {e}"} def _handle_calc(self, params: Dict[str, Any]) -> Dict[str, Any]: """/calc - safe math/code eval via local sympy + restricted eval.""" expr = (params.get("expr") or params.get("expression") or params.get("text") or "").strip() if not expr: return {"success": False, "error": "Usage: /calc "} try: import sympy from sympy.parsing.sympy_parser import parse_expr as sp_parse, standard_transformations, implicit_multiplication_application try: parsed = sp_parse(expr, transformations=standard_transformations + (implicit_multiplication_application,)) val = sympy.N(parsed) out = f"๐Ÿงฎ {expr} = {val}" return {"success": True, "output": out, "error": "", "data": {"result": str(val)}} except Exception: pass allowed = {"__builtins__": {}} val = eval(expr, allowed, {}) out = f"๐Ÿงฎ {expr} = {val}" return {"success": True, "output": out, "error": "", "data": {"result": str(val)}} except Exception as e: return {"success": False, "error": f"Calc error: {e}"} def _handle_doc(self, params: Dict[str, Any]) -> Dict[str, Any]: """/doc - summarize a workspace file.""" path = (params.get("path") or params.get("file") or params.get("text") or "").strip() if not path: return {"success": False, "error": "Usage: /doc "} try: full = path if path.startswith("/") else os.path.join(self.workspace, path) if not os.path.exists(full): return {"success": False, "error": f"File not found: {full}"} res = self.file_manager.read_file(full, max_size=60000) if not res.get("success"): return {"success": False, "error": res.get("error", "read failed")} content = res.get("content", "") if len(content) > 4000: try: from tools.summarize import summarize_sources s = summarize_sources([{"text": content[:20000]}], max_words=200) summary = s.get("summary", content[:1500]) except Exception: summary = content[:1500] + "\nโ€ฆ(truncated)" else: summary = content out = f"๐Ÿ“„ {os.path.basename(full)} ({res.get('size', len(content))} bytes)\n\n{summary}" return {"success": True, "output": out, "error": "", "data": {"path": full}} except Exception as e: return {"success": False, "error": f"Doc error: {e}"} def _handle_vision(self, params: Dict[str, Any]) -> Dict[str, Any]: """/vision - describe an image via OpenRouter vision model.""" ref = (params.get("image") or params.get("url") or params.get("path") or params.get("text") or "").strip() if not ref: return {"success": False, "error": "Usage: /vision "} try: import base64, requests, mimetypes key = os.environ.get("Open_Router_API") or os.environ.get("OPENROUTER_API_KEY") if not key: return {"success": False, "error": "OpenRouter key missing (vision unavailable)"} if ref.startswith("http://") or ref.startswith("https://"): data_url = ref else: full = ref if ref.startswith("/") else os.path.join(self.workspace, ref) if not os.path.exists(full): return {"success": False, "error": f"Image not found: {full}"} mime = mimetypes.guess_type(full)[0] or "image/png" b64 = base64.b64encode(open(full, "rb").read()).decode() data_url = f"data:{mime};base64,{b64}" model = "google/gemini-2.5-flash" resp = requests.post( "https://openrouter.ai/api/v1/chat/completions", headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"}, json={"model": model, "messages": [{ "role": "user", "content": [ {"type": "text", "text": "Describe this image in detail."}, {"type": "image_url", "image_url": {"url": data_url}}, ], }]}, timeout=60, ) if resp.status_code != 200: return {"success": False, "error": f"Vision API {resp.status_code}: {resp.text[:200]}"} desc = resp.json()["choices"][0]["message"]["content"] return {"success": True, "output": f"๐Ÿ‘๏ธ {desc}", "error": "", "data": {"model": model}} except Exception as e: return {"success": False, "error": f"Vision error: {e}"} def _handle_remind(self, params: Dict[str, Any]) -> Dict[str, Any]: """/remind