Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 25 additions & 55 deletions backend/app/api/v1/module_application/ai/tools/ai_util.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -73,61 +67,37 @@ def _friendly_error_message(self, e: Exception) -> str:
# 默认兜底
return f"处理您的请求时出现错误:{msg}"

async def process(self, query: str) -> AsyncGenerator[str, None]:
async def process(self, query: str) -> AsyncGenerator[str, Any]:
"""
处理查询并返回流式响应

参数:
- 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)}")
4 changes: 3 additions & 1 deletion backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,6 @@ fastapi-limiter==0.1.6
# motor==3.6.0 # mongodb 驱动

# amqp==5.3.1
# python-socketio==5.14.3
# python-socketio==5.14.3
langchain
langchain-openai
75 changes: 62 additions & 13 deletions frontend/src/views/module_application/ai/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,23 @@
</strong>
</div>
<div class="message-body">
<!-- 折叠/展开按钮 -->
<el-button
v-if="message.content.length > 200"
text
size="small"
:icon="message.collapsed ? ArrowDown : ArrowUp"
class="fold-button"
@click="toggleMessageFold(message)"
>
{{ message.collapsed ? "展开" : "收起" }}
</el-button>
<!-- 实时显示累积的消息内容 -->
<div class="message-text" v-html="formatMessage(message.content)"></div>
<div
class="message-text"
:class="{ collapsed: message.collapsed }"
v-html="formatMessage(message.content)"
></div>
<!-- 只有内容为空且loading时才显示打字指示器 -->
<div
v-if="message.type === 'assistant' && message.loading && !message.content"
Expand Down Expand Up @@ -169,6 +184,8 @@ import {
Setting,
CopyDocument,
RefreshLeft,
ArrowDown,
ArrowUp,
} from "@element-plus/icons-vue";
import MarkdownIt from "markdown-it";
import markdownItHighlightjs from "markdown-it-highlightjs";
Expand All @@ -182,6 +199,7 @@ interface ChatMessage {
content: string;
timestamp: number;
loading?: boolean;
collapsed?: boolean;
}

// 创建MarkdownIt实例并配置插件
Expand Down Expand Up @@ -269,13 +287,8 @@ const connectWebSocket = () => {
};

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) => {
Expand All @@ -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;
}
});
};
Expand All @@ -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;
}
});
};
Expand Down Expand Up @@ -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 连接未建立");
}
Expand All @@ -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());
Expand Down Expand Up @@ -459,6 +475,11 @@ const copyMessage = async (content: string) => {
}
};

// 折叠/展开消息
const toggleMessageFold = (message: ChatMessage) => {
message.collapsed = !message.collapsed;
};

// 滚动到底部
const scrollToBottom = () => {
nextTick(() => {
Expand Down Expand Up @@ -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;
Expand Down