From 2a9138b29003a6a0e0214cc5fe7e5a993ab0acf8 Mon Sep 17 00:00:00 2001 From: Chris Christiansen Date: Thu, 2 Jul 2026 16:15:03 +0000 Subject: [PATCH] chore: oppdater opax-mcp server.py og yaml --- docs/MASTERPLAN.md | 107 +++++++++++++++++++++++++++++++++++++++++++++ opax-mcp.yaml | 96 ++++++++++++++++++++++++++++++++++++++++ opax-mcp/server.py | 43 +++++++++++++++--- 3 files changed, 241 insertions(+), 5 deletions(-) create mode 100644 opax-mcp.yaml diff --git a/docs/MASTERPLAN.md b/docs/MASTERPLAN.md index 0b56b71..c5568f1 100644 --- a/docs/MASTERPLAN.md +++ b/docs/MASTERPLAN.md @@ -305,3 +305,110 @@ GCP-event → Jason → Delivery layer → Webhook (CG5a) / E-post (CG5b) / SMS --- *OSVauco | propane-will-491900-m5 | us-central1 | Autoritativ fra 2026-05-31 | Oppdatert 2026-06-12* + +--- + +## 13. Telefoni & AI-agent Plan — Phase E tillegg + +### Konsept: Twilio som kommunikasjonsryggrad +Et Twilio-nummer er grunnmuren for SMS, tale og AI-agent telefoni. +Alt bygger på samme konto og nummer. + +--- + +### Lag 1 — Grunnmur (gjøres NÅ, Phase D) +Allerede planlagt: +- Twilio-konto opprettet +- TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_FROM_NUMBER + lagret i Secret Manager +- send_sms fungerer via opax-mcp + +--- + +### Lag 2 — Inngående AI-telefonsvarer (Phase E, Uke 3-4) + +#### Konsept +Kunder/kontakter ringer Twilio-nummeret. +Emma-agenten tar samtalen, transkriberer og svarer med tale. + +#### Teknisk stack +- Twilio Programmable Voice + Conversation Relay (WebSocket) +- Ollama (emma modell) på osvauco-dev-vm som LLM +- Whisper eller Twilio Speech Recognition for STT +- Twilio TTS (eller Google TTS) for tale tilbake + +#### Flyt +Innringer → Twilio → WebSocket → opax-mcp → Ollama (Emma) +Emma svarer → TTS → Twilio → Innringer + +#### Hva Emma kan gjøre under samtalen +- Svare på spørsmål om OSVauco-tjenester +- Slå opp kundedata i backend +- Booke møter og sende SMS-bekreftelse +- Si "ett øyeblikk" og hente live data via MCP-tools +- Eskalere til Chris eller Jason med full samtale-kontekst + +#### Implementasjon +1. Legg til /voice webhook-endepunkt i opax-mcp +2. Konfigurer Twilio Voice URL til opax-mcp Cloud Run +3. Bygg ConversationRelay WebSocket-handler +4. Koble til Ollama via eksisterende MCP-infrastruktur +5. Definer Emma sin persona og instrukser for telefon +6. Test med internt nummer først + +--- + +### Lag 3 — Utgående AI-anrop (Phase E, Uke 5-6) + +#### Konsept +Emma ringer proaktivt — f.eks. kostnadsvarsler, +møtepåminnelser, oppfølging av kunder. + +#### Flyt +Trigger (kostnadsspike / planlagt tid) +→ Emma initierer anrop via Twilio Outbound API +→ Samme ConversationRelay-stack som inngående + +#### Bruksområder +- "Hei, jeg ringer fra Vauco — dere nærmer dere + budsjettgrensen på GCP denne måneden" +- Møtepåminnelse dagen før +- Oppfølging etter demo + +--- + +### Lag 4 — Multikanal-orkestrering (Phase F) + +#### Konsept +Emma bestemmer selv hvilken kanal som passer: +- Lav prioritet → SMS +- Medium → E-post (Gmail API, allerede oppe) +- Høy prioritet → Telefonanrop +- Intern → Google Chat webhook + +#### Implementasjon +Legg til kanalvalg-logikk i Emma sin system-prompt: + "Velg kanal basert på hastegrad og brukerpreferanse" + +--- + +### Personas per kanal + +| Kanal | Avsender | Persona | +|---|---|---| +| SMS | +47-Twilio-nummer | Nøytral, kort | +| E-post | jason.vauger@vauco.no | Profesjonell | +| Telefon inngående | Emma | Vennlig, hjelpsom | +| Telefon utgående | Emma | Proaktiv, konsis | + +--- + +### Kostnadsestimat Twilio + +| Tjeneste | Kostnad | +|---|---| +| Telefonnummer | ~$1/mnd | +| SMS utgående (NO) | ~$0.07/SMS | +| Voice inngående | ~$0.0085/min | +| Voice utgående | ~$0.014/min | +| Trial credits | $15 gratis ved oppstart | diff --git a/opax-mcp.yaml b/opax-mcp.yaml new file mode 100644 index 0000000..91f9e5f --- /dev/null +++ b/opax-mcp.yaml @@ -0,0 +1,96 @@ +apiVersion: serving.knative.dev/v1 +kind: Service +metadata: + annotations: + run.googleapis.com/build-enable-automatic-updates: 'false' + run.googleapis.com/build-id: 3deeb5f6-0351-4580-8dab-2dcad7e478dc + run.googleapis.com/build-image-uri: us-central1-docker.pkg.dev/propane-will-491900-m5/cloud-run-source-deploy/opax-mcp + run.googleapis.com/build-name: projects/357036551735/locations/us-central1/builds/3deeb5f6-0351-4580-8dab-2dcad7e478dc + run.googleapis.com/build-source-location: gs://run-sources-propane-will-491900-m5-us-central1/services/opax-mcp/1782754930.916629-ba7acea7e3534f21b658e90dd918d952.zip#1782754931015737 + run.googleapis.com/ingress: all + run.googleapis.com/ingress-status: all + run.googleapis.com/maxScale: '20' + run.googleapis.com/operation-id: 931f0012-fb7c-422c-b857-8e75cc298a5f + run.googleapis.com/urls: '["https://opax-mcp-357036551735.us-central1.run.app","https://opax-mcp-zjbqp3prqq-uc.a.run.app"]' + serving.knative.dev/creator: 357036551735-compute@developer.gserviceaccount.com + serving.knative.dev/lastModifier: chris.christiansen@vauco.no + creationTimestamp: '2026-06-10T01:03:48.098765Z' + generation: 57 + labels: + cloud.googleapis.com/location: us-central1 + name: opax-mcp + namespace: '357036551735' + resourceVersion: AAZVlY5b+Ro + selfLink: /apis/serving.knative.dev/v1/namespaces/357036551735/services/opax-mcp + uid: c95aae34-95db-474f-93dc-f8d89b55b2dd +spec: + template: + metadata: + annotations: + autoscaling.knative.dev/maxScale: '5' + run.googleapis.com/client-name: gcloud + run.googleapis.com/client-version: 570.0.0 + run.googleapis.com/startup-cpu-boost: 'true' + labels: + client.knative.dev/nonce: zcdkbqbeua + run.googleapis.com/startupProbeType: Default + spec: + containerConcurrency: 80 + containers: + - env: + - name: GOOGLE_CLOUD_PROJECT + value: propane-will-491900-m5 + - name: STATIC_BUCKET + value: opax-vauco-static + - name: CLOUD_RUN_SERVICE + value: osvauco-agent + - name: CLOUD_BUILD_TRIGGER_ID + value: 38423976-91ff-4ff4-859e-1f262344c609 + - name: OPAX_IAP_CLIENT_ID + value: 357036551735-kq8nt7ld38hfqlcfb3n52ef7tala4meo.apps.googleusercontent.com + - name: GITEA_URL + value: http://34.158.137.245:3000 + - name: GITEA_TOKEN + valueFrom: + secretKeyRef: + key: latest + name: gitea-token + - name: GMAIL_DEFAULT_SENDER + value: jason.vauger@vauco.no + - name: GMAIL_ALLOWED_SENDERS + value: jason.vauger@vauco.no,emma.vauger@vauco.no + - name: TWILIO_ACCOUNT_SID + valueFrom: + secretKeyRef: + key: latest + name: TWILIO_ACCOUNT_SID + - name: TWILIO_AUTH_TOKEN + valueFrom: + secretKeyRef: + key: latest + name: TWILIO_AUTH_TOKEN + - name: TWILIO_FROM_NUMBER + valueFrom: + secretKeyRef: + key: latest + name: TWILIO_FROM_NUMBER + image: us-central1-docker.pkg.dev/propane-will-491900-m5/osvauco-repo/opax-mcp:latest + name: opax-mcp-1 + ports: + - containerPort: 8080 + name: http1 + resources: + limits: + cpu: '1' + memory: 512Mi + startupProbe: + failureThreshold: 1 + periodSeconds: 240 + tcpSocket: + port: 8080 + timeoutSeconds: 240 + serviceAccountName: jason-vauger@propane-will-491900-m5.iam.gserviceaccount.com + timeoutSeconds: 60 + traffic: + - latestRevision: true + percent: 100 diff --git a/opax-mcp/server.py b/opax-mcp/server.py index ba24f66..ab607e7 100644 --- a/opax-mcp/server.py +++ b/opax-mcp/server.py @@ -9,6 +9,7 @@ import httpx import base64 import google.auth import google.auth.transport.requests +import google.oauth2.id_token from fastapi import FastAPI, Request, HTTPException from fastapi.responses import JSONResponse from typing import Any, Optional @@ -17,9 +18,10 @@ import logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -app = FastAPI(title="opax-mcp", version="3.1.0") +app = FastAPI(title="opax-mcp", version="3.2.0") OPAX_BASE_URL = os.environ.get("OPAX_BASE_URL", "https://opax.vauco.no") +OSVAUCO_AGENT_URL = os.environ.get("OSVAUCO_AGENT_URL", "") OPAX_IAP_CLIENT_ID = os.environ.get("OPAX_IAP_CLIENT_ID", "") MCP_SECRET = os.environ.get("MCP_SECRET", "") GITEA_URL = os.environ.get("GITEA_URL", "http://34.59.131.162:3000") @@ -34,6 +36,7 @@ EMMA_FAST_MODEL = os.environ.get("EMMA_FAST_MODEL", "gemma3:4b") EMMA_LIGHT_MODEL = os.environ.get("EMMA_LIGHT_MODEL", "qwen2.5:3b") logger.info(f"OPAX_IAP_CLIENT_ID: {OPAX_IAP_CLIENT_ID[:10]}...") +logger.info(f"OSVAUCO_AGENT_URL: {OSVAUCO_AGENT_URL}") logger.info(f"OLLAMA_BASE_URL: {OLLAMA_BASE_URL}") logger.info(f"EMMA_MODEL: {EMMA_MODEL}") @@ -56,7 +59,7 @@ def _verify_auth(request: Request): # --------------------------------------------------------------------------- -# OPAX helpers +# OPAX helpers (for opax.vauco.no via IAP) # --------------------------------------------------------------------------- def _opax_identity_token() -> str: @@ -93,6 +96,36 @@ async def _opax_post(path: str, body: dict) -> Any: r.raise_for_status() return r.json() +# --------------------------------------------------------------------------- +# OSVauco Agent helpers (for direct service-to-service calls) +# --------------------------------------------------------------------------- + +def _osvauco_agent_identity_token() -> str: + """Hent identity token for direkte kall til osvauco-agent.""" + try: + audience = os.environ.get("OSVAUCO_AGENT_URL") + if not audience: + raise ValueError("OSVAUCO_AGENT_URL environment variable is not set.") + auth_req = google.auth.transport.requests.Request() + token = google.oauth2.id_token.fetch_id_token(auth_req, audience) + if not token: + raise ValueError("Empty token returned from fetch_id_token") + return token + except Exception as e: + logger.error(f"[SERVICE-TO-SERVICE TOKEN ERROR] {type(e).__name__}: {e}") + raise + +def _osvauco_agent_headers() -> dict: + return {"Authorization": f"Bearer {_osvauco_agent_identity_token()}", "Content-Type": "application/json"} + +async def _osvauco_agent_post(path: str, body: dict) -> Any: + """Kaller osvauco-agent direkte med service-to-service auth.""" + url = f"{os.environ.get('OSVAUCO_AGENT_URL')}{path}" + async with httpx.AsyncClient(timeout=30) as c: + r = await c.post(url, json=body, headers=_osvauco_agent_headers()) + r.raise_for_status() + return r.json() + # --------------------------------------------------------------------------- # Ollama helpers — direkte mot emma-gpu-vm @@ -195,7 +228,7 @@ async def send_email(p): return await _opax_post("/notify/email", {"to": p.get("to"), "subject": p.get("subject"), "event": p.get("event", "digest")}) async def send_sms(p): - return await _opax_post("/notify/sms", {"to": p.get("to"), "body": p.get("message", p.get("body", "")), "event": p.get("event", "spike"), "tier": p.get("tier", "guard")}) + return await _osvauco_agent_post("/notify/sms", {"to": p.get("to"), "body": p.get("message", p.get("body", "")), "event": p.get("event", "spike"), "tier": p.get("tier", "guard")}) async def get_notify_channels(p): return await _opax_get("/notify/channels") @@ -352,7 +385,7 @@ async def mcp_handler(request: Request): return JSONResponse(_jsonrpc_ok(req_id, { "protocolVersion": "2024-11-05", "capabilities": {"tools": {}}, - "serverInfo": {"name": "opax-mcp", "version": "3.1.0"}, + "serverInfo": {"name": "opax-mcp", "version": "3.2.0"}, })) if method == "tools/list": @@ -389,4 +422,4 @@ async def mcp_handler(request: Request): @app.get("/health") async def health(): - return {"status": "ok", "service": "opax-mcp", "version": "3.1.0", "ollama": OLLAMA_BASE_URL, "emma_model": EMMA_MODEL} + return {"status": "ok", "service": "opax-mcp", "version": "3.2.0", "ollama": OLLAMA_BASE_URL, "emma_model": EMMA_MODEL}