diff --git a/opax-mcp/server.py b/opax-mcp/server.py index d66a26c..ba24f66 100644 --- a/opax-mcp/server.py +++ b/opax-mcp/server.py @@ -17,7 +17,7 @@ import logging logging.basicConfig(level=logging.INFO) 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_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") 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"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): if not MCP_SECRET: return - # Authorization: Bearer auth = request.headers.get("Authorization", "") if auth.startswith("Bearer ") and auth[7:] == MCP_SECRET: return - # X-MCP-Secret or api-key if request.headers.get("X-MCP-Secret") == MCP_SECRET: return if request.headers.get("api-key") == MCP_SECRET: @@ -88,6 +94,46 @@ async def _opax_post(path: str, body: dict) -> Any: 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 # --------------------------------------------------------------------------- @@ -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")}) 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_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"]}), "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_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_build_status": (get_build_status, "Hent siste build-status", {}), "get_state": (get_state, "Hent platform-tilstand", {}), @@ -271,19 +348,16 @@ async def mcp_handler(request: Request): req_id = body.get("id") params = body.get("params", {}) - # initialize if method == "initialize": return JSONResponse(_jsonrpc_ok(req_id, { "protocolVersion": "2024-11-05", "capabilities": {"tools": {}}, - "serverInfo": {"name": "opax-mcp", "version": "3.0.0"}, + "serverInfo": {"name": "opax-mcp", "version": "3.1.0"}, })) - # tools/list if method == "tools/list": return JSONResponse(_jsonrpc_ok(req_id, {"tools": _tool_list_result()})) - # tools/call if method == "tools/call": tool_name = params.get("name") or params.get("tool") 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)}] })) - # notifications (fire-and-forget, no response needed) if method.startswith("notifications/"): return JSONResponse(status_code=202, content={}) @@ -316,4 +389,4 @@ async def mcp_handler(request: Request): @app.get("/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}