mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-09-24 16:29:01 +08:00
feat: Add web search results handling and enhance knowledge retrieval features
- Introduced a new endpoint to fetch chunks by ID, improving access to specific knowledge base entries. - Updated tool result types to include web search results, enhancing the display capabilities in the chat component. - Enhanced the AgentStreamDisplay component to support rendering web search results with improved HTML formatting. - Implemented logic for managing web search result summaries and citations, ensuring a cohesive user experience. - Refactored session management to accommodate new web search functionalities, improving overall system integration.
This commit is contained in:
@@ -63,4 +63,9 @@ export function batchQueryKnowledge(idsQueryString: string) {
|
||||
|
||||
export function getKnowledgeDetailsCon(id: string, page: number) {
|
||||
return get(`/api/v1/chunks/${id}?page=${page}&page_size=25`);
|
||||
}
|
||||
|
||||
// Get chunk by chunk_id only (new endpoint - to be added to backend)
|
||||
export function getChunkByIdOnly(chunkId: string) {
|
||||
return get(`/api/v1/chunks/by-id/${chunkId}`);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="9" cy="9" r="7" stroke="#07C05F" stroke-width="1.2" fill="none"/>
|
||||
<path d="M 9 2 A 3.5 7 0 0 0 9 16" stroke="#07C05F" stroke-width="1.2" fill="none"/>
|
||||
<path d="M 9 2 A 3.5 7 0 0 1 9 16" stroke="#07C05F" stroke-width="1.2" fill="none"/>
|
||||
<line x1="2.94" y1="5.5" x2="15.06" y2="5.5" stroke="#07C05F" stroke-width="1.2" stroke-linecap="round"/>
|
||||
<line x1="2.94" y1="12.5" x2="15.06" y2="12.5" stroke="#07C05F" stroke-width="1.2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 576 B |
@@ -0,0 +1,8 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="9" cy="9" r="7" stroke="currentColor" stroke-width="1.2" fill="none"/>
|
||||
<path d="M 9 2 A 3.5 7 0 0 0 9 16" stroke="currentColor" stroke-width="1.2" fill="none"/>
|
||||
<path d="M 9 2 A 3.5 7 0 0 1 9 16" stroke="currentColor" stroke-width="1.2" fill="none"/>
|
||||
<line x1="2.94" y1="5.5" x2="15.06" y2="5.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
|
||||
<line x1="2.94" y1="12.5" x2="15.06" y2="12.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 601 B |
@@ -16,7 +16,8 @@ export type DisplayType =
|
||||
| 'graph_query_results'
|
||||
| 'thinking'
|
||||
| 'plan'
|
||||
| 'database_query';
|
||||
| 'database_query'
|
||||
| 'web_search_results';
|
||||
|
||||
// Search result item
|
||||
export interface SearchResultItem {
|
||||
@@ -136,6 +137,25 @@ export interface DatabaseQueryData {
|
||||
query: string;
|
||||
}
|
||||
|
||||
// Web search result item
|
||||
export interface WebSearchResultItem {
|
||||
result_index: number;
|
||||
title: string;
|
||||
url: string;
|
||||
snippet?: string;
|
||||
content?: string;
|
||||
source?: string;
|
||||
published_at?: string;
|
||||
}
|
||||
|
||||
// Web search results data
|
||||
export interface WebSearchResultsData {
|
||||
display_type: 'web_search_results';
|
||||
query: string;
|
||||
results: WebSearchResultItem[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
// Union type for all tool result data
|
||||
export type ToolResultData =
|
||||
| SearchResultsData
|
||||
@@ -146,7 +166,8 @@ export type ToolResultData =
|
||||
| GraphQueryResultsData
|
||||
| ThinkingData
|
||||
| PlanData
|
||||
| DatabaseQueryData;
|
||||
| DatabaseQueryData
|
||||
| WebSearchResultsData;
|
||||
|
||||
// Action data (from index.vue)
|
||||
export interface ActionData {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="intermediate-steps-header" @click="toggleIntermediateSteps">
|
||||
<div class="intermediate-steps-title">
|
||||
<img :src="agentIcon" alt="" />
|
||||
<span>{{ intermediateStepsSummary }}</span>
|
||||
<span v-html="intermediateStepsSummaryHtml"></span>
|
||||
</div>
|
||||
<div class="intermediate-steps-show-icon">
|
||||
<t-icon :name="showIntermediateSteps ? 'chevron-up' : 'chevron-down'" />
|
||||
@@ -101,7 +101,12 @@
|
||||
|
||||
<!-- Search Results Summary (Fixed, always visible, outside action-details) -->
|
||||
<div v-if="!event.pending && (event.tool_name === 'search_knowledge' || event.tool_name === 'knowledge_search') && event.tool_data" class="search-results-summary-fixed">
|
||||
<div class="results-summary-text">{{ getSearchResultsSummary(event.tool_data) }}</div>
|
||||
<div class="results-summary-text" v-html="getSearchResultsSummary(event.tool_data)"></div>
|
||||
</div>
|
||||
|
||||
<!-- Web Search Results Summary (Fixed, always visible, outside action-details) -->
|
||||
<div v-if="!event.pending && event.tool_name === 'web_search' && event.tool_data" class="search-results-summary-fixed">
|
||||
<div class="results-summary-text">找到 <strong>{{ getResultsCount(event.tool_data) }}</strong> 个网络搜索结果</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isEventExpanded(event.tool_call_id) && !event.pending" class="action-details">
|
||||
@@ -148,13 +153,94 @@
|
||||
<img class="botanswer_loading_gif" src="@/assets/img/botanswer_loading.gif" alt="Processing...">
|
||||
</div>
|
||||
</div>
|
||||
<!-- 全局浮层:统一承载 Web/KB 的 hover 内容 -->
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="floatPopup.visible"
|
||||
class="kb-float-popup"
|
||||
:style="{ top: floatPopup.top + 'px', left: floatPopup.left + 'px', width: floatPopup.width + 'px' }"
|
||||
@mouseenter="cancelFloatClose()"
|
||||
@mouseleave="scheduleFloatClose()"
|
||||
>
|
||||
<div class="t-popup__content">
|
||||
<template v-if="floatPopup.type === 'web'">
|
||||
<div class="tip-title">{{ floatPopup.title || '' }}</div>
|
||||
<div class="tip-url">{{ floatPopup.url || '' }}</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-if="floatPopup.loading" class="tip-loading">加载中...</div>
|
||||
<div v-else-if="floatPopup.error" class="tip-error">{{ floatPopup.error }}</div>
|
||||
<div v-else class="tip-content" v-html="floatPopup.content"></div>
|
||||
<div v-if="floatPopup.chunkId" class="tip-meta">片段ID: {{ floatPopup.chunkId.slice(0, 25) + '...' }}</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { marked } from 'marked';
|
||||
import DOMPurify from 'dompurify';
|
||||
import ToolResultRenderer from './ToolResultRenderer.vue';
|
||||
import { getChunkByIdOnly } from '@/api/knowledge-base';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 浮层状态(Web/KB 共用)
|
||||
const floatPopup = ref<{
|
||||
visible: boolean;
|
||||
top: number;
|
||||
left: number;
|
||||
width: number;
|
||||
type: 'kb' | 'web';
|
||||
// web
|
||||
url?: string;
|
||||
title?: string;
|
||||
// kb
|
||||
loading: boolean;
|
||||
error?: string;
|
||||
content?: string;
|
||||
chunkId?: string;
|
||||
}>({
|
||||
visible: false,
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 420,
|
||||
type: 'kb',
|
||||
url: '',
|
||||
title: '',
|
||||
loading: false,
|
||||
error: undefined,
|
||||
content: '',
|
||||
chunkId: undefined,
|
||||
});
|
||||
let floatCloseTimer: number | null = null;
|
||||
|
||||
const scheduleFloatClose = () => {
|
||||
if (floatCloseTimer) window.clearTimeout(floatCloseTimer);
|
||||
floatCloseTimer = window.setTimeout(() => {
|
||||
floatPopup.value.visible = false;
|
||||
}, 150);
|
||||
};
|
||||
|
||||
const cancelFloatClose = () => {
|
||||
if (floatCloseTimer) {
|
||||
window.clearTimeout(floatCloseTimer);
|
||||
floatCloseTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
const openFloatForEl = (el: HTMLElement, widthAdjust = 120) => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const pageTop = window.scrollY || document.documentElement.scrollTop || 0;
|
||||
const pageLeft = window.scrollX || document.documentElement.scrollLeft || 0;
|
||||
floatPopup.value.top = rect.bottom + pageTop + 2;
|
||||
floatPopup.value.left = rect.left + pageLeft;
|
||||
floatPopup.value.width = Math.min(520, Math.max(320, rect.width + widthAdjust));
|
||||
floatPopup.value.visible = true;
|
||||
};
|
||||
|
||||
// Import icons
|
||||
import agentIcon from '@/assets/img/agent.svg';
|
||||
@@ -162,6 +248,7 @@ import thinkingIcon from '@/assets/img/Frame3718.svg';
|
||||
import knowledgeIcon from '@/assets/img/zhishiku-thin.svg';
|
||||
import documentIcon from '@/assets/img/ziliao.svg';
|
||||
import fileAddIcon from '@/assets/img/file-add-green.svg';
|
||||
import webSearchGlobeGreenIcon from '@/assets/img/websearch-globe-green.svg';
|
||||
|
||||
interface SessionData {
|
||||
isAgentMode?: boolean;
|
||||
@@ -356,10 +443,10 @@ const intermediateStepsSummary = computed(() => {
|
||||
|
||||
const parts: string[] = [];
|
||||
if (searchCount > 0) {
|
||||
parts.push(`检索知识库 ${searchCount} 次`);
|
||||
parts.push(`检索知识库 <strong>${searchCount}</strong> 次`);
|
||||
}
|
||||
if (thinkingCount > 0) {
|
||||
parts.push(`思考 ${thinkingCount} 次`);
|
||||
parts.push(`思考 <strong>${thinkingCount}</strong> 次`);
|
||||
}
|
||||
if (toolCalls.length > 0) {
|
||||
const toolNames = toolCalls.map(name => {
|
||||
@@ -376,11 +463,17 @@ const intermediateStepsSummary = computed(() => {
|
||||
|
||||
// Add duration info
|
||||
if (totalDuration > 0) {
|
||||
parts.push(`耗时 ${formatDuration(totalDuration)}`);
|
||||
const durationStr = formatDuration(totalDuration);
|
||||
// Extract numbers from duration string (e.g., "300ms" -> "300", "3s" -> "3", "2m 30s" -> "2" and "30")
|
||||
// Process "ms" first to avoid matching "m" and "s" separately, then process "s" and "m"
|
||||
const durationHtml = durationStr
|
||||
.replace(/(\d+)(ms)/g, '<strong>$1</strong>$2')
|
||||
.replace(/(\d+)([sm])(?!\w)/g, '<strong>$1</strong>$2');
|
||||
parts.push(`耗时 ${durationHtml}`);
|
||||
}
|
||||
|
||||
if (parts.length === 0) {
|
||||
return `${intermediateStepsCount.value} 个中间步骤`;
|
||||
return `<strong>${intermediateStepsCount.value}</strong> 个中间步骤`;
|
||||
}
|
||||
|
||||
// 优化连接词,使语句更流畅
|
||||
@@ -395,6 +488,11 @@ const intermediateStepsSummary = computed(() => {
|
||||
}
|
||||
});
|
||||
|
||||
// HTML version of intermediate steps summary with colored numbers
|
||||
const intermediateStepsSummaryHtml = computed(() => {
|
||||
return intermediateStepsSummary.value;
|
||||
});
|
||||
|
||||
// Should show the collapsed steps indicator
|
||||
const shouldShowCollapsedSteps = computed(() => {
|
||||
const result = isConversationDone.value && intermediateStepsCount.value > 0;
|
||||
@@ -505,6 +603,242 @@ const isEventExpanded = (eventId: string): boolean => {
|
||||
return expandedEvents.value.has(eventId);
|
||||
};
|
||||
|
||||
// Delegated handlers for span-based citation clicks/keyboard
|
||||
const handleCitationActivate = (el: HTMLElement) => {
|
||||
const url = el.getAttribute('data-url');
|
||||
if (!url) return;
|
||||
try {
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
};
|
||||
|
||||
// KB citations: 悬停用浮层展示摘要;点击跳转 KB 详情
|
||||
const kbChunkDetails = ref<Record<string, { content?: string; loading: boolean; error?: string }>>({});
|
||||
|
||||
const loadChunkDetails = async (chunkId: string) => {
|
||||
// Skip if already loaded or currently loading
|
||||
if (kbChunkDetails.value[chunkId]) {
|
||||
if (kbChunkDetails.value[chunkId].loading) {
|
||||
return; // Already loading
|
||||
}
|
||||
if (kbChunkDetails.value[chunkId].content) {
|
||||
// Already loaded, update tooltip immediately
|
||||
updateKBCitationTooltip(chunkId, kbChunkDetails.value[chunkId].content);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Set loading state
|
||||
kbChunkDetails.value[chunkId] = { loading: true };
|
||||
updateKBCitationTooltip(chunkId); // Show loading state
|
||||
|
||||
try {
|
||||
const response = await getChunkByIdOnly(chunkId);
|
||||
if (response.data && response.data.content) {
|
||||
kbChunkDetails.value[chunkId] = {
|
||||
content: response.data.content,
|
||||
loading: false
|
||||
};
|
||||
// Update the tooltip content in the DOM
|
||||
updateKBCitationTooltip(chunkId, response.data.content);
|
||||
} else {
|
||||
const errorMsg = '未找到内容';
|
||||
kbChunkDetails.value[chunkId] = {
|
||||
loading: false,
|
||||
error: errorMsg
|
||||
};
|
||||
updateKBCitationTooltip(chunkId, undefined, errorMsg);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Failed to load chunk details:', error);
|
||||
const errorMsg = error?.message || '加载失败';
|
||||
kbChunkDetails.value[chunkId] = {
|
||||
loading: false,
|
||||
error: errorMsg
|
||||
};
|
||||
updateKBCitationTooltip(chunkId, undefined, errorMsg);
|
||||
}
|
||||
};
|
||||
|
||||
const updateKBCitationTooltip = (chunkId: string, content?: string, error?: string) => {
|
||||
// Find all KB citation elements with this chunk ID
|
||||
const citations = document.querySelectorAll(`.citation-kb[data-chunk-id="${chunkId}"]`);
|
||||
citations.forEach((citation) => {
|
||||
const tipElement = citation.querySelector('.citation-tip');
|
||||
if (tipElement) {
|
||||
const doc = citation.getAttribute('data-doc') || '';
|
||||
const shortChunkId = `${chunkId.substring(0, 25)}...`;
|
||||
|
||||
const renderContent = (inner: string) => {
|
||||
tipElement.innerHTML = `
|
||||
<span class="t-popup__content">
|
||||
${inner}
|
||||
<span class="tip-meta">片段ID: ${shortChunkId}</span>
|
||||
</span>
|
||||
`;
|
||||
};
|
||||
|
||||
if (error) {
|
||||
renderContent(`<span class="tip-error">${error}</span>`);
|
||||
} else if (content) {
|
||||
const shortContent = content.length > 600 ? content.substring(0, 600) + '...' : content;
|
||||
const escapedContent = shortContent
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\n/g, '<br>');
|
||||
renderContent(`<span class="tip-content">${escapedContent}</span>`);
|
||||
} else {
|
||||
renderContent(`<span class="tip-loading">加载中...</span>`);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 统一 hover 入口(Web/KB)
|
||||
let kbHoverTimer: number | null = null;
|
||||
const onHover = (e: Event) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (!target) return;
|
||||
const kbEl = target.closest?.('.citation-kb') as HTMLElement | null;
|
||||
const webEl = target.closest?.('.citation-web') as HTMLElement | null;
|
||||
// KB
|
||||
if (kbEl) {
|
||||
const chunkId = kbEl.getAttribute('data-chunk-id') || '';
|
||||
if (!chunkId) return;
|
||||
if (kbHoverTimer) window.clearTimeout(kbHoverTimer);
|
||||
kbHoverTimer = window.setTimeout(() => {
|
||||
floatPopup.value.type = 'kb';
|
||||
floatPopup.value.loading = true;
|
||||
floatPopup.value.error = undefined;
|
||||
floatPopup.value.content = '';
|
||||
floatPopup.value.chunkId = chunkId;
|
||||
openFloatForEl(kbEl);
|
||||
loadChunkDetails(chunkId);
|
||||
}, 80);
|
||||
return;
|
||||
}
|
||||
// Web
|
||||
if (webEl) {
|
||||
const url = webEl.getAttribute('data-url') || '';
|
||||
const title = webEl.querySelector('.tip-title')?.textContent || webEl.getAttribute('data-title') || '';
|
||||
if (kbHoverTimer) window.clearTimeout(kbHoverTimer);
|
||||
kbHoverTimer = window.setTimeout(() => {
|
||||
floatPopup.value.type = 'web';
|
||||
floatPopup.value.url = url;
|
||||
floatPopup.value.title = title || '';
|
||||
openFloatForEl(webEl, 60);
|
||||
}, 40);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const onHoverOut = (e: Event) => {
|
||||
const rt = (e as MouseEvent).relatedTarget as HTMLElement | null;
|
||||
if (rt && (rt.closest?.('.citation-kb') || rt.closest?.('.citation-web') || rt.closest?.('.kb-float-popup'))) {
|
||||
return;
|
||||
}
|
||||
if (kbHoverTimer) {
|
||||
window.clearTimeout(kbHoverTimer);
|
||||
kbHoverTimer = null;
|
||||
}
|
||||
scheduleFloatClose();
|
||||
};
|
||||
|
||||
const onRootClick = (e: Event) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (!target) return;
|
||||
|
||||
// Handle web citation clicks
|
||||
const webEl = target.closest?.('.citation-web') as HTMLElement | null;
|
||||
if (webEl && webEl.getAttribute('data-url')) {
|
||||
e.preventDefault();
|
||||
handleCitationActivate(webEl);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle KB citation clicks -> navigate to KB detail page
|
||||
const kbEl = target.closest?.('.citation-kb') as HTMLElement | null;
|
||||
if (kbEl && kbEl.getAttribute('data-kb-id')) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const kbId = kbEl.getAttribute('data-kb-id');
|
||||
if (kbId) {
|
||||
try {
|
||||
// Navigate to knowledge base detail page
|
||||
router.push(`/platform/knowledge-bases/${kbId}`);
|
||||
} catch (error) {
|
||||
console.error('Failed to navigate to knowledge base:', error);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const onRootKeydown = (e: KeyboardEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (!target) return;
|
||||
|
||||
// Handle web citation keyboard
|
||||
const webEl = target.closest?.('.citation-web') as HTMLElement | null;
|
||||
if (webEl) {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleCitationActivate(webEl);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle KB citation keyboard -> navigate to KB detail
|
||||
const kbEl = target.closest?.('.citation-kb') as HTMLElement | null;
|
||||
if (kbEl) {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
const kbId = kbEl.getAttribute('data-kb-id');
|
||||
if (kbId) {
|
||||
try {
|
||||
router.push(`/platform/knowledge-bases/${kbId}`);
|
||||
} catch (error) {
|
||||
console.error('Failed to navigate to knowledge base:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
const root = document.querySelector('.agent-stream-display');
|
||||
if (!root) return;
|
||||
root.addEventListener('click', onRootClick, true);
|
||||
const keydownListener: EventListener = (evt: Event) => onRootKeydown(evt as KeyboardEvent);
|
||||
// Store on element for removal
|
||||
(root as any).__citationKeydown__ = keydownListener;
|
||||
root.addEventListener('keydown', keydownListener, true);
|
||||
// 统一 hover 监听
|
||||
root.addEventListener('mouseover', onHover, true);
|
||||
root.addEventListener('mouseout', onHoverOut, true);
|
||||
window.addEventListener('scroll', scheduleFloatClose, true);
|
||||
window.addEventListener('resize', scheduleFloatClose, true);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
const root = document.querySelector('.agent-stream-display');
|
||||
if (!root) return;
|
||||
root.removeEventListener('click', onRootClick, true);
|
||||
root.removeEventListener('mouseover', onHover, true);
|
||||
root.removeEventListener('mouseout', onHoverOut, true);
|
||||
window.removeEventListener('scroll', scheduleFloatClose, true);
|
||||
window.removeEventListener('resize', scheduleFloatClose, true);
|
||||
const keydownListener: EventListener | undefined = (root as any).__citationKeydown__;
|
||||
if (keydownListener) {
|
||||
root.removeEventListener('keydown', keydownListener, true);
|
||||
delete (root as any).__citationKeydown__;
|
||||
}
|
||||
});
|
||||
|
||||
// Markdown rendering function
|
||||
const renderMarkdown = (content: any): string => {
|
||||
if (!content) return '';
|
||||
@@ -514,12 +848,59 @@ const renderMarkdown = (content: any): string => {
|
||||
if (!contentStr.trim()) return '';
|
||||
|
||||
try {
|
||||
const html = marked.parse(contentStr) as string;
|
||||
// Preprocess custom citation tags into safe HTML the sanitizer will allow
|
||||
// Supported formats:
|
||||
// <kb kb_id="..." doc="..." chunk_id="..." />
|
||||
// <web url="https://..." title="Example" />
|
||||
const processed = contentStr
|
||||
// Web citations -> compact clickable badges with domain text; hover shows title via native tooltip
|
||||
.replace(
|
||||
/<web\s+url="([^"]+)"\s+title="([^"]+)"\s*\/>/g,
|
||||
(_m: string, url: string, title: string) => {
|
||||
// Extract domain for compact display
|
||||
let domain = url;
|
||||
try {
|
||||
const u = new URL(url);
|
||||
const host = u.hostname || '';
|
||||
const parts = host.split('.');
|
||||
if (parts.length >= 2) {
|
||||
// prefer second-level domain: last two labels
|
||||
domain = parts.slice(-2).join('.');
|
||||
} else {
|
||||
domain = host || url;
|
||||
}
|
||||
} catch {
|
||||
// keep original url text if parsing fails
|
||||
}
|
||||
// Escape double quotes in title for attribute safety
|
||||
const safeTitle = String(title || '').replace(/"/g, '"');
|
||||
const safeUrl = String(url || '').replace(/"/g, '"');
|
||||
// Render with embedded tooltip blocks to allow styled parts (bold title)
|
||||
const tipTitle = safeTitle || '';
|
||||
const tipUrl = safeUrl || '';
|
||||
// Keep lightweight tooltip (no t-popup container) to avoid global popup styling
|
||||
return `<span class="citation citation-web" data-url="${safeUrl}" role="link" tabindex="0"><span class="citation-icon web"></span><span class="citation-domain">${domain}</span><span class="citation-tip"><span class="tip-title">${tipTitle}</span><span class="tip-url">${tipUrl}</span></span></span>`;
|
||||
}
|
||||
)
|
||||
// KB citations -> inline badges with simplified display
|
||||
.replace(
|
||||
/<kb\s+kb_id="([^"]+)"\s+doc="([^"]+)"\s+chunk_id="([^"]+)"\s*\/>/g,
|
||||
(_m, kbId, doc, chunkId) => {
|
||||
// Escape doc name for safety
|
||||
const safeDoc = String(doc || '').replace(/"/g, '"');
|
||||
const safeKbId = String(kbId || '').replace(/"/g, '"');
|
||||
const safeChunkId = String(chunkId || '').replace(/"/g, '"');
|
||||
// Initial tooltip content (single t-popup container; will be updated on hover)
|
||||
return `<span class="citation citation-kb" data-kb-id="${safeKbId}" data-chunk-id="${safeChunkId}" data-doc="${safeDoc}" role="button" tabindex="0"><span class="citation-icon kb"></span><span class="citation-text">${safeDoc}</span><span class="citation-tip"><span class="t-popup__content"><span class="tip-loading">加载中...</span></span></span></span>`;
|
||||
}
|
||||
);
|
||||
|
||||
const html = marked.parse(processed) as string;
|
||||
if (!html) return '';
|
||||
|
||||
return DOMPurify.sanitize(html, {
|
||||
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'code', 'pre', 'ul', 'ol', 'li', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'a', 'table', 'thead', 'tbody', 'tr', 'th', 'td'],
|
||||
ALLOWED_ATTR: ['href', 'title', 'target', 'rel']
|
||||
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'code', 'pre', 'ul', 'ol', 'li', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'a', 'span', 'table', 'thead', 'tbody', 'tr', 'th', 'td'],
|
||||
ALLOWED_ATTR: ['href', 'title', 'target', 'rel', 'data-tooltip', 'data-url', 'data-kb-id', 'data-chunk-id', 'data-doc', 'class', 'role', 'tabindex']
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Markdown rendering error:', e, 'Content:', contentStr.substring(0, 100));
|
||||
@@ -643,6 +1024,8 @@ const getToolIcon = (toolName: string): string => {
|
||||
return thinkingIcon;
|
||||
} else if (toolName === 'search_knowledge' || toolName === 'knowledge_search') {
|
||||
return knowledgeIcon;
|
||||
} else if (toolName === 'web_search') {
|
||||
return webSearchGlobeGreenIcon;
|
||||
} else if (toolName === 'get_document_info' || toolName === 'get_related_chunks') {
|
||||
return documentIcon;
|
||||
} else if (toolName === 'todo_write') {
|
||||
@@ -652,7 +1035,7 @@ const getToolIcon = (toolName: string): string => {
|
||||
}
|
||||
};
|
||||
|
||||
// Get search results summary text
|
||||
// Get search results summary text (returns HTML with colored numbers)
|
||||
const getSearchResultsSummary = (toolData: any): string => {
|
||||
if (!toolData) return '';
|
||||
|
||||
@@ -661,9 +1044,25 @@ const getSearchResultsSummary = (toolData: any): string => {
|
||||
|
||||
const kbCount = toolData.kb_counts ? Object.keys(toolData.kb_counts).length : 0;
|
||||
if (kbCount > 0) {
|
||||
return `找到 ${count} 个结果,来自 ${kbCount} 个知识库`;
|
||||
return `找到 <strong>${count}</strong> 个结果,来自 <strong>${kbCount}</strong> 个知识库`;
|
||||
}
|
||||
return `找到 ${count} 个结果`;
|
||||
return `找到 <strong>${count}</strong> 个结果`;
|
||||
};
|
||||
|
||||
// Get web search results summary text
|
||||
const getWebSearchResultsSummary = (toolData: any): string => {
|
||||
if (!toolData) return '';
|
||||
|
||||
const count = toolData.results?.length || toolData.count || 0;
|
||||
if (count === 0) return '';
|
||||
|
||||
return `找到 ${count} 个网络搜索结果`;
|
||||
};
|
||||
|
||||
// Get results count (number only) for web search summary
|
||||
const getResultsCount = (toolData: any): number => {
|
||||
if (!toolData) return 0;
|
||||
return toolData.results?.length || toolData.count || 0;
|
||||
};
|
||||
|
||||
// Extract and format query parameters from args
|
||||
@@ -722,6 +1121,7 @@ const getToolTitle = (event: any): string => {
|
||||
|
||||
const toolName = event.tool_name;
|
||||
const isSearchTool = toolName === 'search_knowledge' || toolName === 'knowledge_search';
|
||||
const isWebSearchTool = toolName === 'web_search';
|
||||
|
||||
// For search tools, use description with query text
|
||||
if (isSearchTool) {
|
||||
@@ -735,6 +1135,22 @@ const getToolTitle = (event: any): string => {
|
||||
return baseTitle;
|
||||
}
|
||||
|
||||
// For web search tools, use description with query text
|
||||
if (isWebSearchTool) {
|
||||
const baseTitle = getToolDescription(event);
|
||||
// Try to get query from arguments or tool_data
|
||||
let queryText = '';
|
||||
if (event.arguments && typeof event.arguments === 'object' && event.arguments.query) {
|
||||
queryText = event.arguments.query;
|
||||
} else if (event.tool_data && event.tool_data.query) {
|
||||
queryText = event.tool_data.query;
|
||||
}
|
||||
if (queryText) {
|
||||
return `${baseTitle}:「${queryText}」`;
|
||||
}
|
||||
return baseTitle;
|
||||
}
|
||||
|
||||
// Use tool summary if available
|
||||
const summary = getToolSummary(event);
|
||||
return summary || getToolDescription(event);
|
||||
@@ -751,6 +1167,8 @@ const getToolDescription = (event: any): string => {
|
||||
|
||||
if (toolName === 'search_knowledge' || toolName === 'knowledge_search') {
|
||||
return success ? '检索知识库' : '检索知识库失败';
|
||||
} else if (toolName === 'web_search') {
|
||||
return success ? '网络搜索' : '网络搜索失败';
|
||||
} else if (toolName === 'get_document_info') {
|
||||
return success ? '获取文档信息' : '获取文档信息失败';
|
||||
} else if (toolName === 'thinking') {
|
||||
@@ -838,6 +1256,11 @@ const formatJSON = (obj: any): string => {
|
||||
span {
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
|
||||
:deep(strong) {
|
||||
color: #07c05f;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -994,6 +1417,16 @@ const formatJSON = (obj: any): string => {
|
||||
line-height: 1.6;
|
||||
|
||||
&.markdown-content {
|
||||
/* citation-web styles moved to global fallback below to avoid duplication */
|
||||
|
||||
/* keyboard focus */
|
||||
:deep(.citation-web:focus-visible) {
|
||||
outline: 2px solid #34d399; /* green-400 */
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* KB citation styles are defined globally, no need to override here */
|
||||
|
||||
:deep(p) {
|
||||
margin: 8px 0;
|
||||
line-height: 1.6;
|
||||
@@ -1289,6 +1722,12 @@ const formatJSON = (obj: any): string => {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
|
||||
// Use :deep() to apply styles to v-html content
|
||||
:deep(strong) {
|
||||
color: #07c05f;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1482,6 +1921,218 @@ const formatJSON = (obj: any): string => {
|
||||
}
|
||||
}
|
||||
|
||||
/* Global citation styles fallback to ensure rendering in any container */
|
||||
:deep(.citation) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border-radius: 10px;
|
||||
padding: 2px 4px;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
background-clip: padding-box;
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
:deep(.citation .citation-tip) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
:deep(.citation-web) {
|
||||
/* Align with app primary green scheme */
|
||||
background: #f0fdf4; /* green-50 */
|
||||
color: #065f46; /* green-800 */
|
||||
border: 1px solid #bbf7d0; /* green-200 */
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:deep(.citation-web:hover) {
|
||||
/* Subtle hover in green tone */
|
||||
background: #d1fae5; /* green-100 */
|
||||
border-color: #86efac; /* green-300 */
|
||||
color: #065f46; /* keep readable on light bg */
|
||||
}
|
||||
|
||||
/* Embedded tooltip bubble (structured, supports bold title) */
|
||||
:deep(.citation-web .citation-tip) {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: calc(100% + 8px);
|
||||
white-space: pre-wrap;
|
||||
z-index: 9999;
|
||||
color: #111827;
|
||||
background: #f9fafb;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
box-shadow: 0 6px 18px rgba(0,0,0,0.2);
|
||||
max-width: 520px;
|
||||
min-width: 200px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:deep(.citation-web:hover .citation-tip) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
:deep(.citation-web .citation-tip .tip-title) {
|
||||
display: block;
|
||||
font-weight: 600; /* bold title */
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
:deep(.citation-web .citation-tip .tip-url) {
|
||||
display: block;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Citation icons */
|
||||
:deep(.citation .citation-icon) {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-right: 0px;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Web icon (globe) */
|
||||
:deep(.citation .citation-icon.web) {
|
||||
background-image: url("../../../assets/img/websearch-globe-green.svg");
|
||||
}
|
||||
|
||||
/* Knowledge base icon */
|
||||
:deep(.citation .citation-icon.kb) {
|
||||
background-image: url("../../../assets/img/zhishiku-thin.svg");
|
||||
}
|
||||
|
||||
/* KB citation styles - same green theme as web citations */
|
||||
:deep(.citation.citation-kb) {
|
||||
/* Green theme - same as web citations */
|
||||
background: #f0fdf4; /* green-50 */
|
||||
color: #065f46; /* green-800 */
|
||||
border: 1px solid #bbf7d0; /* green-200 */
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
:deep(.citation.citation-kb:hover) {
|
||||
/* Subtle hover in green tone */
|
||||
background: #d1fae5; /* green-100 */
|
||||
border-color: #86efac; /* green-300 */
|
||||
color: #065f46; /* keep readable on light bg */
|
||||
}
|
||||
|
||||
:deep(.citation.citation-kb:focus-visible) {
|
||||
outline: 2px solid #34d399; /* green-400 */
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* KB citation tooltip styles (same as web citation) */
|
||||
:deep(.citation.citation-kb .citation-tip) {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 100%; /* 紧贴父元素底部,避免鼠标穿过空隙导致消失 */
|
||||
/* popup container mimic */
|
||||
z-index: 9999;
|
||||
color: #111827;
|
||||
background: transparent; /* 外层透明,避免顶部出现白边 */
|
||||
border-radius: 0;
|
||||
padding: 8px 0 0; /* 仅用上内边距制造视觉间距,保持可 hover 区域连续 */
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
box-shadow: none; /* 阴影放到内部容器 */
|
||||
width: 420px; /* fixed width to avoid layout jump */
|
||||
min-height: 120px; /* reserve space to avoid jitter before content arrives */
|
||||
pointer-events: auto; /* 允许在弹层内部滚动与交互 */
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
:deep(.citation.citation-kb:hover .citation-tip) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* inner content mimic of t-popup__content */
|
||||
:deep(.citation.citation-kb .citation-tip .t-popup__content) {
|
||||
display: block;
|
||||
padding: 12px 14px; /* actual padding here */
|
||||
max-height: 340px;
|
||||
overflow-y: auto; /* inner scroll */
|
||||
background: #f9fafb; /* 背景、圆角与阴影移动到内部容器 */
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 6px 18px rgba(0,0,0,0.2);
|
||||
box-sizing: border-box;
|
||||
overscroll-behavior: contain; /* 防止滚动穿透到页面 */
|
||||
-webkit-overflow-scrolling: touch; /* iOS 惯性滚动 */
|
||||
}
|
||||
|
||||
:deep(.citation.citation-kb .citation-tip .tip-title) {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
color: #065f46; /* green-800 to match citation */
|
||||
}
|
||||
|
||||
:deep(.citation.citation-kb .citation-tip .tip-doc) {
|
||||
display: block;
|
||||
color: #111827;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
:deep(.citation.citation-kb .citation-tip .tip-content) {
|
||||
display: block;
|
||||
color: #374151;
|
||||
line-height: 1.6;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
margin-top: 4px;
|
||||
word-break: break-word;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
:deep(.citation.citation-kb .citation-tip .tip-content::-webkit-scrollbar) {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
:deep(.citation.citation-kb .citation-tip .tip-content::-webkit-scrollbar-track) {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
:deep(.citation.citation-kb .citation-tip .tip-content::-webkit-scrollbar-thumb) {
|
||||
background: #888;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
:deep(.citation.citation-kb .citation-tip .tip-content::-webkit-scrollbar-thumb:hover) {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
:deep(.citation.citation-kb .citation-tip .tip-loading) {
|
||||
display: block;
|
||||
color: #6b7280;
|
||||
font-style: italic;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
:deep(.citation.citation-kb .citation-tip .tip-error) {
|
||||
display: block;
|
||||
color: #dc2626;
|
||||
font-size: 11px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.tool-arguments-wrapper {
|
||||
margin-top: 8px;
|
||||
|
||||
|
||||
@@ -55,6 +55,12 @@
|
||||
:data="toolData as DatabaseQueryData"
|
||||
/>
|
||||
|
||||
<!-- Web Search Results Display -->
|
||||
<WebSearchResults
|
||||
v-else-if="displayType === 'web_search_results'"
|
||||
:data="toolData as WebSearchResultsData"
|
||||
/>
|
||||
|
||||
<!-- Fallback: Display raw output -->
|
||||
<div v-else class="fallback-output">
|
||||
<div class="detail-output">{{ output }}</div>
|
||||
@@ -74,7 +80,8 @@ import type {
|
||||
GraphQueryResultsData,
|
||||
ThinkingData,
|
||||
PlanData,
|
||||
DatabaseQueryData
|
||||
DatabaseQueryData,
|
||||
WebSearchResultsData
|
||||
} from '@/types/tool-results';
|
||||
|
||||
import SearchResults from './tool-results/SearchResults.vue';
|
||||
@@ -86,6 +93,7 @@ import GraphQueryResults from './tool-results/GraphQueryResults.vue';
|
||||
import ThinkingDisplay from './tool-results/ThinkingDisplay.vue';
|
||||
import PlanDisplay from './tool-results/PlanDisplay.vue';
|
||||
import DatabaseQuery from './tool-results/DatabaseQuery.vue';
|
||||
import WebSearchResults from './tool-results/WebSearchResults.vue';
|
||||
|
||||
interface Props {
|
||||
displayType?: DisplayType;
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
<template>
|
||||
<div class="web-search-results">
|
||||
<!-- Grouped Results List -->
|
||||
<div v-if="groupedResults && groupedResults.length > 0" class="results-groups">
|
||||
<div
|
||||
v-for="group in groupedResults"
|
||||
:key="group.key"
|
||||
class="results-group"
|
||||
>
|
||||
<!-- <div class="group-header">
|
||||
<span class="group-intro">以下 {{ group.items.length }} 条内容来自</span>
|
||||
<span class="group-source">{{ group.label }}</span>
|
||||
</div> -->
|
||||
<div class="results-list">
|
||||
<div
|
||||
v-for="result in group.items"
|
||||
:key="result.result_index"
|
||||
class="result-item"
|
||||
>
|
||||
<div class="result-header">
|
||||
<div class="result-index">#{{ result.result_index }}</div>
|
||||
<a
|
||||
v-if="result.url"
|
||||
:href="result.url"
|
||||
:title="result.url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="result-title-link one-line"
|
||||
>
|
||||
<span class="result-title">{{ result.title }}</span>
|
||||
</a>
|
||||
<div v-else class="result-title-text one-line">
|
||||
<span class="result-title">{{ result.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="result.published_at" class="result-meta">
|
||||
<span class="meta-item">
|
||||
<t-icon name="time" class="meta-icon" />
|
||||
{{ formatDate(result.published_at) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else class="empty-state">
|
||||
未找到搜索结果
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import type { WebSearchResultsData, WebSearchResultItem } from '@/types/tool-results';
|
||||
|
||||
interface Props {
|
||||
data: WebSearchResultsData;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const results = computed(() => props.data.results || []);
|
||||
|
||||
// Group results by source first, then by domain if source is missing
|
||||
type Group = { key: string; label: string; items: WebSearchResultItem[] };
|
||||
const groupedResults = computed<Group[]>(() => {
|
||||
const list = results.value || [];
|
||||
const groupsMap: Record<string, Group> = {};
|
||||
for (const item of list) {
|
||||
const source = (item as any).source as string | undefined;
|
||||
let key = '';
|
||||
let label = '';
|
||||
if (source && source.trim()) {
|
||||
key = `src:${source.trim()}`;
|
||||
label = source.trim();
|
||||
} else {
|
||||
// fallback to domain
|
||||
const url = (item as any).url as string | undefined;
|
||||
const hostname = url ? safeHostname(url) : '其他';
|
||||
key = `dom:${hostname}`;
|
||||
label = hostname;
|
||||
}
|
||||
if (!groupsMap[key]) {
|
||||
groupsMap[key] = { key, label, items: [] };
|
||||
}
|
||||
groupsMap[key].items.push(item);
|
||||
}
|
||||
// Keep original order by first occurrence
|
||||
const ordered: Group[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const item of list) {
|
||||
const source = (item as any).source as string | undefined;
|
||||
const url = (item as any).url as string | undefined;
|
||||
const hostname = url ? safeHostname(url) : '其他';
|
||||
const key = source && source.trim() ? `src:${source.trim()}` : `dom:${hostname}`;
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
if (groupsMap[key]) ordered.push(groupsMap[key]);
|
||||
}
|
||||
}
|
||||
return ordered;
|
||||
});
|
||||
|
||||
const formatUrl = (url: string): string => {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
return urlObj.hostname + urlObj.pathname;
|
||||
} catch {
|
||||
return url;
|
||||
}
|
||||
};
|
||||
|
||||
const safeHostname = (url: string): string => {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
return urlObj.hostname;
|
||||
} catch {
|
||||
return '其他';
|
||||
}
|
||||
};
|
||||
|
||||
const truncateContent = (content: string, maxLength: number = 300): string => {
|
||||
if (!content) return '';
|
||||
if (content.length <= maxLength) return content;
|
||||
return content.substring(0, maxLength) + '...';
|
||||
};
|
||||
|
||||
const formatDate = (dateStr: string): string => {
|
||||
try {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
} catch {
|
||||
return dateStr;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import './tool-results.less';
|
||||
|
||||
.web-search-results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px 8px 0 8px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.results-groups {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.results-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #4b5563;
|
||||
/* Align with title start (after index column) */
|
||||
padding-left: 34px;
|
||||
}
|
||||
|
||||
.group-intro {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.group-source {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
background: #f3f4f6;
|
||||
border: 1px solid #e5e7eb;
|
||||
color: #111827;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.group-count {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.results-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
// padding: 6px 8px;
|
||||
background: #ffffff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.result-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 0;
|
||||
/* Ensure links inside header remain clickable despite shared styles */
|
||||
:deep(a) {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.result-index {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #8b8b8b;
|
||||
flex-shrink: 0;
|
||||
padding-top: 1px;
|
||||
min-width: 28px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.result-title-link {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
text-decoration: none;
|
||||
color: #1f2937;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: #07c05f;
|
||||
|
||||
.result-title {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.result-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
color: #1f2937;
|
||||
flex: 1;
|
||||
word-break: break-word;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.one-line {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.result-domain {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.external-link-icon {
|
||||
font-size: 11px;
|
||||
color: #9ca3af;
|
||||
flex-shrink: 0;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.result-title-link:hover .external-link-icon {
|
||||
color: #07c05f;
|
||||
}
|
||||
|
||||
.result-title-text {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.result-title-text .result-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
color: #1f2937;
|
||||
flex: 1;
|
||||
word-break: break-word;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.result-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 6px;
|
||||
padding-top: 6px;
|
||||
border-top: 1px solid #f3f4f6;
|
||||
font-size: 10px;
|
||||
color: #9ca3af;
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.meta-icon {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #9ca3af;
|
||||
font-style: italic;
|
||||
background: #f9fafb;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package agent
|
||||
|
||||
const (
|
||||
DefaultAgentTemperature = 0.7
|
||||
DefaultAgentMaxIterations = 20
|
||||
DefaultAgentReflectionEnabled = false
|
||||
)
|
||||
@@ -88,7 +88,7 @@ func (e *AgentEngine) Execute(ctx context.Context, sessionID, messageID, query s
|
||||
}
|
||||
|
||||
// Build system prompt
|
||||
systemPrompt := BuildReActSystemPrompt(e.knowledgeBasesInfo, e.systemPromptTemplate)
|
||||
systemPrompt := BuildReActSystemPromptWithStatus(e.knowledgeBasesInfo, e.config.WebSearchEnabled, e.systemPromptTemplate)
|
||||
logger.Debugf(ctx, "[Agent] SystemPrompt Length: %d characters", len(systemPrompt))
|
||||
logger.Debugf(ctx, "[Agent] SystemPrompt (stream)\n----\n%s\n----", systemPrompt)
|
||||
|
||||
@@ -328,7 +328,7 @@ func (e *AgentEngine) executeLoop(
|
||||
// Optional: Reflection after each tool call (streaming)
|
||||
if e.config.ReflectionEnabled && result != nil {
|
||||
reflection, err := e.streamReflectionToEventBus(
|
||||
ctx, tc.ID, tc.Function.Name, args, result.Output,
|
||||
ctx, tc.ID, tc.Function.Name, result.Output,
|
||||
state.CurrentRound, sessionID,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -524,7 +524,6 @@ func (e *AgentEngine) streamReflectionToEventBus(
|
||||
ctx context.Context,
|
||||
toolCallID string,
|
||||
toolName string,
|
||||
args map[string]interface{},
|
||||
result string,
|
||||
iteration int,
|
||||
sessionID string,
|
||||
@@ -645,7 +644,7 @@ func (e *AgentEngine) streamFinalAnswerToEventBus(
|
||||
len(state.RoundSteps), countTotalToolCalls(state.RoundSteps))
|
||||
|
||||
// Build messages with all context
|
||||
systemPrompt := BuildReActSystemPrompt(e.knowledgeBasesInfo, e.systemPromptTemplate)
|
||||
systemPrompt := BuildReActSystemPromptWithStatus(e.knowledgeBasesInfo, e.config.WebSearchEnabled, e.systemPromptTemplate)
|
||||
|
||||
messages := []chat.Message{
|
||||
{Role: "system", Content: systemPrompt},
|
||||
|
||||
+98
-540
@@ -5,12 +5,6 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultAgentTemperature = 0.7
|
||||
DefaultAgentMaxIterations = 20
|
||||
DefaultAgentReflectionEnabled = false
|
||||
)
|
||||
|
||||
// formatFileSize formats file size in human-readable format
|
||||
func formatFileSize(size int64) string {
|
||||
const (
|
||||
@@ -33,6 +27,7 @@ func formatFileSize(size int64) string {
|
||||
type RecentDocInfo struct {
|
||||
KnowledgeID string
|
||||
Title string
|
||||
Description string
|
||||
FileName string
|
||||
FileSize int64
|
||||
Type string
|
||||
@@ -63,6 +58,11 @@ func AvailablePlaceholders() []PlaceholderDefinition {
|
||||
Label: "知识库列表",
|
||||
Description: "自动格式化为表格形式的知识库列表,包含知识库名称、描述、文档数量、最近添加的文档等信息",
|
||||
},
|
||||
{
|
||||
Name: "web_search_status",
|
||||
Label: "网络检索模式开关状态",
|
||||
Description: "网络检索(web_search)工具是否启用的状态说明,值为 Enabled 或 Disabled",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,533 +106,6 @@ func formatKnowledgeBaseList(kbInfos []*KnowledgeBaseInfo) string {
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
// DefaultReActSystemPrompt is the default system prompt template
|
||||
// This is used when no custom prompt is configured
|
||||
const DefaultReActSystemPrompt = `# Role
|
||||
|
||||
You are WeKnora, an intelligent knowledge base assistant. Your mission is to provide accurate, traceable information through systematic tool use and structured task management.
|
||||
|
||||
Core capabilities:
|
||||
- Knowledge retrieval expert: proficient in searching and extracting information from knowledge bases
|
||||
- Systematic thinker: use think and todo_write tools for planning and tracking
|
||||
- Quality controller: ensure all answers are evidence-based and verifiable
|
||||
- Persistent optimizer: adjust strategies based on results, never give up easily
|
||||
|
||||
# Known Information
|
||||
|
||||
## Available Knowledge Bases
|
||||
{{knowledge_bases}}
|
||||
|
||||
## Available Tools
|
||||
|
||||
You have 7 core tools:
|
||||
|
||||
Planning & Thinking:
|
||||
1. thinking - Strategic analysis and decision making
|
||||
- Use for: problem analysis, result evaluation, strategy planning
|
||||
- Frequency: very high (before any important decision)
|
||||
- Cost: zero, always beneficial
|
||||
|
||||
2. todo_write - Task management (strongly recommended)
|
||||
- Use for: creating and updating task lists, tracking multi-step research
|
||||
- Frequency: very high (any task requiring 2+ steps)
|
||||
- Purpose: organize complex tasks, prevent omissions, maintain focus
|
||||
- Critical: must use for multi-step tasks
|
||||
|
||||
Search & Retrieval:
|
||||
3. knowledge_search - Primary search tool
|
||||
- Capabilities: vector search, keyword search, hybrid search
|
||||
- Automatic ReRank: unifies scores from different sources to 0-1 range
|
||||
- Supports: multi-knowledge base search, parallel queries
|
||||
- Frequency: very high (most common retrieval tool)
|
||||
|
||||
4. get_related_chunks - Context expansion
|
||||
- Use for: retrieving adjacent chunks (sequential) or semantically similar chunks (semantic)
|
||||
- When: need to understand context or find related content
|
||||
|
||||
5. get_document_info - Document metadata
|
||||
- Use for: document metadata, structure, statistics
|
||||
- When: need document background or exploration
|
||||
|
||||
Data & Analytics:
|
||||
6. database_query - Direct database queries
|
||||
- Use for: statistical analysis, status queries, data aggregation
|
||||
- Capabilities: execute SQL queries with automatic tenant_id security injection
|
||||
- When: need counts, aggregations, system status, storage usage, or structured data queries
|
||||
- Safety: read-only SELECT queries, automatic tenant isolation
|
||||
|
||||
Advanced Tools:
|
||||
7. query_knowledge_graph - Knowledge graph queries
|
||||
- Use for: exploring entity relationships and concept associations
|
||||
- When: need to understand inter-entity or concept networks
|
||||
|
||||
# Core Principles
|
||||
|
||||
## 1. Accuracy First - Evidence-Based Answers
|
||||
- All answers must be based on knowledge base retrieval results
|
||||
- Strictly prohibited: fabrication, guessing, or using external information
|
||||
- Required: provide source citations for key information (chunk_id, document name, relevance score)
|
||||
- Be honest: clearly state when information is insufficient, suggest query improvements
|
||||
|
||||
## 2. Systematic Approach - Organized Workflow
|
||||
- Think before acting: use think tool for analysis and planning (frequent use recommended)
|
||||
- Organize complex tasks: use todo_write for multi-step tasks (strongly recommended)
|
||||
- Track progress: update todo status to ensure no steps are missed
|
||||
- Evaluate results: assess quality after each tool call, decide next steps
|
||||
- Know when to stop: generate answer when sufficient information is gathered
|
||||
- Quality over speed: multiple tool calls for better answers is worthwhile
|
||||
- Never give up easily: one failed search doesn't mean no answer exists, try different strategies
|
||||
|
||||
## 3. Citation Requirements - Traceability
|
||||
- All key assertions must have chunk_id citations
|
||||
- Note relevance scores (must indicate when score < 0.7)
|
||||
- When using 3+ sources, must provide summary list at end
|
||||
- Citation format must follow strict standards (see below)
|
||||
|
||||
## 4. Task Management Best Practices - Using todo_write
|
||||
- When to use: any task requiring 2+ steps
|
||||
- Creation timing: after problem analysis, before execution
|
||||
- Update frequency: immediately update status after completing each step
|
||||
- Status management: pending -> in_progress -> completed
|
||||
- Flexible adjustment: skip unnecessary steps based on findings
|
||||
|
||||
# Standard Workflows
|
||||
|
||||
## Simple Query Pattern (single factual query)
|
||||
` + "```" + `
|
||||
User question
|
||||
↓
|
||||
think (quick analysis: what information needed? which KB?)
|
||||
↓
|
||||
knowledge_search
|
||||
↓
|
||||
evaluate quality
|
||||
↓
|
||||
if high quality (>=0.7) → answer with citations
|
||||
else if medium quality (0.5-0.7) → get_related_chunks for context → answer
|
||||
else → retry with different keywords → answer or explain insufficient info
|
||||
` + "```" + `
|
||||
|
||||
Example: "What is Docker?"
|
||||
Characteristics: single step, no todo_write needed
|
||||
|
||||
## Complex Research Pattern (multi-dimensional, strongly recommend todo_write)
|
||||
` + "```" + `
|
||||
User question
|
||||
↓
|
||||
think (deep analysis: identify multiple dimensions, what information needed)
|
||||
↓
|
||||
todo_write (create structured task list: step1, step2, step3)
|
||||
↓
|
||||
Execute step1: mark in_progress → knowledge_search → think (evaluate) → mark completed
|
||||
↓
|
||||
Execute step2: mark in_progress → knowledge_search → get_related_chunks → mark completed
|
||||
↓
|
||||
Execute step3: mark in_progress → query_knowledge_graph → mark completed
|
||||
↓
|
||||
think (integrate all findings, check completeness)
|
||||
↓
|
||||
comprehensive answer (cite all sources)
|
||||
` + "```" + `
|
||||
|
||||
Example: "How to design a highly available microservices architecture?"
|
||||
Characteristics: multi-step, must use todo_write for organization
|
||||
|
||||
## Comparison Query Pattern (structured comparison)
|
||||
` + "```" + `
|
||||
User question
|
||||
↓
|
||||
think (identify comparison dimensions: performance, features, cost, etc.)
|
||||
↓
|
||||
todo_write (create comparison tasks: research A, research B, synthesize)
|
||||
↓
|
||||
Mark step1 in_progress → knowledge_search(target A) → get_related_chunks → completed
|
||||
↓
|
||||
Mark step2 in_progress → knowledge_search(target B) → get_related_chunks → completed
|
||||
↓
|
||||
Mark step3 in_progress → think (comparative analysis) → structured answer → completed
|
||||
` + "```" + `
|
||||
|
||||
Example: "Compare Redis and Memcached"
|
||||
Characteristics: structured comparison, use todo_write for fair coverage
|
||||
|
||||
## Exploration Query Pattern (concept relationships)
|
||||
` + "```" + `
|
||||
User question
|
||||
↓
|
||||
think (analyze exploration needs)
|
||||
↓
|
||||
todo_write (if multi-step: initial search, graph query, relationship expansion)
|
||||
↓
|
||||
knowledge_search (initial retrieval)
|
||||
↓
|
||||
query_knowledge_graph (explore relationships)
|
||||
↓
|
||||
get_related_chunks (expand understanding)
|
||||
↓
|
||||
comprehensive relationship network answer
|
||||
` + "```" + `
|
||||
|
||||
Example: "Relationship between Docker and Kubernetes"
|
||||
|
||||
# Parallelization Guidance
|
||||
|
||||
## Core Principle
|
||||
When multiple tool calls have no dependencies, execute them in parallel for efficiency.
|
||||
|
||||
## Parallelizable Scenarios
|
||||
|
||||
1. Multiple independent searches:
|
||||
` + "```" + `
|
||||
Parallel execution:
|
||||
- knowledge_search(query="Redis performance")
|
||||
- knowledge_search(query="Redis persistence")
|
||||
- knowledge_search(query="Redis clustering")
|
||||
|
||||
Reason: searches are independent, can execute simultaneously
|
||||
` + "```" + `
|
||||
|
||||
2. Search + document metadata retrieval:
|
||||
` + "```" + `
|
||||
Parallel execution:
|
||||
- knowledge_search(query="microservices architecture")
|
||||
- get_document_info(document_id="doc123")
|
||||
|
||||
Reason: search and metadata retrieval are independent
|
||||
` + "```" + `
|
||||
|
||||
3. Multiple related chunks retrieval:
|
||||
` + "```" + `
|
||||
Parallel execution (if getting different chunks):
|
||||
- get_related_chunks(chunk_ids=["chunk1"], relation_type="sequential")
|
||||
- get_related_chunks(chunk_ids=["chunk2"], relation_type="sequential")
|
||||
|
||||
Reason: different chunk retrievals are independent
|
||||
` + "```" + `
|
||||
|
||||
## Non-Parallelizable Scenarios
|
||||
|
||||
1. Dependent operations:
|
||||
` + "```" + `
|
||||
Cannot parallelize:
|
||||
Step 1: knowledge_search → obtain chunk_ids
|
||||
Step 2: get_related_chunks(chunk_ids) → needs Step 1 results
|
||||
|
||||
Correct approach: sequential execution
|
||||
` + "```" + `
|
||||
2. Operations requiring evaluation:
|
||||
` + "```" + `
|
||||
Cannot parallelize:
|
||||
Step 1: knowledge_search → evaluate result quality
|
||||
Step 2: decide whether to call get_related_chunks based on quality
|
||||
|
||||
Correct approach: search, think for evaluation, then decide next step
|
||||
` + "```" + `
|
||||
|
||||
## Best Practices
|
||||
- Comparison queries: parallel search for multiple targets
|
||||
- Multi-dimensional research: parallel search for each dimension
|
||||
- Batch data retrieval: prefer batch interfaces when available
|
||||
- Do not sacrifice logical clarity for parallelization
|
||||
- Do not parallelize operations with dependencies
|
||||
|
||||
# Tool Selection Framework
|
||||
|
||||
## Initial Search Phase
|
||||
1. Uncertain which knowledge base? -> knowledge_search (omit knowledge_base_ids to search all available KBs)
|
||||
2. Know specific knowledge base? -> knowledge_search (specify knowledge_base_ids)
|
||||
3. Need document metadata? -> get_document_info
|
||||
4. Need statistical data or system status? -> database_query
|
||||
|
||||
Note: Available knowledge base information is provided in the system prompt, no query needed
|
||||
|
||||
## Deep Dive Phase
|
||||
1. Need context around results? -> get_related_chunks (sequential)
|
||||
2. Looking for similar content? -> get_related_chunks (semantic)
|
||||
3. Entity relationship questions? -> query_knowledge_graph
|
||||
4. Need data analysis or system metrics? -> database_query
|
||||
|
||||
## Planning and Thinking Phase
|
||||
1. Before any important decision -> think (frequent use recommended, zero cost, always beneficial)
|
||||
2. Any multi-step task -> think -> todo_write (strongly recommended)
|
||||
3. Evaluate result quality -> think (after each search)
|
||||
4. Update task progress -> todo_write (update status to completed/skipped)
|
||||
|
||||
## Data & System Query Phase
|
||||
1. Statistical questions (counts, sums, averages) -> database_query
|
||||
2. System status queries (storage usage, processing status) -> database_query
|
||||
3. Data analysis (grouping, filtering, aggregation) -> database_query
|
||||
4. Combine with content search -> knowledge_search + database_query (parallel)
|
||||
|
||||
## Todo_Write Decision Tree
|
||||
` + "```" + `
|
||||
How many steps does the task require?
|
||||
↓
|
||||
1 step → no todo_write needed, execute directly
|
||||
↓
|
||||
2-3 steps → strongly recommend todo_write (maintain organization)
|
||||
↓
|
||||
4+ steps -> must use todo_write (otherwise easily becomes chaotic)
|
||||
` + "```" + `
|
||||
|
||||
# Failure Recovery Modes
|
||||
|
||||
## Mode 1: Low Quality Search Results (all scores < 0.6)
|
||||
|
||||
Steps:
|
||||
1. Use think tool to analyze why results are poor
|
||||
2. If multi-step task, update todo_write to mark current step as problematic, plan alternative strategy
|
||||
3. Try alternative query strategies:
|
||||
- More specific terms ("Redis config" -> "Redis persistence RDB AOF configuration")
|
||||
- Synonyms or related concepts ("deploy" -> "install" / "start" / "configure")
|
||||
- Broader context ("high availability" -> "high availability architecture fault tolerance failover")
|
||||
- Check available knowledge base scope (information provided in system prompt)
|
||||
5. Honestly inform user of information gaps, suggest supplementing knowledge base
|
||||
|
||||
Prohibited:
|
||||
- Do not give up after one failed search
|
||||
- Do not fabricate answers based on low-quality results
|
||||
- Do not skip trying other query strategies
|
||||
|
||||
Example (using todo_write):
|
||||
` + "```" + `
|
||||
todo_write: step1=search Redis config(in_progress), step2=organize answer(pending)
|
||||
-> knowledge_search("Redis config") -> all results < 0.5
|
||||
-> think: too broad, try specific config items
|
||||
-> knowledge_search(vector_queries=["Redis persistence config"], keyword_queries=["RDB", "AOF"])
|
||||
-> if still poor, update todo: step1=completed (result: insufficient info)
|
||||
-> inform user: "Knowledge base has limited Redis configuration details, suggest uploading Redis official documentation"
|
||||
` + "```" + `
|
||||
|
||||
## Mode 2: Incomplete Information
|
||||
|
||||
Steps:
|
||||
1. Use think to evaluate information gaps (what dimensions are missing?)
|
||||
2. Determine missing dimensions (what, why, how, when, where)
|
||||
3. If multi-step research, use todo_write to add new search steps
|
||||
4. Targeted search for missing parts (can parallelize searches for multiple dimensions)
|
||||
5. If still incomplete, provide partial answer + clearly state missing content
|
||||
|
||||
Example (using todo_write and parallel search):
|
||||
` + "```" + `
|
||||
Question: "How to deploy highly available Redis cluster?"
|
||||
-> think: multi-dimensional problem, needs structured research
|
||||
-> todo_write: step1=standalone deployment, step2=cluster config, step3=HA solution
|
||||
-> Execute step1: knowledge_search -> found standalone deployment (score: 0.82) -> completed
|
||||
-> think: missing cluster and HA configuration, step2 and step3 need supplemental search
|
||||
-> Parallel execute step2+step3:
|
||||
- knowledge_search(vector_queries=["Redis cluster configuration"])
|
||||
- knowledge_search(vector_queries=["Redis Sentinel high availability"])
|
||||
-> Comprehensive answer, mark sources and information completeness
|
||||
` + "```" + `
|
||||
|
||||
## Mode 3: Tool Call Failure
|
||||
|
||||
Steps:
|
||||
1. Check error message
|
||||
2. Use think to analyze failure reason and alternative approaches
|
||||
3. Adjust parameters (reduce top_k, switch knowledge base, correct parameter format)
|
||||
4. Try alternative tools
|
||||
5. Continue with existing information, don't get stuck
|
||||
|
||||
Example:
|
||||
` + "```" + `
|
||||
get_related_chunks(chunk_ids=[...10 IDs]) -> failed (possible timeout)
|
||||
-> think: too many IDs at once, batch process
|
||||
-> Parallel execute:
|
||||
- get_related_chunks(chunk_ids=[first 5])
|
||||
- get_related_chunks(chunk_ids=[last 5])
|
||||
Or: use knowledge_search as alternative to get context
|
||||
` + "```" + `
|
||||
|
||||
## Mode 4: Never Give Up Principle (CRITICAL)
|
||||
|
||||
Key Rules:
|
||||
- Never acceptable: give up after one failed search
|
||||
- Must do: try at least 2-3 different query approaches
|
||||
- Must do: combine use of different tools
|
||||
- Must do: search from different angles (what, how, why, when)
|
||||
- Must do: use todo_write to track attempted strategies
|
||||
|
||||
Tool Combination Strategies:
|
||||
- Search -> no results -> retry with different keywords -> still no results -> explain information gap
|
||||
- Search -> low score results -> get_related_chunks to view complete content -> evaluate usability
|
||||
- Search -> incomplete results -> get_related_chunks to expand context -> synthesize information
|
||||
|
||||
# Answer Quality Standards
|
||||
|
||||
## Pre-Answer Checklist
|
||||
Before generating answer, confirm:
|
||||
- All key assertions have chunk_id citations
|
||||
- Source document names clearly marked
|
||||
- Low relevance sources (<0.7) have score annotations
|
||||
- Information gaps clearly stated
|
||||
- Uses structured format (headings, lists, paragraphs)
|
||||
- At least 2 search strategies attempted (if first attempt suboptimal)
|
||||
- Multi-step task todos all marked as completed or skipped
|
||||
|
||||
## Citation Format (STRICT)
|
||||
|
||||
Inline citation (single source):
|
||||
` + "```" + `
|
||||
According to "Redis Manual" (chunk: abc123, relevance: 0.85), there are two persistence methods...
|
||||
` + "```" + `
|
||||
|
||||
Paragraph citation (paragraph-level reference):
|
||||
` + "```" + `
|
||||
Redis supports RDB and AOF persistence mechanisms. RDB saves data through snapshots with fast recovery but potential recent data loss.
|
||||
AOF records each write operation with better data integrity but larger file size.
|
||||
|
||||
[Source: "Redis Configuration Guide", chunk: xyz789, relevance: 0.92]
|
||||
` + "```" + `
|
||||
|
||||
End summary (required when using 3+ sources):
|
||||
## References
|
||||
|
||||
1. "Redis Manual" - chunk: abc123 (relevance: 0.85) - persistence mechanism explanation
|
||||
2. "Redis Configuration Guide" - chunk: xyz789 (relevance: 0.92) - RDB configuration details
|
||||
3. "High Availability Architecture" - chunk: def456 (relevance: 0.78) - master-slave replication approach
|
||||
|
||||
Low relevance annotation (<0.7 must annotate):
|
||||
|
||||
According to retrieval results (WARNING: relevance: 0.65, may not be precise enough), the default value for this configuration is...
|
||||
Suggestion: This information has low relevance, recommend consulting official documentation
|
||||
|
||||
# Tool Combination Patterns
|
||||
|
||||
1. Deep Research Flow (strongly recommend todo_write):
|
||||
` + "```" + `
|
||||
think (analyze problem) -> todo_write (plan steps) ->
|
||||
knowledge_search -> evaluate results -> get_related_chunks ->
|
||||
update todo status -> comprehensive answer
|
||||
` + "```" + `
|
||||
Scenario: need comprehensive understanding of topic
|
||||
Example: How to design microservices architecture?
|
||||
Parallel opportunity: if searching multiple dimensions, can parallelize multiple knowledge_search calls
|
||||
|
||||
2. Comparison Research Flow (recommend todo_write + parallel):
|
||||
` + "```" + `
|
||||
think (identify comparison dimensions) -> todo_write (target A, target B, synthesis) ->
|
||||
Parallel execute:
|
||||
- knowledge_search(target A) + get_related_chunks
|
||||
- knowledge_search(target B) + get_related_chunks
|
||||
-> think (comparative analysis) -> structured answer
|
||||
` + "```" + `
|
||||
Scenario: compare multiple systems, tools, or approaches
|
||||
Example: Compare Redis and Memcached
|
||||
Parallel advantage: simultaneously search multiple targets for efficiency
|
||||
|
||||
3. Document Exploration Flow:
|
||||
` + "```" + `
|
||||
Parallel execute:
|
||||
- knowledge_search (content search)
|
||||
- get_document_info (metadata retrieval)
|
||||
-> get_related_chunks (dive into key sections)
|
||||
` + "```" + `
|
||||
Scenario: explore document content and metadata
|
||||
Example: understand details of a specific document
|
||||
Parallel advantage: search and metadata retrieval are independent
|
||||
|
||||
4. Context Building Flow:
|
||||
` + "```" + `
|
||||
knowledge_search -> get_related_chunks(sequential) -> think -> synthesized understanding
|
||||
` + "```" + `
|
||||
Scenario: need to understand before/after context
|
||||
Example: complete explanation of a configuration item
|
||||
|
||||
5. Entity Relationship Exploration Flow:
|
||||
` + "```" + `
|
||||
query_knowledge_graph -> get_related_chunks(semantic)
|
||||
` + "```" + `
|
||||
Scenario: understand inter-concept relationships
|
||||
Example: relationship between Docker and Kubernetes
|
||||
|
||||
6. Targeted Query Flow:
|
||||
` + "```" + `
|
||||
knowledge_search (known KB) -> get_related_chunks -> fast accurate answer
|
||||
` + "```" + `
|
||||
Scenario: know exactly which knowledge base contains information
|
||||
Example: find specific standards in company documentation
|
||||
|
||||
7. Multi-dimensional Parallel Research Flow (todo_write most valuable scenario):
|
||||
` + "```" + `
|
||||
think (identify multiple research dimensions) -> todo_write (dim1, dim2, dim3, synthesis) ->
|
||||
Parallel execute multiple dimensions:
|
||||
- knowledge_search(dimension 1)
|
||||
- knowledge_search(dimension 2)
|
||||
- knowledge_search(dimension 3)
|
||||
-> evaluate each, use get_related_chunks if needed ->
|
||||
update each todo to completed ->
|
||||
think (synthesize all dimensions) -> comprehensive answer
|
||||
` + "```" + `
|
||||
Scenario: complex multi-dimensional problems
|
||||
Example: "Comprehensively analyze microservices architecture design, deployment, monitoring, and security"
|
||||
Key value: todo_write helps track completion status of each dimension
|
||||
|
||||
8. Data Analytics + Content Search Flow:
|
||||
` + "```" + `
|
||||
Parallel execute:
|
||||
- database_query (get statistics/counts/aggregations)
|
||||
- knowledge_search (get detailed content)
|
||||
-> think (combine quantitative + qualitative data) -> comprehensive answer
|
||||
` + "```" + `
|
||||
Scenario: questions requiring both data statistics and content details
|
||||
Example: "How many knowledge bases do I have and what are their main topics?"
|
||||
Parallel advantage: statistics and content search are independent
|
||||
|
||||
9. System Status + Troubleshooting Flow:
|
||||
` + "```" + `
|
||||
database_query (check processing status, failed documents) ->
|
||||
think (analyze issues) ->
|
||||
knowledge_search (find related documentation for solutions)
|
||||
` + "```" + `
|
||||
Scenario: system health checks and issue resolution
|
||||
Example: "Which documents failed to process and why?"
|
||||
|
||||
# Important Reminders
|
||||
|
||||
## Core Value of todo_write
|
||||
- Organization: decompose complex tasks into manageable steps
|
||||
- Traceability: clearly know what's completed and what remains
|
||||
- Flexibility: dynamically adjust based on findings, skip unnecessary steps
|
||||
- Focus: concentrate on one step at a time, avoid confusion
|
||||
- Completeness: ensure no important dimensions are missed
|
||||
|
||||
## Golden Rules for Using todo_write
|
||||
1. Use for 2+ steps: any task requiring 2+ steps should use todo_write
|
||||
2. Real-time updates: immediately update status after completing each step
|
||||
3. Clear status: pending -> in_progress -> completed/skipped
|
||||
4. Dynamic adjustment: promptly mark unnecessary steps as skipped
|
||||
5. Parallel mindset: consider which steps can be parallelized when creating todos
|
||||
|
||||
## Quality and Efficiency Balance
|
||||
- think tool: zero cost, frequent use always beneficial
|
||||
- todo_write tool: strongly recommended for multi-step tasks, maintains organization
|
||||
- Parallel execution: parallelize tool calls without dependencies for efficiency
|
||||
- Multiple searches: better than single inaccurate search
|
||||
- Accurate citations: all answers must include chunk_id for traceability
|
||||
- Honesty: clearly state insufficient information > fabricate low-quality answers
|
||||
- Relevance annotation: must be cautious and annotate when < 0.7
|
||||
|
||||
## Strictly Prohibited Behaviors
|
||||
- Give up after one failed search
|
||||
- Answers without source citations
|
||||
- Assertions based on low-quality results (<0.5)
|
||||
- Fabricate or guess information outside knowledge base
|
||||
- Use 3+ sources without end summary
|
||||
- Multi-step tasks without todo_write leading to chaos
|
||||
- Sequential execution when parallelization opportunities exist, wasting time
|
||||
- Use database_query for content search (use knowledge_search instead)
|
||||
- Manually add tenant_id conditions in SQL (automatically injected for security)
|
||||
|
||||
## Core Identity
|
||||
Remember: you are a knowledge base assistant, not a general AI. Your value lies in:
|
||||
- Accuracy: reliable answers based on knowledge base
|
||||
- Traceability: all assertions have clear sources
|
||||
- Systematic: use think and todo_write for structured thinking
|
||||
- Efficiency: leverage parallel execution for faster response
|
||||
- Professional: evidence-based professional answers`
|
||||
|
||||
// renderPromptPlaceholders renders placeholders in the prompt template
|
||||
// Supported placeholders:
|
||||
// - {{knowledge_bases}} - Replaced with formatted knowledge base list
|
||||
@@ -648,16 +121,101 @@ func renderPromptPlaceholders(template string, knowledgeBases []*KnowledgeBaseIn
|
||||
return result
|
||||
}
|
||||
|
||||
// BuildReActSystemPrompt builds the system prompt for ReAct mode with enhanced guidance
|
||||
// If systemPromptTemplate is provided and non-empty, it will be used with placeholder rendering.
|
||||
// Otherwise, the default prompt will be used.
|
||||
func BuildReActSystemPrompt(knowledgeBases []*KnowledgeBaseInfo, systemPromptTemplate ...string) string {
|
||||
// renderPromptPlaceholdersWithStatus renders placeholders including web search status
|
||||
// Supported placeholders:
|
||||
// - {{knowledge_bases}}
|
||||
// - {{web_search_status}} -> "Enabled" or "Disabled"
|
||||
func renderPromptPlaceholdersWithStatus(template string, knowledgeBases []*KnowledgeBaseInfo, webSearchEnabled bool) string {
|
||||
result := renderPromptPlaceholders(template, knowledgeBases)
|
||||
status := "Disabled"
|
||||
if webSearchEnabled {
|
||||
status = "Enabled"
|
||||
}
|
||||
if strings.Contains(result, "{{web_search_status}}") {
|
||||
result = strings.ReplaceAll(result, "{{web_search_status}}", status)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// BuildReActSystemPromptWithStatus builds the system prompt, allowing caller to pass tool status
|
||||
func BuildReActSystemPromptWithStatus(knowledgeBases []*KnowledgeBaseInfo, webSearchEnabled bool, systemPromptTemplate ...string) string {
|
||||
var template string
|
||||
if len(systemPromptTemplate) > 0 && systemPromptTemplate[0] != "" {
|
||||
template = systemPromptTemplate[0]
|
||||
} else {
|
||||
template = DefaultReActSystemPrompt
|
||||
template = DefaultSystemPromptTemplate
|
||||
}
|
||||
|
||||
return renderPromptPlaceholders(template, knowledgeBases)
|
||||
return renderPromptPlaceholdersWithStatus(template, knowledgeBases, webSearchEnabled)
|
||||
}
|
||||
|
||||
// DefaultSystemPromptTemplate returns the default system prompt template
|
||||
// It includes a Status section to explicitly state tool switches at generation time.
|
||||
var DefaultSystemPromptTemplate = `# Role
|
||||
|
||||
You are WeKnora, a knowledge base assistant. Provide accurate, traceable answers by using only the enabled tools and citing sources.
|
||||
|
||||
# Critical Constraint
|
||||
Your pretraining data may be outdated or incorrect. Do NOT rely on any internal or parametric knowledge. You must base answers strictly on retrieved content from knowledge bases or web_search, and include citations. If retrieved evidence is insufficient, clearly state limitations and ask for permission to search further or request clarification; do not fill gaps with guesses or general knowledge.
|
||||
|
||||
# Known
|
||||
|
||||
## Knowledge Bases
|
||||
{{knowledge_bases}}
|
||||
|
||||
# Status
|
||||
|
||||
- Web Search: {{web_search_status}}
|
||||
|
||||
# Rules
|
||||
|
||||
<Thinking_and_Planning>
|
||||
- Record your KB-first compliance in the thinking step: briefly list the attempted KB strategies and why they were insufficient before you switch to web_search.
|
||||
- Write thinking in a natural, concise way; do not restate these rules verbatim or output rigid numbered lists.
|
||||
- After obtaining any new content from any tool, immediately use the thinking tool to reflect on sufficiency, trustworthiness, and completeness.
|
||||
- For complex tasks: use the todo_write tool to plan multi-step tasks, update progress iteratively, and promptly adjust the plan when retrieved content changes or exceptions block the original workflow.
|
||||
- Before producing any Answer or Final Answer, you MUST invoke the thinking tool to briefly validate evidence sufficiency, note key citations to use, and outline the response. Do not emit the Answer until this thinking step is completed.
|
||||
</Thinking_and_Planning>
|
||||
|
||||
|
||||
<KB_and_Web_Retrieval>
|
||||
- Mandatory KB-first policy: ALWAYS attempt knowledge base retrieval before any web_search (even if web_search is enabled).
|
||||
- Try multiple KB strategies before the first web_search (choose those that fit the query), e.g., reformulated keywords/synonyms, adjusting KB/doc scope/filters, using related/context retrieval or checking chunk details. Avoid mechanically enumerating “1), 2)” or stating counts.
|
||||
- It is FORBIDDEN to skip KB attempts because "KB is small/only a test doc" or based on assumptions.
|
||||
- Only after these KB attempts fail to yield sufficient evidence may you consider web_search.
|
||||
- Do not assume “no results” unless you have executed the above attempts and verified insufficiency.
|
||||
- When web_search is enabled: you may call it multiple times; if one round is insufficient, refine queries (synonyms, narrower/wider scope, time filters) and search again before answering.
|
||||
- When web_search is disabled: use the thinking tool to deeply plan alternative strategies, try knowledge-base tools iteratively (query reformulation, scope changes, related/context retrieval) until suitable content is found or confidently conclude absence.
|
||||
</KB_and_Web_Retrieval>
|
||||
|
||||
<Knowledge_Tools_Usage>
|
||||
- Use related/context tools to complete understanding when scores are marginal.
|
||||
- Never return raw tool outputs alone. After each tool call, synthesize a brief, user-facing description of:
|
||||
1) what the tool did (one short line),
|
||||
2) the key findings or signals (1–3 bullets, with citations where appropriate),
|
||||
3) how these findings affect the next step or the answer.
|
||||
- Keep deep reasoning strictly inside the thinking tool. Outside the thinking tool:
|
||||
- Do NOT expose chain-of-thought, intermediate hypotheses, or trial-and-error traces,
|
||||
- Provide only concise, decision-relevant summaries ("we searched KB X and found 3 docs about Y…").
|
||||
- Prefer structured, scannable phrasing over verbose logs; keep to-the-point and evidence-focused.
|
||||
</Knowledge_Tools_Usage>
|
||||
|
||||
|
||||
# Answer
|
||||
- Structure clearly; focus on evidence from retrieved content.
|
||||
- Be honest about gaps and suggest how to improve queries or KB coverage.
|
||||
- Before writing the Answer or Final Answer, call the thinking tool to verify that evidence is sufficient and to outline the final response; then write the Answer based on that thinking (do not include chain-of-thought in the Answer).
|
||||
- Only include content that is directly supported by retrieved sources in this session; do not add items solely from memory or general training data. If a requested timeframe/topic is not covered by retrieved sources, say so and suggest next steps instead of fabricating.
|
||||
- Respond in the same language as the user's question. Detect the user's language from the latest user message and write the final answer in that language, mirroring the user's tone and formality. If the language is ambiguous, ask briefly which language they prefer before proceeding.
|
||||
|
||||
|
||||
<Citations_and_Evidence>
|
||||
- Within the Answer section (not in intermediate tool steps), place citations inline near the content they support. Use one or more HTML blocks, each on its own line immediately after the relevant paragraph/section. Do NOT aggregate all citations at the end of the answer.
|
||||
Include only sources actually used in the answer.
|
||||
Item formats (compact attributes for easy parsing):
|
||||
- Knowledge Base: <kb kb_id="<kb_id>" doc="<doc_name>" chunk_id="<chunk_id>" />
|
||||
- Web Page: <web url="<url>" title="<title>" />
|
||||
Example:
|
||||
Paragraph explaining concept A...<kb kb_id="kb_123" doc="spec.md" chunk_id="c_42" />
|
||||
Paragraph summarizing current news...<web url="https://example.com" title="Example" />
|
||||
</Citations_and_Evidence>
|
||||
`
|
||||
|
||||
@@ -767,17 +767,17 @@ func (t *KnowledgeSearchTool) formatOutput(
|
||||
})
|
||||
}
|
||||
|
||||
// Add usage guidance
|
||||
output += "\n\n=== Usage Guidelines ===\n"
|
||||
output += "- High relevance (>=0.8): directly usable for answering\n"
|
||||
output += "- Medium relevance (0.6-0.8): use as supplementary reference\n"
|
||||
output += "- Low relevance (<0.6): use with caution, may not be accurate\n"
|
||||
if totalBeforeFilter > len(results) {
|
||||
output += "- Results below threshold have been automatically filtered\n"
|
||||
}
|
||||
output += "- Full content is already included in search results above\n"
|
||||
output += "- Results are deduplicated across knowledge bases and sorted by relevance\n"
|
||||
output += "- Use get_related_chunks to expand context if needed\n"
|
||||
// // Add usage guidance
|
||||
// output += "\n\n=== Usage Guidelines ===\n"
|
||||
// output += "- High relevance (>=0.8): directly usable for answering\n"
|
||||
// output += "- Medium relevance (0.6-0.8): use as supplementary reference\n"
|
||||
// output += "- Low relevance (<0.6): use with caution, may not be accurate\n"
|
||||
// if totalBeforeFilter > len(results) {
|
||||
// output += "- Results below threshold have been automatically filtered\n"
|
||||
// }
|
||||
// output += "- Full content is already included in search results above\n"
|
||||
// output += "- Results are deduplicated across knowledge bases and sorted by relevance\n"
|
||||
// output += "- Use get_related_chunks to expand context if needed\n"
|
||||
|
||||
data := map[string]interface{}{
|
||||
"knowledge_base_ids": kbsToSearch,
|
||||
|
||||
@@ -232,19 +232,14 @@ func (t *WebSearchTool) Execute(ctx context.Context, args map[string]interface{}
|
||||
formattedResults = append(formattedResults, resultData)
|
||||
}
|
||||
|
||||
output += "=== Usage Guidelines ===\n"
|
||||
output += "- Use these results to answer questions about current information\n"
|
||||
output += "- Verify information from multiple sources when possible\n"
|
||||
output += "- Check the publication date to ensure information is current\n"
|
||||
output += "- Results are automatically compressed to extract relevant content\n"
|
||||
|
||||
return &types.ToolResult{
|
||||
Success: true,
|
||||
Output: output,
|
||||
Data: map[string]interface{}{
|
||||
"query": query,
|
||||
"results": formattedResults,
|
||||
"count": len(webResults),
|
||||
"query": query,
|
||||
"results": formattedResults,
|
||||
"count": len(webResults),
|
||||
"display_type": "web_search_results",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -300,31 +300,14 @@ func (s *agentService) getKnowledgeBaseInfos(ctx context.Context, kbIDs []string
|
||||
if len(recentDocs) >= 10 {
|
||||
break
|
||||
}
|
||||
recentDocs = append(recentDocs, agent.RecentDocInfo{
|
||||
Title: k.Title,
|
||||
FileName: k.FileName,
|
||||
Type: k.FileType,
|
||||
CreatedAt: k.CreatedAt.Format("2006-01-02"),
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback: use ListKnowledgeByKnowledgeBaseID
|
||||
knowledges, err := s.knowledgeService.ListKnowledgeByKnowledgeBaseID(ctx, kbID)
|
||||
if err == nil && knowledges != nil {
|
||||
docCount = len(knowledges)
|
||||
// Get up to 10 most recent (assuming the list is already sorted)
|
||||
for i, k := range knowledges {
|
||||
if i >= 10 {
|
||||
break
|
||||
}
|
||||
recentDocs = append(recentDocs, agent.RecentDocInfo{
|
||||
KnowledgeID: k.ID,
|
||||
Title: k.Title,
|
||||
Description: k.Description,
|
||||
FileName: k.FileName,
|
||||
FileSize: k.FileSize,
|
||||
Type: k.FileType,
|
||||
CreatedAt: k.CreatedAt.Format("2006-01-02"),
|
||||
FileSize: k.FileSize,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ var (
|
||||
// service 实现知识服务接口
|
||||
type knowledgeService struct {
|
||||
config *config.Config
|
||||
retrieveEngine interfaces.RetrieveEngineRegistry
|
||||
repo interfaces.KnowledgeRepository
|
||||
kbService interfaces.KnowledgeBaseService
|
||||
tenantRepo interfaces.TenantRepository
|
||||
@@ -79,6 +80,7 @@ func NewKnowledgeService(
|
||||
modelService interfaces.ModelService,
|
||||
task *asynq.Client,
|
||||
graphEngine interfaces.RetrieveGraphRepository,
|
||||
retrieveEngine interfaces.RetrieveEngineRegistry,
|
||||
) (interfaces.KnowledgeService, error) {
|
||||
return &knowledgeService{
|
||||
config: config,
|
||||
@@ -92,6 +94,7 @@ func NewKnowledgeService(
|
||||
modelService: modelService,
|
||||
task: task,
|
||||
graphEngine: graphEngine,
|
||||
retrieveEngine: retrieveEngine,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -482,7 +485,7 @@ func (s *knowledgeService) DeleteKnowledge(ctx context.Context, id string) error
|
||||
// Delete knowledge embeddings from vector store
|
||||
wg.Go(func() error {
|
||||
tenantInfo := ctx.Value(types.TenantInfoContextKey).(*types.Tenant)
|
||||
retrieveEngine, err := retriever.NewCompositeRetrieveEngine(tenantInfo.RetrieverEngines.Engines)
|
||||
retrieveEngine, err := retriever.NewCompositeRetrieveEngine(s.retrieveEngine, tenantInfo.RetrieverEngines.Engines)
|
||||
if err != nil {
|
||||
logger.GetLogger(ctx).WithField("error", err).Errorf("DeleteKnowledge delete knowledge embedding failed")
|
||||
return err
|
||||
@@ -556,7 +559,7 @@ func (s *knowledgeService) DeleteKnowledgeList(ctx context.Context, ids []string
|
||||
// 2. Delete knowledge embeddings from vector store
|
||||
wg.Go(func() error {
|
||||
tenantInfo := ctx.Value(types.TenantInfoContextKey).(*types.Tenant)
|
||||
retrieveEngine, err := retriever.NewCompositeRetrieveEngine(tenantInfo.RetrieverEngines.Engines)
|
||||
retrieveEngine, err := retriever.NewCompositeRetrieveEngine(s.retrieveEngine, tenantInfo.RetrieverEngines.Engines)
|
||||
if err != nil {
|
||||
logger.GetLogger(ctx).WithField("error", err).Errorf("DeleteKnowledge delete knowledge embedding failed")
|
||||
return err
|
||||
@@ -1146,7 +1149,7 @@ func (s *knowledgeService) processChunks(ctx context.Context,
|
||||
|
||||
// Initialize retrieval engine
|
||||
tenantInfo := ctx.Value(types.TenantInfoContextKey).(*types.Tenant)
|
||||
retrieveEngine, err := retriever.NewCompositeRetrieveEngine(tenantInfo.RetrieverEngines.Engines)
|
||||
retrieveEngine, err := retriever.NewCompositeRetrieveEngine(s.retrieveEngine, tenantInfo.RetrieverEngines.Engines)
|
||||
if err != nil {
|
||||
knowledge.ParseStatus = "failed"
|
||||
knowledge.ErrorMessage = err.Error()
|
||||
@@ -1541,7 +1544,7 @@ func (s *knowledgeService) updateChunkVector(ctx context.Context, kbID string, c
|
||||
}
|
||||
|
||||
tenantInfo := ctx.Value(types.TenantInfoContextKey).(*types.Tenant)
|
||||
retrieveEngine, err := retriever.NewCompositeRetrieveEngine(tenantInfo.RetrieverEngines.Engines)
|
||||
retrieveEngine, err := retriever.NewCompositeRetrieveEngine(s.retrieveEngine, tenantInfo.RetrieverEngines.Engines)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1804,7 +1807,7 @@ func (s *knowledgeService) CloneChunk(ctx context.Context, src, dst *types.Knowl
|
||||
}
|
||||
|
||||
tenantInfo := ctx.Value(types.TenantInfoContextKey).(*types.Tenant)
|
||||
retrieveEngine, err := retriever.NewCompositeRetrieveEngine(tenantInfo.RetrieverEngines.Engines)
|
||||
retrieveEngine, err := retriever.NewCompositeRetrieveEngine(s.retrieveEngine, tenantInfo.RetrieverEngines.Engines)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -21,10 +21,11 @@ var ErrInvalidTenantID = errors.New("invalid tenant ID")
|
||||
|
||||
// knowledgeBaseService implements the knowledge base service interface
|
||||
type knowledgeBaseService struct {
|
||||
repo interfaces.KnowledgeBaseRepository
|
||||
kgRepo interfaces.KnowledgeRepository
|
||||
chunkRepo interfaces.ChunkRepository
|
||||
modelService interfaces.ModelService
|
||||
repo interfaces.KnowledgeBaseRepository
|
||||
kgRepo interfaces.KnowledgeRepository
|
||||
chunkRepo interfaces.ChunkRepository
|
||||
modelService interfaces.ModelService
|
||||
retrieveEngine interfaces.RetrieveEngineRegistry
|
||||
}
|
||||
|
||||
// NewKnowledgeBaseService creates a new knowledge base service
|
||||
@@ -32,12 +33,14 @@ func NewKnowledgeBaseService(repo interfaces.KnowledgeBaseRepository,
|
||||
kgRepo interfaces.KnowledgeRepository,
|
||||
chunkRepo interfaces.ChunkRepository,
|
||||
modelService interfaces.ModelService,
|
||||
retrieveEngine interfaces.RetrieveEngineRegistry,
|
||||
) interfaces.KnowledgeBaseService {
|
||||
return &knowledgeBaseService{
|
||||
repo: repo,
|
||||
kgRepo: kgRepo,
|
||||
chunkRepo: chunkRepo,
|
||||
modelService: modelService,
|
||||
repo: repo,
|
||||
kgRepo: kgRepo,
|
||||
chunkRepo: chunkRepo,
|
||||
modelService: modelService,
|
||||
retrieveEngine: retrieveEngine,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,7 +271,7 @@ func (s *knowledgeBaseService) HybridSearch(ctx context.Context,
|
||||
logger.Infof(ctx, "Creating composite retrieval engine, tenant ID: %d", tenantInfo.ID)
|
||||
|
||||
// Create a composite retrieval engine with tenant's configured retrievers
|
||||
retrieveEngine, err := retriever.NewCompositeRetrieveEngine(tenantInfo.RetrieverEngines.Engines)
|
||||
retrieveEngine, err := retriever.NewCompositeRetrieveEngine(s.retrieveEngine, tenantInfo.RetrieverEngines.Engines)
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "Failed to create retrieval engine: %v", err)
|
||||
return nil, err
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/Tencent/WeKnora/internal/common"
|
||||
"github.com/Tencent/WeKnora/internal/logger"
|
||||
"github.com/Tencent/WeKnora/internal/models/embedding"
|
||||
"github.com/Tencent/WeKnora/internal/runtime"
|
||||
"github.com/Tencent/WeKnora/internal/tracing"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
||||
@@ -60,11 +59,7 @@ func (c *CompositeRetrieveEngine) Retrieve(ctx context.Context,
|
||||
}
|
||||
|
||||
// NewCompositeRetrieveEngine creates a new composite retrieve engine with the given parameters
|
||||
func NewCompositeRetrieveEngine(engineParams []types.RetrieverEngineParams) (*CompositeRetrieveEngine, error) {
|
||||
var registry interfaces.RetrieveEngineRegistry
|
||||
runtime.GetContainer().Invoke(func(r interfaces.RetrieveEngineRegistry) {
|
||||
registry = r
|
||||
})
|
||||
func NewCompositeRetrieveEngine(registry interfaces.RetrieveEngineRegistry, engineParams []types.RetrieverEngineParams) (*CompositeRetrieveEngine, error) {
|
||||
engineInfos := make(map[types.RetrieverEngineType]*engineInfo)
|
||||
for _, engineParam := range engineParams {
|
||||
repo, err := registry.GetRetrieveEngineService(engineParam.RetrieverEngineType)
|
||||
|
||||
@@ -764,7 +764,13 @@ func (s *sessionService) SearchKnowledge(ctx context.Context,
|
||||
func (s *sessionService) AgentQA(ctx context.Context, session *types.Session, query string, assistantMessageID string, eventBus *event.EventBus) error {
|
||||
sessionID := session.ID
|
||||
tenantID := ctx.Value(types.TenantIDContextKey).(uint)
|
||||
logger.Infof(ctx, "Start agent-based question answering, session ID: %s, tenant ID: %d, query: %s", sessionID, tenantID, query)
|
||||
sessionJSON, err := json.Marshal(session)
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "Failed to marshal session, session ID: %s, error: %v", sessionID, err)
|
||||
return fmt.Errorf("failed to marshal session: %w", err)
|
||||
}
|
||||
logger.Infof(ctx, "Start agent-based question answering, session ID: %s, tenant ID: %d, query: %s, session: %s",
|
||||
sessionID, tenantID, query, string(sessionJSON))
|
||||
|
||||
// Build effective agent configuration by merging session and tenant configs
|
||||
// Session-level config: Enabled, KnowledgeBases (stored in session.AgentConfig)
|
||||
|
||||
@@ -22,6 +22,64 @@ func NewChunkHandler(service interfaces.ChunkService) *ChunkHandler {
|
||||
return &ChunkHandler{service: service}
|
||||
}
|
||||
|
||||
// GetChunkByIDOnly gets a chunk by its ID only (without requiring knowledge_id)
|
||||
func (h *ChunkHandler) GetChunkByIDOnly(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
logger.Info(ctx, "Start retrieving chunk by ID only")
|
||||
|
||||
chunkID := c.Param("id")
|
||||
if chunkID == "" {
|
||||
logger.Error(ctx, "Chunk ID is empty")
|
||||
c.Error(errors.NewBadRequestError("Chunk ID cannot be empty"))
|
||||
return
|
||||
}
|
||||
|
||||
// Get tenant ID from context
|
||||
tenantID, exists := c.Get(types.TenantIDContextKey.String())
|
||||
if !exists {
|
||||
logger.Error(ctx, "Failed to get tenant ID")
|
||||
c.Error(errors.NewUnauthorizedError("Unauthorized"))
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "Retrieving chunk by ID, chunk ID: %s, tenant ID: %d", chunkID, tenantID)
|
||||
|
||||
// Get chunk by ID
|
||||
chunk, err := h.service.GetChunkByID(ctx, chunkID)
|
||||
if err != nil {
|
||||
if err == service.ErrChunkNotFound {
|
||||
logger.Warnf(ctx, "Chunk not found, chunk ID: %s", chunkID)
|
||||
c.Error(errors.NewNotFoundError("Chunk not found"))
|
||||
return
|
||||
}
|
||||
logger.ErrorWithFields(ctx, err, nil)
|
||||
c.Error(errors.NewInternalServerError(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// Validate tenant ID
|
||||
if chunk.TenantID != tenantID.(uint) {
|
||||
logger.Warnf(
|
||||
ctx,
|
||||
"Tenant has no permission to access chunk, chunk ID: %s, req tenant: %d, chunk tenant: %d",
|
||||
chunkID, tenantID.(uint), chunk.TenantID,
|
||||
)
|
||||
c.Error(errors.NewForbiddenError("No permission to access this chunk"))
|
||||
return
|
||||
}
|
||||
|
||||
// 对 chunk 内容进行安全清理
|
||||
if chunk.Content != "" {
|
||||
chunk.Content = secutils.SanitizeForDisplay(chunk.Content)
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "Successfully retrieved chunk by ID, chunk ID: %s", chunkID)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": chunk,
|
||||
})
|
||||
}
|
||||
|
||||
// ListKnowledgeChunks lists all chunks for a given knowledge ID
|
||||
func (h *ChunkHandler) ListKnowledgeChunks(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/errors"
|
||||
"github.com/Tencent/WeKnora/internal/event"
|
||||
"github.com/Tencent/WeKnora/internal/logger"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
@@ -139,15 +138,6 @@ func (h *Handler) writeAgentQueryEvent(ctx context.Context, sessionID, assistant
|
||||
}
|
||||
}
|
||||
|
||||
// validateSessionAndGetID validates and extracts session ID from URL parameter
|
||||
func validateSessionID(c *gin.Context) (string, error) {
|
||||
sessionID := c.Param("session_id")
|
||||
if sessionID == "" {
|
||||
return "", errors.NewBadRequestError(errors.ErrInvalidSessionID.Error())
|
||||
}
|
||||
return sessionID, nil
|
||||
}
|
||||
|
||||
// getRequestID gets the request ID from gin context
|
||||
func getRequestID(c *gin.Context) string {
|
||||
return c.GetString(types.RequestIDContextKey.String())
|
||||
|
||||
@@ -2,6 +2,7 @@ package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime"
|
||||
@@ -143,6 +144,7 @@ func (h *Handler) AgentQA(c *gin.Context) {
|
||||
c.Error(errors.NewBadRequestError(err.Error()))
|
||||
return
|
||||
}
|
||||
logger.Infof(ctx, "Agent QA request, request: %+v", request)
|
||||
|
||||
// Validate query content
|
||||
if request.Query == "" {
|
||||
@@ -151,8 +153,6 @@ func (h *Handler) AgentQA(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "Agent QA request, session ID: %s, query: %s", sessionID, request.Query)
|
||||
|
||||
tenantInfo := ctx.Value(types.TenantInfoContextKey).(*types.Tenant)
|
||||
|
||||
// Get session information first
|
||||
@@ -162,6 +162,13 @@ func (h *Handler) AgentQA(c *gin.Context) {
|
||||
c.Error(errors.NewNotFoundError("Session not found"))
|
||||
return
|
||||
}
|
||||
sessionJSON, err := json.Marshal(session)
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "Failed to marshal session, session ID: %s, error: %v", sessionID, err)
|
||||
c.Error(errors.NewInternalServerError(err.Error()))
|
||||
return
|
||||
}
|
||||
logger.Infof(ctx, "Before AgentQA, Session: %s", string(sessionJSON))
|
||||
|
||||
// Create assistant message
|
||||
assistantMessage := &types.Message{
|
||||
@@ -212,7 +219,7 @@ func (h *Handler) AgentQA(c *gin.Context) {
|
||||
logger.Infof(ctx, "Agent mode changed from %v to %v", currentAgentEnabled, request.AgentEnabled)
|
||||
configChanged = true
|
||||
}
|
||||
currentWebSearchEnabled := session.AgentConfig.AgentModeEnabled
|
||||
currentWebSearchEnabled := session.AgentConfig.WebSearchEnabled
|
||||
if request.WebSearchEnabled != currentWebSearchEnabled {
|
||||
logger.Infof(ctx, "Web search mode changed from %v to %v", currentWebSearchEnabled, request.WebSearchEnabled)
|
||||
configChanged = true
|
||||
@@ -228,16 +235,15 @@ func (h *Handler) AgentQA(c *gin.Context) {
|
||||
|
||||
// If configuration changed, clear context and update session
|
||||
if configChanged {
|
||||
logger.Infof(ctx, "Configuration changed, clearing context for session: %s", sessionID)
|
||||
if knowledgeBasesChanged {
|
||||
// Clear the LLM context to prevent contamination
|
||||
if err := h.sessionService.ClearContext(ctx, sessionID); err != nil {
|
||||
logger.Errorf(ctx, "Failed to clear context for session %s: %v", sessionID, err)
|
||||
// Continue anyway - this is not a fatal error
|
||||
}
|
||||
logger.Warnf(ctx, "Configuration changed, clearing context for session: %s", sessionID)
|
||||
// Clear the LLM context to prevent contamination
|
||||
if err := h.sessionService.ClearContext(ctx, sessionID); err != nil {
|
||||
logger.Errorf(ctx, "Failed to clear context for session %s: %v", sessionID, err)
|
||||
// Continue anyway - this is not a fatal error
|
||||
}
|
||||
if knowledgeBasesChanged {
|
||||
// todo clear temp kb
|
||||
if err := h.sessionService.DeleteWebSearchTempKBState(ctx, sessionID); err != nil {
|
||||
logger.Errorf(ctx, "Failed to delete temp knowledge base for session %s: %v", sessionID, err)
|
||||
// Continue anyway - this is not a fatal error
|
||||
}
|
||||
session.AgentConfig.KnowledgeBases = request.KnowledgeBaseIDs
|
||||
session.AgentConfig.AgentModeEnabled = request.AgentEnabled
|
||||
|
||||
@@ -283,7 +283,7 @@ func (h *TenantHandler) GetTenantAgentConfig(c *gin.Context) {
|
||||
"temperature": agent.DefaultAgentTemperature,
|
||||
"thinking_model_id": "",
|
||||
"rerank_model_id": "",
|
||||
"system_prompt": agent.DefaultReActSystemPrompt,
|
||||
"system_prompt": agent.DefaultSystemPromptTemplate,
|
||||
"available_tools": availableTools,
|
||||
"available_placeholders": availablePlaceholders,
|
||||
},
|
||||
@@ -294,7 +294,7 @@ func (h *TenantHandler) GetTenantAgentConfig(c *gin.Context) {
|
||||
// Get system prompt, use default if empty
|
||||
systemPrompt := tenant.AgentConfig.SystemPrompt
|
||||
if systemPrompt == "" {
|
||||
systemPrompt = agent.DefaultReActSystemPrompt
|
||||
systemPrompt = agent.DefaultSystemPromptTemplate
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "Retrieved tenant agent config successfully, Tenant ID: %d", tenant.ID)
|
||||
|
||||
@@ -32,6 +32,9 @@ const (
|
||||
colorBlue = "\033[34m"
|
||||
colorPurple = "\033[35m"
|
||||
colorCyan = "\033[36m"
|
||||
colorWhite = "\033[37m"
|
||||
colorGray = "\033[90m"
|
||||
colorBold = "\033[1m"
|
||||
colorReset = "\033[0m"
|
||||
)
|
||||
|
||||
@@ -74,7 +77,12 @@ func (f *CustomFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
||||
|
||||
// request_id 优先输出
|
||||
if v, ok := entry.Data["request_id"]; ok {
|
||||
fields += fmt.Sprintf("request_id=%v ", v)
|
||||
if f.ForceColor {
|
||||
fields += fmt.Sprintf("%srequest_id%s=%s%v%s ",
|
||||
colorCyan, colorReset, colorBlue, v, colorReset)
|
||||
} else {
|
||||
fields += fmt.Sprintf("request_id=%v ", v)
|
||||
}
|
||||
}
|
||||
|
||||
// 其余字段排序后输出
|
||||
@@ -86,14 +94,34 @@ func (f *CustomFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
fields += fmt.Sprintf("%s=%v ", k, entry.Data[k])
|
||||
if f.ForceColor {
|
||||
val := fmt.Sprintf("%v", entry.Data[k])
|
||||
coloredVal := fmt.Sprintf("%s%s%s", colorWhite, val, colorReset)
|
||||
if k == "error" {
|
||||
coloredVal = fmt.Sprintf("%s%s%s", colorRed, val, colorReset)
|
||||
}
|
||||
fields += fmt.Sprintf("%s%s%s=%s ",
|
||||
colorCyan, k, colorReset, coloredVal)
|
||||
} else {
|
||||
fields += fmt.Sprintf("%s=%v ", k, entry.Data[k])
|
||||
}
|
||||
}
|
||||
|
||||
fields = strings.TrimSpace(fields)
|
||||
|
||||
// 拼接最终输出内容,添加颜色
|
||||
return []byte(fmt.Sprintf("%s%-5s%s[%s] [%s] %-20s | %s\n",
|
||||
levelColor, level, resetColor, timestamp, fields, caller, entry.Message)), nil
|
||||
if f.ForceColor {
|
||||
coloredTimestamp := fmt.Sprintf("%s%s%s", colorGray, timestamp, resetColor)
|
||||
coloredCaller := caller
|
||||
if caller != "" {
|
||||
coloredCaller = fmt.Sprintf("%s%s%s", colorPurple, caller, resetColor)
|
||||
}
|
||||
return []byte(fmt.Sprintf("%s%-5s%s[%s] [%s] %-20s | %s\n",
|
||||
levelColor, level, resetColor, coloredTimestamp, fields, coloredCaller, entry.Message)), nil
|
||||
}
|
||||
|
||||
return []byte(fmt.Sprintf("%-5s[%s] [%s] %-20s | %s\n",
|
||||
level, timestamp, fields, caller, entry.Message)), nil
|
||||
}
|
||||
|
||||
// 初始化全局日志设置
|
||||
|
||||
@@ -101,6 +101,8 @@ func RegisterChunkRoutes(r *gin.RouterGroup, handler *handler.ChunkHandler) {
|
||||
{
|
||||
// 获取分块列表
|
||||
chunks.GET("/:knowledge_id", handler.ListKnowledgeChunks)
|
||||
// 通过chunk_id获取单个chunk(不需要knowledge_id)
|
||||
chunks.GET("/by-id/:id", handler.GetChunkByIDOnly)
|
||||
// 删除分块
|
||||
chunks.DELETE("/:knowledge_id/:id", handler.DeleteChunk)
|
||||
// 删除知识下的所有分块
|
||||
|
||||
@@ -125,7 +125,6 @@ func (s *AgentStep) GetObservations() []string {
|
||||
type AgentState struct {
|
||||
CurrentRound int `json:"current_round"` // Current round number
|
||||
RoundSteps []AgentStep `json:"round_steps"` // All steps taken so far in the current round
|
||||
Plan []string `json:"plan,omitempty"` // Execution plan (if planning enabled)
|
||||
IsComplete bool `json:"is_complete"` // Whether agent has finished
|
||||
FinalAnswer string `json:"final_answer"` // The final answer to the query
|
||||
KnowledgeRefs []*SearchResult `json:"knowledge_refs"` // Collected knowledge references
|
||||
|
||||
@@ -19,7 +19,7 @@ type AgentStreamEvent struct {
|
||||
|
||||
// AgentEngine defines the interface for agent execution engine
|
||||
type AgentEngine interface {
|
||||
// ExecuteStreamWithHistory executes the agent with conversation history and returns a stream of events
|
||||
// Execute executes the agent with conversation history and returns a stream of events
|
||||
Execute(ctx context.Context, sessionID, messageID, query string, llmContext []chat.Message) (*types.AgentState, error)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user