Skip to content

Commit e9ed9e3

Browse files
authored
Merge pull request #16 from metadroix35/main
Added Support for Chat history
2 parents e3ee1f1 + ba0dd0f commit e9ed9e3

File tree

2 files changed

+556
-2
lines changed

2 files changed

+556
-2
lines changed

backend/chat_history.py

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
"""
2+
Chat History Manager for Pixly
3+
Stores conversation history in ChromaDB with timestamps and game context
4+
"""
5+
from datetime import datetime, timedelta
6+
from typing import List, Dict, Optional
7+
import chromadb
8+
from chromadb.config import Settings
9+
10+
11+
class ChatHistoryManager:
12+
"""Manages chat history storage and retrieval using ChromaDB"""
13+
14+
def __init__(self, persist_directory: str = "./vector_db", max_history: int = 30):
15+
"""
16+
Initialize chat history manager
17+
18+
Args:
19+
persist_directory: Directory for ChromaDB persistence
20+
max_history: Maximum number of chat messages to store per game
21+
"""
22+
self.max_history = max_history
23+
self.client = chromadb.PersistentClient(
24+
path=persist_directory,
25+
settings=Settings(anonymized_telemetry=False)
26+
)
27+
28+
def _get_collection_name(self, game: str) -> str:
29+
"""Generate collection name for game's chat history"""
30+
return f"{game.lower().replace(' ', '_')}_chat_history"
31+
32+
def _get_or_create_collection(self, game: str):
33+
"""Get or create ChromaDB collection for a game's chat history"""
34+
collection_name = self._get_collection_name(game)
35+
try:
36+
collection = self.client.get_collection(name=collection_name)
37+
except:
38+
collection = self.client.create_collection(
39+
name=collection_name,
40+
metadata={"game": game, "type": "chat_history"}
41+
)
42+
return collection
43+
44+
def add_message(self, game: str, user_message: str, assistant_response: str) -> None:
45+
"""
46+
Add a chat exchange to history
47+
48+
Args:
49+
game: Name of the game
50+
user_message: User's input message
51+
assistant_response: Assistant's response
52+
"""
53+
collection = self._get_or_create_collection(game)
54+
timestamp = datetime.now()
55+
56+
# Create unique ID using timestamp
57+
message_id = f"{game}_{timestamp.timestamp()}"
58+
59+
# Store the full conversation exchange as one document
60+
conversation_text = f"User: {user_message}\nAssistant: {assistant_response}"
61+
62+
collection.add(
63+
documents=[conversation_text],
64+
metadatas=[{
65+
"game": game,
66+
"timestamp": timestamp.isoformat(),
67+
"user_message": user_message,
68+
"assistant_response": assistant_response,
69+
"unix_timestamp": timestamp.timestamp()
70+
}],
71+
ids=[message_id]
72+
)
73+
74+
# Maintain max history limit
75+
self._trim_history(game)
76+
77+
def _trim_history(self, game: str) -> None:
78+
"""Remove oldest messages if exceeding max_history limit"""
79+
collection = self._get_or_create_collection(game)
80+
81+
# Get all messages sorted by timestamp
82+
all_messages = collection.get(
83+
include=["metadatas"]
84+
)
85+
86+
if len(all_messages["ids"]) > self.max_history:
87+
# Sort by unix timestamp
88+
messages_with_time = [
89+
(msg_id, meta["unix_timestamp"])
90+
for msg_id, meta in zip(all_messages["ids"], all_messages["metadatas"])
91+
]
92+
messages_with_time.sort(key=lambda x: x[1])
93+
94+
# Delete oldest messages
95+
to_delete = len(all_messages["ids"]) - self.max_history
96+
ids_to_delete = [msg[0] for msg in messages_with_time[:to_delete]]
97+
98+
if ids_to_delete:
99+
collection.delete(ids=ids_to_delete)
100+
101+
def get_recent_history(
102+
self,
103+
game: str,
104+
limit: Optional[int] = None,
105+
hours_ago: Optional[int] = None
106+
) -> List[Dict[str, str]]:
107+
"""
108+
Get recent chat history for a game
109+
110+
Args:
111+
game: Name of the game
112+
limit: Maximum number of messages to retrieve (default: all up to max_history)
113+
hours_ago: Only get messages from last N hours (optional)
114+
115+
Returns:
116+
List of chat exchanges with timestamps
117+
"""
118+
collection = self._get_or_create_collection(game)
119+
120+
try:
121+
all_data = collection.get(include=["metadatas", "documents"])
122+
123+
if not all_data["ids"]:
124+
return []
125+
126+
# Filter by time if specified
127+
messages = []
128+
cutoff_time = None
129+
if hours_ago:
130+
cutoff_time = (datetime.now() - timedelta(hours=hours_ago)).timestamp()
131+
132+
for meta, doc in zip(all_data["metadatas"], all_data["documents"]):
133+
if cutoff_time and meta["unix_timestamp"] < cutoff_time:
134+
continue
135+
136+
messages.append({
137+
"user_message": meta["user_message"],
138+
"assistant_response": meta["assistant_response"],
139+
"timestamp": meta["timestamp"],
140+
"unix_timestamp": meta["unix_timestamp"]
141+
})
142+
143+
# Sort by timestamp (newest first)
144+
messages.sort(key=lambda x: x["unix_timestamp"], reverse=True)
145+
146+
# Apply limit
147+
if limit:
148+
messages = messages[:limit]
149+
150+
# Return in chronological order (oldest first for context)
151+
return list(reversed(messages))
152+
153+
except Exception as e:
154+
print(f"Error retrieving chat history: {e}")
155+
return []
156+
157+
def get_history_context(
158+
self,
159+
game: str,
160+
limit: int = 5,
161+
hours_ago: Optional[int] = None
162+
) -> str:
163+
"""
164+
Get formatted chat history as context string for Gemini
165+
166+
Args:
167+
game: Name of the game
168+
limit: Number of recent messages to include
169+
hours_ago: Only include messages from last N hours
170+
171+
Returns:
172+
Formatted string of recent chat history
173+
"""
174+
history = self.get_recent_history(game, limit=limit, hours_ago=hours_ago)
175+
176+
if not history:
177+
return ""
178+
179+
context_parts = ["Previous conversation history:"]
180+
for msg in history:
181+
timestamp = datetime.fromisoformat(msg["timestamp"])
182+
time_str = timestamp.strftime("%Y-%m-%d %H:%M")
183+
context_parts.append(f"[{time_str}] User: {msg['user_message']}")
184+
context_parts.append(f"[{time_str}] Assistant: {msg['assistant_response']}")
185+
186+
return "\n".join(context_parts)
187+
188+
def search_history(
189+
self,
190+
game: str,
191+
query: str,
192+
n_results: int = 5
193+
) -> List[Dict[str, str]]:
194+
"""
195+
Search chat history using semantic search
196+
197+
Args:
198+
game: Name of the game
199+
query: Search query
200+
n_results: Number of results to return
201+
202+
Returns:
203+
List of relevant chat exchanges
204+
"""
205+
collection = self._get_or_create_collection(game)
206+
207+
try:
208+
results = collection.query(
209+
query_texts=[query],
210+
n_results=n_results,
211+
include=["metadatas", "documents", "distances"]
212+
)
213+
214+
if not results["ids"][0]:
215+
return []
216+
217+
messages = []
218+
for meta, doc, distance in zip(
219+
results["metadatas"][0],
220+
results["documents"][0],
221+
results["distances"][0]
222+
):
223+
messages.append({
224+
"user_message": meta["user_message"],
225+
"assistant_response": meta["assistant_response"],
226+
"timestamp": meta["timestamp"],
227+
"relevance_score": 1 - distance # Convert distance to similarity
228+
})
229+
230+
return messages
231+
232+
except Exception as e:
233+
print(f"Error searching chat history: {e}")
234+
return []
235+
236+
def clear_history(self, game: str) -> None:
237+
"""Clear all chat history for a game"""
238+
collection_name = self._get_collection_name(game)
239+
try:
240+
self.client.delete_collection(name=collection_name)
241+
except:
242+
pass
243+
244+
def get_stats(self, game: str) -> Dict[str, any]:
245+
"""Get statistics about chat history for a game"""
246+
collection = self._get_or_create_collection(game)
247+
248+
try:
249+
all_data = collection.get(include=["metadatas"])
250+
251+
if not all_data["ids"]:
252+
return {
253+
"total_messages": 0,
254+
"oldest_message": None,
255+
"newest_message": None
256+
}
257+
258+
timestamps = [meta["unix_timestamp"] for meta in all_data["metadatas"]]
259+
260+
return {
261+
"total_messages": len(all_data["ids"]),
262+
"oldest_message": datetime.fromtimestamp(min(timestamps)).isoformat(),
263+
"newest_message": datetime.fromtimestamp(max(timestamps)).isoformat()
264+
}
265+
except Exception as e:
266+
print(f"Error getting stats: {e}")
267+
return {"total_messages": 0}

0 commit comments

Comments
 (0)