Skip to content

Commit 3538890

Browse files
ChatGPT w/ persistent chat
Setup DB Setup ChatGPT Added Persistent Chat for chatGPT
1 parent e8dbcbb commit 3538890

File tree

7 files changed

+407
-0
lines changed

7 files changed

+407
-0
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.env
2+
.DS_Store
3+
/lb_venv
4+
*.code-workspace
5+
/src/__pycache__
6+
/testing

requirements.txt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
openai
2+
# psycopg2-binary
3+
psycopg2
4+
aiogram
5+
asyncpg
6+
# dependency-injector
7+
8+
9+
10+
11+
12+
# manual db
13+
# brew install postgresql
14+
# brew services start postgresql
15+
# pg_isready
16+
# create new db and user
17+
# createdb mydb
18+
# psql postgres -c "CREATE USER myuser WITH ENCRYPTED PASSWORD 'mypassword';"
19+
# psql postgres -c "GRANT ALL PRIVILEGES ON DATABASE mydb TO myuser;"
20+
# psql postgres -c "CREATE TABLE users (user_id BIGINT PRIMARY KEY, username TEXT, first_name TEXT, last_name TEXT, joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);"
21+
# psql postgres -c "CREATE TABLE messages (id SERIAL PRIMARY KEY, user_id BIGINT REFERENCES users(user_id), message TEXT, timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP);"
22+

schema.sql

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
-- psql -U lbot -d lbproject -f schema.sql
2+
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
3+
CREATE TABLE userchats (
4+
userid BIGINT PRIMARY KEY,
5+
-- Telegram User ID (Persistent)
6+
chatids UUID [],
7+
-- Array of Chat UUIDs associated with the user
8+
activechatid UUID -- The currently active Chat UUID
9+
);
10+
CREATE TABLE ChatsTable (
11+
chatid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
12+
-- Unique Chat UUID
13+
chatmemorysummary TEXT,
14+
-- Summary of the conversation
15+
chatdailyconversation JSONB,
16+
-- JSON formatted daily conversation
17+
chatsettings JSONB -- JSON formatted chat settings
18+
);

src/chat.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import os
2+
3+
from openai import OpenAI
4+
from dotenv import load_dotenv
5+
6+
from database import Database
7+
8+
load_dotenv()
9+
10+
openAI_client = OpenAI(
11+
api_key=os.getenv("OPENAI_KEY")
12+
)
13+
14+
class MyChatGPT:
15+
def __init__(self, database, model="gpt-4o-mini-2024-07-18"):
16+
self.model = model
17+
self.database = database # Store database reference
18+
19+
async def message_chatgpt(self, text: str, user_id: int):
20+
21+
chat_id = await self.database.get_current_chat_id(user_id)
22+
message = await self.generate_prompt(chat_id, text) # get past memory from DB with userID
23+
assistant_response = await self.get_response(message) # get chatgpt response
24+
await self.database.store_conversation(chat_id, text, assistant_response) # store user_message and chatgpt response
25+
26+
return assistant_response
27+
28+
29+
async def get_response(self, message):
30+
completion = openAI_client.chat.completions.create(
31+
model=self.model,
32+
# store=True,
33+
messages=message
34+
)
35+
return completion.choices[0].message.content
36+
37+
38+
async def generate_prompt(self, chat_id: str, user_message: str) -> list:
39+
40+
past_conversation = await self.database.retrieve_conversation(chat_id)
41+
42+
messages = [
43+
{
44+
"role": "developer",
45+
"content": [
46+
{
47+
"type": "text",
48+
"text": "You are a helpful assistant that answers programming questions."
49+
}
50+
]
51+
}
52+
]
53+
54+
# Append previous conversation messages
55+
for msg in past_conversation:
56+
messages.append({
57+
"role": "user",
58+
"content": [{"type": "text", "text": msg["user"]}]
59+
})
60+
messages.append({
61+
"role": "assistant",
62+
"content": [{"type": "text", "text": msg["assistant"]}]
63+
})
64+
65+
# Append the new user message
66+
messages.append({
67+
"role": "user",
68+
"content": [{"type": "text", "text": user_message}]
69+
})
70+
71+
print("sending message: ", messages)
72+
73+
return messages

