From a88a4c95c032b420d7f6c302ee00e7ab07beae3c Mon Sep 17 00:00:00 2001 From: LessVegetables <57289239+LessVegetables@users.noreply.github.com> Date: Tue, 1 Apr 2025 16:32:05 +0700 Subject: [PATCH 1/2] Wrote stuff in the readme --- README.md | 95 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5326647..c0fb633 100644 --- a/README.md +++ b/README.md @@ -1 +1,94 @@ -# language-bot \ No newline at end of file +# ChatTutor + +ChatTutor is Telegram chatbot designed to complement language learning by providing realistic conversation practice. It is designed to supplement traditional language learning by offering users a realistic conversation partner—helping them build practical language skills through natural dialogue. + + +## Overview + +- **Purpose**: ChatTutor offers users the opportunity to practice a language in a conversational setting. The bot simulates real-life interactions, encouraging users to communicate in the language they are learning. It is intended as a supportive tool in the broader language learning process, complementing the role of human tutors rather than replacing them. + +- **Current Features**: + - **Text-Only Interaction**: For now, the bot supports unlimited text messaging with one "character" acting as your tutor. + - **Natural Conversations**: The bot responds naturally, keeping the flow of conversation engaging and lifelike. It can explain concepts if asked, but its main role is to act as a conversation partner. + + +## Planned Enhancements + +The project roadmap includes several exciting features aimed at enriching the learning experience and expanding functionality: + +- **Multiple Tutor Personalities**: Allowing users to choose from various tutor characters with different personality traits. + +- **Media Integration**: Adding support for sticker recognition, voice messages, and potentially video messages. + +- **Tutoring Plans**: Future monetization strategies may offer tutoring plans where educators can invest in bulk usage, providing their students with free access to ChatTutor’s features while keeping core interactions accessible to all users. + +- **Monetization Model**: Although messaging is currently unlimited, a future monetization strategy will introduce rate limits. Early adopters will retain unlimited text messaging and receive limited free access to premium features (like voice messages) as a token of appreciation. + +## Technical Details + +- **Language & Frameworks**: + - **Python**: The project is built using Python, leveraging its robust ecosystem for asynchronous programming. + - **Asynchronous Operations**: All interactions and database operations run asynchronously for efficient performance. +- **Database**: + - **PostgreSQL**: Utilized for managing user data, conversation logs, and configuration settings. +- **Localization**: + - **gettext**: Implemented for language localization in the settings menu and onboarding process, ensuring a smooth user experience across multiple languages. + +## Installation & Setup + +If you’d like to run or contribute to ChatTutor locally, follow these steps: + + +1. **Clone the Repository**: + + ```bash + git clone https://github.com/LessVegetables/language-bot + ``` +2. **Navigate to the Project Directory**: + + ```bash + cd language-bot + ``` +3. **Create virtual environment** (optional but recommended): + + ```bash + python -m venv venv + ``` +4. **Install Dependencies**: + + ```bash + pip install -r requirements.txt + ``` +5. **Configure Environment Variables**: + Create a `.env` file with the following: + + ```env + BOT_TOKEN= + OPENAI_API_KEY= + + POSTGRES_USER= + POSTGRES_PASSWORD= + POSTGRES_DB= + ``` +6. **Run the Bot**: + + ```bash + python src/mvp.py + ``` + +## Collaborators + +ChatTutor represents a forward-thinking project that leverages natural language processing, asynchronous programming in Python, and robust database management to create a dynamic language learning tool. Key highlights include: + +- **Scalable Architecture**: Efficiently handles high concurrency and user growth through asynchronous operations and PostgreSQL. +- **User-Centric Design**: Focused on enhancing the language learning process by providing a realistic, engaging conversation experience—not as a replacement for human tutors, but as a valuable supplement. +- **Internationalization**: Built with localization in mind, ensuring accessibility and usability for a global audience. +- **Future-Proofing**: With a clear roadmap that includes multi-character support, media enhancements, and tutoring plans, ChatTutor is poised to evolve with the needs of modern language learners. + +## Contributing + +Contributions are welcome! If you'd like to improve ChatTutor, please fork the repository, make your changes, and open a pull request. For major changes, please open an issue first to discuss your ideas. + +## License + +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. \ No newline at end of file From bde611c4f24681b5b4c93cb7a021f5501538f36e Mon Sep 17 00:00:00 2001 From: LessVegetables <57289239+LessVegetables@users.noreply.github.com> Date: Sun, 18 May 2025 17:15:53 -0500 Subject: [PATCH 2/2] Repo restructuring --- app/dockerfile | 15 ++ requirements.txt => app/requirements.txt | 0 app/src/chat.py | 169 ++++++++++++++++++ {src => app/src}/database.py | 54 ++++++ {src => app/src}/db_management.py | 0 {locales => app/src/locales}/bot.pot | 0 .../src/locales}/ru/LC_MESSAGES/bot.mo | Bin .../src/locales}/ru/LC_MESSAGES/bot.po | 0 .../src/locales}/ru/LC_MESSAGES/bot.po~ | 0 app/src/manager.py | 28 +++ {src => app/src}/mvp.py | 20 ++- schema.sql => db/init.sql | 7 +- docker-compose.yaml | 27 +++ src/chat.py | 73 -------- 14 files changed, 317 insertions(+), 76 deletions(-) create mode 100644 app/dockerfile rename requirements.txt => app/requirements.txt (100%) create mode 100644 app/src/chat.py rename {src => app/src}/database.py (74%) rename {src => app/src}/db_management.py (100%) rename {locales => app/src/locales}/bot.pot (100%) rename {locales => app/src/locales}/ru/LC_MESSAGES/bot.mo (100%) rename {locales => app/src/locales}/ru/LC_MESSAGES/bot.po (100%) rename {locales => app/src/locales}/ru/LC_MESSAGES/bot.po~ (100%) create mode 100644 app/src/manager.py rename {src => app/src}/mvp.py (82%) rename schema.sql => db/init.sql (79%) create mode 100644 docker-compose.yaml delete mode 100644 src/chat.py diff --git a/app/dockerfile b/app/dockerfile new file mode 100644 index 0000000..f4e0b66 --- /dev/null +++ b/app/dockerfile @@ -0,0 +1,15 @@ +# Use an official Python image +FROM python:3.11 + +# Set the working directory inside the container +WORKDIR /app + +# Copy requirements and install dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy only the src directory +COPY src/ src/ + +# Run the bot +CMD ["python", "mvp.py"] \ No newline at end of file diff --git a/requirements.txt b/app/requirements.txt similarity index 100% rename from requirements.txt rename to app/requirements.txt diff --git a/app/src/chat.py b/app/src/chat.py new file mode 100644 index 0000000..a1da6b3 --- /dev/null +++ b/app/src/chat.py @@ -0,0 +1,169 @@ +import os +import datetime + +from openai import OpenAI +from dotenv import load_dotenv + +from database import Database + +load_dotenv() + +openAI_client = OpenAI( + api_key=os.getenv("OPENAI_KEY") +) +class MyChatGPT: + def __init__(self, database, model="gpt-4o-mini-2024-07-18"): + self.model = model + self.database = database # Store database reference + + async def message_chatgpt(self, text: str, user_id: int): + + chat_id = await self.database.get_current_chat_id(user_id) + message = await self.generate_prompt(chat_id, text) # get past memory from DB with userID + assistant_response = await self.get_response(message) # get chatgpt response + await self.database.store_conversation(chat_id, text, assistant_response) # store user_message and chatgpt response + + return assistant_response + + + async def get_response(self, message): + completion = openAI_client.chat.completions.create( + model=self.model, + # store=True, + messages=message + ) + return completion.choices[0].message.content + + + # async def generate_prompt(self, chat_id: str, user_message: str) -> list: + + # past_conversation = await self.database.retrieve_conversation(chat_id) + + # messages = [ + # { + # "role": "developer", + # "content": [ + # { + # "type": "text", + # "text": "You are a helpful assistant that answers programming questions." + # } + # ] + # } + # ] + + # # Append previous conversation messages + # for msg in past_conversation: + # messages.append({ + # "role": "user", + # "content": [{"type": "text", "text": msg["user"]}] + # }) + # messages.append({ + # "role": "assistant", + # "content": [{"type": "text", "text": msg["assistant"]}] + # }) + + # # Append the new user message + # messages.append({ + # "role": "user", + # "content": [{"type": "text", "text": user_message}] + # }) + + # print("sending message: ", messages) + + # return messages + + async def generate_prompt(self, chat_id: str, user_message: str) -> list: + # Retrieve dynamic context from your DB + chat_history = await self.database.retrieve_conversation(chat_id) + conversation_summary = await self.database.get_conversation_summary(chat_id) + personality_summary = await self.database.get_bot_personality_summary(chat_id) + user_details = await self.database.get_user_details(chat_id) + + # Dynamic time context + now = datetime.datetime.now() + dynamic_time_context = f"Today is {now.strftime('%B %d, %Y')}. Current time is {now.strftime('%I:%M %p')}." + + # Build system messages with context + messages = [ + # Developer message (highest priority instructions) + { + "role": "developer", + "content": [ + { + "type": "text", + "text": ( + "You are Dave, a friendly, engaging language practice chatbot. Your goal is to help users practice " + "and improve their language skills through natural conversation. Always respond in short, human-like " + "messages. If asked, state: 'My name is Dave and I'm a chatbot.' Avoid overwhelming the user, gently " + "correct mistakes, and adapt to the user's language level. This service is a supplement to tutoring, not " + "a replacement." + ) + } + ] + }, + # System message for dynamic time context and global dynamic context + { + "role": "system", + "content": [ + { + "type": "text", + "text": ( + f"{dynamic_time_context}\n" + f"Conversation Summary: {conversation_summary}\n" + f"Your personality Summary: {personality_summary}\n" + f"User Details: {user_details}\n" + "Note: These summaries and details are dynamically updated to help personalize your responses." + ) + } + ] + } + ] + + # Append previous conversation messages from chat history + for msg in chat_history: + messages.append({ + "role": "user", + "content": [{"type": "text", "text": msg["user"]}] + }) + messages.append({ + "role": "assistant", + "content": [{"type": "text", "text": msg["assistant"]}] + }) + + # Append the new user message + messages.append({ + "role": "user", + "content": [{"type": "text", "text": user_message}] + }) + + print("Sending message payload:", messages) + return messages + + +class HelperChatGPT: + def __init__(self, database, model="gpt-4o-mini-2024-07-18"): + self.model = model + self.database = database + async def generate_conversation_summary(self, conversation_history, current_summary) -> str: + + message = [ + { + "role": "developer", + "content": [ + { + "type" : "text", + "text" : ( + "Please create a summary of the convesation hisotry between an AI assistant and a bot user." + "You are given the convesations history" + ) + } + ] + } + ] + + async def generate_personality_summary(self, personality_summary, current_summary) -> str: + pass + + async def generate_user_details(self, user_details, current_summary) -> str: + pass + diff --git a/src/database.py b/app/src/database.py similarity index 74% rename from src/database.py rename to app/src/database.py index 3fc50da..74174c3 100644 --- a/src/database.py +++ b/app/src/database.py @@ -89,6 +89,7 @@ async def fetch_chat(self, chat_id: str): return await conn.fetchrow("SELECT * FROM chatstable WHERE chatid = $1", uuid.UUID(chat_id)) + # getting informatio from db async def retrieve_conversation(self, chat_id: str) -> list: await self.check_connection() @@ -154,7 +155,45 @@ async def retrieve_conversation(self, chat_id: str) -> list: # ) # return + + async def get_conversation_summary(self, chat_id: str) -> str: + await self.check_connection() + + async with self.pool.acquire() as conn: + async with conn.transaction(): + row = await conn.fetchrow("SELECT conversation_summary FROM chatstable WHERE chatid = $1", chat_id) + if row and row["conversation_summary"]: + conversation_summary = row["conversation_summary"] + else: + conversation_summary = "there is not summary yet. This most likely means that the user is chatting with you for the first time." + return conversation_summary + + async def get_bot_personality_summary(self, chat_id: str) -> str: + await self.check_connection() + + async with self.pool.acquire() as conn: + async with conn.transaction(): + row = await conn.fetchrow("SELECT personality_summary FROM chatstable WHERE chatid = $1", chat_id) + if row and row["personality_summary"]: + bot_personality_summary = row["personality_summary"] + else: + bot_personality_summary = "you have yet to create a personality." + return bot_personality_summary + + async def get_user_details(self, chat_id: str) -> str: + await self.check_connection() + + async with self.pool.acquire() as conn: + async with conn.transaction(): + row = await conn.fetchrow("SELECT user_details FROM chatstable WHERE chatid = $1", chat_id) + if row and row["user_details"]: + user_details = row["user_details"] + else: + user_details = "the user has not provided any details about him/her self yet." + return user_details + + # writing informatio to db async def store_conversation(self, chat_id: str, user_message: str, assistant_response: str): await self.check_connection() @@ -186,3 +225,18 @@ async def store_conversation(self, chat_id: str, user_message: str, assistant_re "UPDATE chatstable SET chatdailyconversation = $1 WHERE chatid = $2", json.dumps(conversation), chat_id ) + + async def store_conversation_summary(self, chat_id: str, new_summary: str) -> str: + await self.check_connection() + + async with self.pool.acquire() as conn: + async with conn.transaction(): + row = await conn.fetchrow( + "SELECT conversation_summary FROM chatstable WHERE chatid = $1", chat_id + ) + + # Store back into the database + await conn.execute( + "UPDATE chatstable SET conversation_summary = $1 WHERE chatid = $2", + new_summary, chat_id + ) diff --git a/src/db_management.py b/app/src/db_management.py similarity index 100% rename from src/db_management.py rename to app/src/db_management.py diff --git a/locales/bot.pot b/app/src/locales/bot.pot similarity index 100% rename from locales/bot.pot rename to app/src/locales/bot.pot diff --git a/locales/ru/LC_MESSAGES/bot.mo b/app/src/locales/ru/LC_MESSAGES/bot.mo similarity index 100% rename from locales/ru/LC_MESSAGES/bot.mo rename to app/src/locales/ru/LC_MESSAGES/bot.mo diff --git a/locales/ru/LC_MESSAGES/bot.po b/app/src/locales/ru/LC_MESSAGES/bot.po similarity index 100% rename from locales/ru/LC_MESSAGES/bot.po rename to app/src/locales/ru/LC_MESSAGES/bot.po diff --git a/locales/ru/LC_MESSAGES/bot.po~ b/app/src/locales/ru/LC_MESSAGES/bot.po~ similarity index 100% rename from locales/ru/LC_MESSAGES/bot.po~ rename to app/src/locales/ru/LC_MESSAGES/bot.po~ diff --git a/app/src/manager.py b/app/src/manager.py new file mode 100644 index 0000000..c83402e --- /dev/null +++ b/app/src/manager.py @@ -0,0 +1,28 @@ +import asyncpg +import asyncio +import uuid +import json + +from chat import HelperChatGPT + + +class BackgroundUserChatProcess: + def __init__(self, database, gpt_helper): + self.database = database + self.gpt_helper = gpt_helper + + async def run_conversation_summary(self, chat_id: str): + ''' + Is in charge of getting/processisng/storing/updating the conversation_summary + + Flow: + 1. gets current conversation_history + 2. if it exist - the process continues + 3. gets current conversation_summary + 4. calls HelperChatGPT to generate a summary + 5. calls database to write the new summary + ''' + + + + pass \ No newline at end of file diff --git a/src/mvp.py b/app/src/mvp.py similarity index 82% rename from src/mvp.py rename to app/src/mvp.py index d8b73de..745a2f1 100644 --- a/src/mvp.py +++ b/app/src/mvp.py @@ -8,7 +8,8 @@ from dotenv import load_dotenv from database import Database -from chat import MyChatGPT +from chat import MyChatGPT, HelperChatGPT +from manager import BackgroundUserChatProcess load_dotenv() @@ -29,6 +30,10 @@ database = Database(DB_DSN) chatgpt = MyChatGPT(database) +helper = HelperChatGPT() +manager = BackgroundUserChatProcess(helper) + + async def keep_typing(chat_id): asyncio.sleep(5) # wait before "typing" the text while True: @@ -70,6 +75,19 @@ async def start_handler(message: Message): await message.answer(response) +# /chat-summary +@dp.message(Command("chat-summary")) +async def chat_summary(message: Message): + + chat_id = await database.get_current_chat_id(message.from_user.id) + summary = await manager.run_conversation_summary(chat_id) + + lang = message.from_user.language_code # Get user’s language + _ = get_translator(lang).gettext # Load correct translation + + await message.answer(_("Your chat summary:\n") + summary) + + @dp.message() async def message_handler(message: Message): user_info = f"Received message from {message.from_user.full_name} (ID: {message.from_user.id})" diff --git a/schema.sql b/db/init.sql similarity index 79% rename from schema.sql rename to db/init.sql index 5b664b6..3d8400d 100644 --- a/schema.sql +++ b/db/init.sql @@ -14,5 +14,8 @@ CREATE TABLE ChatsTable ( -- Summary of the conversation chatdailyconversation JSONB, -- JSON formatted daily conversation - chatsettings JSONB -- JSON formatted chat settings -); + chat_settings JSONB, -- JSON formatted chat settings + conversation_summary TEXT, + personality_summary TEXT, + user_details TEXT +); \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..54107ec --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,27 @@ +version: '3.8' + +services: + bot: + build: . + container_name: language-bot + env_file: + - .env + depends_on: + - db + working_dir: /app/src + restart: always + + db: + image: postgres:16 + container_name: language-bot-db + environment: + POSTGRES_USER: "${POSTGRES_USER}" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" + POSTGRES_DB: "${POSTGRES_DB}" + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + +volumes: + postgres_data: diff --git a/src/chat.py b/src/chat.py deleted file mode 100644 index 0852f84..0000000 --- a/src/chat.py +++ /dev/null @@ -1,73 +0,0 @@ -import os - -from openai import OpenAI -from dotenv import load_dotenv - -from database import Database - -load_dotenv() - -openAI_client = OpenAI( - api_key=os.getenv("OPENAI_KEY") -) - -class MyChatGPT: - def __init__(self, database, model="gpt-4o-mini-2024-07-18"): - self.model = model - self.database = database # Store database reference - - async def message_chatgpt(self, text: str, user_id: int): - - chat_id = await self.database.get_current_chat_id(user_id) - message = await self.generate_prompt(chat_id, text) # get past memory from DB with userID - assistant_response = await self.get_response(message) # get chatgpt response - await self.database.store_conversation(chat_id, text, assistant_response) # store user_message and chatgpt response - - return assistant_response - - - async def get_response(self, message): - completion = openAI_client.chat.completions.create( - model=self.model, - # store=True, - messages=message - ) - return completion.choices[0].message.content - - - async def generate_prompt(self, chat_id: str, user_message: str) -> list: - - past_conversation = await self.database.retrieve_conversation(chat_id) - - messages = [ - { - "role": "developer", - "content": [ - { - "type": "text", - "text": "You are a helpful assistant that answers programming questions." - } - ] - } - ] - - # Append previous conversation messages - for msg in past_conversation: - messages.append({ - "role": "user", - "content": [{"type": "text", "text": msg["user"]}] - }) - messages.append({ - "role": "assistant", - "content": [{"type": "text", "text": msg["assistant"]}] - }) - - # Append the new user message - messages.append({ - "role": "user", - "content": [{"type": "text", "text": user_message}] - }) - - print("sending message: ", messages) - - return messages