diff --git a/bot.py b/bot.py index b65f74c..5ced1a0 100644 --- a/bot.py +++ b/bot.py @@ -31,6 +31,7 @@ from livekit import api BOT_DEVICE_ID = "AIBOT" CALL_MEMBER_TYPE = "org.matrix.msc3401.call.member" MODEL_STATE_TYPE = "ai.agiliton.model" +RENAME_STATE_TYPE = "ai.agiliton.auto_rename" logger = logging.getLogger("matrix-ai-bot") @@ -52,13 +53,23 @@ WILDFILES_ORG = os.environ.get("WILDFILES_ORG", "") SYSTEM_PROMPT = """You are a helpful AI assistant in a Matrix chat room. Keep answers concise but thorough. Use markdown formatting when helpful. -If document context is provided, use it to inform your answers.""" + +IMPORTANT RULES — FOLLOW THESE STRICTLY: +- When document context is provided below, use it to answer. Always include any links. +- NEVER tell the user to run commands or type anything special. No commands exist. +- NEVER mention "!ai", "!ai search", "!ai read", or any slash/bang commands. +- NEVER say you cannot access files, documents, or links. +- NEVER ask the user where documents are stored, how they were uploaded, or under what filename. +- NEVER suggest contacting an administrator, using a web interface, or checking another system. +- NEVER ask follow-up questions about document storage or file locations. +- If no relevant documents were found, simply say you don't have information on that topic and ask if you can help with something else. Do NOT speculate about why or suggest the user look elsewhere.""" HELP_TEXT = """**AI Bot Commands** - `!ai help` — Show this help - `!ai models` — List available models - `!ai set-model ` — Set model for this room - `!ai search ` — Search documents (WildFiles) +- `!ai auto-rename on|off` — Auto-rename room based on conversation topic - **@mention the bot** or start with `!ai` for a regular AI response""" @@ -77,7 +88,7 @@ class DocumentRAG: async with httpx.AsyncClient(timeout=5.0) as client: resp = await client.post( f"{self.base_url}/api/v1/rag/search", - json={"query": query, "org": self.org, "top_k": top_k}, + json={"query": query, "organization": self.org, "limit": top_k}, ) resp.raise_for_status() return resp.json().get("results", []) @@ -88,11 +99,24 @@ class DocumentRAG: def format_context(self, results: list[dict]) -> str: if not results: return "" - parts = ["**Relevant documents:**"] + parts = ["The following documents were found in our document archive:"] for r in results: + doc_id = r.get("id", "") title = r.get("title", r.get("filename", "Untitled")) - snippet = r.get("content", r.get("text", ""))[:500] - parts.append(f"- **{title}**: {snippet}") + filename = r.get("metadata", {}).get("original_filename", "") + category = r.get("category", "") + date = r.get("detected_date", "") + link = r.get("metadata", {}).get("source_url", "") + parts.append(f"- Title: {title}") + if filename: + parts.append(f" Filename: {filename}") + if category: + parts.append(f" Category: {category}") + if date: + parts.append(f" Date: {date}") + if link: + parts.append(f" Link: {link}") + parts.append("\nUse this information to answer the user. Always include document links when referencing documents.") return "\n".join(parts) @@ -116,6 +140,9 @@ class Bot: self.rag = DocumentRAG(WILDFILES_BASE_URL, WILDFILES_ORG) self.llm = AsyncOpenAI(base_url=LITELLM_URL, api_key=LITELLM_KEY) if LITELLM_URL else None self.room_models: dict[str, str] = {} # room_id -> model name + self.auto_rename_rooms: set[str] = set() # rooms with auto-rename enabled + self.renamed_rooms: set[str] = set() # rooms already renamed this session + self._loaded_rooms: set[str] = set() # rooms where we've loaded state self._sync_token_received = False self._verifications: dict[str, dict] = {} # txn_id -> verification state @@ -269,6 +296,26 @@ class Bot: except Exception: logger.exception("Failed to leave call in %s", room_id) + async def _load_room_settings(self, room_id: str): + """Load persisted model and auto-rename settings from room state.""" + if room_id in self._loaded_rooms: + return + self._loaded_rooms.add(room_id) + for state_type, target in [ + (MODEL_STATE_TYPE, "model"), + (RENAME_STATE_TYPE, "rename"), + ]: + try: + resp = await self.client.room_get_state_event(room_id, state_type, "") + if hasattr(resp, "content"): + content = resp.content + if target == "model" and "model" in content: + self.room_models[room_id] = content["model"] + elif target == "rename" and content.get("enabled"): + self.auto_rename_rooms.add(room_id) + except Exception: + pass # State event doesn't exist yet + async def on_text_message(self, room, event: RoomMessageText): """Handle text messages: commands and AI responses.""" if event.sender == BOT_USER: @@ -280,6 +327,7 @@ class Bot: if time.time() - server_ts > 30: return + await self._load_room_settings(room.room_id) body = event.body.strip() # Command handling @@ -347,6 +395,26 @@ class Bot: logger.debug("Could not persist model to room state", exc_info=True) await self._send_text(room.room_id, f"Model set to `{model}` for this room.") + elif cmd.startswith("auto-rename "): + setting = cmd[12:].strip().lower() + if setting not in ("on", "off"): + await self._send_text(room.room_id, "Usage: `!ai auto-rename on|off`") + return + enabled = setting == "on" + if enabled: + self.auto_rename_rooms.add(room.room_id) + else: + self.auto_rename_rooms.discard(room.room_id) + try: + await self.client.room_put_state( + room.room_id, RENAME_STATE_TYPE, + {"enabled": enabled}, state_key="", + ) + except Exception: + logger.debug("Could not persist auto-rename to room state", exc_info=True) + status = "enabled" if enabled else "disabled" + await self._send_text(room.room_id, f"Auto-rename **{status}** for this room.") + elif cmd.startswith("search "): query = cmd[7:].strip() if not query: @@ -379,7 +447,10 @@ class Bot: doc_results = await self.rag.search(user_message) doc_context = self.rag.format_context(doc_results) if doc_context: + logger.info("RAG found %d docs for: %s", len(doc_results), user_message[:50]) messages.append({"role": "system", "content": doc_context}) + else: + logger.info("RAG found 0 docs for: %s", user_message[:50]) # Fetch last N messages from room via API try: @@ -406,10 +477,43 @@ class Bot: ) reply = resp.choices[0].message.content await self._send_text(room.room_id, reply) + # Auto-rename room after first AI response + if (room.room_id in self.auto_rename_rooms + and room.room_id not in self.renamed_rooms): + await self._auto_rename_room(room, user_message, reply) except Exception: logger.exception("LLM call failed") await self._send_text(room.room_id, "Sorry, I couldn't generate a response.") + async def _auto_rename_room(self, room, user_message: str, ai_reply: str): + """Generate a short topic title and set it as the room name.""" + try: + resp = await self.llm.chat.completions.create( + model=self.room_models.get(room.room_id, DEFAULT_MODEL), + messages=[ + {"role": "system", "content": ( + "Generate a very short room title (3-6 words, no quotes) " + "that captures the topic of this conversation. " + "Reply with ONLY the title, nothing else." + )}, + {"role": "user", "content": user_message}, + {"role": "assistant", "content": ai_reply[:200]}, + {"role": "user", "content": "What is a good short title for this conversation?"}, + ], + max_tokens=30, + ) + title = resp.choices[0].message.content.strip().strip('"\'') + if not title or len(title) > 80: + return + await self.client.room_put_state( + room.room_id, "m.room.name", + {"name": title}, state_key="", + ) + self.renamed_rooms.add(room.room_id) + logger.info("Auto-renamed room %s to: %s", room.room_id, title) + except Exception: + logger.debug("Auto-rename failed", exc_info=True) + @staticmethod def _md_to_html(text: str) -> str: """Minimal markdown to HTML for Matrix formatted_body."""