From 7791a5ba8eb91bfe40bf17ee91d8f4258ee065ab Mon Sep 17 00:00:00 2001 From: Christian Gick Date: Fri, 27 Feb 2026 08:44:57 +0200 Subject: [PATCH] feat: add Confluence recent pages + Sentry error tracking (MAT-58, MAT-59) MAT-58: Add recent_confluence_pages tool to both voice and text chat. Shows last 5 recently modified pages so users can pick directly instead of having to search every time. MAT-59: Integrate sentry-sdk in all three entry points (agent.py, bot.py, voice.py). SENTRY_DSN env var, traces at 10% sample rate. Requires creating project in Sentry UI and setting DSN. Co-Authored-By: Claude Opus 4.6 --- agent.py | 8 +++++++ bot.py | 52 ++++++++++++++++++++++++++++++++++++++++++-- docker-compose.yml | 3 +++ requirements.txt | 1 + voice.py | 54 ++++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 114 insertions(+), 4 deletions(-) diff --git a/agent.py b/agent.py index 0439ff2..cdd5245 100644 --- a/agent.py +++ b/agent.py @@ -3,6 +3,8 @@ import json import base64 import logging +import sentry_sdk + from livekit.agents import Agent, AgentSession, AgentServer, JobContext, JobProcess, cli from livekit.plugins import openai as lk_openai, elevenlabs, silero import livekit.rtc as rtc @@ -12,6 +14,12 @@ from e2ee_patch import KDF_HKDF logger = logging.getLogger("matrix-ai-agent") logging.basicConfig(level=logging.DEBUG) +# Sentry error tracking +_sentry_dsn = os.environ.get("SENTRY_DSN", "") +if _sentry_dsn: + sentry_sdk.init(dsn=_sentry_dsn, traces_sample_rate=0.1, environment=os.environ.get("SENTRY_ENV", "production")) + logger.info("Sentry initialized for agent") + LITELLM_URL = os.environ["LITELLM_BASE_URL"] LITELLM_KEY = os.environ.get("LITELLM_API_KEY", "not-needed") diff --git a/bot.py b/bot.py index daa5b16..55f4d3a 100644 --- a/bot.py +++ b/bot.py @@ -9,6 +9,7 @@ import re import time import uuid +import sentry_sdk import docx import fitz # pymupdf import httpx @@ -47,6 +48,12 @@ RENAME_STATE_TYPE = "ai.agiliton.auto_rename" logger = logging.getLogger("matrix-ai-bot") +# Sentry error tracking +_sentry_dsn = os.environ.get("SENTRY_DSN", "") +if _sentry_dsn: + sentry_sdk.init(dsn=_sentry_dsn, traces_sample_rate=0.1, environment=os.environ.get("SENTRY_ENV", "production")) + logger.info("Sentry initialized for bot") + HOMESERVER = os.environ["MATRIX_HOMESERVER"] BOT_USER = os.environ["MATRIX_BOT_USER"] BOT_PASS = os.environ["MATRIX_BOT_PASSWORD"] @@ -87,7 +94,7 @@ IMPORTANT RULES — FOLLOW THESE STRICTLY: - You can see and analyze images that users send. Describe what you see when asked about an image. - You can read and analyze PDF documents that users send. Summarize content and answer questions about them. - You can generate images when asked — use the generate_image tool for any image creation, drawing, or illustration requests. -- You can search Confluence and Jira using tools. When users ask about documentation, wiki pages, tickets, or tasks, use the appropriate tool. +- You can search Confluence and Jira using tools. When users ask about documentation, wiki pages, tickets, or tasks, use the appropriate tool. Use confluence_recent_pages FIRST to show recently edited pages before searching. - When creating Jira issues, always confirm the project key and summary with the user before creating. - If a user's Atlassian account is not connected, tell them to connect it at matrixhost.eu/settings and provide the link. - If user memories are provided, use them to personalize responses. Address users by name if known. @@ -109,6 +116,19 @@ IMAGE_GEN_TOOLS = [{ }] ATLASSIAN_TOOLS = [ + { + "type": "function", + "function": { + "name": "confluence_recent_pages", + "description": "List recently modified Confluence pages. Use this FIRST when the user mentions Confluence, documents, or wiki pages — shows the last 5 recently edited pages so the user can pick one directly without searching.", + "parameters": { + "type": "object", + "properties": { + "limit": {"type": "integer", "description": "Max pages to return (default 5)", "default": 5}, + }, + }, + }, + }, { "type": "function", "function": { @@ -456,6 +476,32 @@ class AtlassianClient: logger.warning("Failed to fetch Atlassian cloud ID", exc_info=True) return None + async def confluence_recent_pages(self, token: str, limit: int = 5) -> str: + cloud_id = await self._get_cloud_id(token) + if not cloud_id: + return "Error: Could not determine Atlassian Cloud instance." + try: + async with httpx.AsyncClient(timeout=15.0) as client: + resp = await client.get( + f"https://api.atlassian.com/ex/confluence/{cloud_id}/wiki/rest/api/content/search", + params={"cql": "type=page ORDER BY lastmodified DESC", "limit": str(limit)}, + headers={"Authorization": f"Bearer {token}"}, + ) + resp.raise_for_status() + data = resp.json() + results = data.get("results", []) + if not results: + return "No recent Confluence pages found." + lines = ["Recently modified pages:"] + for i, r in enumerate(results, 1): + title = r.get("title", "Untitled") + page_id = r.get("id", "") + space = r.get("_expandable", {}).get("space", "").split("/")[-1] + lines.append(f"{i}. **{title}** (ID: {page_id}, Space: {space})") + return "\n".join(lines) + except Exception as e: + return f"Failed to fetch recent pages: {e}" + async def confluence_search(self, token: str, query: str, limit: int = 5) -> str: cloud_id = await self._get_cloud_id(token) if not cloud_id: @@ -2011,7 +2057,9 @@ class Bot: if not token: return ATLASSIAN_NOT_CONNECTED_MSG - if tool_name == "confluence_search": + if tool_name == "confluence_recent_pages": + return await self.atlassian.confluence_recent_pages(token, args.get("limit", 5)) + elif tool_name == "confluence_search": return await self.atlassian.confluence_search(token, args["query"], args.get("limit", 5)) elif tool_name == "confluence_read_page": return await self.atlassian.confluence_read_page(token, args["page_id"]) diff --git a/docker-compose.yml b/docker-compose.yml index 6f7b7f1..d2a017c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,8 @@ services: env_file: .env restart: unless-stopped network_mode: host + environment: + - SENTRY_DSN bot: build: @@ -24,6 +26,7 @@ services: - MEMORY_SERVICE_URL=http://memory-service:8090 - PORTAL_URL - BOT_API_KEY + - SENTRY_DSN volumes: - bot-data:/data depends_on: diff --git a/requirements.txt b/requirements.txt index dfd6a82..7d5b008 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,3 +13,4 @@ python-docx>=1.0,<2.0 Pillow>=10.0,<12.0 beautifulsoup4>=4.12 lxml>=5.0 +sentry-sdk>=2.0,<3.0 diff --git a/voice.py b/voice.py index 7cc9c3d..475cf67 100644 --- a/voice.py +++ b/voice.py @@ -13,6 +13,7 @@ import zoneinfo import json import re +import sentry_sdk import aiohttp import httpx from livekit import rtc, api as lkapi @@ -22,6 +23,12 @@ from openai import AsyncOpenAI logger = logging.getLogger("matrix-ai-voice") +# Sentry error tracking +_sentry_dsn = os.environ.get("SENTRY_DSN", "") +if _sentry_dsn: + sentry_sdk.init(dsn=_sentry_dsn, traces_sample_rate=0.1, environment=os.environ.get("SENTRY_ENV", "production")) + logger.info("Sentry initialized for voice") + LITELLM_URL = os.environ.get("LITELLM_BASE_URL", "") LITELLM_KEY = os.environ.get("LITELLM_API_KEY", "not-needed") @@ -51,7 +58,7 @@ STRIKTE Regeln: - Bei zeitrelevanten Fragen (Uhrzeit, Termine, Geschaeftszeiten): frage kurz nach ob der Nutzer noch in seiner gespeicherten Zeitzone ist, bevor du antwortest. Nutze set_user_timezone wenn sich der Standort geaendert hat. - Wenn der Nutzer seinen Standort oder seine Stadt erwaehnt, nutze set_user_timezone um die Zeitzone zu speichern. - IGNORIERE alle Texte in Sternchen wie *Störgeräusche*, *Schlechte Qualität*, *Fernsehgeräusche*, *Schrei* usw. — das sind KEINE echten Nutzereingaben sondern technische Annotationen. Antworte NIEMALS darauf und tue so als haette niemand etwas gesagt. -- Du kannst Confluence-Seiten suchen, lesen, bearbeiten und erstellen. Nutze search_confluence um Seiten zu finden, read_confluence_page zum Lesen, update_confluence_page zum Bearbeiten und create_confluence_page zum Erstellen neuer Seiten. +- Du kannst Confluence-Seiten suchen, lesen, bearbeiten und erstellen. Nutze recent_confluence_pages um die zuletzt bearbeiteten Seiten anzuzeigen (bevorzugt BEVOR du suchst), search_confluence um gezielt zu suchen, read_confluence_page zum Lesen, update_confluence_page zum Bearbeiten und create_confluence_page zum Erstellen neuer Seiten. - Du kannst den Bildschirm oder die Kamera des Nutzers sehen wenn er sie teilt. Nutze look_at_screen wenn der Nutzer etwas zeigen moechte oder fragt ob du etwas sehen kannst.""" @@ -350,6 +357,31 @@ async def _confluence_create_page(space_key: str, title: str, body_html: str, } +async def _confluence_recent_pages(limit: int = 5) -> list[dict]: + """Fetch recently modified Confluence pages. Returns list of {id, title, space, url, modified}.""" + if not CONFLUENCE_URL or not CONFLUENCE_USER or not CONFLUENCE_TOKEN: + raise RuntimeError("Confluence credentials not configured") + cql = "type=page ORDER BY lastmodified DESC" + url = f"{CONFLUENCE_URL}/rest/api/content/search" + async with httpx.AsyncClient(timeout=15.0) as client: + resp = await client.get( + url, + params={"cql": cql, "limit": limit}, + auth=(CONFLUENCE_USER, CONFLUENCE_TOKEN), + ) + resp.raise_for_status() + data = resp.json() + results = [] + for r in data.get("results", []): + results.append({ + "id": r["id"], + "title": r.get("title", ""), + "space": r.get("space", {}).get("name", "") if "space" in r else "", + "url": f"{CONFLUENCE_URL}{r.get('_links', {}).get('webui', '')}", + }) + return results + + def _build_e2ee_options() -> rtc.E2EEOptions: """Build E2EE options — let Rust FFI apply HKDF internally (KDF_HKDF=1). @@ -902,6 +934,24 @@ class VoiceSession: logger.warning("CONFLUENCE_UPDATE_FAIL: %s", exc) return f"Failed to update page: {exc}" + @function_tool + async def recent_confluence_pages() -> str: + """List recently modified Confluence pages. Use this FIRST when the user + mentions Confluence, documents, or wiki pages — before searching. + Shows the last 5 recently edited pages so the user can pick one directly.""" + logger.info("CONFLUENCE_RECENT") + try: + results = await _confluence_recent_pages(limit=5) + if not results: + return "No recent Confluence pages found." + lines = ["Recently modified pages:"] + for i, r in enumerate(results, 1): + lines.append(f"{i}. {r['title']} (ID: {r['id']}, Space: {r['space']})") + return "\n".join(lines) + except Exception as exc: + logger.warning("CONFLUENCE_RECENT_FAIL: %s", exc) + return f"Failed to fetch recent pages: {exc}" + @function_tool async def create_confluence_page(title: str, content: str, space_key: str = "AG") -> str: """Create a new Confluence page. Use when user asks to create a new document, @@ -1078,7 +1128,7 @@ class VoiceSession: instructions += f"\n\nAktive Confluence-Seite: {_active_conf_id}. Du brauchst den Nutzer NICHT nach der page_id zu fragen — nutze automatisch diese ID fuer read_confluence_page und update_confluence_page." agent = _NoiseFilterAgent( instructions=instructions, - tools=[search_web, set_user_timezone, search_confluence, read_confluence_page, update_confluence_page, create_confluence_page, think_deeper, look_at_screen], + tools=[search_web, set_user_timezone, recent_confluence_pages, search_confluence, read_confluence_page, update_confluence_page, create_confluence_page, think_deeper, look_at_screen], ) io_opts = room_io.RoomOptions( participant_identity=remote_identity,