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")), KnowledgeBaseID: getString(refMap, "knowledge_base_id"), } 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": types.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 if data.IsFallback { h.assistantMessage.IsFallback = true } // 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, } } if data.IsFallback { metadata["is_fallback"] = true } 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 } // Use background context for title event since it may arrive after stream completion bgCtx := context.Background() // Append title event to stream if err := h.streamManager.AppendEvent(bgCtx, 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).Warn("Append session title event to stream failed (stream may have ended)", "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 }