feat: Add confluence_search tool to voice bot

Voice bot could read/update Confluence pages but could not search.
Users asking to search Confluence got a refusal. Now the voice bot
has search_confluence using CQL queries via the service account.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Christian Gick
2026-02-26 12:48:50 +02:00
parent a3365626ae
commit b19300d3ce

View File

@@ -51,7 +51,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 lesen und bearbeiten. Nutze read_confluence_page und update_confluence_page wenn der Nutzer Dokumente besprechen oder aendern moechte. - Du kannst Confluence-Seiten suchen, lesen und bearbeiten. Nutze search_confluence um Seiten zu finden, read_confluence_page zum Lesen und update_confluence_page zum Bearbeiten.
- 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."""
@@ -253,6 +253,31 @@ async def _store_voice_exchange(user_text: str, agent_text: str,
logger.warning("Voice memory store failed: %s", exc) logger.warning("Voice memory store failed: %s", exc)
async def _confluence_search(query: str, limit: int = 5) -> list[dict]:
"""Search Confluence pages by CQL query. Returns list of {id, title, space, url}."""
if not CONFLUENCE_URL or not CONFLUENCE_USER or not CONFLUENCE_TOKEN:
raise RuntimeError("Confluence credentials not configured")
cql = f'type=page AND (title~"{query}" OR text~"{query}")'
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
async def _confluence_read_page(page_id: str) -> tuple[str, str, int]: async def _confluence_read_page(page_id: str) -> tuple[str, str, int]:
"""Read a Confluence page and return (title, plain_text, version_number).""" """Read a Confluence page and return (title, plain_text, version_number)."""
if not CONFLUENCE_URL or not CONFLUENCE_USER or not CONFLUENCE_TOKEN: if not CONFLUENCE_URL or not CONFLUENCE_USER or not CONFLUENCE_TOKEN:
@@ -785,6 +810,24 @@ class VoiceSession:
if _conf_ids: if _conf_ids:
_active_conf_id = _conf_ids[0] _active_conf_id = _conf_ids[0]
@function_tool
async def search_confluence(query: str) -> str:
"""Search Confluence for pages matching a query. Use when user asks
to find, search, or look up documents or pages in Confluence.
Returns a list of matching pages with titles and IDs."""
logger.info("CONFLUENCE_SEARCH: query=%s", query)
try:
results = await _confluence_search(query, limit=5)
if not results:
return f"No Confluence pages found for '{query}'."
lines = [f"Found {len(results)} pages:"]
for r in results:
lines.append(f"- {r['title']} (ID: {r['id']}, Space: {r['space']})")
return "\n".join(lines)
except Exception as exc:
logger.warning("CONFLUENCE_SEARCH_FAIL: %s", exc)
return f"Search failed: {exc}"
@function_tool @function_tool
async def read_confluence_page(page_id: str = "") -> str: async def read_confluence_page(page_id: str = "") -> str:
"""Read a Confluence page. Use when user asks to read, review, """Read a Confluence page. Use when user asks to read, review,
@@ -982,7 +1025,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, read_confluence_page, update_confluence_page, think_deeper, look_at_screen], tools=[search_web, set_user_timezone, search_confluence, read_confluence_page, update_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,