Skip to content

Commit 80bf426

Browse files
committed
✨Knowledge_base summary: frontend streaming output
1 parent 3c1a736 commit 80bf426

File tree

4 files changed

+92
-87
lines changed

4 files changed

+92
-87
lines changed

backend/apps/knowledge_summary_app.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ async def auto_summary(
2929
)
3030
except Exception as e:
3131
async def generate_error():
32-
yield f"data: {json.dumps({'status': 'error', 'message': str(e)})}\n\n"
32+
yield f"data: {{\"status\": \"error\", \"message\": \"知识库摘要生成失败: {e}\"}}\n\n"
3333
return StreamingResponse(
3434
generate_error(),
3535
media_type="text/event-stream",
@@ -51,7 +51,7 @@ def change_summary(
5151
# Try to list indices as a health check
5252
return ElasticSearchService().change_summary(index_name=index_name,summary_result=summary_result, es_core=es_core,user_id=user_id)
5353
except Exception as e:
54-
raise HTTPException(status_code=500, detail=f"{str(e)}")
54+
raise HTTPException(status_code=500, detail=f"知识库摘要更新失败: {str(e)}")
5555

5656

5757
@router.get("/{index_name}/summary")
@@ -63,4 +63,4 @@ def get_summary(
6363
# Try to list indices as a health check
6464
return ElasticSearchService().get_summary(index_name=index_name)
6565
except Exception as e:
66-
raise HTTPException(status_code=500, detail=f"{str(e)}")
66+
raise HTTPException(status_code=500, detail=f"获取知识库摘要失败: {str(e)}")

backend/services/elasticsearch_service.py

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
3. Support for multiple search methods: exact search, semantic search, and hybrid search
99
4. Health check interface
1010
"""
11-
11+
import asyncio
12+
import logging
1213
import os
1314
import time
1415
from typing import Optional
@@ -632,23 +633,24 @@ async def generate_summary():
632633
token_join = []
633634
try:
634635
for new_token in generate_knowledge_summary_stream(keywords_for_summary):
635-
print(new_token)
636-
token_join.append(new_token)
637-
yield f"data: {{\"status\": \"success\", \"message\": \"Index {index_name} summary successfully\", \"summary\": \"{new_token}\"}}\n\n"
638-
639-
if new_token == "END":
640-
model_output = "".join(token_join)
641-
# updata sql
642-
knowledge_record = get_knowledge_by_name(index_name)
643-
if knowledge_record:
644-
update_data = {
645-
"knowledge_describe": model_output,
646-
"updated_by": user_id,
647-
}
648-
update_knowledge_record(knowledge_record["knowledge_id"], update_data)
636+
if new_token == "END":
637+
model_output = "".join(token_join)
638+
knowledge_record = get_knowledge_by_name(index_name)
639+
if knowledge_record:
640+
update_data = {
641+
"knowledge_describe": model_output,
642+
"updated_by": user_id,
643+
}
644+
update_knowledge_record(knowledge_record["knowledge_id"], update_data)
645+
break
646+
else:
647+
token_join.append(new_token)
648+
logging.info(new_token)
649+
yield f"data: {{\"status\": \"success\", \"message\": \"{new_token}\"}}\n\n"
650+
await asyncio.sleep(0.1)
649651

650652
except Exception as e:
651-
yield f"data: {{\"status\": \"error\", \"message\": \"{str(e)}\"}}\n\n"
653+
yield f"data: {{\"status\": \"error\", \"message\": \"{e}\"}}\n\n"
652654

653655
# Return the flow response
654656
return StreamingResponse(
@@ -778,10 +780,10 @@ def get_summary(self,
778780
knowledge_record = get_knowledge_by_name(index_name)
779781
if knowledge_record:
780782
summary_result = knowledge_record["knowledge_describe"]
781-
return {"status": "success", "message": f"Index {index_name} summary successfully", "summary": summary_result}
783+
return {"status": "success", "message": f"索引 {index_name} 摘要获取成功", "summary": summary_result}
782784
raise HTTPException(
783785
status_code=500,
784-
detail=f"Failed to get index {index_name} summary"
786+
detail=f"无法获取索引 {index_name} 的摘要"
785787
)
786788
except Exception as e:
787-
raise HTTPException(status_code=500, detail=f"{str(e)}")
789+
raise HTTPException(status_code=500, detail=f"获取摘要失败: {str(e)}")

frontend/app/setup/knowledgeBaseSetup/document/DocumentListLayout.tsx

Lines changed: 25 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ const DocumentListLayout: React.FC<DocumentListLayoutProps> = ({
124124
const [summary, setSummary] = useState('');
125125
const [isSummarizing, setIsSummarizing] = useState(false);
126126
const [isSaving, setIsSaving] = useState(false);
127-
const { summaryIndex } = useKnowledgeBaseContext();
127+
const { } = useKnowledgeBaseContext();
128128

129129
// Reset showDetail state when knowledge base name changes
130130
React.useEffect(() => {
@@ -155,14 +155,19 @@ const DocumentListLayout: React.FC<DocumentListLayoutProps> = ({
155155
}
156156

157157
setIsSummarizing(true);
158+
// 清空现有的摘要,准备流式更新
159+
setSummary('');
160+
158161
try {
159-
const result = await summaryIndex(knowledgeBaseName, 1000);
160-
if (result) {
161-
setSummary(result);
162-
message.success('知识库总结完成');
163-
} else {
164-
message.warning('知识库总结为空');
165-
}
162+
// 使用带有进度回调的summaryIndex方法
163+
await knowledgeBaseService.summaryIndex(
164+
knowledgeBaseName,
165+
1000,
166+
(newText) => {
167+
setSummary(prev => prev + newText);
168+
}
169+
);
170+
message.success('知识库总结完成');
166171
} catch (error) {
167172
message.error('获取知识库总结失败');
168173
console.error('获取知识库总结失败:', error);
@@ -281,35 +286,19 @@ const DocumentListLayout: React.FC<DocumentListLayoutProps> = ({
281286
自动总结
282287
</Button>
283288
</div>
284-
{isSummarizing ? (
285-
<div style={{
289+
<Input.TextArea
290+
value={summary}
291+
onChange={(e) => setSummary(e.target.value)}
292+
style={{
286293
flex: 1,
287-
display: 'flex',
288-
flexDirection: 'column',
289-
justifyContent: 'center',
290-
alignItems: 'center',
291-
backgroundColor: '#fafafa',
292-
borderRadius: '8px',
293-
marginBottom: 20
294-
}}>
295-
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mb-4"></div>
296-
<p style={{ fontSize: 16, color: '#666' }}>正在总结知识库内容,请稍候...</p>
297-
</div>
298-
) : (
299-
<Input.TextArea
300-
value={summary}
301-
onChange={(e) => setSummary(e.target.value)}
302-
style={{
303-
flex: 1,
304-
minHeight: 0,
305-
marginBottom: 20,
306-
resize: 'none',
307-
fontSize: 18,
308-
lineHeight: 1.7,
309-
padding: 20
310-
}}
311-
/>
312-
)}
294+
minHeight: 0,
295+
marginBottom: 20,
296+
resize: 'none',
297+
fontSize: 18,
298+
lineHeight: 1.7,
299+
padding: 20
300+
}}
301+
/>
313302
<div style={{ display: 'flex', gap: 12, justifyContent: 'flex-end' }}>
314303
<Button
315304
type="primary"

frontend/services/knowledgeBaseService.ts

Lines changed: 43 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ class KnowledgeBaseService {
341341
}
342342

343343
// Summary index content
344-
async summaryIndex(indexName: string, batchSize: number = 1000): Promise<string> {
344+
async summaryIndex(indexName: string, batchSize: number = 1000, onProgress?: (text: string) => void): Promise<string> {
345345
try {
346346
const response = await fetch(API_ENDPOINTS.knowledgeBase.summary(indexName) + `?batch_size=${batchSize}`, {
347347
method: 'POST',
@@ -352,36 +352,50 @@ class KnowledgeBaseService {
352352
throw new Error(`HTTP error! status: ${response.status}`);
353353
}
354354

355-
// 创建 EventSource 来处理流式响应
356-
const eventSource = new EventSource(API_ENDPOINTS.knowledgeBase.summary(indexName) + `?batch_size=${batchSize}`);
357-
358-
return new Promise((resolve, reject) => {
359-
let summary = '';
355+
if (!response.body) {
356+
throw new Error('Response body is null');
357+
}
358+
359+
// 处理流式响应
360+
const reader = response.body.getReader();
361+
const decoder = new TextDecoder('utf-8');
362+
let summary = '';
363+
364+
while (true) {
365+
const { done, value } = await reader.read();
366+
if (done) break;
367+
368+
// 解码二进制数据为文本
369+
const chunk = decoder.decode(value, { stream: true });
360370

361-
eventSource.onmessage = (event) => {
362-
const data = JSON.parse(event.data);
363-
364-
switch(data.status) {
365-
case 'processing':
366-
console.log('Processing:', data.message);
367-
break;
368-
case 'success':
369-
summary = data.summary;
370-
eventSource.close();
371-
resolve(summary);
372-
break;
373-
case 'error':
374-
eventSource.close();
375-
reject(new Error(data.message));
376-
break;
371+
// 处理SSE格式的数据
372+
const lines = chunk.split('\n\n');
373+
for (const line of lines) {
374+
if (line.trim().startsWith('data:')) {
375+
try {
376+
// 提取JSON数据
377+
const jsonStr = line.substring(line.indexOf('{'));
378+
const data = JSON.parse(jsonStr);
379+
380+
if (data.status === 'success') {
381+
// 累加消息部分到摘要
382+
summary += data.message;
383+
384+
// 如果提供了进度回调,则调用它
385+
if (onProgress) {
386+
onProgress(data.message);
387+
}
388+
} else if (data.status === 'error') {
389+
throw new Error(data.message);
390+
}
391+
} catch (e) {
392+
console.error('解析SSE数据失败:', e, line);
393+
}
377394
}
378-
};
379-
380-
eventSource.onerror = (error) => {
381-
eventSource.close();
382-
reject(new Error('EventSource failed'));
383-
};
384-
});
395+
}
396+
}
397+
398+
return summary;
385399
} catch (error) {
386400
console.error('Error summarizing index:', error);
387401
throw error;

0 commit comments

Comments
 (0)