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