From fb578fbf40fe291c82d3b5091e768a7cb011bbd9 Mon Sep 17 00:00:00 2001 From: Joao Figueiredo Date: Sat, 3 May 2025 01:56:55 +0100 Subject: [PATCH 1/6] WIP: moving to services, refining matrix_service --- .env.example | 10 +- ai_service/Dockerfile | 12 ++ ai_service/main.py | 45 ++++++ ai_service/requirements.txt | 6 + docker-compose.yml | 30 +++- main.py | 129 ----------------- Dockerfile => matrix_service/Dockerfile | 10 +- matrix_service/main.py | 133 ++++++++++++++++++ .../requirements.txt | 0 9 files changed, 233 insertions(+), 142 deletions(-) create mode 100644 ai_service/Dockerfile create mode 100644 ai_service/main.py create mode 100644 ai_service/requirements.txt delete mode 100644 main.py rename Dockerfile => matrix_service/Dockerfile (58%) create mode 100644 matrix_service/main.py rename requirements.txt => matrix_service/requirements.txt (100%) diff --git a/.env.example b/.env.example index 7280ffe..9f7fed6 100644 --- a/.env.example +++ b/.env.example @@ -2,9 +2,13 @@ LOG_LEVEL=INFO # Matrix Configuration -HOMESERVER_URL = "https://matrix.org" -USER_ID = "@botbot_user:matrix.org" -PASSWORD = "botbot_password" +MATRIX_HOMESERVER_URL = "https://matrix.org" +MATRIX_USER_ID = "@botbot_user:matrix.org" +MATRIX_PASSWORD = "botbot_password" +MATRIX_LOGIN_TRIES = 5 # Number of login attempts before giving up, default is 5 +MATRIX_LOGIN_DELAY_INCREMENT = 5 # Delay increment,in seconds, between login attempts, default is 5 +AI_HANDLER_URL = "http://ai_service:8000" # OpenAI API Key OPENAI_API_KEY=your_openai_api_key_here +AI_HANDLER_TOKEN=common_token_here diff --git a/ai_service/Dockerfile b/ai_service/Dockerfile new file mode 100644 index 0000000..1d3d01f --- /dev/null +++ b/ai_service/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY requirements.txt ./ + +RUN pip install --no-cache-dir -r requirements.txt + +COPY main.py ./ + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] + diff --git a/ai_service/main.py b/ai_service/main.py new file mode 100644 index 0000000..f785b1c --- /dev/null +++ b/ai_service/main.py @@ -0,0 +1,45 @@ +import os +from dotenv import load_dotenv +from fastapi import FastAPI, Header, HTTPException +from pydantic import BaseModel +import openai +import redis + +AI_TOKEN = os.environ["AI_HANDLER_TOKEN"] +openai.api_key = os.environ["OPENAI_API_KEY"] +r = redis.Redis.from_url(os.environ.get("REDIS_URL", "redis://redis:6379")) + +class MessagePayload(BaseModel): + roomId: str + userId: str + content: str + eventId: str + timestamp: int + +app = FastAPI() + +@app.post("/api/v1/message") +async def message( + payload: MessagePayload, + authorization: str = Header(None) +): + if authorization != f"Bearer {AI_TOKEN}": + raise HTTPException(status_code=401, detail="Unauthorized") + + # Idempotency: ignore duplicates + if r.get(payload.eventId): + return {"reply": r.get(payload.eventId).decode()} + + # Build prompt (very simple example) + prompt = f"User {payload.userId} said: {payload.content}\nBot:" + resp = openai.Completion.create( + model="text-davinci-003", + prompt=prompt, + max_tokens=150 + ) + reply = resp.choices[0].text.strip() + + # Cache reply for idempotency + r.set(payload.eventId, reply, ex=3600) + + return {"reply": reply} diff --git a/ai_service/requirements.txt b/ai_service/requirements.txt new file mode 100644 index 0000000..b481e28 --- /dev/null +++ b/ai_service/requirements.txt @@ -0,0 +1,6 @@ +python-dotenv>=1.0.0 +openai +fastapi>=0.95 +uvicorn>=0.22 +redis>=4.5 +pydantic>=1.10 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 4a8fdf8..c7221a2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,11 +1,29 @@ services: - botbot: - build: . - env_file: - - .env + matrix_service: + build: ./matrix_service + environment: + - MATRIX_HOMESERVER_URL + - MATRIX_USER_ID + - MATRIX_PASSWORD + - AI_HANDLER_URL + - AI_HANDLER_TOKEN volumes: - - ./:/app # Mount source for hot-reload - - matrix_data:/app/data # Persist Matrix client store and tokens + - ./matrix_service:/app + - matrix_data:/app/data + depends_on: + - ai_service + + ai_service: + build: ./ai_service + environment: + - OPENAI_API_KEY=${OPENAI_API_KEY} + - AI_HANDLER_TOKEN=${AI_HANDLER_TOKEN} + - REDIS_URL=redis://redis:6379 + depends_on: + - redis + + redis: + image: redis:7 restart: unless-stopped volumes: diff --git a/main.py b/main.py deleted file mode 100644 index d500ca2..0000000 --- a/main.py +++ /dev/null @@ -1,129 +0,0 @@ -import os -import asyncio -import logging -from dotenv import load_dotenv -from nio import AsyncClient, AsyncClientConfig, MatrixRoom, RoomMessageText, InviteMemberEvent -from nio.responses import LoginResponse -from openai import AsyncOpenAI - -# --- Load environment variables --- -load_dotenv() -HOMESERVER_URL = os.getenv("HOMESERVER_URL") -USER_ID = os.getenv("USER_ID") -PASSWORD = os.getenv("PASSWORD") -LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper() -OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") - -if not OPENAI_API_KEY: - raise RuntimeError("OPENAI_API_KEY is not set in environment") - -# --- Initialize Async OpenAI client --- -openai_client = AsyncOpenAI(api_key=OPENAI_API_KEY) - -# --- Logging Setup --- -numeric_level = getattr(logging, LOG_LEVEL, logging.INFO) -logging.basicConfig( - level=numeric_level, - format="%(asctime)s %(levelname)s %(name)s: %(message)s" -) -logger = logging.getLogger(__name__) - -async def trust_all_devices(client) -> None: - """ - Programmatically verify all devices to allow sharing encryption keys. - """ - for room_id in client.rooms: - try: - devices = await client.room_devices(room_id) - if isinstance(devices, dict): - for user, dev_ids in devices.items(): - if user == USER_ID: - continue - for dev_id in dev_ids: - device = client.crypto.device_store.get_device(user, dev_id) - if device and not client.crypto.device_store.is_device_verified(device): - logger.info(f"Trusting {dev_id} for {user}") - client.verify_device(device) - except Exception: - logger.exception(f"Error trusting devices in {room_id}") - -async def message_callback(room: MatrixRoom, event: RoomMessageText): - """Handle incoming text messages.""" - if event.sender == USER_ID: - return - body = event.body.strip() - lower = body.lower() - logger.info("Received '%s' from %s in %s", body, event.sender, room.display_name) - - send_kwargs = { - "room_id": room.room_id, - "message_type": "m.room.message", - "ignore_unverified_devices": True - } - - # Simple ping - if lower == "!ping": - await client.room_send(**send_kwargs, content={"msgtype": "m.text", "body": "Pong!"}) - return - - # Ask OpenAI via chat completion - if lower.startswith("!ask "): - question = body[5:].strip() - if not question: - await client.room_send(**send_kwargs, content={"msgtype": "m.text", "body": "Provide a question after !ask."}) - return - logger.info("Querying OpenAI: %s", question) - try: - response = await openai_client.chat.completions.create( - model="gpt-3.5-turbo", - messages=[ - {"role": "system", "content": "You are a helpful assistant."}, - {"role": "user", "content": question} - ], - max_tokens=150, - timeout=300 - ) - answer = response.choices[0].message.content.strip() - except Exception: - logger.exception("OpenAI API error") - answer = "Sorry, I encountered an error contacting the AI service." - await client.room_send(**send_kwargs, content={"msgtype": "m.text", "body": answer}) - return - - # Greeting - if lower == "hello botbot": - await client.room_send(**send_kwargs, content={"msgtype": "m.text", "body": "Hello! How can I assist you today?"}) - -async def main() -> None: - """Initialize and run the Matrix bot.""" - global client - config = AsyncClientConfig(store_sync_tokens=True, encryption_enabled=True) - client = AsyncClient(HOMESERVER_URL, USER_ID, store_path="/app/data", config=config) - - login_resp = await client.login(password=PASSWORD) - if isinstance(login_resp, LoginResponse): - logger.info("Logged in as %s", USER_ID) - else: - logger.error("Login failed: %s", login_resp) - return - - await trust_all_devices(client) - - # Auto-join and trust - async def on_invite(room, event): - if isinstance(event, InviteMemberEvent): - await client.join(room.room_id) - logger.info("Joined %s", room.room_id) - await trust_all_devices(client) - client.add_event_callback(on_invite, InviteMemberEvent) - client.add_event_callback(message_callback, RoomMessageText) - - logger.info("Starting sync loop") - await client.sync_forever(timeout=30000) - -if __name__ == "__main__": - try: - asyncio.run(main()) - except KeyboardInterrupt: - logger.info("Shutting down") - asyncio.run(client.close()) diff --git a/Dockerfile b/matrix_service/Dockerfile similarity index 58% rename from Dockerfile rename to matrix_service/Dockerfile index 8713cae..1c25f4e 100644 --- a/Dockerfile +++ b/matrix_service/Dockerfile @@ -4,10 +4,12 @@ FROM python:3.11-slim WORKDIR /app # Install system dependencies -RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - libolm-dev build-essential python3-dev && \ - rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y --no-install-recommends \ + libolm-dev \ + build-essential \ + python3-dev \ + && \ + apt-get clean && rm -rf /var/lib/apt/lists/* # Install dependencies COPY requirements.txt . diff --git a/matrix_service/main.py b/matrix_service/main.py new file mode 100644 index 0000000..fdefdc6 --- /dev/null +++ b/matrix_service/main.py @@ -0,0 +1,133 @@ +import os +import logging +from dotenv import load_dotenv + +import asyncio +import httpx + +from nio import AsyncClient, AsyncClientConfig, MatrixRoom, RoomMessageText, InviteMemberEvent +from nio.responses import LoginResponse + + +# --- Load environment variables --- +load_dotenv() +MATRIX_HOMESERVER_URL = os.getenv("MATRIX_HOMESERVER_URL") +MATRIX_USER_ID = os.getenv("MATRIX_USER_ID") +MATRIX_PASSWORD = os.getenv("MATRIX_PASSWORD") +MATRIX_LOGIN_TRIES = int(os.getenv("MATRIX_LOGIN_TRIES", 5)) +MATRIX_LOGIN_DELAY_INCREMENT = int(os.getenv("MATRIX_LOGIN_DELAY_INCREMENT", 5)) +LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper() +AI_URL = os.getenv("AI_URL") +AI_TOKEN = os.getenv("AI_TOKEN") + + + +# --- Logging Setup --- +numeric_level = getattr(logging, LOG_LEVEL, logging.INFO) +logging.basicConfig( + level=numeric_level, + format="%(asctime)s %(levelname)s %(name)s: %(message)s" +) +logger = logging.getLogger(__name__) + + +async def main() -> None: + + async def trust_all_devices(client) -> None: + """ + Programmatically verify all devices to allow sharing encryption keys. + """ + for room_id in client.rooms: + try: + devices = await client.room_devices(room_id) + if isinstance(devices, dict): + for user, dev_ids in devices.items(): + if user == USER_ID: + continue + for dev_id in dev_ids: + device = client.crypto.device_store.get_device(user, dev_id) + if device and not client.crypto.device_store.is_device_verified(device): + logger.info(f"Trusting {dev_id} for {user}") + client.verify_device(device) + except Exception: + logger.exception(f"Error trusting devices in {room_id}") + + async def on_invite(room, event): + """ + Handle an invite event by joining the room and trusting all devices. + """ + if isinstance(event, InviteMemberEvent): + await client.join(room.room_id) + logger.info("Joined %s", room.room_id) + await trust_all_devices(client) + + async def on_message(room: MatrixRoom, event: RoomMessageText) -> None: + """ + Handle incoming messages. + """ + + # Check if the message is from the bot itself + if event.sender == client.user_id: + return + + logger.info("Received '%s' from %s in %s", event.body.strip(), event.sender, room.display_name) + + + if isinstance(event, RoomMessageText): + logger.info(f"Received message in {room.room_id}: {event.body}") + payload = { + "roomId": event["room_id"], + "userId": event["sender"], + "content": event["content"]["body"], + "eventId": event["event_id"], + "timestamp": event["origin_server_ts"] + } + headers = {"Authorization": f"Bearer {AI_TOKEN}"} + async with httpx.AsyncClient() as http: + resp = await http.post(f"{AI_URL}/api/v1/message", json=payload, headers=headers) + resp.raise_for_status() + data = resp.json() + if data.get("reply"): + client.send_message(event["room_id"], data["reply"]) + + # --- Initialize the client --- + # Create the data directory if it doesn't exist + try: + os.makedirs("/app/data", exist_ok=True) + except Exception as e: + logger.error(f"Error creating data directory: {e}") + return + + # Initialize the client + config = AsyncClientConfig(store_sync_tokens=True, encryption_enabled=True) + client = AsyncClient(MATRIX_HOMESERVER_URL, MATRIX_USER_ID, store_path="/app/data", config=config) + + for i in range(MATRIX_LOGIN_TRIES): + try: + login_response=await client.login(password=MATRIX_PASSWORD) + break + except Exception as e: + logger.error("Login failed: %s", login_response) + logger.error(f"Login attempt {i+1} failed: {e}") + if i == MATRIX_LOGIN_TRIES - 1: + return + await asyncio.sleep(MATRIX_LOGIN_DELAY_INCREMENT * (i + 1)) + logger.info("Logged in successfully") + + await trust_all_devices(client) + + client.add_event_callback(on_invite, InviteMemberEvent) + client.add_event_callback(on_message, RoomMessageText) + + logger.info("Starting sync loop") + await client.sync_forever(timeout=30000) # timeout should be moved to Variable + + + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt: + logger.info("Shutting down") + asyncio.run(client.close()) + diff --git a/requirements.txt b/matrix_service/requirements.txt similarity index 100% rename from requirements.txt rename to matrix_service/requirements.txt -- 2.49.1 From 302dd7e9651f2b453d0312de554ae277ed659ad1 Mon Sep 17 00:00:00 2001 From: Joao Figueiredo Date: Sat, 3 May 2025 18:52:37 +0100 Subject: [PATCH 2/6] WIP: tunning --- .env.example | 12 ++++++------ ai_service/main.py | 10 +++++----- matrix_service/main.py | 28 +++++++++++++++------------- matrix_service/requirements.txt | 3 ++- 4 files changed, 28 insertions(+), 25 deletions(-) diff --git a/.env.example b/.env.example index 9f7fed6..2911e09 100644 --- a/.env.example +++ b/.env.example @@ -2,12 +2,12 @@ LOG_LEVEL=INFO # Matrix Configuration -MATRIX_HOMESERVER_URL = "https://matrix.org" -MATRIX_USER_ID = "@botbot_user:matrix.org" -MATRIX_PASSWORD = "botbot_password" -MATRIX_LOGIN_TRIES = 5 # Number of login attempts before giving up, default is 5 -MATRIX_LOGIN_DELAY_INCREMENT = 5 # Delay increment,in seconds, between login attempts, default is 5 -AI_HANDLER_URL = "http://ai_service:8000" +MATRIX_HOMESERVER_URL="https://matrix.org" +MATRIX_USER_ID="@botbot_user:matrix.org" +MATRIX_PASSWORD="botbot_password" +MATRIX_LOGIN_TRIES=5 # Number of login attempts before giving up, default is 5 +MATRIX_LOGIN_DELAY_INCREMENT=5 # Delay increment,in seconds, between login attempts, default is 5 +AI_HANDLER_URL="http://ai_service:8000" # OpenAI API Key OPENAI_API_KEY=your_openai_api_key_here diff --git a/ai_service/main.py b/ai_service/main.py index f785b1c..4566499 100644 --- a/ai_service/main.py +++ b/ai_service/main.py @@ -10,11 +10,11 @@ openai.api_key = os.environ["OPENAI_API_KEY"] r = redis.Redis.from_url(os.environ.get("REDIS_URL", "redis://redis:6379")) class MessagePayload(BaseModel): - roomId: str - userId: str - content: str - eventId: str - timestamp: int + roomId: str + userId: str + eventId: str + serverTimestamp: int + content: str app = FastAPI() diff --git a/matrix_service/main.py b/matrix_service/main.py index fdefdc6..aec13cd 100644 --- a/matrix_service/main.py +++ b/matrix_service/main.py @@ -11,14 +11,16 @@ from nio.responses import LoginResponse # --- Load environment variables --- load_dotenv() -MATRIX_HOMESERVER_URL = os.getenv("MATRIX_HOMESERVER_URL") -MATRIX_USER_ID = os.getenv("MATRIX_USER_ID") -MATRIX_PASSWORD = os.getenv("MATRIX_PASSWORD") -MATRIX_LOGIN_TRIES = int(os.getenv("MATRIX_LOGIN_TRIES", 5)) +MATRIX_HOMESERVER_URL = os.getenv("MATRIX_HOMESERVER_URL") +MATRIX_USER_ID = os.getenv("MATRIX_USER_ID") +MATRIX_PASSWORD = os.getenv("MATRIX_PASSWORD") +MATRIX_LOGIN_TRIES = int(os.getenv("MATRIX_LOGIN_TRIES", 5)) MATRIX_LOGIN_DELAY_INCREMENT = int(os.getenv("MATRIX_LOGIN_DELAY_INCREMENT", 5)) + LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper() -AI_URL = os.getenv("AI_URL") -AI_TOKEN = os.getenv("AI_TOKEN") + +AI_HANDLER_URL = os.getenv("AI_HANDLER_URL") +AI_HANDLER_TOKEN = os.getenv("AI_HANDLER_TOKEN") @@ -76,15 +78,15 @@ async def main() -> None: if isinstance(event, RoomMessageText): logger.info(f"Received message in {room.room_id}: {event.body}") payload = { - "roomId": event["room_id"], - "userId": event["sender"], - "content": event["content"]["body"], - "eventId": event["event_id"], - "timestamp": event["origin_server_ts"] + "roomId": event.room_id, + "userId": event.sender, + "eventId": event.event_id, + "serverTimestamp": event.server_timestamp, + "content": event.body } - headers = {"Authorization": f"Bearer {AI_TOKEN}"} + headers = {"Authorization": f"Bearer {AI_HANDLER_TOKEN}"} async with httpx.AsyncClient() as http: - resp = await http.post(f"{AI_URL}/api/v1/message", json=payload, headers=headers) + resp = await http.post(f"{AI_HANDLER_URL}/api/v1/message", json=payload, headers=headers) resp.raise_for_status() data = resp.json() if data.get("reply"): diff --git a/matrix_service/requirements.txt b/matrix_service/requirements.txt index b634f8a..170594a 100644 --- a/matrix_service/requirements.txt +++ b/matrix_service/requirements.txt @@ -1,3 +1,4 @@ matrix-nio[e2e]>=0.25.0 python-dotenv>=1.0.0 -openai \ No newline at end of file +httpx>=0.23.0 +pydantic>=1.10 \ No newline at end of file -- 2.49.1 From afeaeba3136e0bf7533da08cb8323fd38d072c07 Mon Sep 17 00:00:00 2001 From: Joao Figueiredo Date: Sat, 3 May 2025 19:21:12 +0100 Subject: [PATCH 3/6] WIP: tunning --- ai_service/main.py | 13 ++++++++----- ai_service/requirements.txt | 2 +- matrix_service/main.py | 11 ++++++++--- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/ai_service/main.py b/ai_service/main.py index 4566499..42f5eba 100644 --- a/ai_service/main.py +++ b/ai_service/main.py @@ -32,12 +32,15 @@ async def message( # Build prompt (very simple example) prompt = f"User {payload.userId} said: {payload.content}\nBot:" - resp = openai.Completion.create( - model="text-davinci-003", - prompt=prompt, - max_tokens=150 + chat_response = opeai.ChatCompletion.create( + model="gpt-3.5-turbo", + messages=[ + {"role": "system", "content": "-"}, + {"role": "user", "content": prompt} + ] + temperature=0.7, ) - reply = resp.choices[0].text.strip() + reply = chat_response.choices[0].text.strip() # Cache reply for idempotency r.set(payload.eventId, reply, ex=3600) diff --git a/ai_service/requirements.txt b/ai_service/requirements.txt index b481e28..fe52b67 100644 --- a/ai_service/requirements.txt +++ b/ai_service/requirements.txt @@ -1,5 +1,5 @@ python-dotenv>=1.0.0 -openai +openai>=1.0.0 fastapi>=0.95 uvicorn>=0.22 redis>=4.5 diff --git a/matrix_service/main.py b/matrix_service/main.py index aec13cd..3954169 100644 --- a/matrix_service/main.py +++ b/matrix_service/main.py @@ -86,9 +86,14 @@ async def main() -> None: } headers = {"Authorization": f"Bearer {AI_HANDLER_TOKEN}"} async with httpx.AsyncClient() as http: - resp = await http.post(f"{AI_HANDLER_URL}/api/v1/message", json=payload, headers=headers) - resp.raise_for_status() - data = resp.json() + try: + resp = await http.post(f"{AI_HANDLER_URL}/api/v1/message", json=payload, headers=headers) + resp.raise_for_status() + data = resp.json() + except httpx.HTTPStatusError as e: + logger.error(f"HTTP error: {e.response.status_code} - {e.response.text}") + except Exception: + logger.exception("Error while calling AI handler") if data.get("reply"): client.send_message(event["room_id"], data["reply"]) -- 2.49.1 From dcef90d11f7e92f8cbfde79b0cb150530238d20b Mon Sep 17 00:00:00 2001 From: Joao Figueiredo Date: Sun, 4 May 2025 01:02:40 +0100 Subject: [PATCH 4/6] WIP: fixed order of commands, still debugging --- matrix_service/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix_service/main.py b/matrix_service/main.py index 3954169..c5e6ec4 100644 --- a/matrix_service/main.py +++ b/matrix_service/main.py @@ -90,12 +90,12 @@ async def main() -> None: resp = await http.post(f"{AI_HANDLER_URL}/api/v1/message", json=payload, headers=headers) resp.raise_for_status() data = resp.json() + if data.get("reply"): + client.send_message(event["room_id"], data["reply"]) except httpx.HTTPStatusError as e: logger.error(f"HTTP error: {e.response.status_code} - {e.response.text}") except Exception: logger.exception("Error while calling AI handler") - if data.get("reply"): - client.send_message(event["room_id"], data["reply"]) # --- Initialize the client --- # Create the data directory if it doesn't exist -- 2.49.1 From 5c9bdc771dcf068cb56f12eac7d9c3e0261fa159 Mon Sep 17 00:00:00 2001 From: Joao Figueiredo Date: Sun, 4 May 2025 01:15:36 +0100 Subject: [PATCH 5/6] WIP: added key upload on Matrix login, still debugging --- matrix_service/main.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/matrix_service/main.py b/matrix_service/main.py index c5e6ec4..e4ce5f8 100644 --- a/matrix_service/main.py +++ b/matrix_service/main.py @@ -119,7 +119,14 @@ async def main() -> None: if i == MATRIX_LOGIN_TRIES - 1: return await asyncio.sleep(MATRIX_LOGIN_DELAY_INCREMENT * (i + 1)) - logger.info("Logged in successfully") + logger.info("Logged in successfully") + + logger.debug("Upload one time Olm keys") + try: + await client.upload_keys() + except Exception as e: + logger.error(f"Error uploading keys: {e}") + return await trust_all_devices(client) -- 2.49.1 From 5bf54f0d4467a6294ea7f0ea40e2b390ad881d3c Mon Sep 17 00:00:00 2001 From: Joao Figueiredo Date: Sun, 4 May 2025 15:39:57 +0100 Subject: [PATCH 6/6] into services --- ai_service/main.py | 30 +++++++++++++++++----- matrix_service/main.py | 57 +++++++++++++++++++++++++++++++----------- 2 files changed, 66 insertions(+), 21 deletions(-) diff --git a/ai_service/main.py b/ai_service/main.py index 42f5eba..c44fd2b 100644 --- a/ai_service/main.py +++ b/ai_service/main.py @@ -2,13 +2,27 @@ import os from dotenv import load_dotenv from fastapi import FastAPI, Header, HTTPException from pydantic import BaseModel -import openai +from openai import OpenAI +import logging import redis +LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper() AI_TOKEN = os.environ["AI_HANDLER_TOKEN"] -openai.api_key = os.environ["OPENAI_API_KEY"] + +AIclient = OpenAI( + api_key=os.environ["OPENAI_API_KEY"], +) + r = redis.Redis.from_url(os.environ.get("REDIS_URL", "redis://redis:6379")) +# --- Logging Setup --- +numeric_level = getattr(logging, LOG_LEVEL, logging.INFO) +logging.basicConfig( + level=numeric_level, + format="%(asctime)s %(levelname)s %(name)s: %(message)s" +) +logger = logging.getLogger(__name__) + class MessagePayload(BaseModel): roomId: str userId: str @@ -32,15 +46,19 @@ async def message( # Build prompt (very simple example) prompt = f"User {payload.userId} said: {payload.content}\nBot:" - chat_response = opeai.ChatCompletion.create( + chat_response = AIclient.chat.completions.create( model="gpt-3.5-turbo", messages=[ - {"role": "system", "content": "-"}, + {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": prompt} - ] + ], + max_tokens=150, + n=1, + stop=None, temperature=0.7, ) - reply = chat_response.choices[0].text.strip() + + reply = chat_response.choices[0].message.content.strip() # Cache reply for idempotency r.set(payload.eventId, reply, ex=3600) diff --git a/matrix_service/main.py b/matrix_service/main.py index e4ce5f8..008f6a5 100644 --- a/matrix_service/main.py +++ b/matrix_service/main.py @@ -37,20 +37,19 @@ async def main() -> None: async def trust_all_devices(client) -> None: """ - Programmatically verify all devices to allow sharing encryption keys. + Mark every other user's device as verified so we can receive their + future Megolm keys without being blocked. """ for room_id in client.rooms: try: - devices = await client.room_devices(room_id) - if isinstance(devices, dict): - for user, dev_ids in devices.items(): - if user == USER_ID: - continue - for dev_id in dev_ids: - device = client.crypto.device_store.get_device(user, dev_id) - if device and not client.crypto.device_store.is_device_verified(device): - logger.info(f"Trusting {dev_id} for {user}") - client.verify_device(device) + devices_in_room = client.room_devices(room_id) + for user_id, user_devices in devices_in_room.items(): + if user_id == client.user_id: + continue + for dev_id, device in user_devices.items(): + if not client.device_store.is_device_verified(device): + logger.info(f"Trusting {dev_id} for {user}") + client.verify_device(device) except Exception: logger.exception(f"Error trusting devices in {room_id}") @@ -63,6 +62,32 @@ async def main() -> None: logger.info("Joined %s", room.room_id) await trust_all_devices(client) + async def send_message(room: MatrixRoom, message: str) -> None: + """ + Send a message to `room`. + """ + try: + await trust_all_devices(client) + + await client.share_group_session( + room.room_id, + ignore_unverified_devices=True + ) + + await client.room_send( + room_id=room.room_id, + message_type="m.room.message", + content={ + "msgtype": "m.text", + "body": message + }, + ignore_unverified_devices=True + ) + logger.info("Sent message to %s: %s", room.room_id, message) + except Exception as e: + logger.error(f"Error sending message: {e}") + + async def on_message(room: MatrixRoom, event: RoomMessageText) -> None: """ Handle incoming messages. @@ -80,8 +105,8 @@ async def main() -> None: payload = { "roomId": event.room_id, "userId": event.sender, - "eventId": event.event_id, - "serverTimestamp": event.server_timestamp, + "eventId": event.event_id, + "serverTimestamp": event.server_timestamp, "content": event.body } headers = {"Authorization": f"Bearer {AI_HANDLER_TOKEN}"} @@ -91,7 +116,9 @@ async def main() -> None: resp.raise_for_status() data = resp.json() if data.get("reply"): - client.send_message(event["room_id"], data["reply"]) + await trust_all_devices(client) + await send_message(room, data["reply"]) + logger.info("Reply sent: %s", data["reply"]) except httpx.HTTPStatusError as e: logger.error(f"HTTP error: {e.response.status_code} - {e.response.text}") except Exception: @@ -123,7 +150,7 @@ async def main() -> None: logger.debug("Upload one time Olm keys") try: - await client.upload_keys() + await client.keys_upload() except Exception as e: logger.error(f"Error uploading keys: {e}") return -- 2.49.1