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 <noreply@anthropic.com>
This commit is contained in:
8
agent.py
8
agent.py
@@ -3,6 +3,8 @@ import json
|
|||||||
import base64
|
import base64
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import sentry_sdk
|
||||||
|
|
||||||
from livekit.agents import Agent, AgentSession, AgentServer, JobContext, JobProcess, cli
|
from livekit.agents import Agent, AgentSession, AgentServer, JobContext, JobProcess, cli
|
||||||
from livekit.plugins import openai as lk_openai, elevenlabs, silero
|
from livekit.plugins import openai as lk_openai, elevenlabs, silero
|
||||||
import livekit.rtc as rtc
|
import livekit.rtc as rtc
|
||||||
@@ -12,6 +14,12 @@ from e2ee_patch import KDF_HKDF
|
|||||||
logger = logging.getLogger("matrix-ai-agent")
|
logger = logging.getLogger("matrix-ai-agent")
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
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_URL = os.environ["LITELLM_BASE_URL"]
|
||||||
LITELLM_KEY = os.environ.get("LITELLM_API_KEY", "not-needed")
|
LITELLM_KEY = os.environ.get("LITELLM_API_KEY", "not-needed")
|
||||||
|
|
||||||
|
|||||||
52
bot.py
52
bot.py
@@ -9,6 +9,7 @@ import re
|
|||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
import sentry_sdk
|
||||||
import docx
|
import docx
|
||||||
import fitz # pymupdf
|
import fitz # pymupdf
|
||||||
import httpx
|
import httpx
|
||||||
@@ -47,6 +48,12 @@ RENAME_STATE_TYPE = "ai.agiliton.auto_rename"
|
|||||||
|
|
||||||
logger = logging.getLogger("matrix-ai-bot")
|
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"]
|
HOMESERVER = os.environ["MATRIX_HOMESERVER"]
|
||||||
BOT_USER = os.environ["MATRIX_BOT_USER"]
|
BOT_USER = os.environ["MATRIX_BOT_USER"]
|
||||||
BOT_PASS = os.environ["MATRIX_BOT_PASSWORD"]
|
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 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 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 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.
|
- 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 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.
|
- 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 = [
|
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",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
@@ -456,6 +476,32 @@ class AtlassianClient:
|
|||||||
logger.warning("Failed to fetch Atlassian cloud ID", exc_info=True)
|
logger.warning("Failed to fetch Atlassian cloud ID", exc_info=True)
|
||||||
return None
|
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:
|
async def confluence_search(self, token: str, query: str, limit: int = 5) -> str:
|
||||||
cloud_id = await self._get_cloud_id(token)
|
cloud_id = await self._get_cloud_id(token)
|
||||||
if not cloud_id:
|
if not cloud_id:
|
||||||
@@ -2011,7 +2057,9 @@ class Bot:
|
|||||||
if not token:
|
if not token:
|
||||||
return ATLASSIAN_NOT_CONNECTED_MSG
|
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))
|
return await self.atlassian.confluence_search(token, args["query"], args.get("limit", 5))
|
||||||
elif tool_name == "confluence_read_page":
|
elif tool_name == "confluence_read_page":
|
||||||
return await self.atlassian.confluence_read_page(token, args["page_id"])
|
return await self.atlassian.confluence_read_page(token, args["page_id"])
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ services:
|
|||||||
env_file: .env
|
env_file: .env
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
network_mode: host
|
network_mode: host
|
||||||
|
environment:
|
||||||
|
- SENTRY_DSN
|
||||||
|
|
||||||
bot:
|
bot:
|
||||||
build:
|
build:
|
||||||
@@ -24,6 +26,7 @@ services:
|
|||||||
- MEMORY_SERVICE_URL=http://memory-service:8090
|
- MEMORY_SERVICE_URL=http://memory-service:8090
|
||||||
- PORTAL_URL
|
- PORTAL_URL
|
||||||
- BOT_API_KEY
|
- BOT_API_KEY
|
||||||
|
- SENTRY_DSN
|
||||||
volumes:
|
volumes:
|
||||||
- bot-data:/data
|
- bot-data:/data
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -13,3 +13,4 @@ python-docx>=1.0,<2.0
|
|||||||
Pillow>=10.0,<12.0
|
Pillow>=10.0,<12.0
|
||||||
beautifulsoup4>=4.12
|
beautifulsoup4>=4.12
|
||||||
lxml>=5.0
|
lxml>=5.0
|
||||||
|
sentry-sdk>=2.0,<3.0
|
||||||
|
|||||||
54
voice.py
54
voice.py
@@ -13,6 +13,7 @@ import zoneinfo
|
|||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
import sentry_sdk
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import httpx
|
import httpx
|
||||||
from livekit import rtc, api as lkapi
|
from livekit import rtc, api as lkapi
|
||||||
@@ -22,6 +23,12 @@ from openai import AsyncOpenAI
|
|||||||
|
|
||||||
logger = logging.getLogger("matrix-ai-voice")
|
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_URL = os.environ.get("LITELLM_BASE_URL", "")
|
||||||
LITELLM_KEY = os.environ.get("LITELLM_API_KEY", "not-needed")
|
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.
|
- 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.
|
- 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.
|
- 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."""
|
- 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:
|
def _build_e2ee_options() -> rtc.E2EEOptions:
|
||||||
"""Build E2EE options — let Rust FFI apply HKDF internally (KDF_HKDF=1).
|
"""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)
|
logger.warning("CONFLUENCE_UPDATE_FAIL: %s", exc)
|
||||||
return f"Failed to update page: {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
|
@function_tool
|
||||||
async def create_confluence_page(title: str, content: str, space_key: str = "AG") -> str:
|
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,
|
"""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."
|
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(
|
agent = _NoiseFilterAgent(
|
||||||
instructions=instructions,
|
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(
|
io_opts = room_io.RoomOptions(
|
||||||
participant_identity=remote_identity,
|
participant_identity=remote_identity,
|
||||||
|
|||||||
Reference in New Issue
Block a user