mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-01-30 06:12:06 +00:00
feat: add conversation history feature to AI assistant
- Add ConversationHistory dropdown component with list of past conversations - Add useConversations hook for fetching and managing conversations via React Query - Implement conversation switching with proper state management - Fix bug where reopening panel showed new greeting instead of resuming conversation - Fix bug where selecting from history caused conversation ID to revert - Add server-side history context loading for resumed conversations - Add Playwright E2E tests for conversation history feature - Add logging for debugging conversation flow Key changes: - AssistantPanel: manages conversation state with localStorage persistence - AssistantChat: header with [+] New Chat and [History] buttons - Server: skips greeting for resumed conversations, loads history context on first message - Fixed race condition in onConversationCreated callback
This commit is contained in:
@@ -269,18 +269,23 @@ async def assistant_chat_websocket(websocket: WebSocket, project_name: str):
|
||||
elif msg_type == "start":
|
||||
# Get optional conversation_id to resume
|
||||
conversation_id = message.get("conversation_id")
|
||||
logger.info(f"Processing start message with conversation_id={conversation_id}")
|
||||
|
||||
try:
|
||||
# Create a new session
|
||||
logger.info(f"Creating session for {project_name}")
|
||||
session = await create_session(
|
||||
project_name,
|
||||
project_dir,
|
||||
conversation_id=conversation_id,
|
||||
)
|
||||
logger.info(f"Session created, starting...")
|
||||
|
||||
# Stream the initial greeting
|
||||
async for chunk in session.start():
|
||||
logger.info(f"Sending chunk: {chunk.get('type')}")
|
||||
await websocket.send_json(chunk)
|
||||
logger.info("Session start complete")
|
||||
except Exception as e:
|
||||
logger.exception(f"Error starting assistant session for {project_name}")
|
||||
await websocket.send_json({
|
||||
|
||||
@@ -23,6 +23,7 @@ from dotenv import load_dotenv
|
||||
from .assistant_database import (
|
||||
add_message,
|
||||
create_conversation,
|
||||
get_messages,
|
||||
)
|
||||
|
||||
# Load environment variables from .env file if present
|
||||
@@ -178,6 +179,7 @@ class AssistantChatSession:
|
||||
self.client: Optional[ClaudeSDKClient] = None
|
||||
self._client_entered: bool = False
|
||||
self.created_at = datetime.now()
|
||||
self._history_loaded: bool = False # Track if we've loaded history for resumed conversations
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Clean up resources and close the Claude client."""
|
||||
@@ -195,10 +197,14 @@ class AssistantChatSession:
|
||||
Initialize session with the Claude client.
|
||||
|
||||
Creates a new conversation if none exists, then sends an initial greeting.
|
||||
For resumed conversations, skips the greeting since history is loaded from DB.
|
||||
Yields message chunks as they stream in.
|
||||
"""
|
||||
# Track if this is a new conversation (for greeting decision)
|
||||
is_new_conversation = self.conversation_id is None
|
||||
|
||||
# Create a new conversation if we don't have one
|
||||
if self.conversation_id is None:
|
||||
if is_new_conversation:
|
||||
conv = create_conversation(self.project_dir, self.project_name)
|
||||
self.conversation_id = conv.id
|
||||
yield {"type": "conversation_created", "conversation_id": self.conversation_id}
|
||||
@@ -260,6 +266,7 @@ class AssistantChatSession:
|
||||
model = os.getenv("ANTHROPIC_DEFAULT_OPUS_MODEL", "claude-opus-4-5-20251101")
|
||||
|
||||
try:
|
||||
logger.info("Creating ClaudeSDKClient...")
|
||||
self.client = ClaudeSDKClient(
|
||||
options=ClaudeAgentOptions(
|
||||
model=model,
|
||||
@@ -276,25 +283,35 @@ class AssistantChatSession:
|
||||
env=sdk_env,
|
||||
)
|
||||
)
|
||||
logger.info("Entering Claude client context...")
|
||||
await self.client.__aenter__()
|
||||
self._client_entered = True
|
||||
logger.info("Claude client ready")
|
||||
except Exception as e:
|
||||
logger.exception("Failed to create Claude client")
|
||||
yield {"type": "error", "content": f"Failed to initialize assistant: {str(e)}"}
|
||||
return
|
||||
|
||||
# Send initial greeting
|
||||
try:
|
||||
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?"
|
||||
# Send initial greeting only for NEW conversations
|
||||
# Resumed conversations already have history loaded from the database
|
||||
if is_new_conversation:
|
||||
# New conversations don't need history loading
|
||||
self._history_loaded = True
|
||||
try:
|
||||
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?"
|
||||
|
||||
# Store the greeting in the database
|
||||
add_message(self.project_dir, self.conversation_id, "assistant", greeting)
|
||||
# Store the greeting in the database
|
||||
add_message(self.project_dir, self.conversation_id, "assistant", greeting)
|
||||
|
||||
yield {"type": "text", "content": greeting}
|
||||
yield {"type": "text", "content": greeting}
|
||||
yield {"type": "response_done"}
|
||||
except Exception as e:
|
||||
logger.exception("Failed to send greeting")
|
||||
yield {"type": "error", "content": f"Failed to start conversation: {str(e)}"}
|
||||
else:
|
||||
# For resumed conversations, history will be loaded on first message
|
||||
# _history_loaded stays False so send_message() will include history
|
||||
yield {"type": "response_done"}
|
||||
except Exception as e:
|
||||
logger.exception("Failed to send greeting")
|
||||
yield {"type": "error", "content": f"Failed to start conversation: {str(e)}"}
|
||||
|
||||
async def send_message(self, user_message: str) -> AsyncGenerator[dict, None]:
|
||||
"""
|
||||
@@ -321,8 +338,30 @@ class AssistantChatSession:
|
||||
# Store user message in database
|
||||
add_message(self.project_dir, self.conversation_id, "user", user_message)
|
||||
|
||||
# For resumed conversations, include history context in first message
|
||||
message_to_send = user_message
|
||||
if not self._history_loaded:
|
||||
self._history_loaded = True
|
||||
history = get_messages(self.project_dir, self.conversation_id)
|
||||
# Exclude the message we just added (last one)
|
||||
history = history[:-1] if history else []
|
||||
if history:
|
||||
# Format history as context for Claude
|
||||
history_lines = ["[Previous conversation history for context:]"]
|
||||
for msg in history:
|
||||
role = "User" if msg["role"] == "user" else "Assistant"
|
||||
content = msg["content"]
|
||||
# Truncate very long messages
|
||||
if len(content) > 500:
|
||||
content = content[:500] + "..."
|
||||
history_lines.append(f"{role}: {content}")
|
||||
history_lines.append("[End of history. Continue the conversation:]")
|
||||
history_lines.append(f"User: {user_message}")
|
||||
message_to_send = "\n".join(history_lines)
|
||||
logger.info(f"Loaded {len(history)} messages from conversation history")
|
||||
|
||||
try:
|
||||
async for chunk in self._query_claude(user_message):
|
||||
async for chunk in self._query_claude(message_to_send):
|
||||
yield chunk
|
||||
yield {"type": "response_done"}
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user