first commit
This commit is contained in:
@@ -0,0 +1,484 @@
|
||||
"""
|
||||
Conversation-related router module.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional
|
||||
from urllib.parse import quote
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
# Removed unused imports.
|
||||
from ..state import get_server_state, ServerState
|
||||
from ..error_handlers import create_error_response
|
||||
from ..artifacts import (
|
||||
extract_latest_artifacts,
|
||||
guess_content_type,
|
||||
resolve_project_path,
|
||||
)
|
||||
from config.config import get_config
|
||||
|
||||
router = APIRouter(prefix="/v1/conversations", tags=["conversations"])
|
||||
|
||||
|
||||
def _build_artifact_url(conversation_id: str, path: Path) -> str:
|
||||
encoded_path = quote(str(path), safe="")
|
||||
return f"/v1/conversations/{conversation_id}/artifacts/raw?path={encoded_path}"
|
||||
|
||||
|
||||
def _read_text_artifact(path: Path) -> Optional[str]:
|
||||
try:
|
||||
return path.read_text(encoding="utf-8")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _serialize_artifact_file(
|
||||
conversation_id: str,
|
||||
path: Path,
|
||||
include_content: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
payload: Dict[str, Any] = {
|
||||
"path": str(path),
|
||||
"content_type": guess_content_type(path),
|
||||
"url": _build_artifact_url(conversation_id, path),
|
||||
}
|
||||
|
||||
if include_content:
|
||||
payload["content"] = _read_text_artifact(path)
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
@router.get(
|
||||
"",
|
||||
response_model=Dict[str, Any],
|
||||
dependencies=[Depends(get_server_state)],
|
||||
description="List all available conversations",
|
||||
)
|
||||
async def list_conversations(state: ServerState = Depends(get_server_state)):
|
||||
"""List all available conversations."""
|
||||
if not state.conversation_manager:
|
||||
return create_error_response(
|
||||
message="Conversation manager not initialized",
|
||||
error_type="server_error",
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
try:
|
||||
conversations = state.conversation_manager.list_conversations()
|
||||
return {"conversations": conversations, "total_count": len(conversations)}
|
||||
except Exception as e:
|
||||
return create_error_response(
|
||||
message=f"Failed to list conversations: {str(e)}",
|
||||
error_type="server_error",
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"",
|
||||
response_model=Dict[str, Any],
|
||||
dependencies=[Depends(get_server_state)],
|
||||
description="Create a new conversation",
|
||||
)
|
||||
async def create_conversation(
|
||||
state: ServerState = Depends(get_server_state),
|
||||
):
|
||||
"""Create a new conversation.
|
||||
|
||||
Args:
|
||||
llm_interface: LLM interface name; if None, use the default interface
|
||||
max_history_length: Maximum Context history length
|
||||
"""
|
||||
if not state.conversation_manager:
|
||||
return create_error_response(
|
||||
message="Conversation manager not initialized",
|
||||
error_type="server_error",
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
try:
|
||||
llm_obj = get_config().CONTEXT_SUMMARY_INTERFACE
|
||||
max_history_length = get_config().CONTEXT_MAX_HISTORY_LENGTH
|
||||
conversation = state.conversation_manager.create_conversation(
|
||||
llm_interface=llm_obj, max_history_length=max_history_length
|
||||
)
|
||||
|
||||
return {
|
||||
"conversation_id": conversation.uuid,
|
||||
"created_at": conversation.created_at.isoformat(),
|
||||
"last_accessed": conversation.last_accessed.isoformat(),
|
||||
}
|
||||
except Exception as e:
|
||||
return create_error_response(
|
||||
message=f"Failed to create conversation: {str(e)}",
|
||||
error_type="server_error",
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{conversation_id}",
|
||||
response_model=Dict[str, Any],
|
||||
dependencies=[Depends(get_server_state)],
|
||||
description="Get information for the specified conversation",
|
||||
)
|
||||
async def get_conversation(
|
||||
conversation_id: str, state: ServerState = Depends(get_server_state)
|
||||
):
|
||||
"""Get information for the specified conversation."""
|
||||
if not state.conversation_manager:
|
||||
return create_error_response(
|
||||
message="Conversation manager not initialized",
|
||||
error_type="server_error",
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
try:
|
||||
conversation = state.conversation_manager.get_conversation(conversation_id)
|
||||
if conversation is None:
|
||||
return create_error_response(
|
||||
message=f"Conversation {conversation_id} not found",
|
||||
error_type="not_found",
|
||||
status_code=404,
|
||||
)
|
||||
|
||||
# Get conversation statistics.
|
||||
with conversation:
|
||||
try:
|
||||
history_count = conversation.context.get_total_message_count()
|
||||
sketch_stats = conversation.sketch_pad.get_statistics()
|
||||
# Convert the SketchPadStatistics object to a dictionary.
|
||||
sketch_stats_dict = sketch_stats.model_dump()
|
||||
except Exception as e:
|
||||
history_count = 0
|
||||
sketch_stats_dict = {}
|
||||
|
||||
return {
|
||||
"conversation_id": conversation.uuid,
|
||||
"created_at": conversation.created_at.isoformat(),
|
||||
"last_accessed": conversation.last_accessed.isoformat(),
|
||||
"message_count": history_count,
|
||||
"sketch_stats": sketch_stats_dict,
|
||||
}
|
||||
except Exception as e:
|
||||
return create_error_response(
|
||||
message=f"Failed to get conversation: {str(e)}",
|
||||
error_type="server_error",
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"",
|
||||
response_model=Dict[str, Any],
|
||||
dependencies=[Depends(get_server_state)],
|
||||
description="Delete all conversations",
|
||||
)
|
||||
async def delete_all_conversations(state: ServerState = Depends(get_server_state)):
|
||||
"""Delete all conversations."""
|
||||
if not state.conversation_manager:
|
||||
return create_error_response(
|
||||
message="Conversation manager not initialized",
|
||||
error_type="server_error",
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
try:
|
||||
deleted_ids = state.conversation_manager.delete_all_conversations()
|
||||
return {
|
||||
"deleted": True,
|
||||
"deleted_count": len(deleted_ids),
|
||||
"conversation_ids": deleted_ids,
|
||||
}
|
||||
except Exception as e:
|
||||
return create_error_response(
|
||||
message=f"Failed to delete all conversations: {str(e)}",
|
||||
error_type="server_error",
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{conversation_id}",
|
||||
response_model=Dict[str, Any],
|
||||
dependencies=[Depends(get_server_state)],
|
||||
description="Delete the specified conversation",
|
||||
)
|
||||
async def delete_conversation(
|
||||
conversation_id: str, state: ServerState = Depends(get_server_state)
|
||||
):
|
||||
"""Delete the specified conversation."""
|
||||
if not state.conversation_manager:
|
||||
return create_error_response(
|
||||
message="Conversation manager not initialized",
|
||||
error_type="server_error",
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
try:
|
||||
success = state.conversation_manager.delete_conversation(conversation_id)
|
||||
if not success:
|
||||
return create_error_response(
|
||||
message=f"Conversation {conversation_id} not found",
|
||||
error_type="not_found",
|
||||
status_code=404,
|
||||
)
|
||||
|
||||
return {"deleted": True, "conversation_id": conversation_id}
|
||||
except Exception as e:
|
||||
return create_error_response(
|
||||
message=f"Failed to delete conversation: {str(e)}",
|
||||
error_type="server_error",
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{conversation_id}/history",
|
||||
response_model=Dict[str, Any],
|
||||
dependencies=[Depends(get_server_state)],
|
||||
description="Get the conversation history for the specified conversation",
|
||||
)
|
||||
async def get_conversation_history(
|
||||
conversation_id: str,
|
||||
limit: Optional[int] = None,
|
||||
state: ServerState = Depends(get_server_state),
|
||||
):
|
||||
"""Get the conversation history for the specified conversation."""
|
||||
if not state.conversation_manager:
|
||||
return create_error_response(
|
||||
message="Conversation manager not initialized",
|
||||
error_type="server_error",
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
try:
|
||||
conversation = state.conversation_manager.get_conversation(conversation_id)
|
||||
if conversation is None:
|
||||
return create_error_response(
|
||||
message=f"Conversation {conversation_id} not found",
|
||||
error_type="not_found",
|
||||
status_code=404,
|
||||
)
|
||||
|
||||
# Get conversation history.
|
||||
with conversation:
|
||||
try:
|
||||
# Get the complete conversation history.
|
||||
history = [
|
||||
message_item.model_dump()
|
||||
for message_item in conversation.context.retrieve_full_messages()
|
||||
]
|
||||
total_messages = len(history)
|
||||
|
||||
# If a limit is specified, return only the most recent messages.
|
||||
if limit and limit > 0:
|
||||
history = history[-limit:]
|
||||
|
||||
return {
|
||||
"conversation_id": conversation_id,
|
||||
"messages": history,
|
||||
"total_messages": total_messages,
|
||||
"has_more": total_messages > len(history) if limit else False,
|
||||
}
|
||||
except Exception as e:
|
||||
return create_error_response(
|
||||
message=f"Failed to access conversation history: {str(e)}",
|
||||
error_type="server_error",
|
||||
status_code=500,
|
||||
)
|
||||
except Exception as e:
|
||||
return create_error_response(
|
||||
message=f"Failed to get conversation history: {str(e)}",
|
||||
error_type="server_error",
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{conversation_id}/sketchpad",
|
||||
response_model=Dict[str, Any],
|
||||
dependencies=[Depends(get_server_state)],
|
||||
description="Get the SketchPad content for the specified conversation",
|
||||
)
|
||||
async def get_conversation_sketchpad(
|
||||
conversation_id: str, state: ServerState = Depends(get_server_state)
|
||||
):
|
||||
"""Get the SketchPad content for the specified conversation."""
|
||||
if not state.conversation_manager:
|
||||
return create_error_response(
|
||||
message="Conversation manager not initialized",
|
||||
error_type="server_error",
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
try:
|
||||
conversation = state.conversation_manager.get_conversation(conversation_id)
|
||||
if conversation is None:
|
||||
return create_error_response(
|
||||
message=f"Conversation {conversation_id} not found",
|
||||
error_type="not_found",
|
||||
status_code=404,
|
||||
)
|
||||
|
||||
# Get SketchPad content.
|
||||
with conversation:
|
||||
try:
|
||||
# Get sketch items that include concrete content.
|
||||
sketch_items = conversation.sketch_pad.list_items(include_value=True)
|
||||
sketch_stats = conversation.sketch_pad.get_statistics()
|
||||
|
||||
return {
|
||||
"conversation_id": conversation_id,
|
||||
"sketch_items": sketch_items,
|
||||
"statistics": sketch_stats,
|
||||
"total_items": len(sketch_items),
|
||||
}
|
||||
except Exception as e:
|
||||
return create_error_response(
|
||||
message=f"Failed to access SketchPad: {str(e)}",
|
||||
error_type="server_error",
|
||||
status_code=500,
|
||||
)
|
||||
except Exception as e:
|
||||
return create_error_response(
|
||||
message=f"Failed to get SketchPad: {str(e)}",
|
||||
error_type="server_error",
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{conversation_id}/artifacts/latest",
|
||||
response_model=Dict[str, Any],
|
||||
dependencies=[Depends(get_server_state)],
|
||||
description="Get the latest generated code and model preview information for the specified conversation",
|
||||
)
|
||||
async def get_latest_conversation_artifacts(
|
||||
conversation_id: str,
|
||||
state: ServerState = Depends(get_server_state),
|
||||
):
|
||||
if not state.conversation_manager:
|
||||
return create_error_response(
|
||||
message="Conversation manager not initialized",
|
||||
error_type="server_error",
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
try:
|
||||
conversation = state.conversation_manager.get_conversation(conversation_id)
|
||||
if conversation is None:
|
||||
return create_error_response(
|
||||
message=f"Conversation {conversation_id} not found",
|
||||
error_type="not_found",
|
||||
status_code=404,
|
||||
)
|
||||
|
||||
with conversation:
|
||||
history = [
|
||||
message_item.model_dump()
|
||||
for message_item in conversation.context.retrieve_full_messages()
|
||||
]
|
||||
|
||||
artifacts = extract_latest_artifacts(history)
|
||||
code_path = artifacts.get("code_path")
|
||||
code_paths = artifacts.get("code_paths", [])
|
||||
model_path = artifacts.get("model_path")
|
||||
model_paths = artifacts.get("model_paths", [])
|
||||
output_paths = artifacts.get("output_paths", [])
|
||||
|
||||
response: Dict[str, Any] = {
|
||||
"conversation_id": conversation_id,
|
||||
"code_file": None,
|
||||
"code_files": [],
|
||||
"model_file": None,
|
||||
"model_files": [],
|
||||
"output_files": [str(path) for path in output_paths],
|
||||
}
|
||||
|
||||
if isinstance(code_path, Path) and code_path.is_file():
|
||||
response["code_file"] = _serialize_artifact_file(
|
||||
conversation_id,
|
||||
code_path,
|
||||
include_content=True,
|
||||
)
|
||||
|
||||
response["code_files"] = [
|
||||
_serialize_artifact_file(
|
||||
conversation_id,
|
||||
candidate_path,
|
||||
include_content=True,
|
||||
)
|
||||
for candidate_path in code_paths
|
||||
if isinstance(candidate_path, Path) and candidate_path.is_file()
|
||||
]
|
||||
|
||||
if isinstance(model_path, Path) and model_path.is_file():
|
||||
response["model_file"] = _serialize_artifact_file(
|
||||
conversation_id,
|
||||
model_path,
|
||||
)
|
||||
|
||||
response["model_files"] = [
|
||||
_serialize_artifact_file(conversation_id, candidate_path)
|
||||
for candidate_path in model_paths
|
||||
if isinstance(candidate_path, Path) and candidate_path.is_file()
|
||||
]
|
||||
|
||||
return response
|
||||
except Exception as e:
|
||||
return create_error_response(
|
||||
message=f"Failed to get latest artifacts: {str(e)}",
|
||||
error_type="server_error",
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{conversation_id}/artifacts/raw",
|
||||
dependencies=[Depends(get_server_state)],
|
||||
description="Get the raw content of an artifact file for the specified conversation",
|
||||
)
|
||||
async def get_conversation_artifact_file(
|
||||
conversation_id: str,
|
||||
path: str,
|
||||
state: ServerState = Depends(get_server_state),
|
||||
):
|
||||
if not state.conversation_manager:
|
||||
return create_error_response(
|
||||
message="Conversation manager not initialized",
|
||||
error_type="server_error",
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
try:
|
||||
conversation = state.conversation_manager.get_conversation(conversation_id)
|
||||
if conversation is None:
|
||||
return create_error_response(
|
||||
message=f"Conversation {conversation_id} not found",
|
||||
error_type="not_found",
|
||||
status_code=404,
|
||||
)
|
||||
|
||||
resolved_path = resolve_project_path(path)
|
||||
if resolved_path is None or not resolved_path.is_file():
|
||||
return create_error_response(
|
||||
message="Artifact file not found or outside project root",
|
||||
error_type="not_found",
|
||||
status_code=404,
|
||||
)
|
||||
|
||||
return FileResponse(
|
||||
path=str(resolved_path),
|
||||
media_type=guess_content_type(resolved_path),
|
||||
filename=resolved_path.name,
|
||||
)
|
||||
except Exception as e:
|
||||
return create_error_response(
|
||||
message=f"Failed to get artifact file: {str(e)}",
|
||||
error_type="server_error",
|
||||
status_code=500,
|
||||
)
|
||||
Reference in New Issue
Block a user