From e73958e4748f85ec603fe6ba25d130bb93df08cd Mon Sep 17 00:00:00 2001 From: Song Meo Date: Thu, 17 Apr 2025 12:38:43 +0300 Subject: [PATCH 1/9] create reminder db --- src/main.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/main.py b/src/main.py index 04bfc4a..bb36e5c 100755 --- a/src/main.py +++ b/src/main.py @@ -126,6 +126,20 @@ def main() -> None: ) """ ) + + cur.execute( + """ + CREATE TABLE IF NOT EXISTS user_reminder ( + id SERIAL PRIMARY KEY, -- SERIAL handles auto-incrementing + chat_id BIGINT NOT NULL, + action TEXT NOT NULL, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + deadline TIMESTAMPTZ NOT NULL, + is_notified BOOLEAN DEFAULT FALSE + ) + """ + ) + con.commit() async def sticker_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: From 3d6d5e28b967abff94fb73d011f28f2d6e9e10fd Mon Sep 17 00:00:00 2001 From: Song Meo Date: Fri, 18 Apr 2025 18:04:20 +0300 Subject: [PATCH 2/9] add reminder function --- src/tools.json | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/tools.json b/src/tools.json index 9f1a03a..4f70fc7 100644 --- a/src/tools.json +++ b/src/tools.json @@ -16,5 +16,28 @@ "required": ["expression"], "additionalProperties": false } + }, + { + "type": "function", + "function": { + "name": "set reminder", + "description": "set user reminders", + "parameters": { + "type": "object", + "properties": { + "deadline": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 time", + }, + "action": { + "type": "string", + "description": "the description of the reminder" + }, + }, + "required": ["deadline", "action"], + "additionalProperties": false + }, + } } ] From acb21e9ce468978a5e13d799bac937178be3ee5b Mon Sep 17 00:00:00 2001 From: Song Meo Date: Tue, 22 Apr 2025 11:30:08 +0300 Subject: [PATCH 3/9] create reminder in db --- src/db.py | 28 ++++++++++++++++++++++++++++ src/evaluate.py | 13 ------------- src/handler.py | 2 ++ src/llm.py | 20 ++++++++++++++++---- src/main.py | 26 ++------------------------ src/tool_function.py | 30 ++++++++++++++++++++++++++++++ src/tools.json | 16 ++++++++++------ 7 files changed, 88 insertions(+), 47 deletions(-) create mode 100644 src/db.py delete mode 100644 src/evaluate.py create mode 100644 src/tool_function.py diff --git a/src/db.py b/src/db.py new file mode 100644 index 0000000..15cf5a4 --- /dev/null +++ b/src/db.py @@ -0,0 +1,28 @@ +import os +import time + +import psycopg2 +from logger import logger + +DB_USER = os.environ["DB_USER"] +DB_PASSWORD = os.environ["DB_PASSWORD"] +DB_NAME = os.environ["DB_NAME"] +DB_HOST = os.environ["DB_HOST"] + +for _ in range(5): + try: + con = psycopg2.connect( + dbname=DB_NAME, + user=DB_USER, + password=DB_PASSWORD, + host=DB_HOST, + port=5432, + ) + logger.info("Connection successful!") + break # success! no need to repeat + except psycopg2.OperationalError as e: + logger.error("Error while connecting to the database:", e) + time.sleep(5) +else: + logger.error("Can't connect to the database. Abort.") + exit(1) diff --git a/src/evaluate.py b/src/evaluate.py deleted file mode 100644 index 2ac69e2..0000000 --- a/src/evaluate.py +++ /dev/null @@ -1,13 +0,0 @@ -def evaluate(expression: str) -> str: - try: - ans = eval(expression) - except Exception as error: - return str(type(error).__name__) - return str(ans) - - -def test_evaluate() -> None: - assert evaluate("123 + 456") == str(123 + 456) - assert evaluate("455 +_/ 342") == "NameError" - assert evaluate("455 +_( 342") == "SyntaxError" - assert evaluate("455 / 0") == "ZeroDivisionError" diff --git a/src/handler.py b/src/handler.py index bb51bec..b151e3c 100644 --- a/src/handler.py +++ b/src/handler.py @@ -1,6 +1,7 @@ import base64 import os import uuid +from datetime import datetime from typing import Any from pathlib import Path import psycopg2 @@ -23,6 +24,7 @@ Explicit mentions include cases where your name or identifier appears anywhere in the message. If you are not explicitly addressed, always respond with {no_reply_token}. When answering, don't use LaTeX. + If you need today date to set reminder, it is {datetime.now().isoformat()}. """ DB_BLOB_DIR = Path(os.environ["DB_BLOB_DIR"]) DB_BLOB_DIR.mkdir(parents=True, exist_ok=True) diff --git a/src/llm.py b/src/llm.py index 00c640c..2d442bd 100644 --- a/src/llm.py +++ b/src/llm.py @@ -5,7 +5,7 @@ from openai import OpenAI from openai.types.chat import ChatCompletion -from evaluate import evaluate +from tool_function import evaluate, set_reminder from logger import logger # todo: make this a class @@ -40,10 +40,22 @@ def runs_in_background_thread() -> ChatCompletion: while message.tool_calls: tool_call = message.tool_calls[0] + logger.info(f"tool call {tool_call}") + function = tool_call.function.name + answer = "no function is called." + if function == "reminder": + arguments = json.loads(tool_call.function.arguments) + chat_id, action, deadline = ( + arguments["chat_id"], + arguments["action"], + arguments["deadline"], + ) + answer = set_reminder(chat_id, action, deadline) + elif function == "evaluate": + arguments = json.loads(tool_call.function.arguments) + expression = arguments["expression"] + answer = evaluate(expression) logger.info(f"Tool call message: {message}") - arguments = json.loads(tool_call.function.arguments) - expression = arguments["expression"] - answer = evaluate(expression) function_call_result_message = { "role": "tool", "content": json.dumps({"result": answer}), diff --git a/src/main.py b/src/main.py index bb36e5c..0d2c774 100755 --- a/src/main.py +++ b/src/main.py @@ -2,11 +2,11 @@ # Copyright Song Meo import asyncio -import time from datetime import datetime, timedelta, timezone from typing import Any import psycopg2 import os +from db import con from dotenv import load_dotenv import telegram from telegram import Update, error @@ -25,10 +25,6 @@ load_dotenv() -DB_USER = os.environ["DB_USER"] -DB_PASSWORD = os.environ["DB_PASSWORD"] -DB_NAME = os.environ["DB_NAME"] -DB_HOST = os.environ["DB_HOST"] TOKEN = os.environ["TOKEN"] @@ -66,25 +62,7 @@ async def generate_response_loop(con: psycopg2.connect) -> None: def main() -> None: application = Application.builder().token(TOKEN).build() - - for _ in range(5): - try: - con = psycopg2.connect( - dbname=DB_NAME, - user=DB_USER, - password=DB_PASSWORD, - host=DB_HOST, - port=5432, - ) - cur = con.cursor() - logger.info("Connection successful!") - break # success! no need to repeat - except psycopg2.OperationalError as e: - logger.error("Error while connecting to the database:", e) - time.sleep(5) - else: - logger.error("Can't connect to the database. Abort.") - exit(1) + cur = con.cursor() cur.execute( """ diff --git a/src/tool_function.py b/src/tool_function.py new file mode 100644 index 0000000..783e323 --- /dev/null +++ b/src/tool_function.py @@ -0,0 +1,30 @@ +import string +from db import con +from datetime import datetime + + +def set_reminder(chat_id: int, action: str, deadline: str) -> str: + today = datetime.now() + if datetime.fromisoformat(deadline) < today: + return "the deadline is in the past." + cur = con.cursor() + cur.execute( + "INSERT INTO user_reminder (chat_id, action, deadline) VALUES (%s, %s, %s)", (chat_id, action, deadline) + ) + con.commit() + return f"A reminder for {action} is set on {deadline}." + + +def evaluate(expression: str) -> str: + try: + ans = eval(expression) + except Exception as error: + return str(type(error).__name__) + return str(ans) + + +def test_evaluate() -> None: + assert evaluate("123 + 456") == str(123 + 456) + assert evaluate("455 +_/ 342") == "NameError" + assert evaluate("455 +_( 342") == "SyntaxError" + assert evaluate("455 / 0") == "ZeroDivisionError" diff --git a/src/tools.json b/src/tools.json index 4f70fc7..0a4ae24 100644 --- a/src/tools.json +++ b/src/tools.json @@ -20,24 +20,28 @@ { "type": "function", "function": { - "name": "set reminder", + "name": "reminder", "description": "set user reminders", "parameters": { "type": "object", "properties": { + "chat_id": { + "type": "integer", + "description": "the chat id of conversation" + }, "deadline": { "type": "string", "format": "date-time", - "description": "ISO 8601 time", + "description": "ISO 8601 time" }, "action": { "type": "string", "description": "the description of the reminder" - }, - }, - "required": ["deadline", "action"], - "additionalProperties": false + } + } }, + "required": ["deadline", "action", "chat_id"], + "additionalProperties": false } } ] From 57ce0991e20d62f77b0c11f52a4f4b4e8c32b8ca Mon Sep 17 00:00:00 2001 From: Song Meo Date: Tue, 22 Apr 2025 12:32:22 +0300 Subject: [PATCH 4/9] edit reminder --- src/llm.py | 12 ++++++++++-- src/tool_function.py | 7 +++++-- src/tools.json | 30 +++++++++++++++++++++++++++++- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/llm.py b/src/llm.py index 2d442bd..33c1ce0 100644 --- a/src/llm.py +++ b/src/llm.py @@ -43,14 +43,22 @@ def runs_in_background_thread() -> ChatCompletion: logger.info(f"tool call {tool_call}") function = tool_call.function.name answer = "no function is called." - if function == "reminder": + if function == "set_reminder": arguments = json.loads(tool_call.function.arguments) chat_id, action, deadline = ( arguments["chat_id"], arguments["action"], arguments["deadline"], ) - answer = set_reminder(chat_id, action, deadline) + answer = set_reminder(chat_id, action, False, deadline) + if function == "edit_reminder": + arguments = json.loads(tool_call.function.arguments) + chat_id, action, deadline = ( + arguments["chat_id"], + arguments["action"], + arguments["deadline"], + ) + answer = set_reminder(chat_id, action, True, deadline) elif function == "evaluate": arguments = json.loads(tool_call.function.arguments) expression = arguments["expression"] diff --git a/src/tool_function.py b/src/tool_function.py index 783e323..b55633f 100644 --- a/src/tool_function.py +++ b/src/tool_function.py @@ -1,13 +1,16 @@ -import string from db import con from datetime import datetime -def set_reminder(chat_id: int, action: str, deadline: str) -> str: +def set_reminder(chat_id: int, action: str, reset: bool, deadline: str) -> str: today = datetime.now() if datetime.fromisoformat(deadline) < today: return "the deadline is in the past." cur = con.cursor() + if reset: + cur.execute("DELETE FROM user_reminder WHERE action = %s", (action,)) + if cur.rowcount == 0: + return f"A reminder for {action} is not found." cur.execute( "INSERT INTO user_reminder (chat_id, action, deadline) VALUES (%s, %s, %s)", (chat_id, action, deadline) ) diff --git a/src/tools.json b/src/tools.json index 0a4ae24..bdef777 100644 --- a/src/tools.json +++ b/src/tools.json @@ -20,7 +20,7 @@ { "type": "function", "function": { - "name": "reminder", + "name": "set_reminder", "description": "set user reminders", "parameters": { "type": "object", @@ -43,5 +43,33 @@ "required": ["deadline", "action", "chat_id"], "additionalProperties": false } + }, + { + "type": "function", + "function": { + "name": "edit_reminder", + "description": "edit user reminder by deleting old one and adding new one", + "parameters": { + "type": "object", + "properties": { + "chat_id": { + "type": "integer", + "description": "the chat id of conversation" + }, + "deadline": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 time" + }, + "action": { + "type": "string", + "description": "the description of the reminder" + } + } + }, + "required": ["deadline", "action", "chat_id"], + "additionalProperties": false + } + } ] From c88febbe3183bbf5eb83e7c90fdc1e6de590bea5 Mon Sep 17 00:00:00 2001 From: Song Meo Date: Thu, 24 Apr 2025 19:41:25 +0300 Subject: [PATCH 5/9] WIP #11: having issue with timezone --- Dockerfile | 9 +++++++++ pyproject.toml | 1 + src/handler.py | 8 ++++---- src/llm.py | 15 ++++----------- src/main.py | 38 +++++++++++++++++++++++++++++++++++--- src/requirements.txt | 1 + src/tool_function.py | 25 +++++++++++++++---------- src/tools.json | 42 +++++++++++------------------------------- 8 files changed, 80 insertions(+), 59 deletions(-) diff --git a/Dockerfile b/Dockerfile index 78c8167..f710b4a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,18 @@ FROM python:3.12-slim +RUN apt-get update && apt-get install -y \ + build-essential \ + gcc \ + libpq-dev \ + curl \ + git \ + && rm -rf /var/lib/apt/lists/* \ + WORKDIR /app COPY src/ ./ +RUN pip install --upgrade pip setuptools wheel RUN pip install --no-cache-dir -r requirements.txt CMD ["python", "main.py"] diff --git a/pyproject.toml b/pyproject.toml index 42d97b5..e96f779 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ dependencies = [ "sniffio~=1.3.1", "tqdm~=4.67.1", "typing-extensions~=4.12.2", + "isodate>=0.7.2", ] readme = "README.md" requires-python = ">= 3.10" diff --git a/src/handler.py b/src/handler.py index b151e3c..d2eccb6 100644 --- a/src/handler.py +++ b/src/handler.py @@ -1,7 +1,6 @@ import base64 import os import uuid -from datetime import datetime from typing import Any from pathlib import Path import psycopg2 @@ -24,7 +23,8 @@ Explicit mentions include cases where your name or identifier appears anywhere in the message. If you are not explicitly addressed, always respond with {no_reply_token}. When answering, don't use LaTeX. - If you need today date to set reminder, it is {datetime.now().isoformat()}. + When setting/editing reminder, you are not allowed to answer from your own knowledge. + You must call the appropriate tool and return its output. """ DB_BLOB_DIR = Path(os.environ["DB_BLOB_DIR"]) DB_BLOB_DIR.mkdir(parents=True, exist_ok=True) @@ -140,9 +140,9 @@ async def generate_response(chat_id: int, con: psycopg2.connect) -> str: cur.execute( """ INSERT INTO user_message (chat_id, user_id, message) - VALUES (%s, 0, %s) + VALUES (%s, %s, %s) """, - (chat_id, response), + (chat_id, BOT_USER_ID, response), ) con.commit() return response diff --git a/src/llm.py b/src/llm.py index 33c1ce0..f982f50 100644 --- a/src/llm.py +++ b/src/llm.py @@ -45,20 +45,13 @@ def runs_in_background_thread() -> ChatCompletion: answer = "no function is called." if function == "set_reminder": arguments = json.loads(tool_call.function.arguments) - chat_id, action, deadline = ( + chat_id, action, duration, deadline = ( arguments["chat_id"], arguments["action"], - arguments["deadline"], + arguments.get("duration", None), + arguments.get("deadline", None), ) - answer = set_reminder(chat_id, action, False, deadline) - if function == "edit_reminder": - arguments = json.loads(tool_call.function.arguments) - chat_id, action, deadline = ( - arguments["chat_id"], - arguments["action"], - arguments["deadline"], - ) - answer = set_reminder(chat_id, action, True, deadline) + answer = set_reminder(chat_id, action, deadline, duration) elif function == "evaluate": arguments = json.loads(tool_call.function.arguments) expression = arguments["expression"] diff --git a/src/main.py b/src/main.py index 0d2c774..f2eb64a 100755 --- a/src/main.py +++ b/src/main.py @@ -4,7 +4,6 @@ import asyncio from datetime import datetime, timedelta, timezone from typing import Any -import psycopg2 import os from db import con from dotenv import load_dotenv @@ -28,7 +27,39 @@ TOKEN = os.environ["TOKEN"] -async def generate_response_loop(con: psycopg2.connect) -> None: +async def send_reminder_loop() -> None: + while True: + cur = con.cursor() + cur.execute( + "SELECT chat_id, action, deadline FROM user_reminder WHERE deadline <= %s AND is_notified = %s", + ( + datetime.now().isoformat(), + False, + ), + ) + reminders = cur.fetchall() + for r in reminders: + chat_id, action, deadline = r + bot = telegram.Bot(token=TOKEN) + + message = f"This is a reminder to {action} at {deadline}." + + cur.execute( + """ + INSERT INTO user_message (chat_id, user_id, message) + VALUES (%s, %s, %s) + """, + (chat_id, BOT_USER_ID, message), + ) + + await bot.send_message(chat_id=chat_id, text=message) + cur.execute("UPDATE user_reminder SET is_notified = TRUE WHERE action = %s", (action,)) + con.commit() + + await asyncio.sleep(60) + + +async def generate_response_loop() -> None: while True: cur = con.cursor() cur.execute("SELECT chat_id FROM user_message") @@ -153,7 +184,8 @@ async def message_handler_proxy(update: Update, context: ContextTypes.DEFAULT_TY loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) - loop.create_task(generate_response_loop(con)) + loop.create_task(generate_response_loop()) + loop.create_task(send_reminder_loop()) # Run the bot until the user presses Ctrl-C application.run_polling(allowed_updates=Update.ALL_TYPES) diff --git a/src/requirements.txt b/src/requirements.txt index fa116d9..b181981 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -7,6 +7,7 @@ h11~=0.14.0 httpcore~=1.0.7 httpx~=0.28.1 idna~=3.10 +isodate~=0.7.2 jiter~=0.8.0 openai~=1.57.1 psycopg2-binary~=2.9.7 diff --git a/src/tool_function.py b/src/tool_function.py index b55633f..4e338db 100644 --- a/src/tool_function.py +++ b/src/tool_function.py @@ -1,21 +1,26 @@ from db import con -from datetime import datetime +from datetime import datetime, timezone +import isodate -def set_reminder(chat_id: int, action: str, reset: bool, deadline: str) -> str: - today = datetime.now() - if datetime.fromisoformat(deadline) < today: - return "the deadline is in the past." +def set_reminder(chat_id: int, action: str, deadline: str, duration: str) -> str: + today = datetime.now(timezone.utc) + if deadline: + if datetime.fromisoformat(deadline) < today: + return "Sorry deadline is past." + elif duration: + td = isodate.parse_duration(duration) + deadline = today + td + else: + return "You must define deadline or duration." cur = con.cursor() - if reset: - cur.execute("DELETE FROM user_reminder WHERE action = %s", (action,)) - if cur.rowcount == 0: - return f"A reminder for {action} is not found." + cur.execute( "INSERT INTO user_reminder (chat_id, action, deadline) VALUES (%s, %s, %s)", (chat_id, action, deadline) ) con.commit() - return f"A reminder for {action} is set on {deadline}." + + return f"A reminder for '{action}' is set on {deadline}." def evaluate(expression: str) -> str: diff --git a/src/tools.json b/src/tools.json index bdef777..08bb80a 100644 --- a/src/tools.json +++ b/src/tools.json @@ -21,55 +21,35 @@ "type": "function", "function": { "name": "set_reminder", - "description": "set user reminders", + "description": "Set a reminder on a specific date by calling this function.", "parameters": { "type": "object", "properties": { - "chat_id": { - "type": "integer", - "description": "the chat id of conversation" + "duration": { + "type": "string", + "description": "A relative time like 'PT10M' (ISO 8601 duration = '10 minutes from now')" }, "deadline": { "type": "string", - "format": "date-time", - "description": "ISO 8601 time" + "format": "date-time", + "description": "An absolute ISO 8601 time like '2025-04-24T15:00:00Z'" }, - "action": { - "type": "string", - "description": "the description of the reminder" - } - } - }, - "required": ["deadline", "action", "chat_id"], - "additionalProperties": false - } - }, - { - "type": "function", - "function": { - "name": "edit_reminder", - "description": "edit user reminder by deleting old one and adding new one", - "parameters": { - "type": "object", - "properties": { "chat_id": { "type": "integer", "description": "the chat id of conversation" }, - "deadline": { - "type": "string", - "format": "date-time", - "description": "ISO 8601 time" - }, "action": { "type": "string", "description": "the description of the reminder" } } }, - "required": ["deadline", "action", "chat_id"], + "required": ["action", "chat_id"], + "oneOf": [ + { "required": ["duration"] }, + { "required": ["deadline"] } + ], "additionalProperties": false } - } ] From 1f9231c84152a2a4cc4f7f2992ccce0dd9e2deab Mon Sep 17 00:00:00 2001 From: Song Meo Date: Sat, 26 Apr 2025 19:56:59 +0300 Subject: [PATCH 6/9] remove monkeypatching --- Dockerfile | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index f710b4a..ff1198f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,4 @@ -FROM python:3.12-slim - -RUN apt-get update && apt-get install -y \ - build-essential \ - gcc \ - libpq-dev \ - curl \ - git \ - && rm -rf /var/lib/apt/lists/* \ +FROM python:3.12 WORKDIR /app From 89fc29e6c9a6814243f47aa2356afc16556be0dc Mon Sep 17 00:00:00 2001 From: Song Meo Date: Sat, 26 Apr 2025 20:48:18 +0300 Subject: [PATCH 7/9] review --- pyproject.toml | 2 +- src/db.py | 2 ++ src/handler.py | 4 ++++ src/llm.py | 7 +++++-- src/logger.py | 2 ++ src/main.py | 4 ++++ src/tool_function.py | 2 +- 7 files changed, 19 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e96f779..6c4fe69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ dependencies = [ "sniffio~=1.3.1", "tqdm~=4.67.1", "typing-extensions~=4.12.2", - "isodate>=0.7.2", + "isodate~=0.7.2", ] readme = "README.md" requires-python = ">= 3.10" diff --git a/src/db.py b/src/db.py index 15cf5a4..c8f5c32 100644 --- a/src/db.py +++ b/src/db.py @@ -9,6 +9,8 @@ DB_NAME = os.environ["DB_NAME"] DB_HOST = os.environ["DB_HOST"] +# This is needed because the DB container takes a longer time to start, +# so the DB may not be available in the beginning. for _ in range(5): try: con = psycopg2.connect( diff --git a/src/handler.py b/src/handler.py index d2eccb6..e2d237a 100644 --- a/src/handler.py +++ b/src/handler.py @@ -131,6 +131,10 @@ async def generate_response(chat_id: int, con: psycopg2.connect) -> str: { "role": "assistant" if user_id == 0 else "user", "content": f"{user_name} ({user_id}): {message}", + # TODO FIXME: add more metadata; e.g. (change the system prompt and IO code): + # "content": json.dumps({ + # 'user_name': user_name, 'user_id': user_id, 'chat_id': chat_id, 'message': message + # }), } ) logger.info("all messages: %s", messages) diff --git a/src/llm.py b/src/llm.py index f982f50..b66a592 100644 --- a/src/llm.py +++ b/src/llm.py @@ -1,5 +1,6 @@ import asyncio import json +from typing import Any import os import openai from openai import OpenAI @@ -13,10 +14,12 @@ OPENAI_API_KEY = os.environ["OPENAI_API_KEY"] OPENAI_MODEL = "gpt-4o" +TOOL_DEF = json.load(open("tools.json")) + client = OpenAI(api_key=OPENAI_API_KEY) -async def ask_ai(messages: list) -> str: +async def ask_ai(messages: list[dict[str, Any]]) -> str: loop = asyncio.get_running_loop() # gain access to the scheduler def runs_in_background_thread() -> ChatCompletion: @@ -25,7 +28,7 @@ def runs_in_background_thread() -> ChatCompletion: completion = client.chat.completions.create( model=OPENAI_MODEL, messages=messages, - tools=json.load(open("tools.json")), + tools=TOOL_DEF, ) except openai.BadRequestError as e: logger.error(f"OpenAI API error: {e}") diff --git a/src/logger.py b/src/logger.py index dbcc0ab..fba3d39 100644 --- a/src/logger.py +++ b/src/logger.py @@ -1,8 +1,10 @@ import logging +# TODO FIXME: you don't need a separate logger module; these things go into main.py # Enable logging logging.basicConfig(format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO) # set higher logging level for httpx to avoid all GET and POST requests being logged logging.getLogger("httpx").setLevel(logging.WARNING) +# TODO FIXME: create a separate logger object in each Python module logger = logging.getLogger(__name__) diff --git a/src/main.py b/src/main.py index f2eb64a..3eb94c9 100755 --- a/src/main.py +++ b/src/main.py @@ -28,6 +28,7 @@ async def send_reminder_loop() -> None: + # TODO FIXME add logging for single iteration failure and total loop failure. while True: cur = con.cursor() cur.execute( @@ -42,6 +43,9 @@ async def send_reminder_loop() -> None: chat_id, action, deadline = r bot = telegram.Bot(token=TOKEN) + # This is a simplified solution; in the future, we should ask the LLM to process reminder events. + # In response, the LLM can invoke another tool, like message a specific user (doesn't have to be + # the user that created the reminder), or do this and that. message = f"This is a reminder to {action} at {deadline}." cur.execute( diff --git a/src/tool_function.py b/src/tool_function.py index 4e338db..22088fc 100644 --- a/src/tool_function.py +++ b/src/tool_function.py @@ -6,7 +6,7 @@ def set_reminder(chat_id: int, action: str, deadline: str, duration: str) -> str: today = datetime.now(timezone.utc) if deadline: - if datetime.fromisoformat(deadline) < today: + if (datetime.fromisoformat(deadline) - today).seconds < -60: return "Sorry deadline is past." elif duration: td = isodate.parse_duration(duration) From e4a1a1824219c389e434f93571385cad50f64cab Mon Sep 17 00:00:00 2001 From: Song Meo Date: Mon, 28 Apr 2025 23:19:53 +0300 Subject: [PATCH 8/9] WIP: changing IO code to match the new message format --- src/handler.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/handler.py b/src/handler.py index e2d237a..daca3b2 100644 --- a/src/handler.py +++ b/src/handler.py @@ -1,4 +1,5 @@ import base64 +import json import os import uuid from typing import Any @@ -14,8 +15,8 @@ BOT_MESSAGE_ID = 0 no_reply_token = "-" SYSTEM_PROMPT = f""" - Each message in the conversation below is prefixed with the username and their unique - identifier, like this: "username (123456789): MESSAGE...". ' + Each message in the conversation below is sent in json like this + {{"user_name": user_name, "user_id": user_id, "chat_id": chat_id, "message": message}}. You play the role of the user called {BOT_NAME}, or simply Bot; your username and unique identifier are {BOT_NAME} and 0. You are observing the users' conversation and normally you do not interfere @@ -130,11 +131,9 @@ async def generate_response(chat_id: int, con: psycopg2.connect) -> str: messages.append( { "role": "assistant" if user_id == 0 else "user", - "content": f"{user_name} ({user_id}): {message}", - # TODO FIXME: add more metadata; e.g. (change the system prompt and IO code): - # "content": json.dumps({ - # 'user_name': user_name, 'user_id': user_id, 'chat_id': chat_id, 'message': message - # }), + "content": json.dumps( + {"user_name": user_name, "user_id": user_id, "chat_id": chat_id, "message": message} + ), } ) logger.info("all messages: %s", messages) From 015d5022e0f025b6df4bac831a65e7041c65b46b Mon Sep 17 00:00:00 2001 From: Song Meo Date: Tue, 29 Apr 2025 11:49:45 +0300 Subject: [PATCH 9/9] fixed the parsing of the new message format --- src/db.py | 4 +++- src/handler.py | 7 ++++--- src/llm.py | 6 ++++-- src/logger.py | 10 ---------- src/main.py | 8 +++++++- 5 files changed, 18 insertions(+), 17 deletions(-) delete mode 100644 src/logger.py diff --git a/src/db.py b/src/db.py index c8f5c32..713e64d 100644 --- a/src/db.py +++ b/src/db.py @@ -2,7 +2,9 @@ import time import psycopg2 -from logger import logger +import logging + +logger = logging.getLogger(__name__) DB_USER = os.environ["DB_USER"] DB_PASSWORD = os.environ["DB_PASSWORD"] diff --git a/src/handler.py b/src/handler.py index daca3b2..6bca922 100644 --- a/src/handler.py +++ b/src/handler.py @@ -8,7 +8,9 @@ from telegram import Update, Message, PhotoSize from telegram.ext import ExtBot, CallbackContext from llm import ask_ai -from logger import logger +import logging + +logger = logging.getLogger(__name__) BOT_NAME = "ButlerBot" BOT_USER_ID = 0 @@ -138,8 +140,7 @@ async def generate_response(chat_id: int, con: psycopg2.connect) -> str: ) logger.info("all messages: %s", messages) response = await ask_ai(messages) - - response = response.removeprefix(f"{BOT_NAME} ({BOT_USER_ID}): ") + response = json.loads(response).get("message", "no message received.") cur.execute( """ INSERT INTO user_message (chat_id, user_id, message) diff --git a/src/llm.py b/src/llm.py index b66a592..a5d85eb 100644 --- a/src/llm.py +++ b/src/llm.py @@ -7,7 +7,9 @@ from openai.types.chat import ChatCompletion from tool_function import evaluate, set_reminder -from logger import logger +import logging + +logger = logging.getLogger(__name__) # todo: make this a class @@ -19,7 +21,7 @@ client = OpenAI(api_key=OPENAI_API_KEY) -async def ask_ai(messages: list[dict[str, Any]]) -> str: +async def ask_ai(messages: list[Any]) -> str: loop = asyncio.get_running_loop() # gain access to the scheduler def runs_in_background_thread() -> ChatCompletion: diff --git a/src/logger.py b/src/logger.py deleted file mode 100644 index fba3d39..0000000 --- a/src/logger.py +++ /dev/null @@ -1,10 +0,0 @@ -import logging - -# TODO FIXME: you don't need a separate logger module; these things go into main.py -# Enable logging -logging.basicConfig(format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO) -# set higher logging level for httpx to avoid all GET and POST requests being logged -logging.getLogger("httpx").setLevel(logging.WARNING) - -# TODO FIXME: create a separate logger object in each Python module -logger = logging.getLogger(__name__) diff --git a/src/main.py b/src/main.py index 3eb94c9..ae2063c 100755 --- a/src/main.py +++ b/src/main.py @@ -20,7 +20,13 @@ ) from handler import store_message, generate_response, help_command from handler import BOT_NAME, BOT_USER_ID -from logger import logger +import logging + +logging.basicConfig(format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO) +# set higher logging level for httpx to avoid all GET and POST requests being logged +logging.getLogger("httpx").setLevel(logging.WARNING) + +logger = logging.getLogger(__name__) load_dotenv()