diff --git a/agents/core-logic/opax_mcp_client/__init__.py b/agents/core-logic/opax_mcp_client/__init__.py new file mode 100644 index 0000000..64dff59 --- /dev/null +++ b/agents/core-logic/opax_mcp_client/__init__.py @@ -0,0 +1,212 @@ +""" +opax_mcp_client — REST-klient for opax-mcp Cloud Run service. +Pakke-struktur for Vertex AI Agent Engine extra_packages-kompatibilitet. + +Auth-lag: + 1. Cloud Run IAM : Authorization: Bearer (automatisk) + 2. Tool-level : X-MCP-Secret: (frå env) + +URL: https://opax-mcp-zjbqp3prqq-uc.a.run.app +""" + +import os +import httpx +import google.auth +import google.auth.transport.requests + +OPAX_MCP_URL = os.environ.get("MCP_SERVER_URL", "https://opax-mcp-zjbqp3prqq-uc.a.run.app") +MCP_SECRET = os.environ.get("MCP_SECRET", "") + + +def _identity_token() -> str: + metadata_url = ( + "http://metadata.google.internal/computeMetadata/v1/instance" + f"/service-accounts/default/identity?audience={OPAX_MCP_URL}&format=full" + ) + try: + resp = httpx.get(metadata_url, headers={"Metadata-Flavor": "Google"}, timeout=5) + if resp.status_code == 200 and resp.text.strip(): + return resp.text.strip() + except Exception: + pass + credentials, _ = google.auth.default() + credentials.refresh(google.auth.transport.requests.Request()) + return credentials.token + + +def _headers() -> dict: + return { + "Authorization": f"Bearer {_identity_token()}", + "X-MCP-Secret": MCP_SECRET, + "Content-Type": "application/json", + } + + +def call_tool(tool: str, params: dict = None) -> dict: + resp = httpx.post( + f"{OPAX_MCP_URL}/tools/call", + headers=_headers(), + json={"tool": tool, "params": params or {}}, + timeout=30, + ) + resp.raise_for_status() + return resp.json() + + +# --------------------------------------------------------------------------- +# Billing +# --------------------------------------------------------------------------- +def get_billing_summary() -> dict: + """Hent billing-oversikt for OSVauco (GCP-kostnader, token-forbruk).""" + return call_tool("get_billing_summary") + +def get_billing_forecast() -> dict: + """Hent token-estimat og kostnadsframskriving.""" + return call_tool("get_billing_forecast") + +def get_billing_credits() -> dict: + """Hent gjenværende GCP-kreditter og burn-rate.""" + return call_tool("get_billing_credits") + +def get_billing_anomalies() -> dict: + """Sjekk for kostnadanomalier og uventede spiker.""" + return call_tool("get_billing_anomalies") + +def get_billing_history() -> dict: + """Hent historisk kostnadsdata (90 dagar).""" + return call_tool("get_billing_history") + +def get_billing_budget() -> dict: + """Hent gjeldande budsjett for OSVauco.""" + return call_tool("get_billing_budget") + +def set_billing_budget(amount: float) -> dict: + """Sett nytt budsjettgrense i NOK/USD. amount er beløpet.""" + return call_tool("set_billing_budget", {"amount": amount}) + + +# --------------------------------------------------------------------------- +# Onboarding +# --------------------------------------------------------------------------- +def create_invite(email: str, company: str = "", tier: str = "starter") -> dict: + """Opprett invite-link for ny kunde. tier: starter | guard | shield | enterprise.""" + return call_tool("create_invite", {"email": email, "company": company, "tier": tier}) + +def list_customers() -> dict: + """List alle onboardede kunder og systemtilstand.""" + return call_tool("list_customers") + + +# --------------------------------------------------------------------------- +# Notify +# --------------------------------------------------------------------------- +def send_webhook(message: str, url: str = "", title: str = "OPAX varsel") -> dict: + """Send webhook-varsling til Slack/Teams/Discord. url er valgfri override.""" + return call_tool("send_webhook", {"message": message, "url": url, "title": title}) + +def send_email(to: str, subject: str, body: str = "") -> dict: + """Send e-post via SendGrid. to er mottaker-adresse.""" + return call_tool("send_email", {"to": to, "subject": subject, "body": body}) + +def send_sms(to: str, message: str, tier: str = "guard") -> dict: + """Send SMS via Twilio. Krev Guard+-tier. to er telefonnummer med landkode.""" + return call_tool("send_sms", {"to": to, "message": message, "tier": tier}) + +def get_notify_channels() -> dict: + """List konfigurerte varslingskanalar (e-post, SMS, webhook).""" + return call_tool("get_notify_channels") + + +# --------------------------------------------------------------------------- +# Agents +# --------------------------------------------------------------------------- +def run_jason(prompt: str) -> dict: + """Kall Jason-agenten med ein prompt via OPAX /run.""" + return call_tool("run_jason", {"prompt": prompt}) + +def run_emma(prompt: str) -> dict: + """Kall Emma-agenten (Gemma lokal) med ein prompt via OPAX /emma.""" + return call_tool("run_emma", {"prompt": prompt}) + + +# --------------------------------------------------------------------------- +# Platform +# --------------------------------------------------------------------------- +def get_health() -> dict: + """Sjekk helsestatus for OPAX-plattformen.""" + return call_tool("get_health") + +def get_build_status() -> dict: + """Hent status på siste Cloud Build-kjøring.""" + return call_tool("get_build_status") + +def get_state() -> dict: + """Hent gjeldande systemtilstand og agent-statistikk for OSVauco.""" + return call_tool("get_state") + +def get_telemetry() -> dict: + """Hent token-forbruk og ytingsdata per modul.""" + return call_tool("get_telemetry") + +def run_terminal(command: str) -> dict: + """Kjør whitelist-kommando i OPAX terminal. Gyldige: health, billing, build, logs, help.""" + return call_tool("run_terminal", {"command": command}) + + +# --------------------------------------------------------------------------- +# Gitea (repo-tools) +# --------------------------------------------------------------------------- +def list_commits(limit: int = 10) -> dict: + """List siste commits i OSVauco-repoet på Gitea.""" + return call_tool("list_commits", {"limit": limit}) + +def get_file(path: str) -> dict: + """Hent innhald i ein fil frå Gitea-repoet. path er relativ til rot.""" + return call_tool("get_file", {"path": path}) + +def list_open_issues() -> dict: + """List alle opne issues i OSVauco Gitea-repoet.""" + return call_tool("list_open_issues", {}) + +def create_issue(title: str, body: str = "") -> dict: + """Opprett nytt issue i Gitea-repoet.""" + return call_tool("create_issue", {"title": title, "body": body}) + +def push_file(path: str, content: str, message: str = "", sha: str = "") -> dict: + """Push/oppdater ein fil i Gitea. sha krevst ved oppdatering av eksisterande fil.""" + return call_tool("push_file", {"path": path, "content": content, "message": message, "sha": sha}) + + +# --------------------------------------------------------------------------- +# ADK FunctionTools — for bruk i agent.py +# --------------------------------------------------------------------------- +def get_all_function_tools() -> list: + """Returner alle tools som ADK FunctionTool-liste for Jason.""" + from google.adk.tools import FunctionTool + return [ + FunctionTool(func=get_billing_summary), + FunctionTool(func=get_billing_forecast), + FunctionTool(func=get_billing_credits), + FunctionTool(func=get_billing_anomalies), + FunctionTool(func=get_billing_history), + FunctionTool(func=get_billing_budget), + FunctionTool(func=set_billing_budget), + FunctionTool(func=create_invite), + FunctionTool(func=list_customers), + FunctionTool(func=send_webhook), + FunctionTool(func=send_email), + FunctionTool(func=send_sms), + FunctionTool(func=get_notify_channels), + FunctionTool(func=run_jason), + FunctionTool(func=run_emma), + FunctionTool(func=get_health), + FunctionTool(func=get_build_status), + FunctionTool(func=get_state), + FunctionTool(func=get_telemetry), + FunctionTool(func=run_terminal), + FunctionTool(func=list_commits), + FunctionTool(func=get_file), + FunctionTool(func=list_open_issues), + FunctionTool(func=create_issue), + FunctionTool(func=push_file), + ]