diff --git a/README.md b/README.md index c0aa6bb..a85a604 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,10 @@ validated against fields actually seen in the stream before it can be applied. Unknown fields, raw message fields, internal `fgai_*` fields, and unknown detectors are rejected. +Settings also lists locally installed Ollama models from `http://127.0.0.1:11434/api/tags`. +Click a model name to fill both the dashboard analyst model and profile advisor +model fields. + The settings page treats stream enablement and profile editing separately. The checkboxes decide which streams are monitored. Click `Edit profile` on one stream to load its fields and edit only that stream's profile; saving with no active diff --git a/src/fgai/dashboard.py b/src/fgai/dashboard.py index ab0472e..6156ecd 100644 --- a/src/fgai/dashboard.py +++ b/src/fgai/dashboard.py @@ -3,6 +3,7 @@ from __future__ import annotations import json from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path +from urllib import error, request from urllib.parse import parse_qs, urlparse from .config import ConfigStore @@ -13,6 +14,30 @@ from .feedback import FeedbackStore from .incidents import IncidentStore +def _ollama_models(host: str = "http://127.0.0.1:11434") -> dict[str, object]: + try: + req = request.Request(f"{host.rstrip('/')}/api/tags", method="GET") + with request.urlopen(req, timeout=5) as response: + payload = json.loads(response.read().decode("utf-8")) + except error.URLError as exc: + return {"status": "error", "error": str(exc), "models": []} + except (json.JSONDecodeError, OSError) as exc: + return {"status": "error", "error": str(exc), "models": []} + models = payload.get("models", []) + if not isinstance(models, list): + models = [] + output = [] + for item in models: + if not isinstance(item, dict): + continue + output.append({ + "name": str(item.get("name", "")), + "modified_at": str(item.get("modified_at", "")), + "size": int(item.get("size", 0) or 0), + }) + return {"status": "ok", "models": sorted(output, key=lambda model: model["name"])} + + HTML = """
@@ -72,6 +97,8 @@ HTML = """ .graph { width: 100%; height: 300px; background: #04182d; border: 1px solid #163b59; } .sort-button { border: 0; background: transparent; color: #83bce9; cursor: pointer; font: inherit; font-weight: 600; padding: 0; } .sort-button:hover { color: #d9e8f7; } + .model-list { display: flex; flex-wrap: wrap; gap: 8px; } + .model-pill { border: 1px solid #39709a; background: #08243e; color: #d9e8f7; padding: 5px 8px; cursor: pointer; } @media (max-width: 860px) { .hero, .split { grid-template-columns: 1fr; } .hero img { display: none; } } @@ -89,7 +116,7 @@ HTML = """