331 lines
11 KiB
Python
331 lines
11 KiB
Python
"""PixelRAG VLM route readiness.
|
|
|
|
This module checks approved Ollama routes for installed VLM candidate models.
|
|
By default it only reads /api/tags. Execute preflight can opt into a tiny
|
|
/api/generate probe to avoid blindly sending screenshot tiles to a route that
|
|
is installed but cannot generate.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import time
|
|
from datetime import datetime, timezone
|
|
from typing import Any
|
|
|
|
import requests
|
|
|
|
from services.ollama_service import (
|
|
OLLAMA_HOST_FALLBACK,
|
|
OLLAMA_HOST_PRIMARY,
|
|
OLLAMA_HOST_PRIMARY_PROXY,
|
|
OLLAMA_HOST_SECONDARY,
|
|
OLLAMA_HOST_SECONDARY_PROXY,
|
|
get_host_label,
|
|
get_provider_tag,
|
|
)
|
|
|
|
|
|
POLICY = "read_only_pixelrag_vlm_route_readiness_v1"
|
|
DEFAULT_TIMEOUT_SECONDS = 3
|
|
DEFAULT_PROBE_TIMEOUT_SECONDS = 8
|
|
DEFAULT_PROBE_PROMPT = "Return exactly OK."
|
|
DEFAULT_MODEL = (
|
|
os.getenv("PIXELRAG_VLM_MODEL")
|
|
or os.getenv("PPT_VISION_MODEL")
|
|
or "minicpm-v:latest"
|
|
)
|
|
VISION_MODEL_CANDIDATES = (
|
|
"minicpm-v:latest",
|
|
"gemma3:4b",
|
|
"llava:latest",
|
|
)
|
|
|
|
|
|
def _unique(values: list[str] | tuple[str, ...]) -> list[str]:
|
|
seen: set[str] = set()
|
|
result: list[str] = []
|
|
for value in values:
|
|
clean = str(value or "").strip()
|
|
if not clean or clean in seen:
|
|
continue
|
|
seen.add(clean)
|
|
result.append(clean)
|
|
return result
|
|
|
|
|
|
def _approved_hosts() -> list[str]:
|
|
return _unique((
|
|
OLLAMA_HOST_PRIMARY,
|
|
OLLAMA_HOST_PRIMARY_PROXY,
|
|
OLLAMA_HOST_SECONDARY,
|
|
OLLAMA_HOST_SECONDARY_PROXY,
|
|
OLLAMA_HOST_FALLBACK,
|
|
))
|
|
|
|
|
|
def _candidate_models(configured_model: str) -> list[str]:
|
|
env_candidates = [
|
|
item.strip()
|
|
for item in os.getenv("PIXELRAG_VLM_MODEL_CANDIDATES", "").split(",")
|
|
if item.strip()
|
|
]
|
|
return _unique((configured_model, *env_candidates, *VISION_MODEL_CANDIDATES))
|
|
|
|
|
|
def _fetch_models(host: str, timeout: int) -> dict[str, Any]:
|
|
clean_host = str(host or "").rstrip("/")
|
|
item: dict[str, Any] = {
|
|
"host": clean_host,
|
|
"host_label": get_host_label(clean_host),
|
|
"provider": get_provider_tag(clean_host),
|
|
"reachable": False,
|
|
"model_count": 0,
|
|
"models": [],
|
|
"error": "",
|
|
}
|
|
try:
|
|
response = requests.get(f"{clean_host}/api/tags", timeout=max(1, timeout))
|
|
item["http_status"] = response.status_code
|
|
if response.status_code != 200:
|
|
item["error"] = f"HTTP {response.status_code}: {response.text[:180]}"
|
|
return item
|
|
payload = response.json()
|
|
models = [
|
|
str(model.get("name") or "").strip()
|
|
for model in list(payload.get("models") or [])
|
|
if str(model.get("name") or "").strip()
|
|
]
|
|
item.update({
|
|
"reachable": True,
|
|
"model_count": len(models),
|
|
"models": models[:80],
|
|
})
|
|
except Exception as exc:
|
|
item["error"] = f"{type(exc).__name__}: {str(exc)[:180]}"
|
|
return item
|
|
|
|
|
|
def _probe_generate(
|
|
host: str,
|
|
*,
|
|
model: str,
|
|
timeout: int,
|
|
prompt: str,
|
|
) -> dict[str, Any]:
|
|
clean_host = str(host or "").rstrip("/")
|
|
started = time.monotonic()
|
|
result: dict[str, Any] = {
|
|
"generate_probe_performed": True,
|
|
"generate_probe_model": str(model or "").strip(),
|
|
"generate_probe_ok": False,
|
|
"generate_probe_error": "",
|
|
"generate_probe_duration_ms": 0,
|
|
}
|
|
payload = {
|
|
"model": str(model or "").strip(),
|
|
"prompt": str(prompt or DEFAULT_PROBE_PROMPT),
|
|
"stream": False,
|
|
"options": {
|
|
"temperature": 0,
|
|
"num_predict": 8,
|
|
"num_ctx": 512,
|
|
},
|
|
"keep_alive": "1m",
|
|
}
|
|
try:
|
|
response = requests.post(
|
|
f"{clean_host}/api/generate",
|
|
json=payload,
|
|
timeout=max(1, timeout),
|
|
)
|
|
result["generate_probe_http_status"] = response.status_code
|
|
if response.status_code != 200:
|
|
result["generate_probe_error"] = (
|
|
f"HTTP {response.status_code}: {response.text[:180]}"
|
|
)
|
|
return result
|
|
try:
|
|
body = response.json()
|
|
except Exception as exc:
|
|
result["generate_probe_error"] = f"invalid_json: {type(exc).__name__}"
|
|
return result
|
|
result["generate_probe_ok"] = (
|
|
isinstance(body, dict)
|
|
and (
|
|
"response" in body
|
|
or bool(body.get("done"))
|
|
or bool(body.get("context"))
|
|
)
|
|
)
|
|
if not result["generate_probe_ok"]:
|
|
result["generate_probe_error"] = "generate_response_missing_output_fields"
|
|
except Exception as exc:
|
|
result["generate_probe_error"] = f"{type(exc).__name__}: {str(exc)[:180]}"
|
|
finally:
|
|
result["generate_probe_duration_ms"] = int((time.monotonic() - started) * 1000)
|
|
return result
|
|
|
|
|
|
def build_pixelrag_vlm_route_readiness(
|
|
*,
|
|
model: str | None = None,
|
|
timeout_seconds: int | None = None,
|
|
include_models: bool = False,
|
|
probe_generate: bool = False,
|
|
probe_timeout_seconds: int | None = None,
|
|
probe_prompt: str | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Read approved Ollama tags and recommend an installed VLM model."""
|
|
configured_model = str(model or DEFAULT_MODEL).strip() or DEFAULT_MODEL
|
|
timeout = max(1, min(int(timeout_seconds or DEFAULT_TIMEOUT_SECONDS), 20))
|
|
probe_timeout = max(
|
|
1,
|
|
min(int(probe_timeout_seconds or DEFAULT_PROBE_TIMEOUT_SECONDS), 30),
|
|
)
|
|
generated_at = datetime.now(timezone.utc).isoformat()
|
|
candidates = _candidate_models(configured_model)
|
|
host_results = [_fetch_models(host, timeout) for host in _approved_hosts()]
|
|
|
|
reachable_hosts = [item for item in host_results if item.get("reachable")]
|
|
configured_hosts = [
|
|
item for item in reachable_hosts
|
|
if configured_model in set(item.get("models") or [])
|
|
]
|
|
selected_model = ""
|
|
selected_host = ""
|
|
selected_provider = ""
|
|
selected_reason = ""
|
|
for candidate in candidates:
|
|
for host in reachable_hosts:
|
|
if candidate not in set(host.get("models") or []):
|
|
continue
|
|
selected_model = candidate
|
|
selected_host = str(host.get("host") or "")
|
|
selected_provider = str(host.get("provider") or "")
|
|
selected_reason = (
|
|
"configured_model_available"
|
|
if candidate == configured_model
|
|
else "installed_candidate_fallback"
|
|
)
|
|
break
|
|
if selected_model:
|
|
break
|
|
|
|
tag_model_route_ready = bool(selected_model and selected_host)
|
|
generate_probe_performed = bool(probe_generate and tag_model_route_ready)
|
|
generate_probe_ok_count = 0
|
|
generate_ready_model = ""
|
|
generate_ready_host = ""
|
|
generate_ready_provider = ""
|
|
generate_route_ready: bool | None = None
|
|
if generate_probe_performed:
|
|
generate_route_ready = False
|
|
for host in reachable_hosts:
|
|
if selected_model not in set(host.get("models") or []):
|
|
continue
|
|
probe_result = _probe_generate(
|
|
str(host.get("host") or ""),
|
|
model=selected_model,
|
|
timeout=probe_timeout,
|
|
prompt=probe_prompt or DEFAULT_PROBE_PROMPT,
|
|
)
|
|
host.update(probe_result)
|
|
if probe_result.get("generate_probe_ok"):
|
|
generate_probe_ok_count += 1
|
|
if not generate_ready_host:
|
|
generate_ready_model = selected_model
|
|
generate_ready_host = str(host.get("host") or "")
|
|
generate_ready_provider = str(host.get("provider") or "")
|
|
if generate_ready_host:
|
|
selected_host = generate_ready_host
|
|
selected_provider = generate_ready_provider
|
|
generate_route_ready = True
|
|
|
|
model_route_ready = tag_model_route_ready and (
|
|
bool(generate_route_ready) if generate_probe_performed else True
|
|
)
|
|
if not reachable_hosts:
|
|
status = "critical"
|
|
next_action = "repair_approved_ollama_host_connectivity"
|
|
elif not tag_model_route_ready:
|
|
status = "critical"
|
|
next_action = "install_or_configure_pixelrag_vlm_model_on_approved_ollama_host"
|
|
elif generate_probe_performed and not generate_route_ready:
|
|
status = "critical"
|
|
next_action = "repair_ollama_vlm_generate_runtime_or_proxy_timeout"
|
|
elif selected_model != configured_model:
|
|
status = "warning"
|
|
next_action = "run_pixelrag_vlm_replay_worker_execute_with_selected_model"
|
|
else:
|
|
status = "ok"
|
|
next_action = "run_pixelrag_vlm_replay_worker_execute"
|
|
|
|
public_host_results: list[dict[str, Any]] = []
|
|
for item in host_results:
|
|
result = dict(item)
|
|
result["configured_model_available"] = configured_model in set(item.get("models") or [])
|
|
result["candidate_models_available"] = [
|
|
candidate for candidate in candidates
|
|
if candidate in set(item.get("models") or [])
|
|
]
|
|
result.setdefault("generate_probe_performed", False)
|
|
result.setdefault("generate_probe_model", "")
|
|
result.setdefault("generate_probe_ok", False)
|
|
result.setdefault("generate_probe_error", "")
|
|
result.setdefault("generate_probe_duration_ms", 0)
|
|
if not include_models:
|
|
result.pop("models", None)
|
|
public_host_results.append(result)
|
|
|
|
summary = {
|
|
"host_count": len(host_results),
|
|
"reachable_host_count": len(reachable_hosts),
|
|
"configured_model": configured_model,
|
|
"configured_model_available_count": len(configured_hosts),
|
|
"candidate_model": selected_model,
|
|
"candidate_host": selected_host,
|
|
"candidate_provider": selected_provider,
|
|
"candidate_selection_reason": selected_reason,
|
|
"tag_model_route_ready": tag_model_route_ready,
|
|
"model_route_ready": model_route_ready,
|
|
"generate_probe_performed": generate_probe_performed,
|
|
"generate_probe_ok_count": generate_probe_ok_count,
|
|
"generate_ready_model": generate_ready_model,
|
|
"generate_ready_host": generate_ready_host,
|
|
"generate_ready_provider": generate_ready_provider,
|
|
"generate_route_ready": generate_route_ready,
|
|
"model_call_performed": generate_probe_performed,
|
|
"writes_database_count": 0,
|
|
"primary_human_gate_count": 0,
|
|
}
|
|
return {
|
|
"success": status != "critical",
|
|
"policy": POLICY,
|
|
"status": status,
|
|
"generated_at": generated_at,
|
|
"timeout_seconds": timeout,
|
|
"probe_timeout_seconds": probe_timeout if probe_generate else 0,
|
|
"configured_model": configured_model,
|
|
"candidate_models": candidates,
|
|
"summary": summary,
|
|
"hosts": public_host_results,
|
|
"controlled_apply": {
|
|
"network_call": True,
|
|
"model_call": generate_probe_performed,
|
|
"db_write": False,
|
|
"writes_database": False,
|
|
"writes_database_count": 0,
|
|
"secret_read": False,
|
|
"production_price_write": False,
|
|
"primary_human_gate_count": 0,
|
|
},
|
|
"next_machine_action": next_action,
|
|
}
|
|
|
|
|
|
__all__ = [
|
|
"POLICY",
|
|
"build_pixelrag_vlm_route_readiness",
|
|
]
|