feat(osv): add local Ollama pipeline with explicit Emma escalation

This commit is contained in:
chrischristiansen-glitch 2026-06-29 21:40:31 +02:00
parent e63896c6b5
commit 6549c514b0

203
agents/osv/pipeline.py Normal file
View File

@ -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 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))