feat: expand tool catalog and improve 'what' search recall

- Add 32 new tool and dockerfile entries to README.md catalog.
- Increase 'what' shortlist limit to 100 for better search recall.
- Update 'what' default model to gemma4 and improve robust JSON parsing.
This commit is contained in:
tke
2026-05-18 13:21:23 +02:00
parent ac3245b78f
commit ae5d503268
2 changed files with 71 additions and 18 deletions
+43 -18
View File
@@ -21,7 +21,7 @@ from pathlib import Path
REPO_ROOT = Path(__file__).parent.resolve()
README_PATH = REPO_ROOT / "README.md"
DEFAULT_MODEL = os.environ.get("WHAT_OLLAMA_MODEL", "ministral-3:3b")
DEFAULT_MODEL = os.environ.get("WHAT_OLLAMA_MODEL", "gemma4")
CATALOG_HEADING = "## Tool Catalog"
ENTRY_RE = re.compile(
r"^- `([^`]+)` \| goal: (.*?) \| usage: (.*)$"
@@ -123,13 +123,20 @@ def build_prompt(query: str, entries: list[dict[str, str]]) -> str:
return f"""You are selecting tools from a repository catalog.
Use only the catalog below. Prefer direct matches. Use archived tools only if they clearly fit the request.
Return strict JSON only. The response must be a JSON array with up to 8 objects.
Each object must contain:
- "path": exact catalog path
- "reason": one short sentence
Return strict JSON matching this schema exactly:
{{
"results": [
{{
"path": "exact catalog path",
"reason": "one short sentence explaining why this tool matches"
}}
]
}}
Do not invent paths. Do not include markdown.
Prefer the entry whose action best matches the query: compare beats hash for comparison queries, open beats convert for opening queries, and mount beats inspect for mount queries.
Constraints:
- The "results" array must contain up to 8 objects.
- Do not invent paths.
- Prefer the entry whose action best matches the query: compare beats hash for comparison queries, open beats convert for opening queries, and mount beats inspect for mount queries.
Query: {query}
@@ -142,7 +149,7 @@ def tokenize(text: str) -> set[str]:
return set(TOKEN_RE.findall(text.lower()))
def shortlist_entries(query: str, entries: list[dict[str, str]], limit: int = 28) -> list[dict[str, str]]:
def shortlist_entries(query: str, entries: list[dict[str, str]], limit: int = 100) -> list[dict[str, str]]:
query_tokens = tokenize(query)
if not query_tokens:
return entries[:limit]
@@ -163,28 +170,45 @@ def shortlist_entries(query: str, entries: list[dict[str, str]], limit: int = 28
def extract_json_array(output: str) -> list[dict[str, str]]:
match = re.search(r"\[\s*\{.*\}\s*\]", output, re.DOTALL)
# Step 1: Clean and find the root object boundary if Ollama prefixes anything
match = re.search(r"\{\s*.*\}\s*", output, re.DOTALL)
payload = match.group(0) if match else output
data = json.loads(payload)
if not isinstance(data, list):
raise WhatError("Model output must be a JSON array.")
try:
# ALLOW literal newlines/control characters inside string properties
data = json.loads(payload, strict=False)
except json.JSONDecodeError as exc:
raise WhatError(f"Failed to parse model output as JSON: {exc}")
if not isinstance(data, dict):
raise WhatError("Model output must be a root JSON object.")
# Step 2: Safe navigation into the expected schema array
results_list = data.get("results")
if results_list is None:
raise WhatError("Missing 'results' key in model JSON response.")
if not isinstance(results_list, list):
raise WhatError("The 'results' property must be a JSON array.")
# Step 3: Extract and normalize items
normalized: list[dict[str, str]] = []
for item in data:
for item in results_list:
if not isinstance(item, dict):
continue
path = str(item.get("path", "")).strip()
reason = str(item.get("reason", "")).strip()
# Clean up any literal newlines the model injected into the text
reason = str(item.get("reason", "")).replace("\n", " ").strip()
if path:
normalized.append({"path": path, "reason": reason})
return normalized
def run_ollama_once(prompt: str, model: str) -> str:
try:
result = subprocess.run(
["ollama", "run", model, prompt],
["ollama", "run", "--format", "json", "--hidethinking", model, prompt],
capture_output=True,
text=True,
timeout=60,
@@ -206,8 +230,9 @@ def run_ollama(prompt: str, model: str) -> list[dict[str, str]]:
return extract_json_array(first_output)
except (json.JSONDecodeError, WhatError):
repair_prompt = (
"Rewrite the following response as strict JSON only.\n"
'Target format: [{"path":"exact catalog path","reason":"short reason"}]\n'
"Rewrite the following response as strict JSON matching the target schema.\n"
"Target format:\n"
'{\n "results": [{"path":"exact catalog path","reason":"short reason"}]\n}\n'
"Do not add markdown or commentary.\n\n"
f"Response to repair:\n{first_output}\n"
)
@@ -300,4 +325,4 @@ def main() -> int:
if __name__ == "__main__":
raise SystemExit(main())
raise SystemExit(main())