feat(osv): add local Ollama pipeline with explicit Emma escalation
This commit is contained in:
parent
e63896c6b5
commit
6549c514b0
203
agents/osv/pipeline.py
Normal file
203
agents/osv/pipeline.py
Normal 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 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))
|
||||||
Loading…
Reference in New Issue
Block a user