diff --git a/backend/app/api/v1/api.py b/backend/app/api/v1/api.py index 5f60c65..88b190a 100644 --- a/backend/app/api/v1/api.py +++ b/backend/app/api/v1/api.py @@ -1,15 +1,12 @@ -# File: backend/app/api/v1/api.py +# File: backend/app/api/v1/api.py (更新) # Description: 聚合 v1 版本的所有 API 路由 from fastapi import APIRouter -from app.api.v1.endpoints import chat # 导入聊天路由 +from app.api.v1.endpoints import chat, assistants, sessions # 导入新路由 -# 创建 v1 版本的总路由 api_router = APIRouter() -# 将聊天路由包含到 v1 总路由中,并添加前缀 -api_router.include_router(chat.router, prefix="/chat", tags=["chat"]) +api_router.include_router(chat.router, prefix="/chat", tags=["Chat"]) +api_router.include_router(assistants.router, prefix="/assistants", tags=["Assistants"]) # 添加助手路由 +api_router.include_router(sessions.router, prefix="/sessions", tags=["Sessions"]) # 添加会话路由 -# --- 如果有其他路由,也在这里 include --- -# from app.api.v1.endpoints import workflow -# api_router.include_router(workflow.router, prefix="/workflow", tags=["workflow"]) diff --git a/backend/app/api/v1/endpoints/assistants.py b/backend/app/api/v1/endpoints/assistants.py new file mode 100644 index 0000000..899255f --- /dev/null +++ b/backend/app/api/v1/endpoints/assistants.py @@ -0,0 +1,66 @@ +# File: backend/app/api/v1/endpoints/assistants.py (新建) +# Description: 助手的 API 路由 + +from fastapi import APIRouter, HTTPException, Depends, status +from typing import List +from app.models.pydantic_models import AssistantRead, AssistantCreate, AssistantUpdate +from app.services.assistant_service import assistant_service_instance, AssistantService + +router = APIRouter() + +# --- 依赖注入 AssistantService --- +def get_assistant_service() -> AssistantService: + return assistant_service_instance + +@router.post("/", response_model=AssistantRead, status_code=status.HTTP_201_CREATED) +async def create_new_assistant( + assistant_data: AssistantCreate, + service: AssistantService = Depends(get_assistant_service) +): + """创建新助手""" + return service.create_assistant(assistant_data) + +@router.get("/", response_model=List[AssistantRead]) +async def read_all_assistants( + service: AssistantService = Depends(get_assistant_service) +): + """获取所有助手列表""" + return service.get_assistants() + +@router.get("/{assistant_id}", response_model=AssistantRead) +async def read_assistant_by_id( + assistant_id: str, + service: AssistantService = Depends(get_assistant_service) +): + """根据 ID 获取特定助手""" + assistant = service.get_assistant(assistant_id) + if not assistant: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="找不到指定的助手") + return assistant + +@router.put("/{assistant_id}", response_model=AssistantRead) +async def update_existing_assistant( + assistant_id: str, + assistant_data: AssistantUpdate, + service: AssistantService = Depends(get_assistant_service) +): + """更新指定 ID 的助手""" + updated_assistant = service.update_assistant(assistant_id, assistant_data) + if not updated_assistant: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="找不到指定的助手") + return updated_assistant + +@router.delete("/{assistant_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_existing_assistant( + assistant_id: str, + service: AssistantService = Depends(get_assistant_service) +): + """删除指定 ID 的助手""" + deleted = service.delete_assistant(assistant_id) + if not deleted: + # 根据服务层逻辑判断是找不到还是不允许删除 + assistant = service.get_assistant(assistant_id) + if assistant and assistant_id == 'asst-default': + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="不允许删除默认助手") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="找不到指定的助手") + # 成功删除,不返回内容 diff --git a/backend/app/api/v1/endpoints/chat.py b/backend/app/api/v1/endpoints/chat.py index 90e9e40..66fc018 100644 --- a/backend/app/api/v1/endpoints/chat.py +++ b/backend/app/api/v1/endpoints/chat.py @@ -1,41 +1,68 @@ # File: backend/app/api/v1/endpoints/chat.py (更新) -# Description: 聊天功能的 API 路由 (使用 ChatService) +# Description: 聊天功能的 API 路由 (使用更新后的 ChatService) -from fastapi import APIRouter, HTTPException, Depends -from app.models.pydantic_models import ChatRequest, ChatResponse -# 导入 ChatService 实例 +from fastapi import APIRouter, HTTPException, Depends, status +from app.models.pydantic_models import ChatRequest, ChatResponse, SessionCreateRequest from app.services.chat_service import chat_service_instance, ChatService +from app.services.session_service import session_service_instance, SessionService # 导入 SessionService router = APIRouter() -# --- (可选) 使用 FastAPI 的依赖注入来获取 ChatService 实例 --- -# 这样更符合 FastAPI 的风格,方便测试和替换实现 -# async def get_chat_service() -> ChatService: -# return chat_service_instance +# --- 依赖注入 --- +def get_chat_service() -> ChatService: + return chat_service_instance + +def get_session_service() -> SessionService: + return session_service_instance @router.post("/", response_model=ChatResponse) async def handle_chat_message( request: ChatRequest, - # chat_service: ChatService = Depends(get_chat_service) # 使用依赖注入 + chat_service: ChatService = Depends(get_chat_service), + session_service: SessionService = Depends(get_session_service) # 注入 SessionService ): - """ - 处理用户发送的聊天消息,并使用 LangChain 获取 AI 回复 - """ - user_message = request.message - # session_id = request.session_id # 如果 ChatRequest 中包含 session_id - print(f"接收到用户消息: {user_message}") + """处理用户发送的聊天消息 (包含 assistantId 和 sessionId)""" + user_message = request.message + session_id = request.session_id + assistant_id = request.assistant_id - try: - # --- 调用 ChatService 获取 AI 回复 --- - # 使用全局实例 (简单方式) - ai_reply = await chat_service_instance.get_ai_reply(user_message) - # 或者使用依赖注入获取的实例 - # ai_reply = await chat_service.get_ai_reply(user_message, session_id) + print(f"接收到消息: User='{user_message}', Session='{session_id}', Assistant='{assistant_id}'") - print(f"发送 AI 回复: {ai_reply}") - return ChatResponse(reply=ai_reply) + response_session_id = None + response_session_title = None - except Exception as e: - # 如果 ChatService 抛出异常,捕获并返回 HTTP 500 错误 - print(f"处理聊天消息时发生错误: {e}") - raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file + # --- 处理临时新会话 --- + if session_id == 'temp-new-chat': + print("检测到临时新会话,正在创建...") + try: + # 调用 SessionService 创建会话 + create_req = SessionCreateRequest(assistant_id=assistant_id, first_message=user_message) + created_session = await session_service.create_session(create_req) + session_id = created_session.id # 使用新创建的会话 ID + response_session_id = created_session.id # 准备在响应中返回新 ID + response_session_title = created_session.title # 准备在响应中返回新标题 + print(f"新会话已创建: ID={session_id}, Title='{created_session.title}'") + except ValueError as e: # 助手不存在等错误 + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) + except Exception as e: # LLM 调用或其他错误 + print(f"创建会话时出错: {e}") + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="创建会话失败") + + # --- 调用聊天服务获取回复 --- + try: + ai_reply = await chat_service.get_ai_reply( + user_message=user_message, + session_id=session_id, # 使用真实的或新创建的 session_id + assistant_id=assistant_id + ) + print(f"发送 AI 回复: '{ai_reply}'") + return ChatResponse( + reply=ai_reply, + session_id=response_session_id, # 返回新 ID (如果创建了) + session_title=response_session_title # 返回新标题 (如果创建了) + ) + except ValueError as e: # 助手不存在等错误 + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) + except Exception as e: # LLM 调用或其他错误 + print(f"处理聊天消息时发生错误: {e}") + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)) diff --git a/backend/app/api/v1/endpoints/sessions.py b/backend/app/api/v1/endpoints/sessions.py new file mode 100644 index 0000000..a4ddd53 --- /dev/null +++ b/backend/app/api/v1/endpoints/sessions.py @@ -0,0 +1,58 @@ +# File: backend/app/api/v1/endpoints/sessions.py (新建) +# Description: 会话管理的 API 路由 + +from fastapi import APIRouter, HTTPException, Depends, status +from typing import List +from app.models.pydantic_models import SessionRead, SessionCreateRequest, SessionCreateResponse +from app.services.session_service import session_service_instance, SessionService + +router = APIRouter() + +def get_session_service() -> SessionService: + return session_service_instance + +@router.post("/", response_model=SessionCreateResponse, status_code=status.HTTP_201_CREATED) +async def create_new_session( + session_data: SessionCreateRequest, + service: SessionService = Depends(get_session_service) +): + """创建新会话并自动生成标题""" + try: + return await service.create_session(session_data) + except ValueError as e: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) + except Exception as e: + # 处理可能的 LLM 调用错误 + print(f"创建会话时出错: {e}") + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="创建会话失败") + +@router.get("/assistant/{assistant_id}", response_model=List[SessionRead]) +async def read_sessions_for_assistant( + assistant_id: str, + service: SessionService = Depends(get_session_service) +): + """获取指定助手的所有会话列表""" + # TODO: 添加检查助手是否存在 + return service.get_sessions_by_assistant(assistant_id) + +@router.get("/{session_id}", response_model=SessionRead) +async def read_session_by_id( + session_id: str, + service: SessionService = Depends(get_session_service) +): + """获取单个会话信息""" + session = service.get_session(session_id) + if not session: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="找不到指定的会话") + return session + +@router.delete("/{session_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_existing_session( + session_id: str, + service: SessionService = Depends(get_session_service) +): + """删除指定 ID 的会话""" + deleted = service.delete_session(session_id) + if not deleted: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="找不到指定的会话") + diff --git a/backend/app/models/pydantic_models.py b/backend/app/models/pydantic_models.py index e8f1f97..d001128 100644 --- a/backend/app/models/pydantic_models.py +++ b/backend/app/models/pydantic_models.py @@ -1,16 +1,76 @@ -# File: backend/app/models/pydantic_models.py +# File: backend/app/models/pydantic_models.py (更新) # Description: Pydantic 模型定义 API 数据结构 -from pydantic import BaseModel +from pydantic import BaseModel, Field +from typing import Optional, List +import uuid # 用于生成唯一 ID + +# --- Assistant Models --- + +class AssistantBase(BaseModel): + """助手的基础模型,包含通用字段""" + name: str = Field(..., min_length=1, max_length=50, description="助手名称") + description: Optional[str] = Field(None, max_length=200, description="助手描述") + avatar: Optional[str] = Field(None, max_length=5, description="头像 Emoji 或字符") + system_prompt: str = Field(..., min_length=1, max_length=4000, description="系统提示") + model: str = Field(..., description="使用的 LLM 模型") + temperature: float = Field(0.7, ge=0.0, le=1.0, description="温度参数 (0.0-1.0)") + # 可以添加 top_p, max_tokens 等 + +class AssistantCreate(AssistantBase): + """创建助手时使用的模型 (不需要 ID)""" + pass + +class AssistantUpdate(BaseModel): + """更新助手时使用的模型 (所有字段可选)""" + name: Optional[str] = Field(None, min_length=1, max_length=50) + description: Optional[str] = Field(None, max_length=200) + avatar: Optional[str] = Field(None, max_length=5) + system_prompt: Optional[str] = Field(None, min_length=1, max_length=4000) + model: Optional[str] = None + temperature: Optional[float] = Field(None, ge=0.0, le=1.0) + +class AssistantRead(AssistantBase): + """读取助手信息时返回的模型 (包含 ID)""" + id: str = Field(..., description="助手唯一 ID") + + class Config: + from_attributes = True # Pydantic v2: orm_mode = True + +# --- Chat Models (更新) --- class ChatRequest(BaseModel): - """聊天请求模型""" - message: str - # 可以添加更多字段,如 user_id, session_id 等 + """聊天请求模型 (添加 sessionId 和 assistantId)""" + message: str + session_id: str = Field(..., description="当前会话 ID (可以是 'temp-new-chat')") + assistant_id: str = Field(..., description="当前使用的助手 ID") class ChatResponse(BaseModel): - """聊天响应模型""" - reply: str - # 可以添加更多字段,如 message_id, status 等 + """聊天响应模型""" + reply: str + session_id: Optional[str] = None # (可选) 如果创建了新会话,返回新 ID + session_title: Optional[str] = None # (可选) 如果创建了新会话,返回新标题 -# --- 你可以在这里添加其他功能的模型 --- \ No newline at end of file +# --- Session Models --- + +class SessionCreateRequest(BaseModel): + """创建会话请求模型""" + assistant_id: str + first_message: str # 用户的第一条消息,用于生成标题 + +class SessionCreateResponse(BaseModel): + """创建会话响应模型""" + id: str + title: str + assistant_id: str + created_at: str # 返回 ISO 格式时间字符串 + +class SessionRead(BaseModel): + """读取会话信息模型""" + id: str + title: str + assistant_id: str + created_at: str + + class Config: + from_attributes = True \ No newline at end of file diff --git a/backend/app/services/assistant_service.py b/backend/app/services/assistant_service.py new file mode 100644 index 0000000..ae4dfd4 --- /dev/null +++ b/backend/app/services/assistant_service.py @@ -0,0 +1,73 @@ +# File: backend/app/services/assistant_service.py (新建) +# Description: 管理助手数据的服务 (内存实现) + +from typing import Dict, List, Optional +from app.models.pydantic_models import AssistantRead, AssistantCreate, AssistantUpdate +import uuid + +# 使用字典作为内存数据库存储助手 +# key: assistant_id (str), value: AssistantRead object +assistants_db: Dict[str, AssistantRead] = {} + +# 添加默认助手 (确保 ID 与前端 Mock 一致) +default_assistant = AssistantRead( + id='asst-default', + name='默认助手', + description='通用聊天助手', + avatar='🤖', + system_prompt='你是一个乐于助人的 AI 助手。', + model='gpt-3.5-turbo', + temperature=0.7 +) +assistants_db[default_assistant.id] = default_assistant + +class AssistantService: + """助手数据的 CRUD 服务""" + + def get_assistants(self) -> List[AssistantRead]: + """获取所有助手""" + return list(assistants_db.values()) + + def get_assistant(self, assistant_id: str) -> Optional[AssistantRead]: + """根据 ID 获取单个助手""" + return assistants_db.get(assistant_id) + + def create_assistant(self, assistant_data: AssistantCreate) -> AssistantRead: + """创建新助手""" + new_id = f"asst-{uuid.uuid4()}" # 生成唯一 ID + new_assistant = AssistantRead(id=new_id, **assistant_data.model_dump()) + assistants_db[new_id] = new_assistant + print(f"助手已创建: {new_id} - {new_assistant.name}") + return new_assistant + + def update_assistant(self, assistant_id: str, assistant_data: AssistantUpdate) -> Optional[AssistantRead]: + """更新现有助手""" + existing_assistant = assistants_db.get(assistant_id) + if not existing_assistant: + return None + + # 使用 Pydantic 的 model_copy 和 update 来更新字段 + update_data = assistant_data.model_dump(exclude_unset=True) # 只获取设置了值的字段 + if update_data: + updated_assistant = existing_assistant.model_copy(update=update_data) + assistants_db[assistant_id] = updated_assistant + print(f"助手已更新: {assistant_id}") + return updated_assistant + return existing_assistant # 如果没有更新任何字段,返回原始助手 + + def delete_assistant(self, assistant_id: str) -> bool: + """删除助手""" + if assistant_id in assistants_db: + # 添加逻辑:不允许删除默认助手 + if assistant_id == 'asst-default': + print("尝试删除默认助手 - 操作被阻止") + return False # 或者抛出特定异常 + del assistants_db[assistant_id] + print(f"助手已删除: {assistant_id}") + # TODO: 在实际应用中,还需要删除关联的会话和消息 + return True + return False + +# 创建服务实例 +assistant_service_instance = AssistantService() + diff --git a/backend/app/services/chat_service.py b/backend/app/services/chat_service.py index 2dab1d6..ce2770a 100644 --- a/backend/app/services/chat_service.py +++ b/backend/app/services/chat_service.py @@ -1,92 +1,127 @@ -# File: backend/app/services/chat_service.py (新建) -# Description: 封装 LangChain 聊天逻辑 +# File: backend/app/services/chat_service.py (更新) +# Description: 封装 LangChain 聊天逻辑 (支持助手配置和会话历史) from langchain_openai import ChatOpenAI -from langchain_google_genai import ChatGoogleGenerativeAI # 如果使用 Google from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain_core.output_parsers import StrOutputParser -from langchain_core.messages import HumanMessage, AIMessage +from langchain_core.messages import HumanMessage, AIMessage, BaseMessage, SystemMessage +from typing import Dict, List, Optional +from app.services.assistant_service import assistant_service_instance # 获取助手配置 +from app.models.pydantic_models import AssistantRead # 引入助手模型 +import app.core.config as Config -# --- 可选:添加内存管理 --- -# 简单的内存实现 (可以替换为更复杂的 LangChain Memory 类) -chat_history = {} # 使用字典存储不同会话的内存,需要 session_id +# --- 更新内存管理 --- +# 使用字典存储不同会话的内存 +# key: session_id (str), value: List[BaseMessage] +chat_history_db: Dict[str, List[BaseMessage]] = {} class ChatService: - """处理 AI 聊天交互的服务""" + """处理 AI 聊天交互的服务 (支持助手配置)""" - def __init__(self, api_key: str): + def __init__(self, default_api_key: str): + """初始化时可传入默认 API Key""" + self.default_api_key = default_api_key + # 不再在 init 中创建固定的 LLM 和 chain + + def _get_llm(self, assistant: AssistantRead) -> ChatOpenAI: + """根据助手配置动态创建 LLM 实例""" + # TODO: 支持不同模型提供商 (Gemini, Anthropic etc.) + if assistant.model.startswith("gpt"): + return ChatOpenAI( + model=assistant.model, + api_key=self.default_api_key, # 或从助手配置中读取特定 key + temperature=assistant.temperature + ) + elif assistant.model.startswith("gemini"): + from langchain_google_genai import ChatGoogleGenerativeAI + return ChatGoogleGenerativeAI( + model=assistant.model, + api_key=self.default_api_key, # 或从助手配置中读取特定 key + temperature=assistant.temperature + ) + else: + # 默认或抛出错误 + print(f"警告: 模型 {assistant.model} 未明确支持,尝试使用 ChatOpenAI") + return ChatOpenAI( + model=assistant.model, + api_key=self.default_api_key, + temperature=assistant.temperature + ) + + async def get_ai_reply(self, user_message: str, session_id: str, assistant_id: str) -> str: """ - 初始化 ChatService - Args: - api_key (str): 用于 LLM 的 API 密钥 - """ - # --- 选择并初始化 LLM --- - # 使用 OpenAI GPT-3.5 Turbo (推荐) 或 GPT-4 - # self.llm = ChatOpenAI(model="gpt-3.5-turbo", api_key=api_key, temperature=0.7) - # self.llm = ChatOpenAI(model="gpt-4", api_key=api_key, temperature=0.7) - - # --- 如果使用 Google Gemini --- - self.llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash", google_api_key=api_key, convert_system_message_to_human=True) - - # --- 定义 Prompt 模板 --- - # 包含系统消息、历史记录占位符和当前用户输入 - self.prompt = ChatPromptTemplate.from_messages([ - ("system", "你是一个乐于助人的 AI 助手,请用简洁明了的语言回答问题。你的名字叫 CherryAI。"), - MessagesPlaceholder(variable_name="chat_history"), # 用于插入历史消息 - ("human", "{input}"), # 用户当前输入 - ]) - - # --- 定义输出解析器 --- - # 将 LLM 的输出解析为字符串 - self.output_parser = StrOutputParser() - - # --- 构建 LangChain Expression Language (LCEL) 链 --- - self.chain = self.prompt | self.llm | self.output_parser - - async def get_ai_reply(self, user_message: str, session_id: str = "default_session") -> str: - """ - 获取 AI 对用户消息的回复 (异步) + 获取 AI 对用户消息的回复 (使用指定助手和会话历史) Args: user_message (str): 用户发送的消息 - session_id (str): (可选) 用于区分不同对话的会话 ID,以支持内存 + session_id (str): 会话 ID + assistant_id (str): 使用的助手 ID Returns: str: AI 的回复文本 Raises: + ValueError: 如果找不到指定的助手 Exception: 如果调用 AI 服务时发生错误 """ - try: - # --- 获取当前会话的历史记录 (如果需要内存) --- - current_chat_history = chat_history.get(session_id, []) + # 1. 获取助手配置 + assistant = assistant_service_instance.get_assistant(assistant_id) + if not assistant: + raise ValueError(f"找不到助手 ID: {assistant_id}") - # --- 使用 ainvoke 进行异步调用 --- - ai_response = await self.chain.ainvoke({ + # 2. 获取或初始化当前会话的历史记录 + current_chat_history = chat_history_db.get(session_id, []) + + # 3. 构建 Prompt (包含动态系统提示) + prompt = ChatPromptTemplate.from_messages([ + SystemMessage(content=assistant.system_prompt), # 使用助手的系统提示 + MessagesPlaceholder(variable_name="chat_history"), + HumanMessage(content="{input}"), + ]) + + # 4. 获取 LLM 实例 + llm = self._get_llm(assistant) + + # 5. 定义输出解析器 + output_parser = StrOutputParser() + + # 6. 构建 LCEL 链 + chain = prompt | llm | output_parser + + try: + # 7. 调用链获取回复 + ai_response = await chain.ainvoke({ "input": user_message, - "chat_history": current_chat_history, # 传入历史记录 + "chat_history": current_chat_history, }) - # --- 更新会话历史记录 (如果需要内存) --- - # 只保留最近 N 轮对话,防止历史过长 - max_history_length = 10 # 保留最近 5 轮对话 (10条消息) + # 8. 更新会话历史记录 current_chat_history.append(HumanMessage(content=user_message)) current_chat_history.append(AIMessage(content=ai_response)) - # 如果历史记录超过长度,移除最早的消息 + # 限制历史记录长度 (例如最近 10 条消息) + max_history_length = 10 if len(current_chat_history) > max_history_length: - chat_history[session_id] = current_chat_history[-max_history_length:] + chat_history_db[session_id] = current_chat_history[-max_history_length:] else: - chat_history[session_id] = current_chat_history + chat_history_db[session_id] = current_chat_history return ai_response except Exception as e: - print(f"调用 LangChain 时出错: {e}") - # 可以进行更细致的错误处理,例如区分 API 错误和内部错误 + print(f"调用 LangChain 时出错 (助手: {assistant_id}, 会话: {session_id}): {e}") raise Exception(f"AI 服务暂时不可用: {e}") + # (可选) 添加一个简单的文本生成方法用于生成标题 + async def generate_text(self, prompt_text: str, model_name: str = "gpt-3.5-turbo", temperature: float = 0.5) -> str: + """使用指定模型生成文本 (用于标题等)""" + try: + # 使用一个临时的、可能更便宜的模型 + temp_llm = ChatOpenAI(model=model_name, api_key=self.default_api_key, temperature=temperature) + response = await temp_llm.ainvoke(prompt_text) + return response.content + except Exception as e: + print(f"生成文本时出错: {e}") + return "无法生成标题" # 返回默认值或抛出异常 -# --- 在文件末尾,创建 ChatService 的实例 --- -# 从配置中获取 API Key -from app.core.config import GOOGLE_API_KEY -if not GOOGLE_API_KEY: - raise ValueError("请在 .env 文件中设置 GOOGLE_API_KEY") -chat_service_instance = ChatService(api_key=GOOGLE_API_KEY) +# --- 创建 ChatService 实例 --- +if not Config.GOOGLE_API_KEY: + raise ValueError("请在 .env 文件中设置 OPENAI_API_KEY") +chat_service_instance = ChatService(default_api_key=Config.GOOGLE_API_KEY) diff --git a/backend/app/services/session_service.py b/backend/app/services/session_service.py new file mode 100644 index 0000000..579cb76 --- /dev/null +++ b/backend/app/services/session_service.py @@ -0,0 +1,82 @@ +# File: backend/app/services/session_service.py (新建) +# Description: 管理会话数据的服务 (内存实现) + +from typing import Dict, List, Optional +from app.models.pydantic_models import SessionRead, SessionCreateRequest, SessionCreateResponse, AssistantRead +from app.services.assistant_service import assistant_service_instance # 需要获取助手信息 +from datetime import datetime, timezone +import uuid +# 导入 ChatService 以调用 LLM 生成标题 (避免循环导入,考虑重构) +# from app.services.chat_service import chat_service_instance + +# 内存数据库存储会话 +# key: session_id (str), value: SessionRead object +sessions_db: Dict[str, SessionRead] = {} + +class SessionService: + """会话数据的 CRUD 及标题生成服务""" + + async def create_session(self, session_data: SessionCreateRequest) -> SessionCreateResponse: + """创建新会话并生成标题""" + assistant = assistant_service_instance.get_assistant(session_data.assistant_id) + if not assistant: + raise ValueError("指定的助手不存在") + + new_id = f"session-{uuid.uuid4()}" + created_time = datetime.now(timezone.utc) + + # --- TODO: 调用 LLM 生成标题 --- + # title_prompt = f"根据以下用户第一条消息,为此对话生成一个简洁的标题(不超过10个字):\n\n{session_data.first_message}" + # generated_title = await chat_service_instance.generate_text(title_prompt) # 需要一个简单的文本生成方法 + + # 模拟标题生成 + generated_title = f"关于 \"{session_data.first_message[:15]}...\"" + print(f"为新会话 {new_id} 生成标题: {generated_title}") + # --- 模拟结束 --- + + new_session = SessionRead( + id=new_id, + title=generated_title, + assistant_id=session_data.assistant_id, + created_at=created_time.isoformat() # 存储 ISO 格式字符串 + ) + sessions_db[new_id] = new_session + print(f"会话已创建: {new_id}") + + return SessionCreateResponse( + id=new_session.id, + title=new_session.title, + assistant_id=new_session.assistant_id, + created_at=new_session.created_at + ) + + def get_sessions_by_assistant(self, assistant_id: str) -> List[SessionRead]: + """获取指定助手的所有会话""" + return [s for s in sessions_db.values() if s.assistant_id == assistant_id] + + def get_session(self, session_id: str) -> Optional[SessionRead]: + """获取单个会话""" + return sessions_db.get(session_id) + + def delete_session(self, session_id: str) -> bool: + """删除会话""" + if session_id in sessions_db: + del sessions_db[session_id] + print(f"会话已删除: {session_id}") + # TODO: 删除关联的消息 + return True + return False + + def delete_sessions_by_assistant(self, assistant_id: str) -> int: + """删除指定助手的所有会话""" + ids_to_delete = [s.id for s in sessions_db.values() if s.assistant_id == assistant_id] + count = 0 + for session_id in ids_to_delete: + if self.delete_session(session_id): + count += 1 + print(f"删除了助手 {assistant_id} 的 {count} 个会话") + return count + + +# 创建服务实例 +session_service_instance = SessionService() diff --git a/frontend/app/chat/page.tsx b/frontend/app/chat/page.tsx index 7c0be71..426d983 100644 --- a/frontend/app/chat/page.tsx +++ b/frontend/app/chat/page.tsx @@ -1,11 +1,31 @@ -// File: frontend/app/chat/page.tsx (更新会话逻辑) -// Description: AI 聊天界面,实现助手关联会话、临时新对话和发送时创建会话 +// File: frontend/app/chat/page.tsx (更新以使用 API) +// Description: 对接后端 API 实现助手和会话的加载与管理 'use client'; import React, { useState, useRef, useEffect, useCallback } from 'react'; -import { SendHorizontal, Loader2, PanelRightOpen, PanelRightClose, UserPlus, Settings2 } from 'lucide-react'; -import { sendChatMessage } from '@/lib/api'; // 假设 sendChatMessage 只需 message +import { SendHorizontal, Loader2, PanelRightOpen, PanelRightClose, UserPlus, Settings2, Trash2, Edit, RefreshCw } from 'lucide-react'; // 添加刷新图标 +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import * as z from "zod"; + +// Shadcn UI Components +import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, DialogFooter, DialogClose } from "@/components/ui/dialog"; +import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Slider } from "@/components/ui/slider"; +import { Toaster, toast } from "sonner"; +import { Skeleton } from "@/components/ui/skeleton"; // 导入骨架屏 + +// API 函数和类型 +import { + sendChatMessage, getAssistants, createAssistant, updateAssistant, deleteAssistant, + getSessionsByAssistant, deleteSession, + Assistant, Session, AssistantCreateData, AssistantUpdateData, ChatApiResponse +} from '@/lib/api'; // 确保路径正确 // --- 数据接口定义 --- interface Message { @@ -16,192 +36,442 @@ interface Message { } interface ChatSession { - id: string; // 唯一 ID,例如 'session-uuid-123' 或 'temp-new-chat' + id: string; title: string; createdAt: Date; - assistantId: string; // 关联的助手 ID - isTemporary?: boolean; // 标记是否为临时会话 + assistantId: string; + isTemporary?: boolean; } -interface Assistant { - id: string; - name: string; - description: string; - avatar?: string; - systemPrompt: string; - model: string; - temperature: number; -} +// --- Zod Schema for Assistant Form Validation --- +const assistantFormSchema = z.object({ + name: z.string().min(1, { message: "助手名称不能为空" }).max(50, { message: "名称过长" }), + description: z.string().max(200, { message: "描述过长" }).optional(), + avatar: z.string().max(5, { message: "头像/Emoji 过长" }).optional(), // 简单限制长度 + system_prompt: z.string().min(1, { message: "系统提示不能为空" }).max(4000, { message: "系统提示过长" }), + model: z.string({ required_error: "请选择一个模型" }), + temperature: z.number().min(0).max(1), +}); + +type AssistantFormData = z.infer; + +// 可选的模型列表 +const availableModels = [ + { value: "gpt-3.5-turbo", label: "GPT-3.5 Turbo" }, + { value: "gpt-4", label: "GPT-4" }, + { value: "gpt-4-turbo", label: "GPT-4 Turbo" }, + { value: "gemini-2.0-flash", label: "Gemini 2.0 Flash" }, + { value: "deepseek-coder", label: "DeepSeek Coder" }, // 示例 + // 添加更多模型... +]; // --- Helper Function --- -// 查找助手的最新会话 (非临时) const findLastSession = (sessions: ChatSession[], assistantId: string): ChatSession | undefined => { return sessions .filter(s => s.assistantId === assistantId && !s.isTemporary) .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())[0]; }; +// --- Assistant Form Component --- +interface AssistantFormProps { + assistant?: Assistant | null; // 传入表示编辑,否则是创建 + onSave: (data: AssistantFormData, id?: string) => void; // 保存回调 + onClose: () => void; // 关闭 Dialog 的回调 +} + +function AssistantForm({ assistant, onSave, onClose }: AssistantFormProps) { + const form = useForm({ + resolver: zodResolver(assistantFormSchema), + defaultValues: { + name: assistant?.name || "", + description: assistant?.description || "", + avatar: assistant?.avatar || "", + system_prompt: assistant?.system_prompt || "", + model: assistant?.model || availableModels[0].value, // 默认第一个模型 + temperature: assistant?.temperature ?? 0.7, // 默认 0.7 + }, + }); + const [isSaving, setIsSaving] = useState(false); // 添加保存状态 + async function onSubmit(data: AssistantFormData) { + setIsSaving(true); + try { + await onSave(data, assistant?.id); // 调用异步保存函数 + onClose(); // 成功后关闭 + } catch (error) { + // 错误已在 onSave 中处理并 toast + } finally { + setIsSaving(false); + } + } + + return ( +
+ + {/* Name */} + ( + + 助手名称 + + + + + + )} + /> + {/* Description */} + ( + + 描述 (可选) + + + + + + )} + /> + {/* Avatar */} + ( + + 头像 (可选) + + + + 建议使用单个 Emoji。 + + + )} + /> + {/* System Prompt */} + ( + + 系统提示 (System Prompt) + +