diff --git a/backend/app/api/v1/module_application/ai/tools/ai_util.py b/backend/app/api/v1/module_application/ai/tools/ai_util.py index 4d8f69b1..abcd7251 100644 --- a/backend/app/api/v1/module_application/ai/tools/ai_util.py +++ b/backend/app/api/v1/module_application/ai/tools/ai_util.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- -from typing import AsyncGenerator -from openai import AsyncOpenAI, OpenAI -from openai.types.chat.chat_completion import ChatCompletion -import httpx +from typing import Any, AsyncGenerator +from langchain_openai import ChatOpenAI +from langchain_core.messages import SystemMessage, HumanMessage from app.config.setting import settings from app.core.logger import log @@ -15,18 +14,13 @@ class AIClient: """ def __init__(self): - self.model = settings.OPENAI_MODEL - # 创建一个不带冲突参数的httpx客户端 - self.http_client = httpx.AsyncClient( - timeout=30.0, - follow_redirects=True - ) - - # 使用自定义的http客户端 - self.client = AsyncOpenAI( + # 使用LangChain的ChatOpenAI类 + self.client = ChatOpenAI( api_key=settings.OPENAI_API_KEY, base_url=settings.OPENAI_BASE_URL, - http_client=self.http_client + model=settings.OPENAI_MODEL, + temperature=0.7, + streaming=True ) def _friendly_error_message(self, e: Exception) -> str: @@ -73,7 +67,7 @@ class AIClient: # 默认兜底 return f"处理您的请求时出现错误:{msg}" - async def process(self, query: str) -> AsyncGenerator[str, None]: + async def process(self, query: str) -> AsyncGenerator[str, Any]: """ 处理查询并返回流式响应 @@ -81,53 +75,29 @@ class AIClient: - query (str): 用户查询。 返回: - - AsyncGenerator[str, None]: 流式响应内容。 + - AsyncGenerator[str, Any]: 流式响应内容。 """ system_prompt = """你是一个有用的AI助手,可以帮助用户回答问题和提供帮助。请用中文回答用户的问题。""" try: - # 使用 await 调用异步客户端 - response = await self.client.chat.completions.create( - model=self.model, - messages=[ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": query} - ], - stream=True - ) + # 使用LangChain的异步流式生成 + messages = [ + SystemMessage(content=system_prompt), + HumanMessage(content=query) + ] - # 流式返回响应 - async for chunk in response: - if chunk.choices and chunk.choices[0].delta.content: - yield chunk.choices[0].delta.content + # 使用LangChain的流式响应 + async for chunk in self.client.astream(messages): + if chunk.content: + # 确保只返回字符串类型 + if isinstance(chunk.content, str): + yield chunk.content + elif isinstance(chunk.content, (list, dict)): + # 处理列表或字典类型的内容 + import json + yield json.dumps(chunk.content) except Exception as e: # 记录详细错误,返回友好提示 log.error(f"AI处理查询失败: {str(e)}") yield self._friendly_error_message(e) - - async def close(self) -> None: - """ - 关闭客户端连接 - """ - import asyncio - - # 安全关闭OpenAI客户端 - if hasattr(self, 'client'): - try: - # 检查事件循环是否仍在运行 - loop = asyncio.get_event_loop() - if loop.is_running(): - await self.client.close() - except Exception as e: - log.debug(f"关闭OpenAI客户端时发生异常: {str(e)}") - - # 安全关闭HTTP客户端 - if hasattr(self, 'http_client'): - try: - # 检查事件循环是否仍在运行 - loop = asyncio.get_event_loop() - if loop.is_running(): - await self.http_client.aclose() - except Exception as e: - log.debug(f"关闭HTTP客户端时发生异常: {str(e)}") \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index 0fcd72c1..d85322ae 100755 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -40,4 +40,6 @@ fastapi-limiter==0.1.6 # motor==3.6.0 # mongodb 驱动 # amqp==5.3.1 -# python-socketio==5.14.3 \ No newline at end of file +# python-socketio==5.14.3 +langchain +langchain-openai \ No newline at end of file diff --git a/frontend/src/views/module_application/ai/index.vue b/frontend/src/views/module_application/ai/index.vue index 92a4530b..4744b2d2 100644 --- a/frontend/src/views/module_application/ai/index.vue +++ b/frontend/src/views/module_application/ai/index.vue @@ -81,8 +81,23 @@
+ + + {{ message.collapsed ? "展开" : "收起" }} + -
+
{ }; ws.onmessage = (event) => { - try { - const data = JSON.parse(event.data); - handleWebSocketMessage(data); - } catch (err) { - console.error("解析消息失败:", err); - handleWebSocketMessage({ content: event.data }); - } + // 直接处理文本消息,因为后端发送的是流式文本而不是JSON + handleWebSocketMessage({ content: event.data }); }; ws.onclose = (event) => { @@ -287,6 +300,8 @@ const connectWebSocket = () => { messages.value.forEach((message) => { if (message.type === "assistant" && message.loading) { message.loading = false; + // 检查消息长度并设置折叠状态 + message.collapsed = message.content.length > 200; } }); }; @@ -301,6 +316,8 @@ const connectWebSocket = () => { messages.value.forEach((message) => { if (message.type === "assistant" && message.loading) { message.loading = false; + // 检查消息长度并设置折叠状态 + message.collapsed = message.content.length > 200; } }); }; @@ -391,11 +408,8 @@ const sendMessage = async () => { try { // 发送消息到 WebSocket if (ws?.readyState === WebSocket.OPEN) { - const payload = { - message, - timestamp: Date.now(), - }; - ws.send(JSON.stringify(payload)); + // 直接发送纯文本消息,因为后端期望接收纯文本 + ws.send(message); } else { throw new Error("WebSocket 连接未建立"); } @@ -417,6 +431,8 @@ const addMessage = (type: "user" | "assistant", content: string) => { type, content, timestamp: Date.now(), + // 长消息自动折叠 + collapsed: content.length > 200, }; messages.value.push(message); nextTick(() => scrollToBottom()); @@ -459,6 +475,11 @@ const copyMessage = async (content: string) => { } }; +// 折叠/展开消息 +const toggleMessageFold = (message: ChatMessage) => { + message.collapsed = !message.collapsed; +}; + // 滚动到底部 const scrollToBottom = () => { nextTick(() => { @@ -685,11 +706,39 @@ onUnmounted(() => { } .message-body { + .fold-button { + margin-bottom: 8px; + padding: 0; + font-size: 12px; + color: var(--el-text-color-secondary); + + &:hover { + color: var(--el-color-primary); + } + } + .message-text { font-size: 15px; line-height: 1.6; color: var(--el-text-color-primary); word-wrap: break-word; + transition: all 0.3s ease; + + &.collapsed { + max-height: 120px; + overflow: hidden; + position: relative; + + &::after { + content: ""; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 40px; + background: linear-gradient(to bottom, transparent, var(--el-bg-color)); + } + } :deep(p) { margin: 0 0 12px;