Files
ewoooc/services/pixelrag_vlm_route_readiness_service.py
ogt 1a27ca51d0
Some checks failed
CD Pipeline / deploy (push) Has been cancelled
feat(ai): add PixelRAG VLM generate preflight
2026-07-10 00:17:02 +08:00

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",
]