WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
package chatpipeline
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/event"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// PluginChatCompletionStream implements streaming chat completion functionality
|
||||
// as a plugin that can be registered to EventManager
|
||||
type PluginChatCompletionStream struct {
|
||||
modelService interfaces.ModelService // Interface for model operations
|
||||
}
|
||||
|
||||
// NewPluginChatCompletionStream creates a new PluginChatCompletionStream instance
|
||||
// and registers it with the EventManager
|
||||
func NewPluginChatCompletionStream(eventManager *EventManager,
|
||||
modelService interfaces.ModelService,
|
||||
) *PluginChatCompletionStream {
|
||||
res := &PluginChatCompletionStream{
|
||||
modelService: modelService,
|
||||
}
|
||||
eventManager.Register(res)
|
||||
return res
|
||||
}
|
||||
|
||||
// ActivationEvents returns the event types this plugin handles
|
||||
func (p *PluginChatCompletionStream) ActivationEvents() []types.EventType {
|
||||
return []types.EventType{types.CHAT_COMPLETION_STREAM}
|
||||
}
|
||||
|
||||
// OnEvent handles streaming chat completion events
|
||||
// It prepares the chat model, messages, and initiates streaming response
|
||||
func (p *PluginChatCompletionStream) OnEvent(ctx context.Context,
|
||||
eventType types.EventType, chatManage *types.ChatManage, next func() *PluginError,
|
||||
) *PluginError {
|
||||
pipelineInfo(ctx, "Stream", "input", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
"user_question": chatManage.UserContent,
|
||||
"history_rounds": len(chatManage.History),
|
||||
"chat_model": chatManage.ChatModelID,
|
||||
})
|
||||
|
||||
// Prepare chat model and options
|
||||
chatModel, opt, err := prepareChatModel(ctx, p.modelService, chatManage)
|
||||
if err != nil {
|
||||
return ErrGetChatModel.WithError(err)
|
||||
}
|
||||
|
||||
// Prepare base messages without history
|
||||
|
||||
chatMessages, modelContext := prepareMessagesWithModelContext(ctx, chatManage)
|
||||
chatMessages = modelContext.EncodeMessages(chatMessages)
|
||||
ctx = withPromptCacheMetadata(ctx, chatModel, chatMessages, opt, "knowledge_qa")
|
||||
pipelineInfo(ctx, "Stream", "messages_ready", map[string]interface{}{
|
||||
"message_count": len(chatMessages),
|
||||
"system_prompt": chatMessages[0].Content,
|
||||
})
|
||||
pipelineInfo(ctx, "Stream", "user_message", map[string]interface{}{
|
||||
"content": chatMessages[len(chatMessages)-1].Content,
|
||||
})
|
||||
// EventBus is required for event-driven streaming
|
||||
if chatManage.EventBus == nil {
|
||||
pipelineError(ctx, "Stream", "eventbus_missing", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
})
|
||||
return ErrModelCall.WithError(errors.New("EventBus is required for streaming"))
|
||||
}
|
||||
eventBus := chatManage.EventBus
|
||||
|
||||
pipelineInfo(ctx, "Stream", "eventbus_ready", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
})
|
||||
|
||||
// Initiate streaming chat model call with independent context
|
||||
pipelineInfo(ctx, "Stream", "model_call", map[string]interface{}{
|
||||
"chat_model": chatManage.ChatModelID,
|
||||
})
|
||||
responseChan, err := chatModel.ChatStream(ctx, chatMessages, opt)
|
||||
if err != nil {
|
||||
pipelineError(ctx, "Stream", "model_call", map[string]interface{}{
|
||||
"chat_model": chatManage.ChatModelID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return ErrModelCall.WithError(err)
|
||||
}
|
||||
if responseChan == nil {
|
||||
pipelineError(ctx, "Stream", "model_call", map[string]interface{}{
|
||||
"chat_model": chatManage.ChatModelID,
|
||||
"error": "nil_channel",
|
||||
})
|
||||
return ErrModelCall.WithError(errors.New("chat stream returned nil channel"))
|
||||
}
|
||||
|
||||
pipelineInfo(ctx, "Stream", "model_started", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
})
|
||||
|
||||
// Start goroutine to consume channel and emit events directly.
|
||||
// reasoning_content is routed to EventAgentThought (SSE response_type=thinking)
|
||||
// and plain answer text to EventAgentFinalAnswer, matching the Agent pipeline.
|
||||
// The goroutine monitors ctx.Done() to avoid leaking when the context is cancelled
|
||||
// and the upstream channel is not closed promptly.
|
||||
go func() {
|
||||
answerDecoder := modelContext.StreamDecoder()
|
||||
thinkingDecoder := modelContext.StreamDecoder()
|
||||
thinkingID := fmt.Sprintf("%s-thinking", uuid.New().String()[:8])
|
||||
answerID := fmt.Sprintf("%s-answer", uuid.New().String()[:8])
|
||||
thinkingOpen := false
|
||||
answerCompleted := false
|
||||
|
||||
closeThinking := func() {
|
||||
if !thinkingOpen {
|
||||
return
|
||||
}
|
||||
eventBus.Emit(ctx, types.Event{
|
||||
ID: thinkingID,
|
||||
Type: types.EventType(event.EventAgentThought),
|
||||
SessionID: chatManage.SessionID,
|
||||
Data: event.AgentThoughtData{
|
||||
Done: true,
|
||||
},
|
||||
})
|
||||
thinkingOpen = false
|
||||
}
|
||||
|
||||
// flushDecoders drains any handle suffix the stream decoders held back to
|
||||
// bridge references split across provider chunks. Both the normal close
|
||||
// and the cancellation path must call this, otherwise a resource
|
||||
// reference in flight at teardown is silently dropped (and never
|
||||
// persisted, since the assistant message is saved from these events).
|
||||
flushDecoders := func() {
|
||||
thinkingTail := thinkingDecoder.Flush()
|
||||
if thinkingTail != "" {
|
||||
_ = eventBus.Emit(ctx, types.Event{
|
||||
ID: thinkingID,
|
||||
Type: types.EventType(event.EventAgentThought),
|
||||
SessionID: chatManage.SessionID,
|
||||
Data: event.AgentThoughtData{Content: thinkingTail},
|
||||
})
|
||||
}
|
||||
answerTail := answerDecoder.Flush()
|
||||
if answerTail != "" {
|
||||
_ = eventBus.Emit(ctx, types.Event{
|
||||
ID: answerID,
|
||||
Type: types.EventType(event.EventAgentFinalAnswer),
|
||||
SessionID: chatManage.SessionID,
|
||||
Data: event.AgentFinalAnswerData{Content: answerTail},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
flushDecoders()
|
||||
closeThinking()
|
||||
pipelineInfo(ctx, "Stream", "context_cancelled", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
})
|
||||
return
|
||||
|
||||
case response, ok := <-responseChan:
|
||||
if !ok {
|
||||
flushDecoders()
|
||||
closeThinking()
|
||||
pipelineInfo(ctx, "Stream", "channel_close", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if response.ResponseType == types.ResponseTypeError {
|
||||
pipelineError(ctx, "Stream", "stream_error", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
"error": response.Content,
|
||||
})
|
||||
eventBus.Emit(ctx, types.Event{
|
||||
ID: fmt.Sprintf("%s-error", uuid.New().String()[:8]),
|
||||
Type: types.EventType(event.EventError),
|
||||
SessionID: chatManage.SessionID,
|
||||
Data: event.ErrorData{
|
||||
Error: response.Content,
|
||||
Stage: "chat_completion_stream",
|
||||
SessionID: chatManage.SessionID,
|
||||
},
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if response.ResponseType == types.ResponseTypeThinking {
|
||||
response.Content = thinkingDecoder.Feed(response.Content)
|
||||
if response.Done {
|
||||
response.Content += thinkingDecoder.Flush()
|
||||
}
|
||||
if response.Content != "" {
|
||||
thinkingOpen = true
|
||||
eventBus.Emit(ctx, types.Event{
|
||||
ID: thinkingID,
|
||||
Type: types.EventType(event.EventAgentThought),
|
||||
SessionID: chatManage.SessionID,
|
||||
Data: event.AgentThoughtData{
|
||||
Content: response.Content,
|
||||
Done: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
if response.Done {
|
||||
closeThinking()
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if response.ResponseType == types.ResponseTypeAnswer {
|
||||
// Providers can emit a completion once for finish_reason and again
|
||||
// for their EOF sentinel. A final answer is a terminal event for a
|
||||
// single stream, so forwarding a later duplicate would put an answer
|
||||
// after the session's complete event.
|
||||
if answerCompleted {
|
||||
continue
|
||||
}
|
||||
response.Content = answerDecoder.Feed(response.Content)
|
||||
if response.Done {
|
||||
response.Content += answerDecoder.Flush()
|
||||
answerCompleted = true
|
||||
}
|
||||
closeThinking()
|
||||
eventBus.Emit(ctx, types.Event{
|
||||
ID: answerID,
|
||||
Type: types.EventType(event.EventAgentFinalAnswer),
|
||||
SessionID: chatManage.SessionID,
|
||||
Data: event.AgentFinalAnswerData{
|
||||
Content: response.Content,
|
||||
Done: response.Done,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return next()
|
||||
}
|
||||
Reference in New Issue
Block a user