first commit
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
Web Interface Routers Package
|
||||
Contains all API router modules.
|
||||
"""
|
||||
|
||||
from .conversation_router import router as conversation_router
|
||||
from .agent_router import router as agent_router
|
||||
from .chat_router import router as chat_router
|
||||
from .health_router import router as health_router
|
||||
|
||||
__all__ = [
|
||||
"conversation_router",
|
||||
"agent_router",
|
||||
"chat_router",
|
||||
"health_router"
|
||||
]
|
||||
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
Agent-related router module.
|
||||
"""
|
||||
|
||||
import time
|
||||
from typing import Dict, Any
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from ..models import ModelListResponse, ModelInfo
|
||||
from ..state import get_server_state, ServerState
|
||||
from agent import list_available_models
|
||||
|
||||
router = APIRouter(prefix="/v1", tags=["agents"])
|
||||
|
||||
|
||||
@router.get(
|
||||
"/agents",
|
||||
response_model=Dict[str, Any],
|
||||
dependencies=[Depends(get_server_state)],
|
||||
description="List all active Agent instances for debugging and monitoring",
|
||||
)
|
||||
async def list_agents(state: ServerState = Depends(get_server_state)):
|
||||
"""List all active Agent instances for debugging and monitoring."""
|
||||
if not state.agent_registry:
|
||||
return {"error": "Agent registry not initialized"}
|
||||
|
||||
agents_info = {}
|
||||
for model_name in state.agent_registry.list_agents():
|
||||
agent_info = state.agent_registry.get_agent_info(model_name)
|
||||
if agent_info:
|
||||
# Add instance ID for singleton verification.
|
||||
agent = state.agent_registry.get_agent(model_name)
|
||||
agent_info["instance_id"] = id(agent) if agent else None
|
||||
agents_info[model_name] = agent_info
|
||||
|
||||
return {
|
||||
"registry_stats": state.agent_registry.get_agent_stats(),
|
||||
"agents": agents_info,
|
||||
}
|
||||
|
||||
|
||||
@router.get(
|
||||
"/models",
|
||||
response_model=ModelListResponse,
|
||||
dependencies=[Depends(get_server_state)],
|
||||
description="List all available models for model selection",
|
||||
)
|
||||
async def list_models():
|
||||
"""List available models."""
|
||||
available_models = list_available_models()
|
||||
|
||||
models = []
|
||||
for model_name in available_models:
|
||||
model_info = ModelInfo(
|
||||
id=model_name,
|
||||
object="model",
|
||||
created=int(time.time()),
|
||||
owned_by="simpleagent",
|
||||
permission=None,
|
||||
root=None,
|
||||
parent=None,
|
||||
)
|
||||
models.append(model_info)
|
||||
|
||||
return ModelListResponse(object="list", data=models)
|
||||
@@ -0,0 +1,327 @@
|
||||
"""
|
||||
Chat router module.
|
||||
"""
|
||||
|
||||
import time
|
||||
import json
|
||||
from typing import AsyncGenerator, Any
|
||||
from SimpleLLMFunc import push_error
|
||||
from fastapi import APIRouter, Request, Depends, HTTPException
|
||||
from fastapi.responses import StreamingResponse, JSONResponse
|
||||
|
||||
from ..models import (
|
||||
ChatCompletionRequest,
|
||||
)
|
||||
from context.conversation_manager import Conversation
|
||||
from ..state import get_server_state, ServerState
|
||||
from ..utils import (
|
||||
validate_chat_request,
|
||||
get_or_create_conversation,
|
||||
get_agent_for_model,
|
||||
process_agent_response,
|
||||
create_chat_response,
|
||||
persist_request_images,
|
||||
)
|
||||
from ..error_handlers import create_error_response
|
||||
from SimpleLLMFunc.logger import (
|
||||
app_log,
|
||||
push_warning,
|
||||
log_context,
|
||||
get_location,
|
||||
)
|
||||
from agent import BaseAgent
|
||||
from observability import propagate_conversation_session
|
||||
from react_stream import (
|
||||
event_name_for_output,
|
||||
format_sse,
|
||||
is_response_yield,
|
||||
project_response_to_oai_chunk,
|
||||
serialize_react_output,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/v1/chat", tags=["chat"])
|
||||
|
||||
|
||||
async def _persist_conversation(conversation: Conversation) -> None:
|
||||
try:
|
||||
await conversation.context.persist()
|
||||
conversation.sketch_pad.persist()
|
||||
app_log(
|
||||
f"✅ Auto-saved conversation {conversation.uuid} after stream completion"
|
||||
)
|
||||
except Exception as save_error:
|
||||
push_warning(
|
||||
f"⚠️ Warning: Failed to save conversation {conversation.uuid}: {save_error}"
|
||||
)
|
||||
|
||||
|
||||
async def stream_chat_completion(
|
||||
request: ChatCompletionRequest,
|
||||
request_id: str,
|
||||
conversation: Conversation,
|
||||
agent: BaseAgent,
|
||||
) -> AsyncGenerator[str, None]:
|
||||
"""OpenAI-compatible streaming projection built from ReactOutput."""
|
||||
query, _, raw_user_content = validate_chat_request(request)
|
||||
|
||||
app_log(
|
||||
f"🔍 Starting stream chat completion for conversation {conversation.uuid}, the query is {query}, agent is {agent.name}"
|
||||
)
|
||||
|
||||
created_time = int(time.time())
|
||||
try:
|
||||
with propagate_conversation_session(
|
||||
conversation_id=conversation.uuid,
|
||||
metadata={
|
||||
"model": request.model,
|
||||
"agent_name": agent.name,
|
||||
"request_id": request_id,
|
||||
"transport": "oai_stream",
|
||||
},
|
||||
tags=["cadagent", "oai_stream"],
|
||||
):
|
||||
with conversation:
|
||||
sent_role = False
|
||||
async for output in agent.run(query, raw_user_content=raw_user_content):
|
||||
if not is_response_yield(output):
|
||||
continue
|
||||
|
||||
chunk_obj = project_response_to_oai_chunk(
|
||||
output,
|
||||
request_id=request_id,
|
||||
model=request.model,
|
||||
created_time=created_time,
|
||||
sent_role=sent_role,
|
||||
)
|
||||
if chunk_obj is None:
|
||||
continue
|
||||
|
||||
try:
|
||||
json_str = json.dumps(chunk_obj, ensure_ascii=False)
|
||||
except Exception as encode_err:
|
||||
push_warning(f"Failed to encode projected chunk: {encode_err}")
|
||||
json_str = json.dumps(
|
||||
{"error": str(encode_err)}, ensure_ascii=False
|
||||
)
|
||||
|
||||
first_delta = chunk_obj.get("choices", [{}])[0].get("delta", {})
|
||||
if (
|
||||
isinstance(first_delta, dict)
|
||||
and first_delta.get("role") == "assistant"
|
||||
):
|
||||
sent_role = True
|
||||
|
||||
app_log(f"🔍 Forwarding projected chunk: {json_str}")
|
||||
yield f"data: {json_str}\n\n"
|
||||
|
||||
await _persist_conversation(conversation)
|
||||
|
||||
except Exception as e:
|
||||
err_obj: dict[str, Any] = {
|
||||
"id": request_id,
|
||||
"object": "chat.completion.chunk",
|
||||
"created": created_time,
|
||||
"model": request.model,
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"delta": {"role": "assistant", "content": f"Error: {str(e)}"},
|
||||
"finish_reason": "stop",
|
||||
}
|
||||
],
|
||||
}
|
||||
err_json = json.dumps(err_obj, ensure_ascii=False)
|
||||
push_error(f"🔍 Sending error chunk: {err_json}", location=get_location())
|
||||
yield f"data: {err_json}\n\n"
|
||||
yield "data: [DONE]\n\n"
|
||||
return
|
||||
|
||||
# End signal.
|
||||
yield "data: [DONE]\n\n"
|
||||
|
||||
|
||||
async def stream_chat_events(
|
||||
request: ChatCompletionRequest,
|
||||
conversation: Conversation,
|
||||
agent: BaseAgent,
|
||||
) -> AsyncGenerator[str, None]:
|
||||
"""Native SSE stream that exposes our React event protocol."""
|
||||
query, request_id, raw_user_content = validate_chat_request(request)
|
||||
|
||||
try:
|
||||
with propagate_conversation_session(
|
||||
conversation_id=conversation.uuid,
|
||||
metadata={
|
||||
"model": request.model,
|
||||
"agent_name": agent.name,
|
||||
"request_id": request_id,
|
||||
"transport": "event_stream",
|
||||
},
|
||||
tags=["cadagent", "event_stream"],
|
||||
):
|
||||
with conversation:
|
||||
async for output in agent.run(query, raw_user_content=raw_user_content):
|
||||
payload = serialize_react_output(output, delta_consumer="web")
|
||||
yield format_sse(event_name_for_output(output), payload)
|
||||
|
||||
await _persist_conversation(conversation)
|
||||
except Exception as e:
|
||||
error_payload = {
|
||||
"type": "error",
|
||||
"message": str(e),
|
||||
}
|
||||
yield format_sse("error", error_payload)
|
||||
yield format_sse("done", {"ok": False})
|
||||
return
|
||||
|
||||
yield format_sse("done", {"ok": True})
|
||||
|
||||
|
||||
@router.post(
|
||||
"/completions",
|
||||
dependencies=[Depends(get_server_state)],
|
||||
description="Chat completion endpoint compatible with the OpenAI specification",
|
||||
)
|
||||
async def chat_completions(
|
||||
request: ChatCompletionRequest,
|
||||
http_request: Request,
|
||||
state: ServerState = Depends(get_server_state),
|
||||
):
|
||||
"""Chat completion endpoint compatible with the OpenAI specification."""
|
||||
if not state.agent_registry:
|
||||
return create_error_response(
|
||||
message="Agent registry not initialized",
|
||||
error_type="server_error",
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
if not state.conversation_manager:
|
||||
return create_error_response(
|
||||
message="Conversation manager not initialized",
|
||||
error_type="server_error",
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
# Get conversation ID from the custom header.
|
||||
conversation_id = http_request.headers.get("X-Conversation-ID")
|
||||
|
||||
with log_context(conversation_id=conversation_id):
|
||||
try:
|
||||
# Get or create the conversation.
|
||||
conversation, conversation_id = get_or_create_conversation(
|
||||
conversation_id, state.conversation_manager
|
||||
)
|
||||
|
||||
persist_request_images(request, conversation_id)
|
||||
|
||||
# Get Agent.
|
||||
agent = get_agent_for_model(request.model, state.agent_registry)
|
||||
|
||||
# Validate the request and retrieve the necessary information.
|
||||
query, request_id, raw_user_content = validate_chat_request(request)
|
||||
|
||||
app_log(
|
||||
f"🔍 {request_id} request chat completion for conversation {conversation_id}"
|
||||
)
|
||||
|
||||
# Streaming response.
|
||||
if request.stream:
|
||||
return StreamingResponse(
|
||||
stream_chat_completion(request, request_id, conversation, agent),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"X-Conversation-ID": conversation_id,
|
||||
},
|
||||
)
|
||||
|
||||
# Non-streaming response: return text plus token statistics.
|
||||
(
|
||||
full_response,
|
||||
prompt_tokens,
|
||||
completion_tokens,
|
||||
) = await process_agent_response(
|
||||
query,
|
||||
conversation,
|
||||
agent,
|
||||
raw_user_content=raw_user_content,
|
||||
)
|
||||
response = create_chat_response(
|
||||
request_id,
|
||||
request.model,
|
||||
full_response,
|
||||
prompt_tokens,
|
||||
completion_tokens,
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
content=response.model_dump(),
|
||||
headers={"X-Conversation-ID": conversation_id},
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
# Re-raise HTTPException so FastAPI can handle it.
|
||||
raise
|
||||
except Exception as e:
|
||||
return create_error_response(
|
||||
message=f"Internal server error: {str(e)}",
|
||||
error_type="server_error",
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/events",
|
||||
dependencies=[Depends(get_server_state)],
|
||||
description="Chat event stream endpoint using native ReactEvent Stream SSE",
|
||||
)
|
||||
async def chat_events(
|
||||
request: ChatCompletionRequest,
|
||||
http_request: Request,
|
||||
state: ServerState = Depends(get_server_state),
|
||||
):
|
||||
if not state.agent_registry:
|
||||
return create_error_response(
|
||||
message="Agent registry not initialized",
|
||||
error_type="server_error",
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
if not state.conversation_manager:
|
||||
return create_error_response(
|
||||
message="Conversation manager not initialized",
|
||||
error_type="server_error",
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
conversation_id = http_request.headers.get("X-Conversation-ID")
|
||||
|
||||
with log_context(conversation_id=conversation_id):
|
||||
try:
|
||||
conversation, conversation_id = get_or_create_conversation(
|
||||
conversation_id, state.conversation_manager
|
||||
)
|
||||
persist_request_images(request, conversation_id)
|
||||
agent = get_agent_for_model(request.model, state.agent_registry)
|
||||
validate_chat_request(request)
|
||||
|
||||
return StreamingResponse(
|
||||
stream_chat_events(request, conversation, agent),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"X-Conversation-ID": conversation_id,
|
||||
},
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
return create_error_response(
|
||||
message=f"Internal server error: {str(e)}",
|
||||
error_type="server_error",
|
||||
status_code=500,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -0,0 +1,46 @@
|
||||
"""
|
||||
Health check router module.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter, Depends
|
||||
from typing import Dict, Any
|
||||
|
||||
from ..models import HealthResponse, ServerInfoResponse
|
||||
from ..state import get_server_state, ServerState
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=ServerInfoResponse)
|
||||
async def root():
|
||||
"""Server root path information."""
|
||||
return ServerInfoResponse(
|
||||
name="CADDesigner API Server",
|
||||
version="1.0.0",
|
||||
description="OpenAI-compatible API for CADDesigner",
|
||||
api_version="v1",
|
||||
supported_models=["cadagent"],
|
||||
capabilities=[
|
||||
"chat.completions",
|
||||
"streaming",
|
||||
"tool_calling",
|
||||
"conversation_history",
|
||||
"sketch_pad_storage",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@router.get("/health", response_model=HealthResponse)
|
||||
async def health_check(state: ServerState = Depends(get_server_state)):
|
||||
"""Health check endpoint."""
|
||||
default_agent = (
|
||||
state.agent_registry.get_agent("cadagent") if state.agent_registry else None
|
||||
)
|
||||
|
||||
return HealthResponse(
|
||||
status="ok",
|
||||
timestamp=datetime.now().isoformat(),
|
||||
version="1.0.0",
|
||||
agent_name=default_agent.name if default_agent else "Not initialized",
|
||||
)
|
||||
Reference in New Issue
Block a user