chore: oppdater opax-mcp server.py og yaml
This commit is contained in:
parent
371965e2f4
commit
2a9138b290
|
|
@ -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
96
opax-mcp.yaml
Normal 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
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user