src/database.py

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import asyncpg
2+
import asyncio
3+
import uuid
4+
import json
5+
6+
class Database:
7+
def __init__(self, dsn: str):
8+
self.dsn = dsn
9+
self.pool = None
10+
11+
async def connect(self):
12+
if self.pool is None:
13+
self.pool = await asyncpg.create_pool(dsn=self.dsn, min_size=1, max_size=10)
14+
15+
async def check_connection(self):
16+
if not self.pool:
17+
raise RuntimeError("Database not connected")
18+
19+
async def close(self):
20+
"""Close the connection pool."""
21+
if self.pool:
22+
await self.pool.close()
23+
self.pool = None
24+
25+
async def fetch(self, query, *args):
26+
await self.check_connection()
27+
28+
async with self.pool.acquire() as conn:
29+
return await conn.fetch(query, *args)
30+
31+
async def execute(self, query, *args):
32+
await self.check_connection()
33+
34+
async with self.pool.acquire() as conn:
35+
return await conn.execute(query, *args)
36+
37+
async def add_user(self, user_id: int):
38+
await self.check_connection()
39+
40+
async with self.pool.acquire() as conn:
41+
async with conn.transaction():
42+
row = await conn.fetchrow("SELECT * FROM userchats WHERE userid = $1", user_id)
43+
44+
if row is not None:
45+
raise Warning("User already exists in the database")
46+
47+
# init first chat with user
48+
chat_id = await self.create_chat(user_id)
49+
50+
# Insert new user into UserChats with the first chat ID
51+
await conn.execute(
52+
"INSERT INTO userchats (userid, chatids, activechatid) VALUES ($1, ARRAY[$2::uuid], $2)",
53+
user_id, chat_id
54+
)
55+
56+
return chat_id
57+
58+
async def create_chat(self, user_id: int) -> uuid:
59+
await self.check_connection()
60+
61+
"""Creates a new chat entry and links it to a user."""
62+
chat_id = uuid.uuid4() # Generate a new UUID
63+
async with self.pool.acquire() as conn:
64+
async with conn.transaction():
65+
await conn.execute(
66+
"INSERT INTO chatstable (chatid, chatmemorysummary, chatdailyconversation, chatsettings) VALUES ($1, '', '[]', '[]')",
67+
chat_id
68+
)
69+
await conn.execute(
70+
"UPDATE userchats SET chatids = array_append(chatids, $1), activechatid = $1 WHERE userid = $2",
71+
chat_id, user_id
72+
)
73+
return chat_id
74+
75+
async def get_current_chat_id(self, user_id: int):
76+
await self.check_connection()
77+
78+
async with self.pool.acquire() as conn:
79+
row = await conn.fetchrow("SELECT activechatid FROM userchats WHERE userid = $1", user_id)
80+
if row:
81+
return row["activechatid"]
82+
else:
83+
raise Warning("User not found in the database")
84+
85+
86+
async def fetch_chat(self, chat_id: str):
87+
"""Fetches a chat by UUID."""
88+
async with self.pool.acquire() as conn:
89+
return await conn.fetchrow("SELECT * FROM chatstable WHERE chatid = $1", uuid.UUID(chat_id))
90+
91+
92+
async def retrieve_conversation(self, chat_id: str) -> list:
93+
await self.check_connection()
94+
95+
async with self.pool.acquire() as conn:
96+
async with conn.transaction():
97+
row = await conn.fetchrow("SELECT chatdailyconversation FROM chatstable WHERE chatid = $1", chat_id)
98+
if row and row["chatdailyconversation"]:
99+
conversation = json.loads(row["chatdailyconversation"])
100+
else:
101+
conversation = []
102+
return conversation
103+
104+
messages = [
105+
{
106+
"role": "developer",
107+
"content": [
108+
{
109+
"type": "text",
110+
"text": "You are a helpful assistant that answers programming questions."
111+
}
112+
]
113+
}
114+
]
115+
116+
# Append previous conversation messages
117+
for msg in conversation:
118+
messages.append({
119+
"role": "user",
120+
"content": [{"type": "text", "text": msg["user"]}]
121+
})
122+
messages.append({
123+
"role": "assistant",
124+
"content": [{"type": "text", "text": msg["assistant"]}]
125+
})
126+
127+
# Append the new user message
128+
messages.append({
129+
"role": "user",
130+
"content": [{"type": "text", "text": user_message}]
131+
})
132+
133+
return messages
134+
135+
# async def store_conversation(self, chat_id: str, user_message: str, assistant_response: str):
136+
# await self.check_connection()
137+
138+
# async with self.pool.acquire() as conn:
139+
# async with conn.transaction():
140+
# row = await conn.fetchrow("SELECT chatdailyconversation FROM chatstable WHERE chatid = $1", chat_id)
141+
142+
# conversation = row["chatdailyconversation"] if row and row["chatdailyconversation"] else []
143+
144+
# # Append the new message pair
145+
# conversation.append({
146+
# "user": user_message,
147+
# "assistant": assistant_response
148+
# })
149+
150+
# # Store back into the database
151+
# await conn.execute(
152+
# "UPDATE chatstable SET chatdailyconversation = $1 WHERE chatid = $2",
153+
# json.dumps(conversation), chat_id
154+
# )
155+
156+
# return
157+
158+
async def store_conversation(self, chat_id: str, user_message: str, assistant_response: str):
159+
await self.check_connection()
160+
161+
async with self.pool.acquire() as conn:
162+
async with conn.transaction():
163+
row = await conn.fetchrow(
164+
"SELECT chatdailyconversation FROM chatstable WHERE chatid = $1", chat_id
165+
)
166+
167+
if row and row["chatdailyconversation"]:
168+
conversation = json.loads(row["chatdailyconversation"])
169+
# If the conversation is a dict (i.e., stored as "{}"), convert it to an empty list.
170+
if isinstance(conversation, dict):
171+
conversation = []
172+
else:
173+
conversation = []
174+
175+
# print("converstaion: ", conversation)
176+
# print("type: ", type(conversation))
177+
178+
# Append the new message pair
179+
conversation.append({
180+
"user": user_message,
181+
"assistant": assistant_response
182+
})
183+
184+
# Store back into the database
185+
await conn.execute(
186+
"UPDATE chatstable SET chatdailyconversation = $1 WHERE chatid = $2",
187+
json.dumps(conversation), chat_id
188+
)

