chore: oppdater opax-mcp server.py og yaml

This commit is contained in:
Chris Christiansen 2026-07-02 16:15:03 +00:00
parent 371965e2f4
commit 2a9138b290
3 changed files with 241 additions and 5 deletions

View File

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

96
opax-mcp.yaml Normal file
View File

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

View File

@ -9,6 +9,7 @@ import httpx
import base64 import base64
import google.auth import google.auth
import google.auth.transport.requests import google.auth.transport.requests
import google.oauth2.id_token
from fastapi import FastAPI, Request, HTTPException from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from typing import Any, Optional from typing import Any, Optional
@ -17,9 +18,10 @@ import logging
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) 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") 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", "") OPAX_IAP_CLIENT_ID = os.environ.get("OPAX_IAP_CLIENT_ID", "")
MCP_SECRET = os.environ.get("MCP_SECRET", "") MCP_SECRET = os.environ.get("MCP_SECRET", "")
GITEA_URL = os.environ.get("GITEA_URL", "http://34.59.131.162:3000") 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") 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"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"OLLAMA_BASE_URL: {OLLAMA_BASE_URL}")
logger.info(f"EMMA_MODEL: {EMMA_MODEL}") 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: def _opax_identity_token() -> str:
@ -93,6 +96,36 @@ async def _opax_post(path: str, body: dict) -> Any:
r.raise_for_status() r.raise_for_status()
return r.json() 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 # 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")}) return await _opax_post("/notify/email", {"to": p.get("to"), "subject": p.get("subject"), "event": p.get("event", "digest")})
async def send_sms(p): 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") 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, { return JSONResponse(_jsonrpc_ok(req_id, {
"protocolVersion": "2024-11-05", "protocolVersion": "2024-11-05",
"capabilities": {"tools": {}}, "capabilities": {"tools": {}},
"serverInfo": {"name": "opax-mcp", "version": "3.1.0"}, "serverInfo": {"name": "opax-mcp", "version": "3.2.0"},
})) }))
if method == "tools/list": if method == "tools/list":
@ -389,4 +422,4 @@ async def mcp_handler(request: Request):
@app.get("/health") @app.get("/health")
async def 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}