mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-09-01 14:53:07 +08:00
ddf6b4e748
- Removed outdated DEVELOPMENT.md, consolidating development instructions into relevant documentation. - Updated conversation configuration to include new parameters such as max rounds, embedding thresholds, and rewrite prompts. - Enhanced UI components to support new conversation settings, including AgentSettings.vue and related views. - Improved localization in English, Russian, and Chinese for new conversation configuration fields and UI elements. - Implemented logic to apply tenant-specific conversation settings, ensuring defaults are respected.
452 lines
13 KiB
Go
452 lines
13 KiB
Go
package session
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/Tencent/WeKnora/internal/event"
|
|
"github.com/Tencent/WeKnora/internal/logger"
|
|
"github.com/Tencent/WeKnora/internal/types"
|
|
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
|
)
|
|
|
|
// AgentStreamHandler handles agent events for SSE streaming
|
|
// It uses a dedicated EventBus per request to avoid SessionID filtering
|
|
// Events are appended to StreamManager without accumulation
|
|
type AgentStreamHandler struct {
|
|
ctx context.Context
|
|
sessionID string
|
|
assistantMessageID string
|
|
requestID string
|
|
assistantMessage *types.Message
|
|
streamManager interfaces.StreamManager
|
|
|
|
eventBus *event.EventBus
|
|
|
|
// State tracking
|
|
knowledgeRefs []*types.SearchResult
|
|
finalAnswer string
|
|
eventStartTimes map[string]time.Time // Track start time for duration calculation
|
|
mu sync.Mutex
|
|
}
|
|
|
|
// NewAgentStreamHandler creates a new handler for agent SSE streaming
|
|
func NewAgentStreamHandler(
|
|
ctx context.Context,
|
|
sessionID, assistantMessageID, requestID string,
|
|
assistantMessage *types.Message,
|
|
streamManager interfaces.StreamManager,
|
|
eventBus *event.EventBus,
|
|
) *AgentStreamHandler {
|
|
return &AgentStreamHandler{
|
|
ctx: ctx,
|
|
sessionID: sessionID,
|
|
assistantMessageID: assistantMessageID,
|
|
requestID: requestID,
|
|
assistantMessage: assistantMessage,
|
|
streamManager: streamManager,
|
|
eventBus: eventBus,
|
|
knowledgeRefs: make([]*types.SearchResult, 0),
|
|
eventStartTimes: make(map[string]time.Time),
|
|
}
|
|
}
|
|
|
|
// Subscribe subscribes to all agent streaming events on the dedicated EventBus
|
|
// No SessionID filtering needed since we have a dedicated EventBus per request
|
|
func (h *AgentStreamHandler) Subscribe() {
|
|
// Subscribe to all agent streaming events on the dedicated EventBus
|
|
h.eventBus.On(event.EventAgentThought, h.handleThought)
|
|
h.eventBus.On(event.EventAgentToolCall, h.handleToolCall)
|
|
h.eventBus.On(event.EventAgentToolResult, h.handleToolResult)
|
|
h.eventBus.On(event.EventAgentReferences, h.handleReferences)
|
|
h.eventBus.On(event.EventAgentFinalAnswer, h.handleFinalAnswer)
|
|
h.eventBus.On(event.EventAgentReflection, h.handleReflection)
|
|
h.eventBus.On(event.EventError, h.handleError)
|
|
h.eventBus.On(event.EventSessionTitle, h.handleSessionTitle)
|
|
h.eventBus.On(event.EventAgentComplete, h.handleComplete)
|
|
}
|
|
|
|
// handleThought handles agent thought events
|
|
func (h *AgentStreamHandler) handleThought(ctx context.Context, evt event.Event) error {
|
|
data, ok := evt.Data.(event.AgentThoughtData)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
h.mu.Lock()
|
|
|
|
// Track start time on first chunk
|
|
if _, exists := h.eventStartTimes[evt.ID]; !exists {
|
|
h.eventStartTimes[evt.ID] = time.Now()
|
|
}
|
|
|
|
// Calculate duration if done
|
|
var metadata map[string]interface{}
|
|
if data.Done {
|
|
startTime := h.eventStartTimes[evt.ID]
|
|
duration := time.Since(startTime)
|
|
metadata = map[string]interface{}{
|
|
"event_id": evt.ID,
|
|
"duration_ms": duration.Milliseconds(),
|
|
"completed_at": time.Now().Unix(),
|
|
}
|
|
delete(h.eventStartTimes, evt.ID)
|
|
} else {
|
|
metadata = map[string]interface{}{
|
|
"event_id": evt.ID,
|
|
}
|
|
}
|
|
|
|
h.mu.Unlock()
|
|
|
|
// Append this chunk to stream (no accumulation - frontend will accumulate)
|
|
if err := h.streamManager.AppendEvent(h.ctx, h.sessionID, h.assistantMessageID, interfaces.StreamEvent{
|
|
ID: evt.ID,
|
|
Type: types.ResponseTypeThinking,
|
|
Content: data.Content, // Just this chunk
|
|
Done: data.Done,
|
|
Timestamp: time.Now(),
|
|
Data: metadata,
|
|
}); err != nil {
|
|
logger.GetLogger(h.ctx).Error("Append thought event to stream failed", "error", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleToolCall handles tool call events
|
|
func (h *AgentStreamHandler) handleToolCall(ctx context.Context, evt event.Event) error {
|
|
data, ok := evt.Data.(event.AgentToolCallData)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
h.mu.Lock()
|
|
// Track start time for this tool call (use tool_call_id as key)
|
|
h.eventStartTimes[data.ToolCallID] = time.Now()
|
|
h.mu.Unlock()
|
|
|
|
metadata := map[string]interface{}{
|
|
"tool_name": data.ToolName,
|
|
"arguments": data.Arguments,
|
|
"tool_call_id": data.ToolCallID,
|
|
}
|
|
|
|
// Append event to stream
|
|
if err := h.streamManager.AppendEvent(h.ctx, h.sessionID, h.assistantMessageID, interfaces.StreamEvent{
|
|
ID: evt.ID,
|
|
Type: types.ResponseTypeToolCall,
|
|
Content: fmt.Sprintf("Calling tool: %s", data.ToolName),
|
|
Done: false,
|
|
Timestamp: time.Now(),
|
|
Data: metadata,
|
|
}); err != nil {
|
|
logger.GetLogger(h.ctx).Error("Append tool call event to stream failed", "error", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleToolResult handles tool result events
|
|
func (h *AgentStreamHandler) handleToolResult(ctx context.Context, evt event.Event) error {
|
|
data, ok := evt.Data.(event.AgentToolResultData)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
h.mu.Lock()
|
|
// Calculate duration from start time if available, otherwise use provided duration
|
|
var durationMs int64
|
|
if startTime, exists := h.eventStartTimes[data.ToolCallID]; exists {
|
|
durationMs = time.Since(startTime).Milliseconds()
|
|
delete(h.eventStartTimes, data.ToolCallID)
|
|
} else if data.Duration > 0 {
|
|
// Fallback to provided duration if start time not tracked
|
|
durationMs = data.Duration
|
|
}
|
|
h.mu.Unlock()
|
|
|
|
// Send SSE response (both success and failure)
|
|
responseType := types.ResponseTypeToolResult
|
|
content := data.Output
|
|
if !data.Success {
|
|
responseType = types.ResponseTypeError
|
|
if data.Error != "" {
|
|
content = data.Error
|
|
}
|
|
}
|
|
|
|
// Build metadata including tool result data for rich frontend rendering
|
|
metadata := map[string]interface{}{
|
|
"tool_name": data.ToolName,
|
|
"success": data.Success,
|
|
"output": data.Output,
|
|
"error": data.Error,
|
|
"duration_ms": durationMs,
|
|
"tool_call_id": data.ToolCallID,
|
|
}
|
|
|
|
// Merge tool result data (contains display_type, formatted results, etc.)
|
|
if data.Data != nil {
|
|
for k, v := range data.Data {
|
|
metadata[k] = v
|
|
}
|
|
}
|
|
|
|
// Append event to stream
|
|
if err := h.streamManager.AppendEvent(h.ctx, h.sessionID, h.assistantMessageID, interfaces.StreamEvent{
|
|
ID: evt.ID,
|
|
Type: responseType,
|
|
Content: content,
|
|
Done: false,
|
|
Timestamp: time.Now(),
|
|
Data: metadata,
|
|
}); err != nil {
|
|
logger.GetLogger(h.ctx).Error("Append tool result event to stream failed", "error", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleReferences handles knowledge references events
|
|
func (h *AgentStreamHandler) handleReferences(ctx context.Context, evt event.Event) error {
|
|
data, ok := evt.Data.(event.AgentReferencesData)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
// Extract knowledge references
|
|
// Try to cast directly to []*types.SearchResult first
|
|
if searchResults, ok := data.References.([]*types.SearchResult); ok {
|
|
h.knowledgeRefs = append(h.knowledgeRefs, searchResults...)
|
|
} else if refs, ok := data.References.([]interface{}); ok {
|
|
// Fallback: convert from []interface{}
|
|
for _, ref := range refs {
|
|
if sr, ok := ref.(*types.SearchResult); ok {
|
|
h.knowledgeRefs = append(h.knowledgeRefs, sr)
|
|
} else if refMap, ok := ref.(map[string]interface{}); ok {
|
|
// Parse from map if needed
|
|
searchResult := &types.SearchResult{
|
|
ID: getString(refMap, "id"),
|
|
Content: getString(refMap, "content"),
|
|
Score: getFloat64(refMap, "score"),
|
|
KnowledgeID: getString(refMap, "knowledge_id"),
|
|
KnowledgeTitle: getString(refMap, "knowledge_title"),
|
|
ChunkIndex: int(getFloat64(refMap, "chunk_index")),
|
|
}
|
|
|
|
if meta, ok := refMap["metadata"].(map[string]interface{}); ok {
|
|
metadata := make(map[string]string)
|
|
for k, v := range meta {
|
|
if strVal, ok := v.(string); ok {
|
|
metadata[k] = strVal
|
|
}
|
|
}
|
|
searchResult.Metadata = metadata
|
|
}
|
|
|
|
h.knowledgeRefs = append(h.knowledgeRefs, searchResult)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update assistant message references
|
|
h.assistantMessage.KnowledgeReferences = h.knowledgeRefs
|
|
|
|
// Append references event to stream
|
|
if err := h.streamManager.AppendEvent(h.ctx, h.sessionID, h.assistantMessageID, interfaces.StreamEvent{
|
|
ID: evt.ID,
|
|
Type: types.ResponseTypeReferences,
|
|
Content: "",
|
|
Done: false,
|
|
Timestamp: time.Now(),
|
|
Data: map[string]interface{}{
|
|
"references": h.knowledgeRefs,
|
|
},
|
|
}); err != nil {
|
|
logger.GetLogger(h.ctx).Error("Append references event to stream failed", "error", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleFinalAnswer handles final answer events
|
|
func (h *AgentStreamHandler) handleFinalAnswer(ctx context.Context, evt event.Event) error {
|
|
data, ok := evt.Data.(event.AgentFinalAnswerData)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
h.mu.Lock()
|
|
// Track start time on first chunk
|
|
if _, exists := h.eventStartTimes[evt.ID]; !exists {
|
|
h.eventStartTimes[evt.ID] = time.Now()
|
|
}
|
|
|
|
// Accumulate final answer locally for assistant message (database)
|
|
h.finalAnswer += data.Content
|
|
|
|
// Calculate duration if done
|
|
var metadata map[string]interface{}
|
|
if data.Done {
|
|
startTime := h.eventStartTimes[evt.ID]
|
|
duration := time.Since(startTime)
|
|
metadata = map[string]interface{}{
|
|
"event_id": evt.ID,
|
|
"duration_ms": duration.Milliseconds(),
|
|
"completed_at": time.Now().Unix(),
|
|
}
|
|
delete(h.eventStartTimes, evt.ID)
|
|
} else {
|
|
metadata = map[string]interface{}{
|
|
"event_id": evt.ID,
|
|
}
|
|
}
|
|
h.mu.Unlock()
|
|
|
|
// Append this chunk to stream (frontend will accumulate by event ID)
|
|
if err := h.streamManager.AppendEvent(h.ctx, h.sessionID, h.assistantMessageID, interfaces.StreamEvent{
|
|
ID: evt.ID,
|
|
Type: types.ResponseTypeAnswer,
|
|
Content: data.Content, // Just this chunk
|
|
Done: data.Done,
|
|
Timestamp: time.Now(),
|
|
Data: metadata,
|
|
}); err != nil {
|
|
logger.GetLogger(h.ctx).Error("Append answer event to stream failed", "error", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleReflection handles agent reflection events
|
|
func (h *AgentStreamHandler) handleReflection(ctx context.Context, evt event.Event) error {
|
|
data, ok := evt.Data.(event.AgentReflectionData)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
// Append this chunk to stream (frontend will accumulate by event ID)
|
|
if err := h.streamManager.AppendEvent(h.ctx, h.sessionID, h.assistantMessageID, interfaces.StreamEvent{
|
|
ID: evt.ID,
|
|
Type: types.ResponseTypeReflection,
|
|
Content: data.Content, // Just this chunk
|
|
Done: data.Done,
|
|
Timestamp: time.Now(),
|
|
}); err != nil {
|
|
logger.GetLogger(h.ctx).Error("Append reflection event to stream failed", "error", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleError handles error events
|
|
func (h *AgentStreamHandler) handleError(ctx context.Context, evt event.Event) error {
|
|
data, ok := evt.Data.(event.ErrorData)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
// Build error metadata
|
|
metadata := map[string]interface{}{
|
|
"stage": data.Stage,
|
|
"error": data.Error,
|
|
}
|
|
|
|
// Append error event to stream
|
|
if err := h.streamManager.AppendEvent(h.ctx, h.sessionID, h.assistantMessageID, interfaces.StreamEvent{
|
|
ID: evt.ID,
|
|
Type: types.ResponseTypeError,
|
|
Content: data.Error,
|
|
Done: true,
|
|
Timestamp: time.Now(),
|
|
Data: metadata,
|
|
}); err != nil {
|
|
logger.GetLogger(h.ctx).Error("Append error event to stream failed", "error", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleSessionTitle handles session title update events
|
|
func (h *AgentStreamHandler) handleSessionTitle(ctx context.Context, evt event.Event) error {
|
|
data, ok := evt.Data.(event.SessionTitleData)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
// Append title event to stream
|
|
if err := h.streamManager.AppendEvent(h.ctx, h.sessionID, h.assistantMessageID, interfaces.StreamEvent{
|
|
ID: evt.ID,
|
|
Type: types.ResponseTypeSessionTitle,
|
|
Content: data.Title,
|
|
Done: true,
|
|
Timestamp: time.Now(),
|
|
Data: map[string]interface{}{
|
|
"session_id": data.SessionID,
|
|
"title": data.Title,
|
|
},
|
|
}); err != nil {
|
|
logger.GetLogger(h.ctx).Error("Append session title event to stream failed", "error", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleComplete handles agent complete events
|
|
func (h *AgentStreamHandler) handleComplete(ctx context.Context, evt event.Event) error {
|
|
data, ok := evt.Data.(event.AgentCompleteData)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
// Update assistant message with final data
|
|
if data.MessageID == h.assistantMessageID {
|
|
// h.assistantMessage.Content = data.FinalAnswer
|
|
h.assistantMessage.IsCompleted = true
|
|
|
|
// Update knowledge references if provided
|
|
if len(data.KnowledgeRefs) > 0 {
|
|
knowledgeRefs := make([]*types.SearchResult, 0, len(data.KnowledgeRefs))
|
|
for _, ref := range data.KnowledgeRefs {
|
|
if sr, ok := ref.(*types.SearchResult); ok {
|
|
knowledgeRefs = append(knowledgeRefs, sr)
|
|
}
|
|
}
|
|
h.assistantMessage.KnowledgeReferences = knowledgeRefs
|
|
}
|
|
|
|
// Update agent steps if provided
|
|
if data.AgentSteps != nil {
|
|
if steps, ok := data.AgentSteps.([]types.AgentStep); ok {
|
|
h.assistantMessage.AgentSteps = steps
|
|
}
|
|
}
|
|
}
|
|
|
|
// Send completion event to stream manager so SSE can detect completion
|
|
if err := h.streamManager.AppendEvent(h.ctx, h.sessionID, h.assistantMessageID, interfaces.StreamEvent{
|
|
ID: evt.ID,
|
|
Type: types.ResponseTypeComplete,
|
|
Content: "",
|
|
Done: true,
|
|
Timestamp: time.Now(),
|
|
Data: map[string]interface{}{
|
|
"total_steps": data.TotalSteps,
|
|
"total_duration_ms": data.TotalDurationMs,
|
|
},
|
|
}); err != nil {
|
|
logger.GetLogger(h.ctx).Errorf("Append complete event to stream failed: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|