#!/usr/bin/env python3 """ agents/osv/pipeline.py — OSV lokal Ollama-pipeline for osvauco-dev-vm Arkitektur (tre lag, sekvensiell): gemma3:4b (front) — forstår intent, strukturerer oppgave qwen2.5:7b (analyse) — dyptgående analyse, kode, planlegging gemma3:4b (output) — formaterer og leverer svar til bruker Eskalering til Emma (emma-gpu-vm, Gemma 4 27B): - Kun hvis Chris eksplisitt sier "emma" i meldingen - Eller hvis pipeline selv er usikker og bruker bekrefter - Aldri automatisk Basert på ideer fra: - agents/core-logic/heavy_predictor.py (tung-modus trigger) - agents/core-logic/feedback_loop.py (telemetri / kvalitet) - agents/multi_agent/orchestrator.py (delegerings-mønster) """ from __future__ import annotations import os import logging import httpx logger = logging.getLogger(__name__) # ── Konfigurasjon ───────────────────────────────────────────────────────────── OLLAMA_BASE = os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434") EMMA_OLLAMA_BASE = os.environ.get("EMMA_OLLAMA_BASE_URL", "http://emma-gpu-vm:11434") FRONT_MODEL = os.environ.get("OSV_FRONT_MODEL", "gemma3:4b") ANALYST_MODEL = os.environ.get("OSV_ANALYST_MODEL", "qwen2.5:7b") EMMA_MODEL = os.environ.get("OSV_EMMA_MODEL", "gemma4:27b") TIMEOUT = int(os.environ.get("OSV_TIMEOUT", "120")) # Nøkkelord som trigger Emma-eskalering EMMA_TRIGGERS = ["emma", "@emma", "send til emma", "bruk emma"] # Konfidensterskel — under denne spør pipeline om Emma skal brukes CONFIDENCE_THRESHOLD = 0.4 # ── Hjelpefunksjoner ───────────────────────────────────────────────────────── def _ollama_generate(base_url: str, model: str, prompt: str, system: str = "") -> str: """Kaller Ollama /api/generate og returnerer tekst-svaret.""" payload = { "model": model, "prompt": prompt, "stream": False, } if system: payload["system"] = system try: resp = httpx.post( f"{base_url}/api/generate", json=payload, timeout=TIMEOUT, ) resp.raise_for_status() return resp.json().get("response", "").strip() except httpx.HTTPError as e: logger.error("Ollama feil (%s/%s): %s", base_url, model, e) raise def _wants_emma(message: str) -> bool: """Returner True hvis meldingen eksplisitt ber om Emma.""" lower = message.lower() return any(trigger in lower for trigger in EMMA_TRIGGERS) def _confidence_check(response: str) -> float: """ Enkel heuristikk — lav konfidans hvis svaret inneholder tvils-markører. Returnerer verdi mellom 0.0 og 1.0. """ doubt_markers = [ "jeg er usikker", "vet ikke", "ikke sikker", "vanskelig å si", "kan ikke svare", "trenger mer info", "i'm not sure", "unclear", "cannot determine", ] lower = response.lower() hits = sum(1 for m in doubt_markers if m in lower) return max(0.0, 1.0 - (hits * 0.3)) # ── Pipeline-modi ───────────────────────────────────────────────────────────── def run_local(user_message: str) -> dict: """ Kjører tre-lags lokal pipeline: gemma3:4b → qwen2.5:7b → gemma3:4b """ logger.info("[OSV] Starter lokal pipeline") # Lag 1 — Gemma front: forstår og strukturerer front_response = _ollama_generate( OLLAMA_BASE, FRONT_MODEL, user_message, system=( "Du er OSV, en intelligent assistent for Vauco AS. " "Din jobb er å forstå brukerens intent presist og strukturere " "oppgaven klart for videre analyse. Vær kortfattet og presis. " "Svar på norsk med mindre brukeren skriver engelsk." ), ) logger.info("[OSV] Front (gemma3:4b) ferdig") # Lag 2 — Qwen analyst: dyptgående analyse analyst_response = _ollama_generate( OLLAMA_BASE, ANALYST_MODEL, front_response, system=( "Du er en analytiker for Vauco AS. Du mottar en strukturert oppgave " "og skal gi en grundig, handlingsorientert analyse. " "Spesialiser deg på kode, infrastruktur og tekniske beslutninger. " "Vær direkte — Chris har ikke tid til omsvøp." ), ) logger.info("[OSV] Analyst (qwen2.5:7b) ferdig") # Sjekk konfidans — be om Emma-bekreftelse hvis lav confidence = _confidence_check(analyst_response) if confidence < CONFIDENCE_THRESHOLD: logger.warning("[OSV] Lav konfidans (%.2f) — ber om Emma-bekreftelse", confidence) return { "mode": "ask_emma", "confidence": confidence, "partial_response": analyst_response, "ask": "Jeg er usikker på dette. Vil du at jeg sender det til Emma (Gemma 4 27B) for et sterkere svar?", } # Lag 3 — Gemma output: formaterer og leverer final_response = _ollama_generate( OLLAMA_BASE, FRONT_MODEL, analyst_response, system=( "Du er OSV, siste ledd i en analyse-pipeline for Vauco AS. " "Du mottar et analysert svar og skal formatere det rent og " "lesbart for Chris. Bruk lister og tabeller der det er naturlig. " "Vær kortfattet. Svar på norsk med mindre konteksten er engelsk." ), ) logger.info("[OSV] Output (gemma3:4b) ferdig") return { "mode": "local", "confidence": confidence, "response": final_response, } def run_emma(user_message: str) -> dict: """ Eskalerer direkte til Emma-GPU-VM (Gemma 4 27B). Kalles kun når Chris eksplisitt ber om Emma. """ logger.info("[OSV] Eskalerer til Emma (%s)", EMMA_MODEL) response = _ollama_generate( EMMA_OLLAMA_BASE, EMMA_MODEL, user_message, system=( "Du er Emma Vauger, intern AI-assistent for Vauco AS. " "Du hjelper Chris Christiansen med strategiske beslutninger, " "GCP-infrastruktur, kodearkitektur og teknisk planlegging. " "Vær direkte, analytisk og ærlig. Aldri sycophantisk. " "Norsk som standard." ), ) return { "mode": "emma", "model": EMMA_MODEL, "response": response, } # ── Hoved-inngangspunkt ─────────────────────────────────────────────────────── def run(user_message: str, force_emma: bool = False) -> dict: """ Hoved-dispatcher for OSV-pipelinen. Regler: 1. force_emma=True ELLER "emma" i meldingen → run_emma() 2. Alt annet → run_local() med mulig ask_emma retur """ if force_emma or _wants_emma(user_message): return run_emma(user_message) return run_local(user_message) # ── CLI-test ────────────────────────────────────────────────────────────────── if __name__ == "__main__": import sys import json msg = " ".join(sys.argv[1:]) if len(sys.argv) > 1 else "Hva er status på OPAX?" result = run(msg) print(json.dumps(result, ensure_ascii=False, indent=2))