summaryrefslogtreecommitdiff
path: root/test_search.py
diff options
context:
space:
mode:
Diffstat (limited to 'test_search.py')
-rw-r--r--test_search.py221
1 files changed, 221 insertions, 0 deletions
diff --git a/test_search.py b/test_search.py
new file mode 100644
index 0000000..c103bdd
--- /dev/null
+++ b/test_search.py
@@ -0,0 +1,221 @@
+#!/usr/bin/env python3
+"""Standalone test script for MiMo Web Search.
+
+Usage:
+ export XIAOMI_API_KEY="your-key-here"
+ # export XIAOMI_BASE_URL="https://token-plan-cn.xiaomimimo.com/v1" # if using Token Plan
+ python3 test_search.py "小米17解锁"
+ python3 test_search.py "YU7 GT 价格" --limit 10
+ python3 test_search.py "今天天气" --raw # dump full API response
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import os
+import sys
+
+# ---------------------------------------------------------------------------
+# Env loader
+# ---------------------------------------------------------------------------
+
+def _load_env() -> None:
+ """Load ~/.hermes/.env if present (no-op when env vars are already set)."""
+ env_path = os.path.expanduser("~/.hermes/.env")
+ if not os.path.exists(env_path):
+ return
+ with open(env_path) as f:
+ for line in f:
+ line = line.strip()
+ if line and not line.startswith("#") and "=" in line:
+ k, v = line.split("=", 1)
+ k, v = k.strip(), v.strip()
+ if k and k not in os.environ:
+ os.environ[k] = v
+
+# ---------------------------------------------------------------------------
+# Core search (self-contained, no Hermes imports needed)
+# ---------------------------------------------------------------------------
+
+import httpx, re
+
+_DEFAULT_BASE_URL = "https://api.xiaomimimo.com/v1"
+_DEFAULT_MODEL = "mimo-v2.5"
+_JSON_BLOCK_RE = re.compile(r"\{[\s\S]*\}", re.MULTILINE)
+
+
+def mimo_search(
+ query: str,
+ *,
+ limit: int = 5,
+ model: str | None = None,
+ max_keyword: int = 3,
+ raw: bool = False,
+) -> dict:
+ """Call MiMo's Chat Completions API with web_search tool enabled."""
+ api_key = os.environ.get("XIAOMI_API_KEY", "").strip()
+ if not api_key:
+ return {"success": False, "error": "XIAOMI_API_KEY not set"}
+
+ base_url = os.environ.get("XIAOMI_BASE_URL", "").strip().rstrip("/") or _DEFAULT_BASE_URL
+ model = model or _DEFAULT_MODEL
+
+ prompt = (
+ "Use the web_search tool to find current information for the query below, "
+ "then respond with ONLY a single JSON object — no prose, no markdown "
+ "fences — matching this exact schema:\n\n"
+ '{"results": [{"title": "string", "url": "string", '
+ '"description": "1-2 sentence summary"}]}\n\n'
+ f"Return at most {limit} results, ordered by relevance, with absolute "
+ 'https:// URLs. If no usable results exist, return '
+ '{"results": []}.\n\n'
+ f"Query: {query}"
+ )
+
+ payload = {
+ "model": model,
+ "messages": [{"role": "user", "content": prompt}],
+ "tools": [
+ {
+ "type": "web_search",
+ "force_search": True,
+ "max_keyword": max_keyword,
+ "limit": min(limit, 5),
+ }
+ ],
+ "max_completion_tokens": 2048,
+ "temperature": 1.0,
+ "top_p": 0.95,
+ "stream": False,
+ }
+
+ resp = httpx.post(
+ f"{base_url}/chat/completions",
+ headers={"api-key": api_key, "Content-Type": "application/json"},
+ json=payload,
+ timeout=30,
+ )
+ resp.raise_for_status()
+ data = resp.json()
+
+ if raw:
+ return data
+
+ # Parse results from annotations
+ choices = data.get("choices", [])
+ if not choices:
+ return {"success": True, "data": {"web": []}}
+
+ message = choices[0].get("message", {})
+ annotations = message.get("annotations", [])
+
+ seen_urls: set[str] = set()
+ results = []
+ for ann in annotations:
+ if ann.get("type") != "url_citation":
+ continue
+ url = str(ann.get("url", "")).strip()
+ if not url or url in seen_urls:
+ continue
+ seen_urls.add(url)
+ results.append({
+ "title": str(ann.get("title", "")).strip(),
+ "url": url,
+ "description": str(ann.get("summary", "")).strip(),
+ "site_name": str(ann.get("site_name", "")).strip(),
+ "publish_time": str(ann.get("publish_time", "")).strip(),
+ "position": len(results) + 1,
+ })
+ if len(results) >= limit:
+ break
+
+ usage = data.get("usage", {})
+ wsu = usage.get("web_search_usage", {})
+
+ return {
+ "success": True,
+ "data": {"web": results},
+ "answer": message.get("content", ""),
+ "usage": {
+ "prompt_tokens": usage.get("prompt_tokens"),
+ "completion_tokens": usage.get("completion_tokens"),
+ "total_tokens": usage.get("total_tokens"),
+ "search_keywords": wsu.get("tool_usage"),
+ "pages_fetched": wsu.get("page_usage"),
+ },
+ }
+
+
+# ---------------------------------------------------------------------------
+# CLI
+# ---------------------------------------------------------------------------
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description="MiMo Web Search CLI")
+ parser.add_argument("query", help="Search query")
+ parser.add_argument("--limit", type=int, default=5, help="Max results (default: 5)")
+ parser.add_argument("--model", default=None, help="MiMo model (default: mimo-v2.5)")
+ parser.add_argument("--max-keyword", type=int, default=3, help="Max keywords per round")
+ parser.add_argument("--raw", action="store_true", help="Dump raw API response")
+ args = parser.parse_args()
+
+ _load_env()
+
+ try:
+ result = mimo_search(
+ args.query,
+ limit=args.limit,
+ model=args.model,
+ max_keyword=args.max_keyword,
+ raw=args.raw,
+ )
+ except httpx.HTTPStatusError as exc:
+ print(f"HTTP {exc.response.status_code}: {exc.response.text[:500]}", file=sys.stderr)
+ sys.exit(1)
+ except httpx.RequestError as exc:
+ print(f"Connection error: {exc}", file=sys.stderr)
+ sys.exit(1)
+
+ if args.raw:
+ print(json.dumps(result, ensure_ascii=False, indent=2))
+ return
+
+ if not result.get("success"):
+ print(f"Error: {result.get('error')}", file=sys.stderr)
+ sys.exit(1)
+
+ answer = result.get("answer", "")
+ web = result.get("data", {}).get("web", [])
+ usage = result.get("usage", {})
+
+ if answer:
+ print(f"\n{'=' * 60}")
+ print("ANSWER")
+ print('=' * 60)
+ print(answer)
+
+ print(f"\n{'=' * 60}")
+ print(f"SEARCH RESULTS ({len(web)} items)")
+ print('=' * 60)
+ for r in web:
+ site = f" [{r.get('site_name', '')}]" if r.get("site_name") else ""
+ print(f"\n [{r['position']}]{site} {r['title']}")
+ print(f" {r['url']}")
+ desc = r.get("description", "")
+ if desc:
+ print(f" {desc[:150]}{'...' if len(desc) > 150 else ''}")
+
+ print(f"\n{'=' * 60}")
+ print("USAGE")
+ print('=' * 60)
+ print(f" prompt tokens: {usage.get('prompt_tokens', '?')}")
+ print(f" completion tokens: {usage.get('completion_tokens', '?')}")
+ print(f" total tokens: {usage.get('total_tokens', '?')}")
+ print(f" search keywords: {usage.get('search_keywords', '?')}")
+ print(f" pages fetched: {usage.get('pages_fetched', '?')}")
+ print()
+
+
+if __name__ == "__main__":
+ main()