Skip to content

Commit 35ed14d

Browse files
authored
Merge pull request #74 from lirielgozi/feature-conversation-history
feature: add conversation history feature to AI assistant
2 parents 0736b5e + c229e2b commit 35ed14d

12 files changed

Lines changed: 1304 additions & 49 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ coverage.xml
6969
.hypothesis/
7070
.pytest_cache/
7171
nosetests.xml
72+
ui/playwright-report/
7273

7374
# mypy
7475
.mypy_cache/
@@ -147,3 +148,4 @@ Pipfile.lock
147148
.tmp/
148149
.temp/
149150
tmpclaude-*-cwd
151+
ui/test-results/

server/routers/assistant_chat.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ async def assistant_chat_websocket(websocket: WebSocket, project_name: str):
260260
data = await websocket.receive_text()
261261
message = json.loads(data)
262262
msg_type = message.get("type")
263-
logger.info(f"Assistant received message type: {msg_type}")
263+
logger.debug(f"Assistant received message type: {msg_type}")
264264

265265
if msg_type == "ping":
266266
await websocket.send_json({"type": "pong"})
@@ -269,18 +269,24 @@ async def assistant_chat_websocket(websocket: WebSocket, project_name: str):
269269
elif msg_type == "start":
270270
# Get optional conversation_id to resume
271271
conversation_id = message.get("conversation_id")
272+
logger.debug(f"Processing start message with conversation_id={conversation_id}")
272273

273274
try:
274275
# Create a new session
276+
logger.debug(f"Creating session for {project_name}")
275277
session = await create_session(
276278
project_name,
277279
project_dir,
278280
conversation_id=conversation_id,
279281
)
282+
logger.debug("Session created, starting...")
280283

281284
# Stream the initial greeting
282285
async for chunk in session.start():
286+
if logger.isEnabledFor(logging.DEBUG):
287+
logger.debug(f"Sending chunk: {chunk.get('type')}")
283288
await websocket.send_json(chunk)
289+
logger.debug("Session start complete")
284290
except Exception as e:
285291
logger.exception(f"Error starting assistant session for {project_name}")
286292
await websocket.send_json({

server/services/assistant_chat_session.py

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from .assistant_database import (
2424
add_message,
2525
create_conversation,
26+
get_messages,
2627
)
2728

2829
# Load environment variables from .env file if present
@@ -178,6 +179,7 @@ def __init__(self, project_name: str, project_dir: Path, conversation_id: Option
178179
self.client: Optional[ClaudeSDKClient] = None
179180
self._client_entered: bool = False
180181
self.created_at = datetime.now()
182+
self._history_loaded: bool = False # Track if we've loaded history for resumed conversations
181183

182184
async def close(self) -> None:
183185
"""Clean up resources and close the Claude client."""
@@ -195,10 +197,14 @@ async def start(self) -> AsyncGenerator[dict, None]:
195197
Initialize session with the Claude client.
196198
197199
Creates a new conversation if none exists, then sends an initial greeting.
200+
For resumed conversations, skips the greeting since history is loaded from DB.
198201
Yields message chunks as they stream in.
199202
"""
203+
# Track if this is a new conversation (for greeting decision)
204+
is_new_conversation = self.conversation_id is None
205+
200206
# Create a new conversation if we don't have one
201-
if self.conversation_id is None:
207+
if is_new_conversation:
202208
conv = create_conversation(self.project_dir, self.project_name)
203209
self.conversation_id = conv.id
204210
yield {"type": "conversation_created", "conversation_id": self.conversation_id}
@@ -260,6 +266,7 @@ async def start(self) -> AsyncGenerator[dict, None]:
260266
model = os.getenv("ANTHROPIC_DEFAULT_OPUS_MODEL", "claude-opus-4-5-20251101")
261267

262268
try:
269+
logger.info("Creating ClaudeSDKClient...")
263270
self.client = ClaudeSDKClient(
264271
options=ClaudeAgentOptions(
265272
model=model,
@@ -276,25 +283,35 @@ async def start(self) -> AsyncGenerator[dict, None]:
276283
env=sdk_env,
277284
)
278285
)
286+
logger.info("Entering Claude client context...")
279287
await self.client.__aenter__()
280288
self._client_entered = True
289+
logger.info("Claude client ready")
281290
except Exception as e:
282291
logger.exception("Failed to create Claude client")
283292
yield {"type": "error", "content": f"Failed to initialize assistant: {str(e)}"}
284293
return
285294

286-
# Send initial greeting
287-
try:
288-
greeting = f"Hello! I'm your project assistant for **{self.project_name}**. I can help you understand the codebase, explain features, and answer questions about the project. What would you like to know?"
295+
# Send initial greeting only for NEW conversations
296+
# Resumed conversations already have history loaded from the database
297+
if is_new_conversation:
298+
# New conversations don't need history loading
299+
self._history_loaded = True
300+
try:
301+
greeting = f"Hello! I'm your project assistant for **{self.project_name}**. I can help you understand the codebase, explain features, and answer questions about the project. What would you like to know?"
289302

290-
# Store the greeting in the database
291-
add_message(self.project_dir, self.conversation_id, "assistant", greeting)
303+
# Store the greeting in the database
304+
add_message(self.project_dir, self.conversation_id, "assistant", greeting)
292305

293-
yield {"type": "text", "content": greeting}
306+
yield {"type": "text", "content": greeting}
307+
yield {"type": "response_done"}
308+
except Exception as e:
309+
logger.exception("Failed to send greeting")
310+
yield {"type": "error", "content": f"Failed to start conversation: {str(e)}"}
311+
else:
312+
# For resumed conversations, history will be loaded on first message
313+
# _history_loaded stays False so send_message() will include history
294314
yield {"type": "response_done"}
295-
except Exception as e:
296-
logger.exception("Failed to send greeting")
297-
yield {"type": "error", "content": f"Failed to start conversation: {str(e)}"}
298315

299316
async def send_message(self, user_message: str) -> AsyncGenerator[dict, None]:
300317
"""
@@ -321,8 +338,32 @@ async def send_message(self, user_message: str) -> AsyncGenerator[dict, None]:
321338
# Store user message in database
322339
add_message(self.project_dir, self.conversation_id, "user", user_message)
323340

341+
# For resumed conversations, include history context in first message
342+
message_to_send = user_message
343+
if not self._history_loaded:
344+
self._history_loaded = True
345+
history = get_messages(self.project_dir, self.conversation_id)
346+
# Exclude the message we just added (last one)
347+
history = history[:-1] if history else []
348+
# Cap history to last 35 messages to prevent context overload
349+
history = history[-35:] if len(history) > 35 else history
350+
if history:
351+
# Format history as context for Claude
352+
history_lines = ["[Previous conversation history for context:]"]
353+
for msg in history:
354+
role = "User" if msg["role"] == "user" else "Assistant"
355+
content = msg["content"]
356+
# Truncate very long messages
357+
if len(content) > 500:
358+
content = content[:500] + "..."
359+
history_lines.append(f"{role}: {content}")
360+
history_lines.append("[End of history. Continue the conversation:]")
361+
history_lines.append(f"User: {user_message}")
362+
message_to_send = "\n".join(history_lines)
363+
logger.info(f"Loaded {len(history)} messages from conversation history")
364+
324365
try:
325-
async for chunk in self._query_claude(user_message):
366+
async for chunk in self._query_claude(message_to_send):
326367
yield chunk
327368
yield {"type": "response_done"}
328369
except Exception as e:

0 commit comments

Comments
 (0)