From 52500cfdc287ecb794917a2a31c906e6f5c7f811 Mon Sep 17 00:00:00 2001 From: drjones Date: Fri, 18 Sep 2026 18:11:55 -0700 Subject: [PATCH] add internet search: ?query escape + web_search tool (SearXNG) --- README.md | 28 ++++++++++++++++--------- metatron.py | 59 ++++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 70 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 7bafbac..72d8aa1 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,19 @@ # METATRON — lightweight Ollama terminal harness A minimal terminal harness for talking to Ollama (hosted on nightmare `10.30.20.29:11434`) -with full system access. Type `metatron` to launch. +with full system access and internet search. Type `metatron` to launch. ## Flow 1. **Model selection** — fetches the live model list from nightmare, numbered menu. 2. **Chat** — streaming REPL against the selected model. -3. **System access** — two ways: - - `!command` — run a shell command directly (e.g. `!dir`, `!whoami`). - - The model calls `run_command` natively (function calling) to execute commands itself. +3. **System access** — `!command` runs a shell command directly, and the model can also + call `run_command` natively (function calling). +4. **Search** — `?query` searches the internet (SearXNG), and the model can call + `web_search` natively. ## Requirements - Python 3.11 (stdlib only — `urllib`, `subprocess`, `json`). No pip deps. -- Network reach to `10.30.20.29:11434`. +- Network reach to `10.30.20.29:11434` and `10.30.20.35:6969`. ## Install (Commando VM) ``` @@ -22,13 +23,20 @@ setx PATH "%PATH%;C:\Tools\metatron" ``` Then `metatron` from any shell. -## Hardcoded -- Ollama URL: `http://10.30.20.29:11434` -- System prompt + `run_command` tool definition -- Only the model list is dynamic (fetched from `/api/tags`). - ## Controls | Input | Action | |-------|--------| | `exit` / `quit` | leave | | `!` | run a shell command directly | +| `?` | search the internet (SearXNG) directly | + +## Hardcoded +- Ollama URL: `http://10.30.20.29:11434` +- SearXNG URL: `http://10.30.20.35:6969` (self-hosted meta-search, CT 516) +- System prompt + `run_command` / `web_search` tool definitions +- Only the model list is dynamic (fetched from `/api/tags`). + +## Search notes +- `?` is the guaranteed path (no LLM needed). +- Native `web_search` tool-calling can degenerate on nightmare `.29`'s qwen3.8fast — + the `?` escape always works. diff --git a/metatron.py b/metatron.py index 732c163..ce2760f 100644 --- a/metatron.py +++ b/metatron.py @@ -13,14 +13,17 @@ Only the model list is dynamic. import json import subprocess import sys +import urllib.parse import urllib.request OLLAMA = "http://10.30.20.29:11434" # nightmare +SEARXNG = "http://10.30.20.35:6969" # self-hosted meta-search (CT 516) SYSTEM_PROMPT = ( "You are METATRON, a system-access AI harness running on a Windows Commando " "VM. You have full system access through the run_command tool — execute shell " - "commands when asked and report their output. Be concise and direct." + "commands when asked and report their output. Use web_search for any up-to-date " + "or factual internet information. Be concise and direct." ) TOOLS = [{ @@ -37,6 +40,20 @@ TOOLS = [{ "required": ["command"], }, }, +}, { + "type": "function", + "function": { + "name": "web_search", + "description": "Search the internet and return the top results (title, URL, snippet). " + "Use for current events, facts, anything you don't already know.", + "parameters": { + "type": "object", + "properties": { + "query": {"type": "string", "description": "The search query"}, + }, + "required": ["query"], + }, + }, }] C = {"r": "\033[0m", "b": "\033[1m", "c": "\033[36m", "g": "\033[32m", @@ -94,10 +111,26 @@ def run_command(cmd): return f"error: {e}" +def web_search(query): + url = f"{SEARXNG}/search?q={urllib.parse.quote(query)}&format=json" + try: + d = json.load(urllib.request.urlopen(url, timeout=25)) + except Exception as e: + return f"search error: {e}" + results = d.get("results", []) + if not results: + return "no results" + lines = [] + for r in results[:8]: + lines.append(f"- {r.get('title', '')}\n {r.get('url', '')}\n" + f" {(r.get('content') or '')[:220]}") + return "\n".join(lines) + + def chat(model): messages = [{"role": "system", "content": SYSTEM_PROMPT}] print(f"\n{C['b']} METATRON {C['c']}:: {model}{C['r']} " - f"{C['y']}(!cmd = run a command, exit = quit){C['r']}\n") + f"{C['y']}(!cmd = run, ?query = search, exit = quit){C['r']}\n") while True: try: user = input(f"{C['g']}you>{C['r']} ").strip() @@ -111,6 +144,11 @@ def chat(model): print(f"{C['y']} $ {user[1:]}{C['r']}") print(f" {run_command(user[1:])}\n") continue + if user.startswith("?"): + q = user[1:].strip() + print(f"{C['c']} [search] {q}{C['r']}") + print(f" {web_search(q)}\n") + continue messages.append({"role": "user", "content": user}) # agentic loop: let the model call run_command until it's satisfied @@ -138,16 +176,23 @@ def chat(model): "tool_calls": tool_calls}) for tc in tool_calls: fn = tc.get("function", {}) - if fn.get("name") == "run_command": - try: - args = json.loads(fn.get("arguments", "{}")) - except Exception: - args = {} + name = fn.get("name") + try: + args = json.loads(fn.get("arguments", "{}")) + except Exception: + args = {} + if name == "run_command": cmd = args.get("command", "") print(f"\n{C['y']} $ {cmd}{C['r']}") out = run_command(cmd) print(f" {out}") messages.append({"role": "tool", "content": out}) + elif name == "web_search": + q = args.get("query", "") + print(f"\n{C['c']} [search] {q}{C['r']}") + out = web_search(q) + print(f" {out[:600]}") + messages.append({"role": "tool", "content": out}) continue # re-send to model with tool results else: messages.append({"role": "assistant", "content": buf})