src/db_management.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import os
2+
import asyncio
3+
import psycopg2
4+
5+
6+
# self.db_url = f"postgresql://{os.getenv('POSTGRES_USER')}:{os.getenv('POSTGRES_PASSWORD')}@db:5432/{os.getenv('POSTGRES_DB')}"
7+
8+
9+
# Database connection details
10+
DB_NAME = os.getenv('POSTGRES_DB')
11+
DB_USER = os.getenv('POSTGRES_USER')
12+
DB_PASSWORD = os.getenv('POSTGRES_PASSWORD')
13+
DB_HOST = "db" # matches the 'services:' name in 'docker-compose.yml'
14+
DB_PORT = "5432"
15+
16+
try:
17+
# Connect to the database
18+
conn = psycopg2.connect(
19+
dbname=DB_NAME,
20+
user=DB_USER,
21+
password=DB_PASSWORD,
22+
host=DB_HOST,
23+
port=DB_PORT
24+
)
25+
cur = conn.cursor()
26+
27+
# Test query
28+
cur.execute("SELECT version();")
29+
print("PostgreSQL version:", cur.fetchone())
30+
31+
# Close connection
32+
cur.close()
33+
conn.close()
34+
except Exception as e:
35+
print("Error:", e)

0 commit comments

Comments
 (0)