feat: legg til direkte Ollama-verktøy for Emma (gemma3:27b), gemma3:4b og qwen2.5:3b på emma-gpu-vm

This commit is contained in:
chrischristiansen-glitch 2026-06-29 19:03:52 +02:00
parent 7231cd3b31
commit 9ff2da5985

View File

@ -17,7 +17,7 @@ import logging
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
app = FastAPI(title="opax-mcp", version="3.0.2") app = FastAPI(title="opax-mcp", version="3.1.0")
OPAX_BASE_URL = os.environ.get("OPAX_BASE_URL", "https://opax.vauco.no") OPAX_BASE_URL = os.environ.get("OPAX_BASE_URL", "https://opax.vauco.no")
OPAX_IAP_CLIENT_ID = os.environ.get("OPAX_IAP_CLIENT_ID", "") OPAX_IAP_CLIENT_ID = os.environ.get("OPAX_IAP_CLIENT_ID", "")
@ -27,7 +27,15 @@ GITEA_TOKEN = os.environ.get("GITEA_TOKEN", "")
GITEA_REPO = os.environ.get("GITEA_REPO", "chris/OSVauco") GITEA_REPO = os.environ.get("GITEA_REPO", "chris/OSVauco")
GOOGLE_CLOUD_PROJECT = os.environ.get("GOOGLE_CLOUD_PROJECT", "propane-will-491900-m5") GOOGLE_CLOUD_PROJECT = os.environ.get("GOOGLE_CLOUD_PROJECT", "propane-will-491900-m5")
# Emma-vm Ollama — direkte tilkobling
OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "http://34.13.238.133:11434")
EMMA_MODEL = os.environ.get("EMMA_MODEL", "gemma3:27b")
EMMA_FAST_MODEL = os.environ.get("EMMA_FAST_MODEL", "gemma3:4b")
EMMA_LIGHT_MODEL = os.environ.get("EMMA_LIGHT_MODEL", "qwen2.5:3b")
logger.info(f"OPAX_IAP_CLIENT_ID: {OPAX_IAP_CLIENT_ID[:10]}...") logger.info(f"OPAX_IAP_CLIENT_ID: {OPAX_IAP_CLIENT_ID[:10]}...")
logger.info(f"OLLAMA_BASE_URL: {OLLAMA_BASE_URL}")
logger.info(f"EMMA_MODEL: {EMMA_MODEL}")
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -37,11 +45,9 @@ logger.info(f"OPAX_IAP_CLIENT_ID: {OPAX_IAP_CLIENT_ID[:10]}...")
def _verify_auth(request: Request): def _verify_auth(request: Request):
if not MCP_SECRET: if not MCP_SECRET:
return return
# Authorization: Bearer <token>
auth = request.headers.get("Authorization", "") auth = request.headers.get("Authorization", "")
if auth.startswith("Bearer ") and auth[7:] == MCP_SECRET: if auth.startswith("Bearer ") and auth[7:] == MCP_SECRET:
return return
# X-MCP-Secret or api-key
if request.headers.get("X-MCP-Secret") == MCP_SECRET: if request.headers.get("X-MCP-Secret") == MCP_SECRET:
return return
if request.headers.get("api-key") == MCP_SECRET: if request.headers.get("api-key") == MCP_SECRET:
@ -88,6 +94,46 @@ async def _opax_post(path: str, body: dict) -> Any:
return r.json() return r.json()
# ---------------------------------------------------------------------------
# Ollama helpers — direkte mot emma-gpu-vm
# ---------------------------------------------------------------------------
async def _ollama_chat(model: str, prompt: str, system: str = "") -> dict:
"""Kall Ollama chat-API direkte på emma-gpu-vm."""
messages = []
if system:
messages.append({"role": "system", "content": system})
messages.append({"role": "user", "content": prompt})
payload = {
"model": model,
"messages": messages,
"stream": False,
}
try:
async with httpx.AsyncClient(timeout=120) as c:
r = await c.post(f"{OLLAMA_BASE_URL}/api/chat", json=payload)
r.raise_for_status()
data = r.json()
return {
"model": model,
"response": data.get("message", {}).get("content", ""),
"done": data.get("done", True),
"total_duration_ms": round(data.get("total_duration", 0) / 1e6),
}
except Exception as e:
logger.error(f"[OLLAMA ERROR] model={model} {type(e).__name__}: {e}")
raise
async def _ollama_models() -> list:
"""List alle modeller tilgjengelig på emma-gpu-vm."""
async with httpx.AsyncClient(timeout=10) as c:
r = await c.get(f"{OLLAMA_BASE_URL}/api/tags")
r.raise_for_status()
return r.json().get("models", [])
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Gitea helpers # Gitea helpers
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -157,7 +203,32 @@ async def run_jason(p):
return await _opax_post("/run", {"message": p.get("prompt", p.get("message", "")), "user_id": p.get("user_id", "opax"), "session_id": p.get("session_id", "mcp"), "mode": p.get("mode", "light")}) return await _opax_post("/run", {"message": p.get("prompt", p.get("message", "")), "user_id": p.get("user_id", "opax"), "session_id": p.get("session_id", "mcp"), "mode": p.get("mode", "light")})
async def run_emma(p): async def run_emma(p):
return await _opax_post("/emma", {"message": p.get("prompt", p.get("message", "")), "session_id": p.get("session_id", "mcp")}) """Emma Vauger — Gemma 3 27B via Ollama på emma-gpu-vm. Primær lokal AI."""
return await _ollama_chat(
model=EMMA_MODEL,
prompt=p.get("prompt", p.get("message", "")),
system=p.get("system", "Du er Emma Vauger, en intelligent og hjelpsom AI-assistent for Vauco. Du er lokal, rask og priveråd.")
)
async def run_emma_fast(p):
"""Emma Fast — Gemma 3 4B via Ollama. Rask inferens for enkle oppgaver."""
return await _ollama_chat(
model=EMMA_FAST_MODEL,
prompt=p.get("prompt", p.get("message", "")),
system=p.get("system", "Du er en rask og konsis AI-assistent. Svar kort og presist.")
)
async def run_qwen(p):
"""Qwen 2.5 3B via Ollama. Lett hjelper for enkle spørsmål og kodeoppgaver."""
return await _ollama_chat(
model=EMMA_LIGHT_MODEL,
prompt=p.get("prompt", p.get("message", "")),
system=p.get("system", "")
)
async def list_emma_models(p):
"""List alle Ollama-modeller tilgjengelig på emma-gpu-vm."""
return await _ollama_models()
async def get_health(p): return await _opax_get("/health") async def get_health(p): return await _opax_get("/health")
async def get_build_status(p): return await _opax_get("/opax/build-status") async def get_build_status(p): return await _opax_get("/opax/build-status")
@ -221,7 +292,13 @@ TOOLS = {
"send_sms": (send_sms, "Send SMS", {"type":"object","properties":{"to":{"type":"string"},"message":{"type":"string"}},"required":["to","message"]}), "send_sms": (send_sms, "Send SMS", {"type":"object","properties":{"to":{"type":"string"},"message":{"type":"string"}},"required":["to","message"]}),
"get_notify_channels": (get_notify_channels, "Hent varslingkanaler", {}), "get_notify_channels": (get_notify_channels, "Hent varslingkanaler", {}),
"run_jason": (run_jason, "Kjør Jason-agenten med en prompt", {"type":"object","properties":{"prompt":{"type":"string"},"mode":{"type":"string"}},"required":["prompt"]}), "run_jason": (run_jason, "Kjør Jason-agenten med en prompt", {"type":"object","properties":{"prompt":{"type":"string"},"mode":{"type":"string"}},"required":["prompt"]}),
"run_emma": (run_emma, "Kjør Emma-agenten med en prompt", {"type":"object","properties":{"prompt":{"type":"string"}},"required":["prompt"]}),
# — Emma Vauger — direkte Ollama på emma-gpu-vm (34.13.238.133:11434)
"run_emma": (run_emma, "Emma Vauger (gemma3:27b) — primær lokal AI, ingen quota", {"type":"object","properties":{"prompt":{"type":"string"},"system":{"type":"string","description":"Valgfri systemprompt"}},"required":["prompt"]}),
"run_emma_fast": (run_emma_fast, "Emma Fast (gemma3:4b) — rask versjon for enkle oppgaver", {"type":"object","properties":{"prompt":{"type":"string"},"system":{"type":"string"}},"required":["prompt"]}),
"run_qwen": (run_qwen, "Qwen 2.5 3B — lett hjelper og kodeoppgaver", {"type":"object","properties":{"prompt":{"type":"string"},"system":{"type":"string"}},"required":["prompt"]}),
"list_emma_models": (list_emma_models, "List alle Ollama-modeller tilgjengelig på emma-gpu-vm", {}),
"get_health": (get_health, "Hent helsestatus for OPAX", {}), "get_health": (get_health, "Hent helsestatus for OPAX", {}),
"get_build_status": (get_build_status, "Hent siste build-status", {}), "get_build_status": (get_build_status, "Hent siste build-status", {}),
"get_state": (get_state, "Hent platform-tilstand", {}), "get_state": (get_state, "Hent platform-tilstand", {}),
@ -271,19 +348,16 @@ async def mcp_handler(request: Request):
req_id = body.get("id") req_id = body.get("id")
params = body.get("params", {}) params = body.get("params", {})
# initialize
if method == "initialize": if method == "initialize":
return JSONResponse(_jsonrpc_ok(req_id, { return JSONResponse(_jsonrpc_ok(req_id, {
"protocolVersion": "2024-11-05", "protocolVersion": "2024-11-05",
"capabilities": {"tools": {}}, "capabilities": {"tools": {}},
"serverInfo": {"name": "opax-mcp", "version": "3.0.0"}, "serverInfo": {"name": "opax-mcp", "version": "3.1.0"},
})) }))
# tools/list
if method == "tools/list": if method == "tools/list":
return JSONResponse(_jsonrpc_ok(req_id, {"tools": _tool_list_result()})) return JSONResponse(_jsonrpc_ok(req_id, {"tools": _tool_list_result()}))
# tools/call
if method == "tools/call": if method == "tools/call":
tool_name = params.get("name") or params.get("tool") tool_name = params.get("name") or params.get("tool")
tool_args = params.get("arguments", params.get("params", {})) tool_args = params.get("arguments", params.get("params", {}))
@ -303,7 +377,6 @@ async def mcp_handler(request: Request):
"content": [{"type": "text", "text": json.dumps(result, ensure_ascii=False)}] "content": [{"type": "text", "text": json.dumps(result, ensure_ascii=False)}]
})) }))
# notifications (fire-and-forget, no response needed)
if method.startswith("notifications/"): if method.startswith("notifications/"):
return JSONResponse(status_code=202, content={}) return JSONResponse(status_code=202, content={})
@ -316,4 +389,4 @@ async def mcp_handler(request: Request):
@app.get("/health") @app.get("/health")
async def health(): async def health():
return {"status": "ok", "service": "opax-mcp", "version": "3.0.0"} return {"status": "ok", "service": "opax-mcp", "version": "3.1.0", "ollama": OLLAMA_BASE_URL, "emma_model": EMMA_MODEL}