Files
Anton 052591281d Initial clean version - Agent Maxim
- Complete agent system with planning and execution
- Bridge API with FastAPI server
- WhatsApp-style mobile dashboard
- Enhanced web search with query improvement
- Security guards and policy checks
- Comprehensive documentation
2026-04-09 18:07:43 +03:00

220 lines
6.8 KiB
Python

"""Ollama adapter — real local LLM via Ollama HTTP API."""
from __future__ import annotations
import json
import os
import re
import time
import urllib.request
from datetime import datetime, timezone
from pathlib import Path
from urllib.error import URLError
from adapters.base import AdapterResult
_DIAG_DIR = Path(__file__).resolve().parents[3] / "data" / "diagnostics"
def _resolve_host() -> str:
"""Normalize OLLAMA_HOST to a full http:// URL.
Ollama on Windows sets OLLAMA_HOST=0.0.0.0 (bind address, no scheme).
We always talk to localhost:11434 for outbound connections.
"""
raw = os.environ.get("OLLAMA_HOST", "")
if not raw or raw in ("0.0.0.0", "[::]"):
return "http://localhost:11434"
if raw.startswith("http://") or raw.startswith("https://"):
return raw
return f"http://{raw}"
_HOST: str = _resolve_host()
# Use DECISION_MODEL if set, fall back to OLLAMA_MODEL for backwards compatibility
_MODEL: str = os.environ.get("DECISION_MODEL") or os.environ.get(
"OLLAMA_MODEL", "qwen2.5:7b"
)
_FALLBACK_MODEL: str = "llama3.2:3b"
_CJK_RE = re.compile(r"[\u4e00-\u9fff]")
def _scan_unicode(text: str, label: str) -> list[dict]:
"""Scan text for CJK codepoints, return list of findings."""
findings = []
for m in _CJK_RE.finditer(text):
findings.append(
{
"pos": m.start(),
"char": m.group(),
"codepoint": f"U+{ord(m.group()):04X}",
"context": text[max(0, m.start() - 20) : m.end() + 20],
}
)
if findings:
print(f"[CJK DETECTED] in {label}: {len(findings)} occurrence(s)")
for f in findings:
print(f" pos={f['pos']} {f['codepoint']} context={f['context']!r}")
return findings
def _save_diag(trace_id: str, kind: str, payload: dict) -> None:
"""Save diagnostic JSON to data/diagnostics/."""
try:
_DIAG_DIR.mkdir(parents=True, exist_ok=True)
path = _DIAG_DIR / f"llm_raw_{kind}_{trace_id}.json"
path.write_text(
json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8"
)
except Exception as exc:
print(f"[DIAG] save failed: {exc}")
def execute(
prompt: str,
model: str | None = None,
temperature: float = 0.1,
max_tokens: int = 150,
timeout: float = 30.0,
trace_id: str | None = None,
) -> AdapterResult:
print("=== THIS ADAPTER IS USED: ollama_adapter ===")
m = model or _MODEL
call_id = (
trace_id or f"notrace-{datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%S%f')}"
)
# Hard timeout 60s enforced at request layer
hard_timeout = 60.0
request_payload = {
"model": m,
"prompt": prompt,
"stream": False,
"options": {
"temperature": temperature,
"num_predict": max_tokens,
},
}
# --- Log + scan RAW REQUEST ---
print(
f"[RAW REQUEST] trace_id={call_id} model={m} temp={temperature} max_tokens={max_tokens}"
)
print(f"[RAW REQUEST] prompt ({len(prompt)} chars):\n{prompt}")
req_cjk = _scan_unicode(prompt, "request/prompt")
_save_diag(
call_id,
"request",
{
"trace_id": call_id,
"timestamp": datetime.now(timezone.utc).isoformat(),
"model": m,
"temperature": temperature,
"max_tokens": max_tokens,
"prompt": prompt,
"prompt_len": len(prompt),
"cjk_in_request": req_cjk,
},
)
encoded = json.dumps(request_payload).encode()
req = urllib.request.Request(
f"{_HOST}/api/generate",
data=encoded,
headers={"Content-Type": "application/json"},
method="POST",
)
_t = time.time()
try:
with urllib.request.urlopen(req, timeout=hard_timeout) as resp:
data = json.loads(resp.read())
# RAW response text — before ANY processing
raw_text = data.get("response", "")
elapsed = time.time() - _t
tokens_in = data.get("prompt_eval_count", "?")
tokens_out = data.get("eval_count", "?")
print(
f"[TIMING] ollama_http: {elapsed:.3f}s tokens_in={tokens_in} tokens_out={tokens_out}"
)
# --- Log + scan RAW RESPONSE ---
print(
f"[RAW RESPONSE] trace_id={call_id} ({len(raw_text)} chars):\n{raw_text}"
)
resp_cjk = _scan_unicode(raw_text, "response")
_save_diag(
call_id,
"response",
{
"trace_id": call_id,
"timestamp": datetime.now(timezone.utc).isoformat(),
"model": m,
"elapsed_s": round(elapsed, 3),
"tokens_in": tokens_in,
"tokens_out": tokens_out,
"raw_response": raw_text,
"raw_response_len": len(raw_text),
"cjk_in_response": resp_cjk,
"full_ollama_payload": {
k: v for k, v in data.items() if k != "response"
},
},
)
return AdapterResult(
response=raw_text,
error=None,
exit_code=0,
mode="real",
metadata={"model": m},
)
except Exception as exc: # noqa: BLE001
elapsed = time.time() - _t
is_timeout = isinstance(exc, URLError) and "timed out" in str(exc).lower()
if is_timeout:
print(f"[OLLAMA TIMEOUT] trace_id={call_id} timeout=60s")
else:
print(f"[TIMING] ollama_http: {elapsed:.3f}s [ERROR]")
_save_diag(
call_id,
"response",
{
"trace_id": call_id,
"timestamp": datetime.now(timezone.utc).isoformat(),
"model": m,
"elapsed_s": round(elapsed, 3),
"error": f"{type(exc).__name__}: {exc}",
"timeout": is_timeout,
"timeout_seconds": 60 if is_timeout else None,
"raw_response": None,
"cjk_in_response": [],
},
)
return AdapterResult(
response=None,
error=f"{type(exc).__name__}: {exc}",
exit_code=1,
mode="real",
metadata={
"model": m,
"timeout": is_timeout,
"timeout_seconds": 60 if is_timeout else None,
},
)
def available() -> bool:
"""Quick reachability check (used at startup)."""
try:
urllib.request.urlopen(f"{_HOST}/api/tags", timeout=3.0)
return True
except (URLError, OSError):
return False
def mode() -> str:
return "real" if available() else "stub"