mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-01-31 14:43:35 +00:00
Merge pull request #74 from lirielgozi/feature-conversation-history
feature: add conversation history feature to AI assistant
This commit is contained in:
@@ -260,7 +260,7 @@ async def assistant_chat_websocket(websocket: WebSocket, project_name: str):
|
||||
data = await websocket.receive_text()
|
||||
message = json.loads(data)
|
||||
msg_type = message.get("type")
|
||||
logger.info(f"Assistant received message type: {msg_type}")
|
||||
logger.debug(f"Assistant received message type: {msg_type}")
|
||||
|
||||
if msg_type == "ping":
|
||||
await websocket.send_json({"type": "pong"})
|
||||
@@ -269,18 +269,24 @@ 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.debug(f"Processing start message with conversation_id={conversation_id}")
|
||||
|
||||
try:
|
||||
# Create a new session
|
||||
logger.debug(f"Creating session for {project_name}")
|
||||
session = await create_session(
|
||||
project_name,
|
||||
project_dir,
|
||||
conversation_id=conversation_id,
|
||||
)
|
||||
logger.debug("Session created, starting...")
|
||||
|
||||
# Stream the initial greeting
|
||||
async for chunk in session.start():
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug(f"Sending chunk: {chunk.get('type')}")
|
||||
await websocket.send_json(chunk)
|
||||
logger.debug("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,32 @@ 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 []
|
||||
# Cap history to last 35 messages to prevent context overload
|
||||
history = history[-35:] if len(history) > 35 else history
|
||||
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