diff --git a/agents/osv/pipeline.py b/agents/osv/pipeline.py new file mode 100644 index 0000000..8611057 --- /dev/null +++ b/agents/osv/pipeline.py @@ -0,0 +1,203 @@ +#!/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))