Commit 671bc70b authored by drigle's avatar drigle

feat: 接入在线 LLM 聊天能力

parent c49ab07e
......@@ -38,6 +38,11 @@ export default defineConfig({
changeOrigin: true,
pathRewrite: { '^/api': '' },
},
'/llm-api': {
target: 'https://lingsuan.top',
changeOrigin: true,
pathRewrite: { '^/llm-api': '' },
},
'/myapi/upload_invoices': {
target: 'http://1.94.111.115:5173',
changeOrigin: true,
......
This diff is collapsed.
......@@ -13,9 +13,9 @@ import {
Bubble,
Prompts,
Sender,
XStream,
useXAgent,
useXChat,
XStream,
} from '@ant-design/x';
import { App, Button, Flex, Space, Spin, Steps, Tag, Typography } from 'antd';
import { createStyles } from 'antd-style';
......@@ -34,8 +34,11 @@ import {
import {
callMcpTool,
createConversation,
extractLlmChunkContent,
extractLlmChunkReasoning,
llmChat,
queryMcps,
resolveLlmModel,
updateConversation,
} from '../../services/DataService';
......@@ -71,6 +74,14 @@ const SYSTEM_PROMPT = `你叫Mia,是一个有用的的发票智能助手。如
如果你不知道答案,请回答你只能根据上下文的内容回答问题。在识别完文件之后,需要调用mcp工具生成用户可能需要的问题选项。用户确认数量没问题之后,需要生成一个表单供用户填写。接收到的是html的时候,assistent角色回复的消息需要是html格式,只要用户发送了表单信息,那么无论表单信息内容是否为空,都需要调用mcp工具对表单信息进行标准化处理
`;
const INVOICE_TOOL_API_KEY =
process.env.UMI_APP_INVOICE_TOOL_API_KEY ||
process.env.VITE_INVOICE_TOOL_API_KEY;
const INVOICE_TOOL_BASE_URL =
process.env.UMI_APP_INVOICE_TOOL_BASE_URL ||
process.env.VITE_INVOICE_TOOL_BASE_URL ||
'https://api.deepseek.com';
const useStyle = createStyles(({ token, css, responsive }) => {
return {
chat: css`
......@@ -437,8 +448,8 @@ const InvoiceAssistant = forwardRef((props, ref) => {
return originMessage;
}
currentThink = messageData?.choices?.[0]?.delta?.reasoning_content || '';
currentContent = messageData?.choices?.[0]?.delta?.content || '';
currentThink = extractLlmChunkReasoning(messageData);
currentContent = extractLlmChunkContent(messageData);
messageData?.choices?.[0]?.delta?.tool_calls?.forEach((call) => {
const index = call.index;
const toolCall = (toolCalls[index] = toolCalls[index] || {
......@@ -729,7 +740,10 @@ const InvoiceAssistant = forwardRef((props, ref) => {
}
const content = JSON.stringify(requestMessages);
const long = content.length > 900000;
const model = long ? 'qwen-long' : 'qwen-plus';
const model = await resolveLlmModel({
long,
thinking: false,
});
const enable_search = false;
const enable_thinking = false;
......@@ -800,8 +814,10 @@ const InvoiceAssistant = forwardRef((props, ref) => {
) {
mcpArguments = {
...mcpArguments,
apikey: 'sk-bf3698390f3b40dd8435cb73169a7437',
baseurl: 'https://api.deepseek.com',
...(INVOICE_TOOL_API_KEY
? { apikey: INVOICE_TOOL_API_KEY }
: {}),
baseurl: INVOICE_TOOL_BASE_URL,
};
}
......
......@@ -535,7 +535,12 @@ function buildConversationItems(conversations = []) {
return conversations.map((c) => ({
key: c.name,
label: c.metadata.label || '新会话',
icon: c.metadata.topic ? <BorderlessTableOutlined /> : <CommentOutlined />,
icon:
c.metadata.topic || c.metadata.topicConfig ? (
<BorderlessTableOutlined />
) : (
<CommentOutlined />
),
pinned: c.metadata.pinned,
group: '历史会话',
}));
......@@ -545,7 +550,12 @@ function buildCompactConversationItems(conversations = []) {
return conversations.slice(0, 6).map((c) => ({
key: c.name,
label: c.metadata.label || '新会话',
icon: c.metadata.topic ? <BorderlessTableOutlined /> : <CommentOutlined />,
icon:
c.metadata.topic || c.metadata.topicConfig ? (
<BorderlessTableOutlined />
) : (
<CommentOutlined />
),
pinned: c.metadata.pinned,
}));
}
......
......@@ -4,7 +4,117 @@ import { request } from '@umijs/max';
const SUBJECT_CONVERSATION = 'ai_conversation';
const SUBJECT_TOPIC = 'ai_topic';
const SUBJECT_KNOWLEDGEBASE = 'ai_knowledgebase';
const LLM_API_BASE =
process.env.UMI_APP_LLM_API_BASE ||
process.env.VITE_LLM_API_BASE ||
'/llm-api';
const LLM_CHAT_PATH =
process.env.UMI_APP_LLM_CHAT_PATH ||
process.env.VITE_LLM_CHAT_PATH ||
'/v1/chat/completions';
const LLM_API_KEY =
process.env.UMI_APP_LLM_API_KEY || process.env.VITE_LLM_API_KEY;
const LLM_MODEL =
process.env.UMI_APP_LLM_MODEL || process.env.VITE_LLM_MODEL || 'gpt-5.5';
function joinUrl(baseUrl, path) {
return `${baseUrl.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`;
}
export function extractLlmChunkContent(value) {
if (typeof value === 'string') {
return value;
}
if (!value || typeof value !== 'object') {
return '';
}
if (typeof value.content === 'string') {
return value.content;
}
if (typeof value.text === 'string') {
return value.text;
}
const delta = value.delta;
if (delta && typeof delta === 'object') {
const deltaContent = extractLlmChunkContent(delta);
if (deltaContent) {
return deltaContent;
}
}
const message = value.message;
if (message && typeof message === 'object') {
const messageContent = extractLlmChunkContent(message);
if (messageContent) {
return messageContent;
}
}
const data = value.data;
if (data && typeof data === 'object') {
const dataContent = extractLlmChunkContent(data);
if (dataContent) {
return dataContent;
}
}
const choices = value.choices;
if (Array.isArray(choices)) {
return choices.map((choice) => extractLlmChunkContent(choice)).join('');
}
return '';
}
export function extractLlmChunkReasoning(value) {
if (!value || typeof value !== 'object') {
return '';
}
const reasoning =
value.reasoning_content ||
value.reasoning ||
value.thinking_content ||
value.thought ||
value.thoughts;
if (typeof reasoning === 'string') {
return reasoning;
}
const delta = value.delta;
if (delta && typeof delta === 'object') {
const deltaReasoning = extractLlmChunkReasoning(delta);
if (deltaReasoning) {
return deltaReasoning;
}
}
const message = value.message;
if (message && typeof message === 'object') {
const messageReasoning = extractLlmChunkReasoning(message);
if (messageReasoning) {
return messageReasoning;
}
}
const data = value.data;
if (data && typeof data === 'object') {
const dataReasoning = extractLlmChunkReasoning(data);
if (dataReasoning) {
return dataReasoning;
}
}
const choices = value.choices;
if (Array.isArray(choices)) {
return choices.map((choice) => extractLlmChunkReasoning(choice)).join('');
}
return '';
}
export function getApplication(name) {
return request(`/api/application/${name}`, {
......@@ -158,14 +268,37 @@ export async function callMcpTool(mcpName, toolName, data = {}) {
}
export async function llmChat(data, signal = undefined) {
return fetch(`/api/llm/chat`, {
method: 'post',
headers: {
if (!LLM_API_KEY) {
throw new Error('缺少 UMI_APP_LLM_API_KEY,请配置线上模型服务 API Key。');
}
const headers = new Headers({
Accept: 'text/event-stream',
Authorization: `Bearer ${LLM_API_KEY}`,
'Content-Type': 'application/json',
},
});
const response = await fetch(joinUrl(LLM_API_BASE, LLM_CHAT_PATH), {
method: 'POST',
headers,
signal,
body: JSON.stringify(data),
body: JSON.stringify({
...data,
model: LLM_MODEL,
stream: true,
}),
});
if (!response.ok) {
const message = await response.text();
throw new Error(message || `HTTP ${response.status}`);
}
return response;
}
export async function resolveLlmModel(options = {}) {
return LLM_MODEL;
}
export async function queryConversations(query) {
......@@ -179,7 +312,7 @@ export async function queryConversations(query) {
page_size: 20,
mode: 'basic',
select:
'metadata.label metadata.pinned metadata.agent metadata.avatar metadata.topic',
'metadata.label metadata.pinned metadata.agent metadata.agentConfig metadata.avatar metadata.topic metadata.topicConfig metadata.knowledgeBases',
sort: '-metadata.pinned -created_at',
filter: {
'metadata.user': userInfo._id,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment