diff --git a/bot.py b/bot.py index f5b8624..6925c8a 100644 --- a/bot.py +++ b/bot.py @@ -67,7 +67,6 @@ CREDS_FILE = os.path.join(STORE_PATH, "credentials.json") LITELLM_URL = os.environ.get("LITELLM_BASE_URL", "") LITELLM_KEY = os.environ.get("LITELLM_API_KEY", "not-needed") DEFAULT_MODEL = os.environ.get("DEFAULT_MODEL", "claude-sonnet") -USER_KEYS_FILE = os.environ.get("USER_KEYS_FILE", "/data/user_keys.json") MEMORY_SERVICE_URL = os.environ.get("MEMORY_SERVICE_URL", "http://memory-service:8090") CONFLUENCE_URL = os.environ.get("CONFLUENCE_BASE_URL", "") CONFLUENCE_USER = os.environ.get("CONFLUENCE_USER", "") @@ -308,18 +307,12 @@ ATLASSIAN_NOT_CONNECTED_MSG = ( "to use Jira and Confluence features." ) -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 your documents (auto-connected via MatrixHost) -- `!ai docs connect ` — Connect with a custom document API key (optional) -- `!ai docs disconnect` — Disconnect custom document API key -- `!ai auto-rename on|off` — Auto-rename room based on conversation topic -- `!ai forget` — Delete all memories the bot has about you -- `!ai memories` — Show what the bot remembers about you -- **Translate**: Forward a message to this DM — bot detects language and offers translation -- **@mention the bot** or start with `!ai` for a regular AI response""" +HELP_TEXT = """**AI Bot** +Just write naturally — I'll respond to all messages in DMs. +In group chats, @mention me to get my attention. + +I can search your documents, translate messages, and analyze images. +Manage settings at [matrixhost.eu/settings](https://matrixhost.eu/settings).""" class DocumentRAG: @@ -875,7 +868,6 @@ class Bot: self.memory = MemoryClient(MEMORY_SERVICE_URL) self.atlassian = AtlassianClient(PORTAL_URL, BOT_API_KEY) self.llm = AsyncOpenAI(base_url=LITELLM_URL, api_key=LITELLM_KEY) if LITELLM_URL else None - self.user_keys: dict[str, str] = self._load_user_keys() # matrix_user_id -> api_key (legacy) self._documents_cache: dict[str, str | None] = {} # matrix_user_id -> connected status self.room_models: dict[str, str] = {} # room_id -> model name self.auto_rename_rooms: set[str] = set() # rooms with auto-rename enabled @@ -884,27 +876,8 @@ class Bot: 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 - self._pending_connects: dict[str, str] = {} # matrix_user_id -> device_code self._room_document_context: dict[str, list[dict]] = {} # room_id -> [{type, filename, text, timestamp}, ...] - @staticmethod - def _load_user_keys() -> dict[str, str]: - if os.path.exists(USER_KEYS_FILE): - try: - with open(USER_KEYS_FILE) as f: - return json.load(f) - except Exception: - logger.warning("Failed to load user keys file, starting fresh") - return {} - - def _save_user_keys(self): - try: - os.makedirs(os.path.dirname(USER_KEYS_FILE), exist_ok=True) - with open(USER_KEYS_FILE, "w") as f: - json.dump(self.user_keys, f) - except Exception: - logger.exception("Failed to save user keys") - async def _has_documents(self, matrix_user_id: str) -> bool: """Check if user has documents via MatrixHost portal API. @@ -1337,15 +1310,6 @@ class Bot: await self._load_room_settings(room.room_id) body = event.body.strip() - # Command handling - if body.startswith("!ai "): - cmd = body[4:].strip() - await self._handle_command(room, cmd, event) - return - if body == "!ai": - await self._send_text(room.room_id, HELP_TEXT) - return - # In DMs (2 members), respond to all messages; in groups, require @mention is_dm = room.member_count == 2 if not is_dm: @@ -1824,179 +1788,6 @@ class Bot: logger.exception("Text file decode failed") return "" - async def _handle_command(self, room, cmd: str, event=None): - if cmd == "help": - await self._send_text(room.room_id, HELP_TEXT) - - elif cmd == "docs connect" or cmd.startswith("docs connect "): - api_key = cmd[12:].strip() if cmd.startswith("docs connect ") else "" - await self._handle_connect(room, api_key, event) - - elif cmd == "docs disconnect": - await self._handle_disconnect(room, event) - - elif cmd == "models": - if not self.llm: - await self._send_text(room.room_id, "LLM not configured.") - return - try: - models = await self.llm.models.list() - names = sorted(m.id for m in models.data) - current = self.room_models.get(room.room_id, DEFAULT_MODEL) - text = "**Available models:**\n" - text += "\n".join(f"- `{n}` {'← current' if n == current else ''}" for n in names) - await self._send_text(room.room_id, text) - except Exception: - logger.exception("Failed to list models") - await self._send_text(room.room_id, "Failed to fetch model list.") - - elif cmd.startswith("set-model "): - model = cmd[10:].strip() - if not model: - await self._send_text(room.room_id, "Usage: `!ai set-model `") - return - self.room_models[room.room_id] = model - # Persist in room state for cross-restart persistence - try: - await self.client.room_put_state( - room.room_id, MODEL_STATE_TYPE, {"model": model}, state_key="", - ) - except Exception: - 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 == "forget": - sender = event.sender if event else None - if sender: - deleted = await self.memory.delete_user(sender) - await self._send_text(room.room_id, f"All my memories about you have been deleted ({deleted} facts removed).") - else: - await self._send_text(room.room_id, "Could not identify user.") - - elif cmd == "memories": - sender = event.sender if event else None - if sender: - memories = await self.memory.list_all(sender) - if memories: - text = f"**I remember {len(memories)} things about you:**\n" - text += "\n".join(f"- {m['fact']}" for m in memories) - else: - text = "I don't have any memories about you yet." - await self._send_text(room.room_id, text) - else: - await self._send_text(room.room_id, "Could not identify user.") - - elif cmd.startswith("search "): - query = cmd[7:].strip() - if not query: - await self._send_text(room.room_id, "Usage: `!ai search `") - return - sender = event.sender if event else None - user_api_key = self.user_keys.get(sender) if sender else None - has_docs = await self._has_documents(sender) if sender else False - if not has_docs: - await self._send_text(room.room_id, "Documents not available. Manage your documents at [matrixhost.eu/documents](https://matrixhost.eu/documents).") - return - results = await self.rag.search(query, top_k=5, matrix_user_id=sender) - if not results: - await self._send_text(room.room_id, "No documents found.") - return - await self._send_text(room.room_id, self.rag.format_context(results)) - - else: - # Treat unknown commands as AI prompts - if self.llm: - sender = event.sender if event else None - await self.client.room_typing(room.room_id, typing_state=True) - try: - await self._respond_with_ai(room, cmd, sender=sender) - finally: - await self.client.room_typing(room.room_id, typing_state=False) - else: - await self._send_text(room.room_id, f"Unknown command: `{cmd}`\n\n{HELP_TEXT}") - - async def _handle_connect(self, room, api_key: str, event=None): - """Handle !ai connect — SSO device flow, or !ai connect as fallback.""" - sender = event.sender if event else None - - if not self.rag.base_url: - await self._send_text(room.room_id, "Document search is not configured.") - return - - # Fallback: direct API key provided - if api_key: - # Redact the message containing the API key for security - if event: - try: - await self.client.room_redact(room.room_id, event.event_id, reason="API key redacted for security") - except Exception: - logger.debug("Could not redact connect message", exc_info=True) - - stats = await self.rag.validate_key(api_key) - if stats is None: - await self._send_text(room.room_id, "Invalid API key. Please check and try again.") - return - - self.user_keys[sender] = api_key - self._save_user_keys() - org_name = stats.get("organization", "unknown") - total = stats.get("total_documents", 0) - await self._send_text( - room.room_id, - f"Documents connected (org: **{org_name}**, {total} documents). " - f"Your documents are now searchable.", - ) - logger.info("User %s connected WildFiles key (org: %s)", sender, org_name) - return - - # Documents are managed via MatrixHost portal - if sender: - has_docs = await self._has_documents(sender) - if has_docs: - await self._send_text( - room.room_id, - "Documents are connected via your MatrixHost account. " - "Manage documents at [matrixhost.eu/documents](https://matrixhost.eu/documents).", - ) - return - - await self._send_text( - room.room_id, - "Upload documents at [matrixhost.eu/documents](https://matrixhost.eu/documents) " - "to enable AI-powered document search.", - ) - - async def _handle_disconnect(self, room, event=None): - """Handle !ai disconnect — legacy, documents managed via portal now.""" - sender = event.sender if event else None - if sender and sender in self.user_keys: - del self.user_keys[sender] - self._save_user_keys() - await self._send_text(room.room_id, "Legacy document key removed.") - logger.info("User %s removed legacy WildFiles key", sender) - else: - await self._send_text(room.room_id, "Documents are managed at [matrixhost.eu/documents](https://matrixhost.eu/documents).") - async def _brave_search(self, query: str, count: int = 5) -> str: """Call Brave Search API and return formatted results.""" if not BRAVE_API_KEY: