mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-09-04 17:15:18 +03:00
feat(chat-api): refactor design document with concise architecture overview
- Replace detailed implementation plan with high-level architecture design - Focus on core features: project isolation, streaming responses, session management - Remove FlowNet-Lab reference and implementation specifics - Streamline document from 1172 to 483 lines for better maintainability
This commit is contained in:
parent
b6c343c5b8
commit
03ab9cdf6c
File diff suppressed because it is too large
Load Diff
@ -7,8 +7,10 @@ in the project directory.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import AsyncGenerator, Dict, Any, Optional
|
||||
from uuid import uuid4
|
||||
|
||||
@ -17,6 +19,7 @@ from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
|
||||
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
|
||||
|
||||
from gns3server.agent.gns3_copilot.agent.gns3_copilot import agent_builder
|
||||
from gns3server.agent.gns3_copilot.chat_sessions_repository import ChatSessionsRepository
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -84,12 +87,55 @@ class AgentService:
|
||||
# CRITICAL: Initialize database schema
|
||||
await self._checkpointer.setup()
|
||||
|
||||
# Create chat_sessions table in the same database
|
||||
await self._create_chat_sessions_table(conn)
|
||||
|
||||
self._checkpointer_path = checkpointer_path
|
||||
self._initialized = True
|
||||
|
||||
log.info("Project checkpointer created at: %s", checkpointer_path)
|
||||
return self._checkpointer
|
||||
|
||||
async def _create_chat_sessions_table(self, conn: aiosqlite.Connection):
|
||||
"""
|
||||
Create the chat_sessions table in the checkpoint database.
|
||||
|
||||
Args:
|
||||
conn: aiosqlite connection
|
||||
"""
|
||||
await conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS chat_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
thread_id TEXT UNIQUE NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
title TEXT DEFAULT 'New Conversation',
|
||||
|
||||
-- Statistics
|
||||
message_count INTEGER DEFAULT 0,
|
||||
llm_calls_count INTEGER DEFAULT 0,
|
||||
input_tokens INTEGER DEFAULT 0,
|
||||
output_tokens INTEGER DEFAULT 0,
|
||||
total_tokens INTEGER DEFAULT 0,
|
||||
|
||||
-- Timestamps
|
||||
last_message_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- Reserved fields (JSON strings)
|
||||
metadata TEXT DEFAULT '{}',
|
||||
stats TEXT DEFAULT '{}'
|
||||
)
|
||||
""")
|
||||
|
||||
# Create indexes
|
||||
await conn.execute("CREATE INDEX IF NOT EXISTS idx_thread_id ON chat_sessions(thread_id)")
|
||||
await conn.execute("CREATE INDEX IF NOT EXISTS idx_user_project ON chat_sessions(user_id, project_id)")
|
||||
|
||||
await conn.commit()
|
||||
log.debug("chat_sessions table created in checkpoint database")
|
||||
|
||||
async def _get_graph(self):
|
||||
"""Get or compile the LangGraph agent."""
|
||||
if self._graph is None:
|
||||
@ -131,6 +177,21 @@ class AgentService:
|
||||
mode,
|
||||
)
|
||||
|
||||
# Get or create chat session
|
||||
repo = ChatSessionsRepository(self._checkpointer_conn)
|
||||
session = await repo.get_session_by_thread(session_id)
|
||||
is_new_session = session is None
|
||||
|
||||
if is_new_session:
|
||||
# Create new session
|
||||
session = await repo.create_session(
|
||||
thread_id=session_id,
|
||||
user_id=user_id or "",
|
||||
project_id=project_id or "",
|
||||
title="New Conversation"
|
||||
)
|
||||
log.debug("Created new chat session: thread_id=%s", session_id)
|
||||
|
||||
# Set request-scoped context variables (memory-only, not persisted)
|
||||
if jwt_token:
|
||||
from gns3server.agent.gns3_copilot.gns3_client import set_current_jwt_token
|
||||
@ -165,14 +226,59 @@ class AgentService:
|
||||
graph = await self._get_graph()
|
||||
log.debug("LangGraph graph obtained, starting stream")
|
||||
|
||||
# Track statistics for session update
|
||||
message_count = 1 # User message
|
||||
llm_calls_count = 0
|
||||
tool_calls_count = 0
|
||||
input_tokens = 0
|
||||
output_tokens = 0
|
||||
last_message_at = datetime.utcnow().isoformat()
|
||||
|
||||
# Stream events
|
||||
try:
|
||||
async for event in graph.astream_events(inputs, config=config, version="v2"):
|
||||
chunk = self._convert_event_to_chunk(event, session_id)
|
||||
if chunk:
|
||||
# Track statistics
|
||||
if chunk.get("type") == "content":
|
||||
message_count += 1 # AI response
|
||||
elif chunk.get("type") == "tool_start":
|
||||
tool_calls_count += 1
|
||||
elif chunk.get("type") == "tool_end":
|
||||
message_count += 1 # Tool message
|
||||
|
||||
# Track tokens if available
|
||||
if chunk.get("type") == "content" and "input_tokens" in chunk:
|
||||
input_tokens += chunk.get("input_tokens", 0)
|
||||
if chunk.get("type") == "content" and "output_tokens" in chunk:
|
||||
output_tokens += chunk.get("output_tokens", 0)
|
||||
|
||||
log.debug("Yielding chunk: type=%s", chunk.get("type"))
|
||||
yield chunk
|
||||
|
||||
# Update session statistics after successful stream
|
||||
await repo.update_session(
|
||||
thread_id=session_id,
|
||||
message_count=message_count,
|
||||
llm_calls_count=llm_calls_count,
|
||||
input_tokens=input_tokens,
|
||||
output_tokens=output_tokens,
|
||||
total_tokens=input_tokens + output_tokens,
|
||||
last_message_at=last_message_at
|
||||
)
|
||||
log.debug("Updated session statistics: thread_id=%s, messages=%d, tokens=%d",
|
||||
session_id, message_count, input_tokens + output_tokens)
|
||||
|
||||
# Sync auto-generated title from checkpoint state
|
||||
final_state = await graph.aget_state(config)
|
||||
if final_state and "conversation_title" in final_state.values:
|
||||
generated_title = final_state.values["conversation_title"]
|
||||
current_session = await repo.get_session_by_thread(session_id)
|
||||
if current_session and current_session.title != generated_title:
|
||||
await repo.update_session(thread_id=session_id, title=generated_title)
|
||||
log.info("Auto-generated title synced: thread_id=%s, title=%s",
|
||||
session_id, generated_title)
|
||||
|
||||
except Exception as e:
|
||||
log.error("Error in stream_chat: %s", e, exc_info=True)
|
||||
yield {"type": "error", "error": str(e), "session_id": session_id}
|
||||
@ -288,6 +394,58 @@ class AgentService:
|
||||
|
||||
return result
|
||||
|
||||
async def list_sessions(self, user_id: Optional[str] = None, limit: int = 100) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
List chat sessions for this project.
|
||||
|
||||
Args:
|
||||
user_id: Filter by user ID (optional)
|
||||
limit: Maximum number of sessions to return
|
||||
|
||||
Returns:
|
||||
List of session dictionaries
|
||||
"""
|
||||
if not self._checkpointer_conn:
|
||||
await self._get_checkpointer()
|
||||
|
||||
repo = ChatSessionsRepository(self._checkpointer_conn)
|
||||
sessions = await repo.list_sessions(user_id=user_id, limit=limit)
|
||||
return [s.to_dict() for s in sessions]
|
||||
|
||||
async def delete_session(self, session_id: str) -> bool:
|
||||
"""
|
||||
Delete a chat session and its checkpoints.
|
||||
|
||||
Args:
|
||||
session_id: Thread ID to delete
|
||||
|
||||
Returns:
|
||||
True if deleted, False if not found
|
||||
"""
|
||||
if not self._checkpointer_conn:
|
||||
await self._get_checkpointer()
|
||||
|
||||
repo = ChatSessionsRepository(self._checkpointer_conn)
|
||||
return await repo.delete_session(session_id)
|
||||
|
||||
async def rename_session(self, session_id: str, new_title: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Rename a chat session.
|
||||
|
||||
Args:
|
||||
session_id: Thread ID
|
||||
new_title: New title
|
||||
|
||||
Returns:
|
||||
Updated session dictionary or None
|
||||
"""
|
||||
if not self._checkpointer_conn:
|
||||
await self._get_checkpointer()
|
||||
|
||||
repo = ChatSessionsRepository(self._checkpointer_conn)
|
||||
session = await repo.update_session(thread_id=session_id, title=new_title)
|
||||
return session.to_dict() if session else None
|
||||
|
||||
async def close(self):
|
||||
"""
|
||||
Close the checkpointer connection and cleanup resources.
|
||||
|
||||
365
gns3server/agent/gns3_copilot/chat_sessions_repository.py
Normal file
365
gns3server/agent/gns3_copilot/chat_sessions_repository.py
Normal file
@ -0,0 +1,365 @@
|
||||
"""
|
||||
Chat Sessions Repository for managing chat session data.
|
||||
|
||||
Provides CRUD operations for the chat_sessions table in the project's
|
||||
checkpoint database.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
from uuid import UUID
|
||||
|
||||
import aiosqlite
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ChatSession:
|
||||
"""Chat session model."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
id: Optional[int] = None,
|
||||
thread_id: str = "",
|
||||
user_id: str = "",
|
||||
project_id: str = "",
|
||||
title: str = "New Conversation",
|
||||
message_count: int = 0,
|
||||
llm_calls_count: int = 0,
|
||||
input_tokens: int = 0,
|
||||
output_tokens: int = 0,
|
||||
total_tokens: int = 0,
|
||||
last_message_at: Optional[str] = None,
|
||||
created_at: Optional[str] = None,
|
||||
updated_at: Optional[str] = None,
|
||||
metadata: str = "{}",
|
||||
stats: str = "{}"
|
||||
):
|
||||
self.id = id
|
||||
self.thread_id = thread_id
|
||||
self.user_id = user_id
|
||||
self.project_id = project_id
|
||||
self.title = title
|
||||
self.message_count = message_count
|
||||
self.llm_calls_count = llm_calls_count
|
||||
self.input_tokens = input_tokens
|
||||
self.output_tokens = output_tokens
|
||||
self.total_tokens = total_tokens
|
||||
self.last_message_at = last_message_at
|
||||
self.created_at = created_at
|
||||
self.updated_at = updated_at
|
||||
self.metadata = metadata
|
||||
self.stats = stats
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"id": self.id,
|
||||
"thread_id": self.thread_id,
|
||||
"user_id": self.user_id,
|
||||
"project_id": self.project_id,
|
||||
"title": self.title,
|
||||
"message_count": self.message_count,
|
||||
"llm_calls_count": self.llm_calls_count,
|
||||
"input_tokens": self.input_tokens,
|
||||
"output_tokens": self.output_tokens,
|
||||
"total_tokens": self.total_tokens,
|
||||
"last_message_at": self.last_message_at,
|
||||
"created_at": self.created_at,
|
||||
"updated_at": self.updated_at,
|
||||
"metadata": json.loads(self.metadata) if self.metadata else {},
|
||||
"stats": json.loads(self.stats) if self.stats else {},
|
||||
}
|
||||
|
||||
|
||||
class ChatSessionsRepository:
|
||||
"""
|
||||
Repository for managing chat sessions in the checkpoint database.
|
||||
"""
|
||||
|
||||
def __init__(self, conn: aiosqlite.Connection):
|
||||
"""
|
||||
Initialize repository with a database connection.
|
||||
|
||||
Args:
|
||||
conn: aiosqlite connection to the checkpoint database
|
||||
"""
|
||||
self.conn = conn
|
||||
|
||||
async def create_session(
|
||||
self,
|
||||
thread_id: str,
|
||||
user_id: str,
|
||||
project_id: str,
|
||||
title: str = "New Conversation"
|
||||
) -> ChatSession:
|
||||
"""
|
||||
Create a new chat session.
|
||||
|
||||
Args:
|
||||
thread_id: Unique thread identifier
|
||||
user_id: User ID
|
||||
project_id: Project ID
|
||||
title: Session title
|
||||
|
||||
Returns:
|
||||
Created ChatSession
|
||||
"""
|
||||
now = datetime.utcnow().isoformat()
|
||||
cursor = await self.conn.execute(
|
||||
"""
|
||||
INSERT INTO chat_sessions (
|
||||
thread_id, user_id, project_id, title,
|
||||
created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(thread_id, user_id, project_id, title, now, now)
|
||||
)
|
||||
await self.conn.commit()
|
||||
|
||||
session_id = cursor.lastrowid
|
||||
log.info("Created chat session: id=%s, thread_id=%s", session_id, thread_id)
|
||||
|
||||
return await self.get_session_by_id(session_id)
|
||||
|
||||
async def get_session_by_id(self, session_id: int) -> Optional[ChatSession]:
|
||||
"""
|
||||
Get a session by its database ID.
|
||||
|
||||
Args:
|
||||
session_id: Database row ID
|
||||
|
||||
Returns:
|
||||
ChatSession or None
|
||||
"""
|
||||
cursor = await self.conn.execute(
|
||||
"SELECT * FROM chat_sessions WHERE id = ?",
|
||||
(session_id,)
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
|
||||
if row:
|
||||
return self._row_to_session(row)
|
||||
return None
|
||||
|
||||
async def get_session_by_thread(self, thread_id: str) -> Optional[ChatSession]:
|
||||
"""
|
||||
Get a session by thread_id.
|
||||
|
||||
Args:
|
||||
thread_id: Thread identifier
|
||||
|
||||
Returns:
|
||||
ChatSession or None
|
||||
"""
|
||||
cursor = await self.conn.execute(
|
||||
"SELECT * FROM chat_sessions WHERE thread_id = ?",
|
||||
(thread_id,)
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
|
||||
if row:
|
||||
return self._row_to_session(row)
|
||||
return None
|
||||
|
||||
async def list_sessions(
|
||||
self,
|
||||
user_id: Optional[str] = None,
|
||||
project_id: Optional[str] = None,
|
||||
limit: int = 100
|
||||
) -> List[ChatSession]:
|
||||
"""
|
||||
List sessions with optional filters.
|
||||
|
||||
Args:
|
||||
user_id: Filter by user ID
|
||||
project_id: Filter by project ID
|
||||
limit: Maximum number of sessions to return
|
||||
|
||||
Returns:
|
||||
List of ChatSession
|
||||
"""
|
||||
query = "SELECT * FROM chat_sessions"
|
||||
params = []
|
||||
|
||||
conditions = []
|
||||
if user_id:
|
||||
conditions.append("user_id = ?")
|
||||
params.append(user_id)
|
||||
if project_id:
|
||||
conditions.append("project_id = ?")
|
||||
params.append(project_id)
|
||||
|
||||
if conditions:
|
||||
query += " WHERE " + " AND ".join(conditions)
|
||||
|
||||
query += " ORDER BY updated_at DESC LIMIT ?"
|
||||
params.append(limit)
|
||||
|
||||
cursor = await self.conn.execute(query, params)
|
||||
rows = await cursor.fetchall()
|
||||
|
||||
return [self._row_to_session(row) for row in rows]
|
||||
|
||||
async def update_session(
|
||||
self,
|
||||
thread_id: str,
|
||||
title: Optional[str] = None,
|
||||
message_count: Optional[int] = None,
|
||||
llm_calls_count: Optional[int] = None,
|
||||
input_tokens: Optional[int] = None,
|
||||
output_tokens: Optional[int] = None,
|
||||
total_tokens: Optional[int] = None,
|
||||
last_message_at: Optional[str] = None
|
||||
) -> Optional[ChatSession]:
|
||||
"""
|
||||
Update a session.
|
||||
|
||||
Args:
|
||||
thread_id: Thread identifier
|
||||
title: New title
|
||||
message_count: Increment message count
|
||||
llm_calls_count: Increment LLM call count
|
||||
input_tokens: Add to input tokens
|
||||
output_tokens: Add to output tokens
|
||||
total_tokens: Add to total tokens
|
||||
last_message_at: Last message timestamp
|
||||
|
||||
Returns:
|
||||
Updated ChatSession or None
|
||||
"""
|
||||
updates = []
|
||||
params = []
|
||||
|
||||
now = datetime.utcnow().isoformat()
|
||||
|
||||
if title is not None:
|
||||
updates.append("title = ?")
|
||||
params.append(title)
|
||||
|
||||
if message_count is not None:
|
||||
updates.append("message_count = message_count + ?")
|
||||
params.append(message_count)
|
||||
|
||||
if llm_calls_count is not None:
|
||||
updates.append("llm_calls_count = llm_calls_count + ?")
|
||||
params.append(llm_calls_count)
|
||||
|
||||
if input_tokens is not None:
|
||||
updates.append("input_tokens = input_tokens + ?")
|
||||
params.append(input_tokens)
|
||||
|
||||
if output_tokens is not None:
|
||||
updates.append("output_tokens = output_tokens + ?")
|
||||
params.append(output_tokens)
|
||||
|
||||
if total_tokens is not None:
|
||||
updates.append("total_tokens = total_tokens + ?")
|
||||
params.append(total_tokens)
|
||||
|
||||
if last_message_at is not None:
|
||||
updates.append("last_message_at = ?")
|
||||
params.append(last_message_at)
|
||||
|
||||
if not updates:
|
||||
return await self.get_session_by_thread(thread_id)
|
||||
|
||||
updates.append("updated_at = ?")
|
||||
params.append(now)
|
||||
params.append(thread_id)
|
||||
|
||||
query = f"UPDATE chat_sessions SET {', '.join(updates)} WHERE thread_id = ?"
|
||||
|
||||
await self.conn.execute(query, params)
|
||||
await self.conn.commit()
|
||||
|
||||
log.debug("Updated chat session: thread_id=%s", thread_id)
|
||||
return await self.get_session_by_thread(thread_id)
|
||||
|
||||
async def delete_session(self, thread_id: str) -> bool:
|
||||
"""
|
||||
Delete a session by thread_id.
|
||||
|
||||
Args:
|
||||
thread_id: Thread identifier
|
||||
|
||||
Returns:
|
||||
True if deleted, False if not found
|
||||
"""
|
||||
# First, delete the checkpoint data
|
||||
await self.conn.execute(
|
||||
"DELETE FROM checkpoints WHERE thread_id = ?",
|
||||
(thread_id,)
|
||||
)
|
||||
|
||||
# Then delete the session
|
||||
cursor = await self.conn.execute(
|
||||
"DELETE FROM chat_sessions WHERE thread_id = ?",
|
||||
(thread_id,)
|
||||
)
|
||||
await self.conn.commit()
|
||||
|
||||
deleted = cursor.rowcount > 0
|
||||
if deleted:
|
||||
log.info("Deleted chat session and checkpoints: thread_id=%s", thread_id)
|
||||
|
||||
return deleted
|
||||
|
||||
async def delete_all_sessions(self, project_id: str) -> int:
|
||||
"""
|
||||
Delete all sessions for a project.
|
||||
|
||||
Args:
|
||||
project_id: Project ID
|
||||
|
||||
Returns:
|
||||
Number of sessions deleted
|
||||
"""
|
||||
# Get all thread_ids for this project
|
||||
cursor = await self.conn.execute(
|
||||
"SELECT thread_id FROM chat_sessions WHERE project_id = ?",
|
||||
(project_id,)
|
||||
)
|
||||
rows = await cursor.fetchall()
|
||||
thread_ids = [row[0] for row in rows]
|
||||
|
||||
# Delete checkpoints and sessions
|
||||
for thread_id in thread_ids:
|
||||
await self.conn.execute(
|
||||
"DELETE FROM checkpoints WHERE thread_id = ?",
|
||||
(thread_id,)
|
||||
)
|
||||
|
||||
cursor = await self.conn.execute(
|
||||
"DELETE FROM chat_sessions WHERE project_id = ?",
|
||||
(project_id,)
|
||||
)
|
||||
await self.conn.commit()
|
||||
|
||||
deleted_count = cursor.rowcount
|
||||
if deleted_count > 0:
|
||||
log.info("Deleted %d sessions for project: %s", deleted_count, project_id)
|
||||
|
||||
return deleted_count
|
||||
|
||||
def _row_to_session(self, row) -> ChatSession:
|
||||
"""Convert database row to ChatSession object."""
|
||||
return ChatSession(
|
||||
id=row[0],
|
||||
thread_id=row[1],
|
||||
user_id=row[2],
|
||||
project_id=row[3],
|
||||
title=row[4],
|
||||
message_count=row[5],
|
||||
llm_calls_count=row[6],
|
||||
input_tokens=row[7],
|
||||
output_tokens=row[8],
|
||||
total_tokens=row[9],
|
||||
last_message_at=row[10],
|
||||
created_at=row[11],
|
||||
updated_at=row[12],
|
||||
metadata=row[13],
|
||||
stats=row[14],
|
||||
)
|
||||
@ -189,7 +189,7 @@ async def stream_chat(
|
||||
"/sessions",
|
||||
response_model=List[schemas.ChatSession],
|
||||
summary="List chat sessions",
|
||||
description="List all chat sessions for a project (not yet implemented)."
|
||||
description="List all chat sessions for a project."
|
||||
)
|
||||
async def list_sessions(
|
||||
project: Project = Depends(dep_project),
|
||||
@ -197,9 +197,6 @@ async def list_sessions(
|
||||
) -> list[schemas.ChatSession]:
|
||||
"""
|
||||
List chat sessions for a project.
|
||||
|
||||
Note: This endpoint is a placeholder. Full session listing functionality
|
||||
requires checkpoint metadata inspection which is not yet implemented.
|
||||
"""
|
||||
|
||||
# Check if project is opened
|
||||
@ -209,8 +206,15 @@ async def list_sessions(
|
||||
detail=f"Project must be opened to access chat sessions. Current status: {project.status}"
|
||||
)
|
||||
|
||||
# TODO: Implement session listing from checkpoint metadata
|
||||
return []
|
||||
# Get AgentService for this project
|
||||
agent_manager = await get_project_agent_manager()
|
||||
agent_service = await agent_manager.get_agent(str(project.id), project.path)
|
||||
|
||||
# List sessions
|
||||
sessions = await agent_service.list_sessions(user_id=str(current_user.user_id))
|
||||
|
||||
# Convert to schemas
|
||||
return [schemas.ChatSession(**s) for s in sessions]
|
||||
|
||||
|
||||
@router.get(
|
||||
@ -250,7 +254,7 @@ async def get_history(
|
||||
"/sessions/{session_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="Delete a chat session",
|
||||
description="Delete a specific chat session (not yet implemented)."
|
||||
description="Delete a specific chat session and its checkpoints."
|
||||
)
|
||||
async def delete_session(
|
||||
session_id: str,
|
||||
@ -259,9 +263,6 @@ async def delete_session(
|
||||
):
|
||||
"""
|
||||
Delete a chat session.
|
||||
|
||||
Note: This endpoint is a placeholder. Full session deletion functionality
|
||||
requires checkpoint manipulation which is not yet implemented.
|
||||
"""
|
||||
|
||||
# Check if project is opened
|
||||
@ -271,8 +272,54 @@ async def delete_session(
|
||||
detail=f"Project must be opened to delete chat sessions. Current status: {project.status}"
|
||||
)
|
||||
|
||||
# TODO: Implement session deletion from checkpoint
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
||||
detail="Session deletion not yet implemented"
|
||||
)
|
||||
# Get AgentService for this project
|
||||
agent_manager = await get_project_agent_manager()
|
||||
agent_service = await agent_manager.get_agent(str(project.id), project.path)
|
||||
|
||||
# Delete session
|
||||
deleted = await agent_service.delete_session(session_id)
|
||||
|
||||
if not deleted:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Session '{session_id}' not found"
|
||||
)
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/sessions/{session_id}",
|
||||
response_model=schemas.ChatSession,
|
||||
summary="Rename a chat session",
|
||||
description="Rename a specific chat session."
|
||||
)
|
||||
async def rename_session(
|
||||
session_id: str,
|
||||
request: schemas.RenameSession,
|
||||
project: Project = Depends(dep_project),
|
||||
current_user: schemas.User = Depends(get_current_active_user),
|
||||
) -> schemas.ChatSession:
|
||||
"""
|
||||
Rename a chat session.
|
||||
"""
|
||||
|
||||
# Check if project is opened
|
||||
if project.status != "opened":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"Project must be opened to rename chat sessions. Current status: {project.status}"
|
||||
)
|
||||
|
||||
# Get AgentService for this project
|
||||
agent_manager = await get_project_agent_manager()
|
||||
agent_service = await agent_manager.get_agent(str(project.id), project.path)
|
||||
|
||||
# Rename session
|
||||
session = await agent_service.rename_session(session_id, request.title)
|
||||
|
||||
if not session:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Session '{session_id}' not found"
|
||||
)
|
||||
|
||||
return schemas.ChatSession(**session)
|
||||
|
||||
@ -45,7 +45,8 @@ from .controller.chat import (
|
||||
ChatResponse,
|
||||
OpenAIMessage,
|
||||
ConversationHistory,
|
||||
ChatSession
|
||||
ChatSession,
|
||||
RenameSession
|
||||
)
|
||||
from .controller.rbac import RoleCreate, RoleUpdate, Role, Privilege, ACECreate, ACEUpdate, ACE
|
||||
from .controller.pools import Resource, ResourceCreate, ResourcePoolCreate, ResourcePoolUpdate, ResourcePool
|
||||
|
||||
@ -88,8 +88,24 @@ class ConversationHistory(BaseModel):
|
||||
class ChatSession(BaseModel):
|
||||
"""Chat session model."""
|
||||
|
||||
session_id: str = Field(..., description="Session ID")
|
||||
id: Optional[int] = Field(None, description="Database ID")
|
||||
thread_id: str = Field(..., description="Thread/session ID")
|
||||
user_id: str = Field(..., description="User ID")
|
||||
project_id: str = Field(..., description="Associated GNS3 project ID")
|
||||
title: str = Field(..., description="Session title")
|
||||
project_id: Optional[str] = Field(None, description="Associated GNS3 project ID")
|
||||
message_count: int = Field(default=0, description="Number of messages")
|
||||
llm_calls_count: int = Field(default=0, description="Number of LLM calls")
|
||||
input_tokens: int = Field(default=0, description="Input tokens used")
|
||||
output_tokens: int = Field(default=0, description="Output tokens generated")
|
||||
total_tokens: int = Field(default=0, description="Total tokens used")
|
||||
last_message_at: Optional[str] = Field(None, description="Last message timestamp (ISO 8601)")
|
||||
created_at: Optional[str] = Field(None, description="Creation timestamp (ISO 8601)")
|
||||
updated_at: Optional[str] = Field(None, description="Last update timestamp (ISO 8601)")
|
||||
metadata: Dict[str, Any] = Field(default_factory=dict, description="Session metadata")
|
||||
stats: Dict[str, Any] = Field(default_factory=dict, description="Session statistics")
|
||||
|
||||
|
||||
class RenameSession(BaseModel):
|
||||
"""Rename session request model."""
|
||||
|
||||
title: str = Field(..., description="New session title", min_length=1, max_length=255)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user