mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-10-29 18:49:20 +08:00
481 lines
14 KiB
Go
481 lines
14 KiB
Go
package handler
|
|
|
|
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"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// AgentStreamHandler handles agent events for SSE streaming
|
|
// It uses a dedicated EventBus per request to avoid SessionID filtering
|
|
type AgentStreamHandler struct {
|
|
ctx context.Context
|
|
ginContext *gin.Context
|
|
sessionID string
|
|
assistantMessageID string
|
|
requestID string
|
|
assistantMessage *types.Message
|
|
streamManager interfaces.StreamManager
|
|
|
|
eventBus *event.EventBus
|
|
|
|
// State tracking
|
|
knowledgeRefs []*types.SearchResult
|
|
finalAnswer string
|
|
accumulatedContent map[string]string // Accumulate content per event ID (thought, answer, reflection)
|
|
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,
|
|
c *gin.Context,
|
|
sessionID, assistantMessageID, requestID string,
|
|
assistantMessage *types.Message,
|
|
streamManager interfaces.StreamManager,
|
|
eventBus *event.EventBus,
|
|
) *AgentStreamHandler {
|
|
return &AgentStreamHandler{
|
|
ctx: ctx,
|
|
ginContext: c,
|
|
sessionID: sessionID,
|
|
assistantMessageID: assistantMessageID,
|
|
requestID: requestID,
|
|
assistantMessage: assistantMessage,
|
|
streamManager: streamManager,
|
|
eventBus: eventBus,
|
|
knowledgeRefs: make([]*types.SearchResult, 0),
|
|
accumulatedContent: make(map[string]string),
|
|
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.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()
|
|
|
|
// Send SSE response (real-time incremental)
|
|
response := types.StreamResponse{
|
|
ID: h.requestID,
|
|
ResponseType: types.ResponseTypeThinking,
|
|
Content: data.Content,
|
|
Done: data.Done,
|
|
Data: metadata,
|
|
}
|
|
|
|
h.ginContext.SSEvent("message", response)
|
|
h.ginContext.Writer.Flush()
|
|
|
|
// Accumulate content and replace event in stream (so refresh can see progress)
|
|
h.mu.Lock()
|
|
h.accumulateAndReplaceEventWithData(evt.ID, types.ResponseTypeThinking, data.Content, data.Done, metadata)
|
|
h.mu.Unlock()
|
|
|
|
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
|
|
}
|
|
|
|
metadata := map[string]interface{}{
|
|
"tool_name": data.ToolName,
|
|
"arguments": data.Arguments,
|
|
}
|
|
|
|
// Send SSE response
|
|
response := types.StreamResponse{
|
|
ID: h.requestID,
|
|
ResponseType: types.ResponseTypeToolCall,
|
|
Content: fmt.Sprintf("Calling tool: %s", data.ToolName),
|
|
Done: false,
|
|
Data: metadata,
|
|
}
|
|
|
|
h.ginContext.SSEvent("message", response)
|
|
h.ginContext.Writer.Flush()
|
|
|
|
// Push event to stream for replay on refresh
|
|
if err := h.streamManager.PushEvent(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("Push 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
|
|
}
|
|
|
|
// 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": data.Duration,
|
|
}
|
|
|
|
// Merge tool result data (contains display_type, formatted results, etc.)
|
|
if data.Data != nil {
|
|
for k, v := range data.Data {
|
|
metadata[k] = v
|
|
}
|
|
}
|
|
|
|
response := types.StreamResponse{
|
|
ID: h.requestID,
|
|
ResponseType: responseType,
|
|
Content: content,
|
|
Done: false,
|
|
Data: metadata,
|
|
}
|
|
|
|
h.ginContext.SSEvent("message", response)
|
|
h.ginContext.Writer.Flush()
|
|
|
|
// Push event to stream for replay on refresh
|
|
if err := h.streamManager.PushEvent(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("Push 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
|
|
|
|
// Send SSE response
|
|
response := types.StreamResponse{
|
|
ID: h.requestID,
|
|
ResponseType: types.ResponseTypeReferences,
|
|
KnowledgeReferences: h.knowledgeRefs,
|
|
Done: false,
|
|
}
|
|
|
|
h.ginContext.SSEvent("message", response)
|
|
h.ginContext.Writer.Flush()
|
|
|
|
// Update stream references
|
|
if err := h.streamManager.UpdateReferences(
|
|
h.ctx, h.sessionID, h.assistantMessageID, h.knowledgeRefs,
|
|
); err != nil {
|
|
logger.GetLogger(h.ctx).Error("Update stream references failed", "error", err)
|
|
}
|
|
|
|
// Push event to stream for replay on refresh
|
|
if err := h.streamManager.PushEvent(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("Push 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()
|
|
|
|
// Accumulate final answer (this persists and won't be reset)
|
|
h.finalAnswer += data.Content
|
|
h.assistantMessage.Content = h.finalAnswer
|
|
|
|
// Send SSE response (real-time incremental)
|
|
response := types.StreamResponse{
|
|
ID: h.requestID,
|
|
ResponseType: types.ResponseTypeAnswer,
|
|
Content: data.Content,
|
|
Done: data.Done,
|
|
}
|
|
|
|
h.ginContext.SSEvent("message", response)
|
|
h.ginContext.Writer.Flush()
|
|
|
|
// Replace answer event in stream (so refresh can see progress)
|
|
h.accumulateAndReplaceEvent(evt.ID, types.ResponseTypeAnswer, data.Content, data.Done)
|
|
h.mu.Unlock()
|
|
|
|
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
|
|
}
|
|
|
|
// Send reflection as SSE (real-time incremental)
|
|
response := types.StreamResponse{
|
|
ID: h.requestID,
|
|
ResponseType: "reflection", // Special type for reflection
|
|
Content: data.Content,
|
|
Done: data.Done,
|
|
}
|
|
|
|
h.ginContext.SSEvent("message", response)
|
|
h.ginContext.Writer.Flush()
|
|
|
|
// Accumulate reflection per tool call and replace event in stream
|
|
h.mu.Lock()
|
|
h.accumulateAndReplaceEvent(evt.ID, types.ResponseTypeReflection, data.Content, data.Done)
|
|
h.mu.Unlock()
|
|
|
|
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
|
|
}
|
|
|
|
response := types.StreamResponse{
|
|
ID: h.requestID,
|
|
ResponseType: types.ResponseTypeError,
|
|
Content: data.Error,
|
|
Done: true,
|
|
}
|
|
|
|
h.ginContext.SSEvent("message", response)
|
|
h.ginContext.Writer.Flush()
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
// Complete stream
|
|
if err := h.streamManager.CompleteStream(h.ctx, h.sessionID, h.assistantMessageID); err != nil {
|
|
logger.GetLogger(h.ctx).Error("Complete stream failed", "error", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// accumulateAndReplaceEvent accumulates content per event ID and replaces the event in stream
|
|
// This is a common pattern for thought, answer, and reflection events
|
|
func (h *AgentStreamHandler) accumulateAndReplaceEvent(
|
|
evtID string,
|
|
evtType types.ResponseType,
|
|
content string,
|
|
done bool,
|
|
) {
|
|
h.accumulateAndReplaceEventWithData(evtID, evtType, content, done, nil)
|
|
}
|
|
|
|
// accumulateAndReplaceEventWithData accumulates content and replaces event with metadata
|
|
func (h *AgentStreamHandler) accumulateAndReplaceEventWithData(
|
|
evtID string,
|
|
evtType types.ResponseType,
|
|
content string,
|
|
done bool,
|
|
data map[string]interface{},
|
|
) {
|
|
h.accumulatedContent[evtID] += content
|
|
if err := h.streamManager.ReplaceEvent(h.ctx, h.sessionID, h.assistantMessageID, interfaces.StreamEvent{
|
|
ID: evtID,
|
|
Type: evtType,
|
|
Content: h.accumulatedContent[evtID],
|
|
Done: done,
|
|
Timestamp: time.Now(),
|
|
Data: data,
|
|
}); err != nil {
|
|
logger.GetLogger(h.ctx).Error(fmt.Sprintf("Replace %s event in stream failed", evtType), "error", err)
|
|
}
|
|
// Clean up when done
|
|
if done {
|
|
delete(h.accumulatedContent, evtID)
|
|
}
|
|
}
|
|
|
|
// Helper functions
|
|
func getString(m map[string]interface{}, key string) string {
|
|
if val, ok := m[key].(string); ok {
|
|
return val
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func getFloat64(m map[string]interface{}, key string) float64 {
|
|
if val, ok := m[key].(float64); ok {
|
|
return val
|
|
}
|
|
if val, ok := m[key].(int); ok {
|
|
return float64(val)
|
|
}
|
|
return 0.0
|
|
}
|