Make Ollama chat the primary workflow

This commit is contained in:
Hermes Agent
2026-08-29 08:50:03 +10:00
parent 309947d353
commit e1f0589c3c
7 changed files with 316 additions and 87 deletions
+122 -37
View File
@@ -440,6 +440,27 @@ def _metric_values(state: dict[str, Any], status: str | None = None, error: str
}
def _conversation_history(conversation_id: str, exclude_request_id: str = "") -> list[dict[str, str]]:
"""Read canonical prior turns from storage, excluding the current request."""
db = _chat_db()
try:
rows = db.execute(
"SELECT role,content,request_id FROM messages WHERE conversation_id=? ORDER BY id",
(conversation_id,),
).fetchall()
history: list[dict[str, str]] = []
for row in rows:
if exclude_request_id and str(row["request_id"]) == exclude_request_id:
continue
role = str(row["role"] or "")
content = str(row["content"] or "").strip()
if role in {"user", "assistant"} and content:
history.append({"role": role, "content": content})
return history
finally:
db.close()
def _persist_metric(conversation_id: str, request_id: str, model: str, state: dict[str, Any], status: str | None = None, error: str = "") -> dict[str, Any]:
values = _metric_values(state, status=status, error=error)
db = _chat_db()
@@ -774,15 +795,6 @@ def _local_ps() -> list[dict[str, Any]]:
return []
def _local_ps() -> list[dict[str, Any]]:
try:
payload = _json_request(LOCAL_OLLAMA + "/api/ps", timeout=10)
models = payload.get("models", [])
return [item for item in models if isinstance(item, dict)]
except (HTTPError, URLError, OSError, ValueError):
return []
def _model_load_update(name: str, **values: Any) -> None:
with _model_loads_lock:
current = _model_loads.setdefault(name, {"name": name, "state": "queued", "stage": "Queued for Ollama", "started_at": time.time()})
@@ -2064,8 +2076,9 @@ def _run_chat_job(request_id: str) -> None:
_update_chat_job(request_id, status="canceled", finished_at=time.time())
_persist_chat_event(request_id, job.get("conversation_id"), "job-canceled-before-start", level="warning")
return
body = _chat_request_from_job(job)
conversation_id = str(job["conversation_id"])
body = _chat_request_from_job(job)
body.history = _conversation_history(conversation_id, exclude_request_id=request_id)
primary = str(job["primary_model"])
validators = json.loads(job.get("validator_models_json") or "[]")
mode = str(job.get("mode") or "direct")
@@ -2488,7 +2501,8 @@ def chat(body: ChatRequest) -> dict[str, Any]:
attachment_meta = [{"name": item.name, "mime_type": item.mime_type, "url": item.url} for item in body.attachments[:12]]
_persist_message(conversation_id, request_id, "user", body.message.strip() or "[Attachments]", primary, attachment_meta)
_chat_state(request_id, state="queued", stage="Queued for server-side execution", models=selected, primary_model=primary, validator_models=validators, harness=harness, conversation_id=conversation_id)
_create_chat_job(request_id, conversation_id, body, primary, validators, harness)
job_body = body.model_copy(update={"history": []})
_create_chat_job(request_id, conversation_id, job_body, primary, validators, harness)
_submit_chat_job(request_id)
job = _get_chat_job(request_id)
return _job_status_response(job or {"request_id": request_id, "conversation_id": conversation_id, "status": "queued", "mode": "harness" if harness else "direct", "primary_model": primary, "validator_models_json": json.dumps(validators), "result_json": "{}", "error": "", "updated_at": time.time()})
@@ -2549,7 +2563,7 @@ def connections_delete(role: str) -> dict[str, Any]:
@router.get("/status")
def status() -> dict[str, Any]:
def status(include_catalog: bool = False) -> dict[str, Any]:
tags = _local_tags()
ps_rows = _local_ps()
loaded = {str(row.get("name") or row.get("model")): row for row in ps_rows}
@@ -2576,28 +2590,29 @@ def status() -> dict[str, Any]:
family_rows = catalog.get("families") if isinstance(catalog.get("families"), dict) else {}
catalog_rows = list(catalog.get("models", []))
catalog_names = {str(row.get("name") or row.get("model") or "") for row in catalog_rows}
candidate_rows = catalog_rows + [
variant
for rows in family_rows.values()
for variant in rows
if str(variant.get("name") or variant.get("model") or "") not in catalog_names
]
downloadable = []
all_downloadable = []
seen_downloads: set[str] = set()
for rank, row in enumerate(candidate_rows, 1):
name = str(row.get("name") or row.get("model") or "")
if _is_mlx(row) or not name or name in installed_names or name in seen_downloads:
continue
view = catalog_view(row, "catalog")
view["memory_fit"] = _known_ram_fit(view)
view["memory_warning"] = "Estimated runtime RAM exceeds detected host RAM" if view["memory_fit"] is False else ""
if name in catalog_names:
view["popularity_rank"] = rank
seen_downloads.add(name)
all_downloadable.append(view)
if view["memory_fit"]:
downloadable.append(dict(view))
downloadable: list[dict[str, Any]] = []
all_downloadable: list[dict[str, Any]] = []
if include_catalog:
candidate_rows = catalog_rows + [
variant
for rows in family_rows.values()
for variant in rows
if str(variant.get("name") or variant.get("model") or "") not in catalog_names
]
seen_downloads: set[str] = set()
for rank, row in enumerate(candidate_rows, 1):
name = str(row.get("name") or row.get("model") or "")
if _is_mlx(row) or not name or name in installed_names or name in seen_downloads:
continue
view = catalog_view(row, "catalog")
view["memory_fit"] = _known_ram_fit(view)
view["memory_warning"] = "Estimated runtime RAM exceeds detected host RAM" if view["memory_fit"] is False else ""
if name in catalog_names:
view["popularity_rank"] = rank
seen_downloads.add(name)
all_downloadable.append(view)
if view["memory_fit"]:
downloadable.append(dict(view))
popular = _popular_fit_models(
[row for row in catalog.get("models", []) if not _is_mlx(row)],
@@ -2638,6 +2653,7 @@ def status() -> dict[str, Any]:
"disk": _disk_snapshot(),
"models": local,
"popular": popular,
"catalog_count": len(catalog_rows),
"popular_filter": {
"max_expected_ram_gib": _host_ram_gib(),
"basis": "detected MemTotal from the running host",
@@ -2645,8 +2661,6 @@ def status() -> dict[str, Any]:
"requires_known_ram": True,
"smaller_fit_variants_substituted": True,
},
"catalog": downloadable,
"catalog_all": all_downloadable,
"catalog_filter": {
"max_expected_ram_gib": _host_ram_gib(),
"basis": "detected MemTotal from the running host",
@@ -2655,7 +2669,7 @@ def status() -> dict[str, Any]:
},
"catalog_filter_options": {
"types": ["all", "moe", "dense"],
"capabilities": sorted({cap for row in downloadable for cap in row.get("capabilities", [])}),
"capabilities": sorted({cap for row in (downloadable if include_catalog else catalog_rows) for cap in row.get("capabilities", [])}),
},
"catalog_source": catalog.get("source"),
"catalog_updated_at": catalog.get("fetched_at"),
@@ -2663,9 +2677,80 @@ def status() -> dict[str, Any]:
"next_catalog_refresh": _next_refresh(),
"jobs": _job_snapshot(),
"generated_at": time.time(),
**({"catalog": downloadable, "catalog_all": all_downloadable} if include_catalog else {}),
}
@router.get("/catalog")
def catalog(
q: str = "",
page: int = 1,
page_size: int = 50,
model_type: str = "all",
capability: str = "all",
sort: str = "popularity",
recent_only: bool = True,
show_oversized: bool = False,
) -> dict[str, Any]:
snapshot = status(include_catalog=True)
rows = list(snapshot.get("catalog_all", [])) if show_oversized else list(snapshot.get("catalog", []))
needle = str(q or "").strip().lower()
if needle:
rows = [
row for row in rows
if needle in " ".join([
str(row.get("name") or ""), str(row.get("family") or ""),
" ".join(str(value) for value in row.get("strengths", [])),
" ".join(str(value) for value in row.get("capabilities", [])),
]).lower()
]
if model_type in {"moe", "dense"}:
rows = [row for row in rows if bool(row.get("is_moe")) == (model_type == "moe")]
if capability != "all":
rows = [row for row in rows if capability in row.get("capabilities", [])]
if recent_only:
cutoff = time.time() - 365 * 24 * 60 * 60
recent_rows: list[dict[str, Any]] = []
for row in rows:
modified = row.get("modified_at")
timestamp = _catalog_timestamp(modified)
if not modified or timestamp is None or timestamp >= cutoff:
recent_rows.append(row)
rows = recent_rows
if sort == "size_asc":
rows.sort(key=lambda row: int(row.get("size_bytes") or 0))
elif sort == "size_desc":
rows.sort(key=lambda row: int(row.get("size_bytes") or 0), reverse=True)
elif sort == "newest":
rows.sort(key=lambda row: _catalog_timestamp(row.get("modified_at")) or 0, reverse=True)
elif sort == "name":
rows.sort(key=lambda row: str(row.get("name") or "").lower())
page_size = max(1, min(int(page_size), 100))
page = max(1, int(page))
start = (page - 1) * page_size
return {
"catalog": rows[start:start + page_size],
"page": page,
"page_size": page_size,
"total": len(rows),
"has_more": start + page_size < len(rows),
"catalog_count": snapshot.get("catalog_count", 0),
"catalog_filter_options": snapshot.get("catalog_filter_options", {}),
"catalog_source": snapshot.get("catalog_source"),
"catalog_updated_at": snapshot.get("catalog_updated_at"),
"catalog_error": snapshot.get("catalog_error"),
}
def _catalog_timestamp(value: Any) -> float | None:
if not value:
return None
try:
return datetime.fromisoformat(str(value).replace("Z", "+00:00")).timestamp()
except (TypeError, ValueError, OverflowError):
return None
def _ollama_version() -> str | None:
try:
return str(_json_request(LOCAL_OLLAMA + "/api/version", timeout=5).get("version") or "unknown")