add internet search: ?query escape + web_search tool (SearXNG)
This commit is contained in:
28
README.md
28
README.md
@@ -1,18 +1,19 @@
|
|||||||
# METATRON — lightweight Ollama terminal harness
|
# METATRON — lightweight Ollama terminal harness
|
||||||
|
|
||||||
A minimal terminal harness for talking to Ollama (hosted on nightmare `10.30.20.29:11434`)
|
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
|
## Flow
|
||||||
1. **Model selection** — fetches the live model list from nightmare, numbered menu.
|
1. **Model selection** — fetches the live model list from nightmare, numbered menu.
|
||||||
2. **Chat** — streaming REPL against the selected model.
|
2. **Chat** — streaming REPL against the selected model.
|
||||||
3. **System access** — two ways:
|
3. **System access** — `!command` runs a shell command directly, and the model can also
|
||||||
- `!command` — run a shell command directly (e.g. `!dir`, `!whoami`).
|
call `run_command` natively (function calling).
|
||||||
- The model calls `run_command` natively (function calling) to execute commands itself.
|
4. **Search** — `?query` searches the internet (SearXNG), and the model can call
|
||||||
|
`web_search` natively.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
- Python 3.11 (stdlib only — `urllib`, `subprocess`, `json`). No pip deps.
|
- 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)
|
## Install (Commando VM)
|
||||||
```
|
```
|
||||||
@@ -22,13 +23,20 @@ setx PATH "%PATH%;C:\Tools\metatron"
|
|||||||
```
|
```
|
||||||
Then `metatron` from any shell.
|
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
|
## Controls
|
||||||
| Input | Action |
|
| Input | Action |
|
||||||
|-------|--------|
|
|-------|--------|
|
||||||
| `exit` / `quit` | leave |
|
| `exit` / `quit` | leave |
|
||||||
| `!<command>` | run a shell command directly |
|
| `!<command>` | run a shell command directly |
|
||||||
|
| `?<query>` | 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
|
||||||
|
- `?<query>` is the guaranteed path (no LLM needed).
|
||||||
|
- Native `web_search` tool-calling can degenerate on nightmare `.29`'s qwen3.8fast —
|
||||||
|
the `?` escape always works.
|
||||||
|
|||||||
59
metatron.py
59
metatron.py
@@ -13,14 +13,17 @@ Only the model list is dynamic.
|
|||||||
import json
|
import json
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
OLLAMA = "http://10.30.20.29:11434" # nightmare
|
OLLAMA = "http://10.30.20.29:11434" # nightmare
|
||||||
|
SEARXNG = "http://10.30.20.35:6969" # self-hosted meta-search (CT 516)
|
||||||
|
|
||||||
SYSTEM_PROMPT = (
|
SYSTEM_PROMPT = (
|
||||||
"You are METATRON, a system-access AI harness running on a Windows Commando "
|
"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 "
|
"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 = [{
|
TOOLS = [{
|
||||||
@@ -37,6 +40,20 @@ TOOLS = [{
|
|||||||
"required": ["command"],
|
"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",
|
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}"
|
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):
|
def chat(model):
|
||||||
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
|
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
|
||||||
print(f"\n{C['b']} METATRON {C['c']}:: {model}{C['r']} "
|
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:
|
while True:
|
||||||
try:
|
try:
|
||||||
user = input(f"{C['g']}you>{C['r']} ").strip()
|
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"{C['y']} $ {user[1:]}{C['r']}")
|
||||||
print(f" {run_command(user[1:])}\n")
|
print(f" {run_command(user[1:])}\n")
|
||||||
continue
|
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})
|
messages.append({"role": "user", "content": user})
|
||||||
# agentic loop: let the model call run_command until it's satisfied
|
# agentic loop: let the model call run_command until it's satisfied
|
||||||
@@ -138,16 +176,23 @@ def chat(model):
|
|||||||
"tool_calls": tool_calls})
|
"tool_calls": tool_calls})
|
||||||
for tc in tool_calls:
|
for tc in tool_calls:
|
||||||
fn = tc.get("function", {})
|
fn = tc.get("function", {})
|
||||||
if fn.get("name") == "run_command":
|
name = fn.get("name")
|
||||||
try:
|
try:
|
||||||
args = json.loads(fn.get("arguments", "{}"))
|
args = json.loads(fn.get("arguments", "{}"))
|
||||||
except Exception:
|
except Exception:
|
||||||
args = {}
|
args = {}
|
||||||
|
if name == "run_command":
|
||||||
cmd = args.get("command", "")
|
cmd = args.get("command", "")
|
||||||
print(f"\n{C['y']} $ {cmd}{C['r']}")
|
print(f"\n{C['y']} $ {cmd}{C['r']}")
|
||||||
out = run_command(cmd)
|
out = run_command(cmd)
|
||||||
print(f" {out}")
|
print(f" {out}")
|
||||||
messages.append({"role": "tool", "content": 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
|
continue # re-send to model with tool results
|
||||||
else:
|
else:
|
||||||
messages.append({"role": "assistant", "content": buf})
|
messages.append({"role": "assistant", "content": buf})
|
||||||
|
|||||||
Reference in New Issue
Block a user