Commit 671bc70b authored by drigle's avatar drigle

feat: 接入在线 LLM 聊天能力

parent c49ab07e
...@@ -38,6 +38,11 @@ export default defineConfig({ ...@@ -38,6 +38,11 @@ export default defineConfig({
changeOrigin: true, changeOrigin: true,
pathRewrite: { '^/api': '' }, pathRewrite: { '^/api': '' },
}, },
'/llm-api': {
target: 'https://lingsuan.top',
changeOrigin: true,
pathRewrite: { '^/llm-api': '' },
},
'/myapi/upload_invoices': { '/myapi/upload_invoices': {
target: 'http://1.94.111.115:5173', target: 'http://1.94.111.115:5173',
changeOrigin: true, changeOrigin: true,
......
...@@ -17,19 +17,20 @@ import { ...@@ -17,19 +17,20 @@ import {
Bubble, Bubble,
Prompts, Prompts,
Sender, Sender,
useXAgent,
useXChat,
Welcome, Welcome,
XStream, XStream,
useXAgent,
useXChat,
} from '@ant-design/x'; } from '@ant-design/x';
import { useQuery } from '@umijs/max';
import { import {
Button, Button,
Collapse, Collapse,
Flex, Flex,
message,
Space, Space,
theme,
Typography, Typography,
message,
theme,
} from 'antd'; } from 'antd';
import { createStyles } from 'antd-style'; import { createStyles } from 'antd-style';
import 'katex/dist/katex.min.css'; import 'katex/dist/katex.min.css';
...@@ -49,9 +50,13 @@ import { ...@@ -49,9 +50,13 @@ import {
createConversation, createConversation,
documentAnalyze, documentAnalyze,
documentRerank, documentRerank,
extractLlmChunkContent,
extractLlmChunkReasoning,
imageAnalyze, imageAnalyze,
llmChat, llmChat,
queryMcps, queryMcps,
queryTopics,
resolveLlmModel,
semanticSearch, semanticSearch,
updateConversation, updateConversation,
} from '../../services/DataService'; } from '../../services/DataService';
...@@ -76,41 +81,36 @@ const preprocessLaTeX = (content) => { ...@@ -76,41 +81,36 @@ const preprocessLaTeX = (content) => {
const SYSTEM_PROMPT = `你是一个有用的的智能助手。 const SYSTEM_PROMPT = `你是一个有用的的智能助手。
如果用户提供了上下文,请根据上下文回答问题,不要试图创造一个答案。如果你不知道答案,请回答你只能根据上下文的内容回答问题。`; 如果用户提供了上下文,请根据上下文回答问题,不要试图创造一个答案。如果你不知道答案,请回答你只能根据上下文的内容回答问题。`;
const AGENTS = { const AGENT_DEFINITIONS = [
key: '2', {
label: '智能体', key: 'knowledge-copilot',
children: [ icon: <DatabaseOutlined />,
{ label: '知识库问答',
key: '2-5', description: '优先检索已接入知识库,基于命中的资料回答问题并说明依据。',
icon: <FileDoneOutlined />, prompt:
label: '审计检查清单生成', '你是企业知识库问答智能体。回答必须优先依据已检索到的知识库片段和用户上传文件;如果没有命中足够依据,请明确说明知识库中未检索到相关内容,并给出需要补充的资料范围。',
description: },
'根据当前审计项目、整改问题和责任单位上下文,生成可执行的审计检查清单。', {
disabled: false, key: 'audit-checklist',
}, icon: <FileDoneOutlined />,
{ label: '审计检查清单生成',
key: '2-4', description:
icon: <PayCircleOutlined />, '根据当前审计项目、整改问题和责任单位上下文,生成可执行的审计检查清单。',
label: '发票助手', prompt:
description: '支持全票种识别,精准高效助力发票管理', '你是审计检查清单生成智能体。你需要把审计项目、整改问题、责任单位、知识库依据拆解为现场可执行检查点,输出包含检查对象、检查资料、核验方法、风险提示和责任跟进建议。',
disabled: false, topicName: 'mock_audit_rectification_query',
}, },
// { {
// key: '2-2', key: 'invoice-assistant',
// icon: <PaperClipOutlined />, icon: <PayCircleOutlined />,
// label: '存单数据提取', label: '发票助手',
// description: '高效采集存单数据,助力业务精准处理', description: '支持全票种识别,精准高效助力发票管理。',
// disabled: true, prompt:
// }, '你是发票处理智能体。你需要围绕发票识别、票据归集、核验和台账整理提供帮助。',
// { },
// key: '2-3', ];
// icon: <FileDoneOutlined />,
// label: '报告核对', const DEFAULT_AGENT = AGENT_DEFINITIONS[0];
// description: '高效核对报告,精准揪错,提升文档准确性',
// disabled: true,
// },
],
};
const AUDIT_TOPIC = { const AUDIT_TOPIC = {
name: 'mock_audit_rectification_query', name: 'mock_audit_rectification_query',
...@@ -129,6 +129,135 @@ const AUDIT_TOPIC = { ...@@ -129,6 +129,135 @@ const AUDIT_TOPIC = {
}, },
}; };
const FALLBACK_TOPICS = [AUDIT_TOPIC];
function normalizeTopic(topic) {
if (!topic) {
return null;
}
return {
_id: topic._id || topic.id,
name: topic.name,
title: topic.title || topic.metadata?.label || topic.name || '业务话题',
metadata: topic.metadata || {},
};
}
function buildAgentPromptGroup(topics = []) {
return {
key: 'agents',
label: '智能体',
children: AGENT_DEFINITIONS.map((agent) => {
const topic =
topics.find((item) => item.name === agent.topicName) ||
(agent.topicName === AUDIT_TOPIC.name ? AUDIT_TOPIC : undefined);
return {
...agent,
initialContent: agent.description,
topic,
};
}),
};
}
function buildTopicPromptGroup(topics = []) {
const normalizedTopics = topics.length > 0 ? topics : FALLBACK_TOPICS;
return {
key: 'topics',
label: '业务话题',
children: normalizedTopics.flatMap((topic) => {
const questions = topic.metadata?.questions || [];
return [
{
key: topic.name,
description: topic.title,
icon: <FileSearchOutlined />,
topic,
},
...questions.map((question) => ({
key: `${topic.name}_${question}`,
description: question,
icon: <AuditOutlined />,
topic,
content: question,
})),
];
}),
};
}
function getKnowledgeBaseId(kb) {
return kb?._id || kb?.id || kb?.name;
}
function dedupeKnowledgeBases(kbs = []) {
return _.uniqBy(kbs.filter(Boolean), getKnowledgeBaseId);
}
function serializeAgent(agent) {
if (!agent) {
return null;
}
return _.pick(agent, [
'_id',
'key',
'label',
'description',
'prompt',
'topicName',
]);
}
function serializeTopic(topic) {
const normalizedTopic = normalizeTopic(topic);
if (!normalizedTopic) {
return null;
}
return _.pick(normalizedTopic, ['_id', 'name', 'title', 'metadata']);
}
function serializeKnowledgeBase(kb) {
if (!kb) {
return null;
}
return _.pick(kb, ['_id', 'name', 'title', 'metadata']);
}
function isObjectIdLike(value) {
return typeof value === 'string' && /^[a-f\d]{24}$/i.test(value);
}
function getReferenceId(record) {
const id = record?._id || record?.id;
return isObjectIdLike(id) ? id : undefined;
}
function getConversationAgent(metadata = {}) {
const agent = metadata.agentConfig || metadata.agent;
if (agent && typeof agent === 'object' && agent.prompt) {
return serializeAgent(agent);
}
return serializeAgent(DEFAULT_AGENT);
}
function getConversationTopic(metadata = {}) {
const topic = metadata.topicConfig || metadata.topic;
if (topic && typeof topic === 'object') {
return serializeTopic(topic);
}
return null;
}
const AUDIT_OBJECT_GROUPS = [ const AUDIT_OBJECT_GROUPS = [
{ {
key: 'project', key: 'project',
...@@ -532,9 +661,32 @@ const useStyle = createStyles(({ token, css, responsive }) => { ...@@ -532,9 +661,32 @@ const useStyle = createStyles(({ token, css, responsive }) => {
line-height: 1.7; line-height: 1.7;
} }
`, `,
knowledgeContextCard: css`
margin-top: 16px;
padding: 14px 16px;
border-radius: 18px;
background: #ffffff;
border: 1px solid rgba(200, 30, 58, 0.16);
color: #0f172a;
display: grid;
gap: 8px;
box-shadow: 0 12px 26px rgba(159, 18, 57, 0.06);
strong {
color: #9f1239;
font-size: 14px;
}
p {
margin: 0;
color: #475569;
font-size: 13px;
line-height: 1.7;
}
`,
promptRow: css` promptRow: css`
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 16px; gap: 16px;
width: 100%; width: 100%;
min-width: 0; min-width: 0;
...@@ -648,9 +800,37 @@ export default forwardRef((props, ref) => { ...@@ -648,9 +800,37 @@ export default forwardRef((props, ref) => {
const { userInfo, conversation, onAddConversation } = props; const { userInfo, conversation, onAddConversation } = props;
const { styles } = useStyle(); const { styles } = useStyle();
const { token } = theme.useToken(); const { token } = theme.useToken();
const conversationAgent = useMemo(
() => getConversationAgent(conversation?.metadata),
[conversation],
);
const conversationTopic = useMemo(
() => getConversationTopic(conversation?.metadata),
[conversation],
);
// 检测是否是发票助手(根据conversation的label判断) // 检测是否是发票助手(根据conversation的label判断)
const isInvoiceAssistant = conversation?.metadata?.label === '发票助手'; const isInvoiceAssistant =
conversationAgent?.key === 'invoice-assistant' ||
conversation?.metadata?.label === '发票助手';
const { data: configuredTopics = [] } = useQuery({
queryKey: ['ai-topics', userInfo?._id],
enabled: !!userInfo,
queryFn: async () => {
try {
const res = await queryTopics({ page_size: 30 });
const topics = (res.records || []).map(normalizeTopic).filter(Boolean);
const hasAuditTopic = topics.some(
(topic) => topic.name === AUDIT_TOPIC.name,
);
return hasAuditTopic ? topics : [...topics, AUDIT_TOPIC];
} catch (error) {
console.error(error);
return FALLBACK_TOPICS;
}
},
});
const [attachmentsOpen, setAttachmentsOpen] = useState(false); const [attachmentsOpen, setAttachmentsOpen] = useState(false);
const [attachedFiles, setAttachedFiles] = useState([]); const [attachedFiles, setAttachedFiles] = useState([]);
...@@ -668,6 +848,12 @@ export default forwardRef((props, ref) => { ...@@ -668,6 +848,12 @@ export default forwardRef((props, ref) => {
}), }),
[auditContext], [auditContext],
); );
const promptGroups = useMemo(() => {
return [
buildTopicPromptGroup(configuredTopics),
buildAgentPromptGroup(configuredTopics),
];
}, [configuredTopics]);
useImperativeHandle(ref, () => { useImperativeHandle(ref, () => {
return { return {
...@@ -721,8 +907,8 @@ export default forwardRef((props, ref) => { ...@@ -721,8 +907,8 @@ export default forwardRef((props, ref) => {
return originMessage; return originMessage;
} }
currentThink = messageData?.choices?.[0]?.delta?.reasoning_content || ''; currentThink = extractLlmChunkReasoning(messageData);
currentContent = messageData?.choices?.[0]?.delta?.content || ''; currentContent = extractLlmChunkContent(messageData);
messageData?.choices?.[0]?.delta?.tool_calls?.forEach((call) => { messageData?.choices?.[0]?.delta?.tool_calls?.forEach((call) => {
const index = call.index; const index = call.index;
const toolCall = (toolCalls[index] = toolCalls[index] || { const toolCall = (toolCalls[index] = toolCalls[index] || {
...@@ -772,29 +958,51 @@ export default forwardRef((props, ref) => { ...@@ -772,29 +958,51 @@ export default forwardRef((props, ref) => {
); );
}; };
const getActiveKnowledgeBases = (message) => {
const topicKbs = conversationTopic?.metadata?.kbs || [];
const conversationKbs = conversation?.metadata?.knowledgeBases || [];
const messageKbs =
message?.knowledgeBases || message?.settings?.knowledgeBases || [];
const selectedKbs = dedupeKnowledgeBases([
...messageKbs,
...topicKbs,
...conversationKbs,
]);
if (selectedKbs.length > 0) {
return selectedKbs;
}
return [];
};
const getSystemPrompt = async () => { const getSystemPrompt = async () => {
const topic = conversation.metadata.topic; const topic = conversationTopic;
const agent = conversationAgent;
const auditContextText = formatAuditContext( const auditContextText = formatAuditContext(
conversation.metadata.auditContext || [], conversation.metadata.auditContext || [],
); );
const auditPrompt = auditContextText const auditPrompt = auditContextText
? `\n\n当前审计对象上下文:\n${auditContextText}` ? `\n\n当前审计对象上下文:\n${auditContextText}`
: ''; : '';
const agentPrompt = agent?.prompt
? `\n\n当前智能体:${agent.label}\n${agent.prompt}`
: '';
if (!topic) { if (!topic) {
return `${SYSTEM_PROMPT}${auditPrompt}`; return `${SYSTEM_PROMPT}${agentPrompt}${auditPrompt}`;
} }
return `${topic.metadata.prompt || ''}\n${ return `${topic.metadata.prompt || ''}\n${
topic.metadata.context || '' topic.metadata.context || ''
}${auditPrompt}`; }${agentPrompt}${auditPrompt}`;
}; };
const getMessagePrompt = async (message) => { const getMessagePrompt = async (message) => {
const topic = conversation.metadata.topic; const topic = conversationTopic;
const files = message.files || []; const files = message.files || [];
const searchSubjects = topic?.metadata?.search_subjects || []; const searchSubjects = topic?.metadata?.search_subjects || [];
const kbs = topic?.metadata?.kbs || []; const kbs = getActiveKnowledgeBases(message);
const searchResults = []; const searchResults = [];
const docs = []; const docs = [];
let fileAnalyzeResults = []; let fileAnalyzeResults = [];
...@@ -817,33 +1025,47 @@ export default forwardRef((props, ref) => { ...@@ -817,33 +1025,47 @@ export default forwardRef((props, ref) => {
}); });
docs.push( docs.push(
res.hits?.hits?.flatMap((doc) => ...(res.hits?.hits?.flatMap((doc) =>
doc._source.embeddings.map( (doc._source.embeddings || []).map(
(chunk) => (chunk) =>
`# ${doc._source.title}\n${chunk.content || chunk.text || ''}`, `# ${doc._source.title}\n${chunk.content || chunk.text || ''}`,
), ),
) || [], ) || []),
); );
} }
if (kbs.length > 0) { if (kbs.length > 0) {
const res = await semanticSearch('ai_knowledge_base_document', { for (const kb of kbs) {
text: message.content, const kbId = getKnowledgeBaseId(kb);
size: 5, if (!kbId) {
source: ['title', 'embeddings.content', 'embeddings.text'], continue;
terms: { }
knowledge_base: kbs[0]._id,
},
});
docs.push( const res = await semanticSearch('ai_knowledge_base_document', {
...(res.hits?.hits?.flatMap((doc) => text: message.content,
doc._source.embeddings.map( size: 5,
(chunk) => source: [
`# ${doc._source.title}\n${chunk.content || chunk.text || ''}`, 'title',
), 'knowledge_base',
) || []), 'embeddings.content',
); 'embeddings.text',
],
terms: {
knowledge_base: kbId,
},
});
docs.push(
...(res.hits?.hits?.flatMap((doc) =>
(doc._source.embeddings || []).map(
(chunk) =>
`# ${doc._source.title || kb.title}\n知识库:${kb.title}\n${
chunk.content || chunk.text || ''
}`,
),
) || []),
);
}
} }
if (docs.length > 0) { if (docs.length > 0) {
...@@ -914,6 +1136,10 @@ export default forwardRef((props, ref) => { ...@@ -914,6 +1136,10 @@ export default forwardRef((props, ref) => {
if (searchResults.length > 0) { if (searchResults.length > 0) {
searchPrompt = `知识库搜索结果:\n${searchResults.join('\n')}\n\n`; searchPrompt = `知识库搜索结果:\n${searchResults.join('\n')}\n\n`;
} else if (kbs.length > 0) {
searchPrompt = `知识库搜索结果:本次未在已选知识库中检索到足够相关内容。\n已选知识库:${kbs
.map((kb) => kb.title || kb.name)
.join('、')}\n请不要编造知识库中不存在的结论。\n\n`;
} }
if (fileAnalyzeResults.length > 0) { if (fileAnalyzeResults.length > 0) {
...@@ -927,6 +1153,12 @@ export default forwardRef((props, ref) => { ...@@ -927,6 +1153,12 @@ export default forwardRef((props, ref) => {
filePrompt = `用户上传的文件内容:\n${fileContent}\n\n`; filePrompt = `用户上传的文件内容:\n${fileContent}\n\n`;
} }
if (kbs.length > 0) {
kbPrompt = `回答范围:优先基于已选知识库(${kbs
.map((kb) => kb.title || kb.name)
.join('、')})和用户上传文件回答;如果依据不足,请说明缺口。\n\n`;
}
return `${auditPrompt}${searchPrompt}${filePrompt}${kbPrompt}用户提问:${message.content}`; return `${auditPrompt}${searchPrompt}${filePrompt}${kbPrompt}用户提问:${message.content}`;
}; };
...@@ -960,19 +1192,32 @@ export default forwardRef((props, ref) => { ...@@ -960,19 +1192,32 @@ export default forwardRef((props, ref) => {
const content = JSON.stringify(requestMessages); const content = JSON.stringify(requestMessages);
const long = content.length > 900000; const long = content.length > 900000;
const model = long ? 'qwen-long' : 'qwen-plus'; const model = await resolveLlmModel({
const enable_search = long,
conversation.metadata.topic?.metadata?.enable_search; thinking: true,
});
const enable_search = conversationTopic?.metadata?.enable_search;
const enable_thinking = true; const enable_thinking = true;
const requestContext = {
agent: conversationAgent,
topic: conversationTopic
? {
name: conversationTopic.name,
title: conversationTopic.title,
}
: undefined,
knowledgeBases: getActiveKnowledgeBases(message).map(
serializeKnowledgeBase,
),
auditContext: conversation.metadata.auditContext || [],
};
const data = { const data = {
model, model,
stream: true, stream: true,
messages: requestMessages, messages: requestMessages,
tools: context: requestContext,
!conversation.metadata.topic && tools.length > 0 tools: !conversationTopic && tools.length > 0 ? tools : undefined,
? tools
: undefined,
enable_search, enable_search,
enable_thinking, enable_thinking,
}; };
...@@ -1109,7 +1354,7 @@ export default forwardRef((props, ref) => { ...@@ -1109,7 +1354,7 @@ export default forwardRef((props, ref) => {
messages: [ messages: [
{ {
role: 'system', role: 'system',
content: SYSTEM_PROMPT, content: await getSystemPrompt(),
}, },
...data.messages, ...data.messages,
currentMessage, currentMessage,
...@@ -1403,10 +1648,18 @@ export default forwardRef((props, ref) => { ...@@ -1403,10 +1648,18 @@ export default forwardRef((props, ref) => {
topic, topic,
files = null, files = null,
settings = null, settings = null,
agentLabel = null, agent = null,
knowledgeBases = null,
}) => { }) => {
const selectedAgent = serializeAgent(agent || DEFAULT_AGENT);
const selectedTopic = serializeTopic(topic);
const selectedKnowledgeBases = knowledgeBases || null;
const serializedKnowledgeBases =
selectedKnowledgeBases?.map(serializeKnowledgeBase).filter(Boolean) ||
null;
const nextSettings = { const nextSettings = {
...auditSettings, ...auditSettings,
knowledgeBases: serializedKnowledgeBases,
...settings, ...settings,
}; };
const userMessage = content && { const userMessage = content && {
...@@ -1421,14 +1674,31 @@ export default forwardRef((props, ref) => { ...@@ -1421,14 +1674,31 @@ export default forwardRef((props, ref) => {
}; };
if (!conversation) { if (!conversation) {
await addConversation( const conversationData = {
{ label: agent
label: agentLabel || content || topic?.title || AUDIT_TOPIC.title, ? selectedAgent?.label
topic: topic, : content ||
auditContext: nextSettings.auditContext, selectedTopic?.title ||
}, selectedAgent?.label ||
userMessage, AUDIT_TOPIC.title,
); agentConfig: selectedAgent || serializeAgent(DEFAULT_AGENT),
knowledgeBases: serializedKnowledgeBases || [],
auditContext: nextSettings.auditContext,
};
const topicId = getReferenceId(selectedTopic);
const agentId = getReferenceId(selectedAgent);
if (selectedTopic) {
conversationData.topicConfig = selectedTopic;
}
if (topicId) {
conversationData.topic = topicId;
}
if (agentId) {
conversationData.agent = agentId;
}
await addConversation(conversationData, userMessage);
return; return;
} }
...@@ -1465,9 +1735,18 @@ export default forwardRef((props, ref) => { ...@@ -1465,9 +1735,18 @@ export default forwardRef((props, ref) => {
<div className={styles.chatList}> <div className={styles.chatList}>
{conversation ? ( {conversation ? (
<div className={styles.chatListInner}> <div className={styles.chatListInner}>
{conversation.metadata.topic && ( {(conversationTopic || conversationAgent) && (
<Welcome <Welcome
title={conversation.metadata.topic.title} title={
conversationAgent?.label ||
conversationTopic?.title ||
'智能助手'
}
description={
conversationTopic?.title
? `当前话题:${conversationTopic.title}`
: conversationAgent?.description
}
style={{ style={{
background: background:
'linear-gradient(135deg, rgba(0, 156, 217, 0.12) 0%, rgba(186, 230, 253, 0.36) 100%)', 'linear-gradient(135deg, rgba(0, 156, 217, 0.12) 0%, rgba(186, 230, 253, 0.36) 100%)',
...@@ -1476,6 +1755,16 @@ export default forwardRef((props, ref) => { ...@@ -1476,6 +1755,16 @@ export default forwardRef((props, ref) => {
}} }}
/> />
)} )}
{conversation.metadata.knowledgeBases?.length > 0 && (
<div className={styles.knowledgeContextCard}>
<strong>已连接知识库</strong>
<p>
{conversation.metadata.knowledgeBases
.map((kb) => kb.title || kb.name)
.join(' / ')}
</p>
</div>
)}
{conversation.metadata.auditContext?.length > 0 && ( {conversation.metadata.auditContext?.length > 0 && (
<div className={styles.auditContextCard}> <div className={styles.auditContextCard}>
<strong>已读取审计对象上下文</strong> <strong>已读取审计对象上下文</strong>
...@@ -1525,64 +1814,32 @@ export default forwardRef((props, ref) => { ...@@ -1525,64 +1814,32 @@ export default forwardRef((props, ref) => {
) : ( ) : (
<div className={`${styles.chatListInner} ${styles.placeholder}`}> <div className={`${styles.chatListInner} ${styles.placeholder}`}>
<div className={styles.promptRow}> <div className={styles.promptRow}>
<Prompts {promptGroups.map((group) => (
items={[ <Prompts
{ key={group.key}
key: AUDIT_TOPIC.name, items={[group]}
label: '话题', style={{
children: [ flex: 1,
{ }}
key: AUDIT_TOPIC.name, styles={{
description: AUDIT_TOPIC.title, list: { height: '100%' },
icon: <FileSearchOutlined />, subItem: { padding: 0, background: '#ffffff' },
topic: AUDIT_TOPIC, }}
}, onItemClick={async (info) => {
...AUDIT_TOPIC.metadata.questions.map((question) => ({ const selectedAgent = info.data.prompt
key: question, ? info.data
description: question, : info.data.agent;
icon: <AuditOutlined />,
topic: AUDIT_TOPIC, onSubmit({
content: question, content: info.data.content || info.data.initialContent,
})), topic: info.data.topic,
], agent: selectedAgent,
}, knowledgeBases: info.data.knowledgeBases,
]} });
style={{ }}
flex: 1, className={styles.chatPrompt}
}} />
styles={{ ))}
list: { height: '100%' },
subItem: { padding: 0, background: '#ffffff' },
}}
onItemClick={async (info) => {
onSubmit({
content: info.data.content,
topic: info.data.topic,
});
}}
className={styles.chatPrompt}
/>
<Prompts
items={[AGENTS]}
style={{
flex: 1,
}}
styles={{
list: { height: '100%' },
subItem: { background: '#ffffff' },
}}
onItemClick={(info) => {
onSubmit({
content: info.data.description,
topic:
info.data.label === AGENTS.children[0].label
? AUDIT_TOPIC
: undefined,
agentLabel: info.data.label,
});
}}
className={styles.chatPrompt}
/>
</div> </div>
</div> </div>
)} )}
...@@ -1620,7 +1877,7 @@ export default forwardRef((props, ref) => { ...@@ -1620,7 +1877,7 @@ export default forwardRef((props, ref) => {
const chatSender = ( const chatSender = (
<div className={styles.senderShell}> <div className={styles.senderShell}>
{messages.length === 0 && conversation?.metadata?.topic && ( {messages.length === 0 && conversationTopic && (
<Prompts <Prompts
vertical vertical
className={styles.senderPrompt} className={styles.senderPrompt}
...@@ -1629,7 +1886,7 @@ export default forwardRef((props, ref) => { ...@@ -1629,7 +1886,7 @@ export default forwardRef((props, ref) => {
padding: '6px 12px', padding: '6px 12px',
}, },
}} }}
items={conversation.metadata.topic.metadata.questions?.map((q) => ({ items={conversationTopic.metadata.questions?.map((q) => ({
key: q, key: q,
description: q, description: q,
}))} }))}
......
...@@ -13,9 +13,9 @@ import { ...@@ -13,9 +13,9 @@ import {
Bubble, Bubble,
Prompts, Prompts,
Sender, Sender,
XStream,
useXAgent, useXAgent,
useXChat, useXChat,
XStream,
} from '@ant-design/x'; } from '@ant-design/x';
import { App, Button, Flex, Space, Spin, Steps, Tag, Typography } from 'antd'; import { App, Button, Flex, Space, Spin, Steps, Tag, Typography } from 'antd';
import { createStyles } from 'antd-style'; import { createStyles } from 'antd-style';
...@@ -34,8 +34,11 @@ import { ...@@ -34,8 +34,11 @@ import {
import { import {
callMcpTool, callMcpTool,
createConversation, createConversation,
extractLlmChunkContent,
extractLlmChunkReasoning,
llmChat, llmChat,
queryMcps, queryMcps,
resolveLlmModel,
updateConversation, updateConversation,
} from '../../services/DataService'; } from '../../services/DataService';
...@@ -71,6 +74,14 @@ const SYSTEM_PROMPT = `你叫Mia,是一个有用的的发票智能助手。如 ...@@ -71,6 +74,14 @@ const SYSTEM_PROMPT = `你叫Mia,是一个有用的的发票智能助手。如
如果你不知道答案,请回答你只能根据上下文的内容回答问题。在识别完文件之后,需要调用mcp工具生成用户可能需要的问题选项。用户确认数量没问题之后,需要生成一个表单供用户填写。接收到的是html的时候,assistent角色回复的消息需要是html格式,只要用户发送了表单信息,那么无论表单信息内容是否为空,都需要调用mcp工具对表单信息进行标准化处理 如果你不知道答案,请回答你只能根据上下文的内容回答问题。在识别完文件之后,需要调用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 }) => { const useStyle = createStyles(({ token, css, responsive }) => {
return { return {
chat: css` chat: css`
...@@ -437,8 +448,8 @@ const InvoiceAssistant = forwardRef((props, ref) => { ...@@ -437,8 +448,8 @@ const InvoiceAssistant = forwardRef((props, ref) => {
return originMessage; return originMessage;
} }
currentThink = messageData?.choices?.[0]?.delta?.reasoning_content || ''; currentThink = extractLlmChunkReasoning(messageData);
currentContent = messageData?.choices?.[0]?.delta?.content || ''; currentContent = extractLlmChunkContent(messageData);
messageData?.choices?.[0]?.delta?.tool_calls?.forEach((call) => { messageData?.choices?.[0]?.delta?.tool_calls?.forEach((call) => {
const index = call.index; const index = call.index;
const toolCall = (toolCalls[index] = toolCalls[index] || { const toolCall = (toolCalls[index] = toolCalls[index] || {
...@@ -729,7 +740,10 @@ const InvoiceAssistant = forwardRef((props, ref) => { ...@@ -729,7 +740,10 @@ const InvoiceAssistant = forwardRef((props, ref) => {
} }
const content = JSON.stringify(requestMessages); const content = JSON.stringify(requestMessages);
const long = content.length > 900000; 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_search = false;
const enable_thinking = false; const enable_thinking = false;
...@@ -800,8 +814,10 @@ const InvoiceAssistant = forwardRef((props, ref) => { ...@@ -800,8 +814,10 @@ const InvoiceAssistant = forwardRef((props, ref) => {
) { ) {
mcpArguments = { mcpArguments = {
...mcpArguments, ...mcpArguments,
apikey: 'sk-bf3698390f3b40dd8435cb73169a7437', ...(INVOICE_TOOL_API_KEY
baseurl: 'https://api.deepseek.com', ? { apikey: INVOICE_TOOL_API_KEY }
: {}),
baseurl: INVOICE_TOOL_BASE_URL,
}; };
} }
......
...@@ -535,7 +535,12 @@ function buildConversationItems(conversations = []) { ...@@ -535,7 +535,12 @@ function buildConversationItems(conversations = []) {
return conversations.map((c) => ({ return conversations.map((c) => ({
key: c.name, key: c.name,
label: c.metadata.label || '新会话', label: c.metadata.label || '新会话',
icon: c.metadata.topic ? <BorderlessTableOutlined /> : <CommentOutlined />, icon:
c.metadata.topic || c.metadata.topicConfig ? (
<BorderlessTableOutlined />
) : (
<CommentOutlined />
),
pinned: c.metadata.pinned, pinned: c.metadata.pinned,
group: '历史会话', group: '历史会话',
})); }));
...@@ -545,7 +550,12 @@ function buildCompactConversationItems(conversations = []) { ...@@ -545,7 +550,12 @@ function buildCompactConversationItems(conversations = []) {
return conversations.slice(0, 6).map((c) => ({ return conversations.slice(0, 6).map((c) => ({
key: c.name, key: c.name,
label: c.metadata.label || '新会话', label: c.metadata.label || '新会话',
icon: c.metadata.topic ? <BorderlessTableOutlined /> : <CommentOutlined />, icon:
c.metadata.topic || c.metadata.topicConfig ? (
<BorderlessTableOutlined />
) : (
<CommentOutlined />
),
pinned: c.metadata.pinned, pinned: c.metadata.pinned,
})); }));
} }
......
...@@ -4,7 +4,117 @@ import { request } from '@umijs/max'; ...@@ -4,7 +4,117 @@ import { request } from '@umijs/max';
const SUBJECT_CONVERSATION = 'ai_conversation'; const SUBJECT_CONVERSATION = 'ai_conversation';
const SUBJECT_TOPIC = 'ai_topic'; 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) { export function getApplication(name) {
return request(`/api/application/${name}`, { return request(`/api/application/${name}`, {
...@@ -158,14 +268,37 @@ export async function callMcpTool(mcpName, toolName, data = {}) { ...@@ -158,14 +268,37 @@ export async function callMcpTool(mcpName, toolName, data = {}) {
} }
export async function llmChat(data, signal = undefined) { export async function llmChat(data, signal = undefined) {
return fetch(`/api/llm/chat`, { if (!LLM_API_KEY) {
method: 'post', throw new Error('缺少 UMI_APP_LLM_API_KEY,请配置线上模型服务 API Key。');
headers: { }
'Content-Type': 'application/json',
}, 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, 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) { export async function queryConversations(query) {
...@@ -179,7 +312,7 @@ export async function queryConversations(query) { ...@@ -179,7 +312,7 @@ export async function queryConversations(query) {
page_size: 20, page_size: 20,
mode: 'basic', mode: 'basic',
select: 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', sort: '-metadata.pinned -created_at',
filter: { filter: {
'metadata.user': userInfo._id, '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