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,130 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/event"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
)
|
||||
|
||||
// quickAnswerTimelineTools are the stages a fast-answer turn draws on its
|
||||
// timeline. A turn that retrieved nothing still reports the search it ran, so
|
||||
// the set is what decides whether the step survives a reload — not whether it
|
||||
// produced citations. Keep in sync with RAG_TIMELINE_TOOL_NAMES on the frontend.
|
||||
var quickAnswerTimelineTools = map[string]struct{}{
|
||||
"query_understand": {},
|
||||
"knowledge_search": {},
|
||||
"attachment_parsing": {},
|
||||
"image_analysis": {},
|
||||
}
|
||||
|
||||
// quickAnswerTimelineRecorder persists the fast-answer pipeline's timeline into
|
||||
// the assistant message's AgentSteps.
|
||||
//
|
||||
// Reconstructing the timeline from knowledge_references only works for turns
|
||||
// that cited something: a turn that searched and found nothing, or that only
|
||||
// parsed an attachment, came back from history with no steps at all and lost
|
||||
// the timeline it had while streaming.
|
||||
type quickAnswerTimelineRecorder struct {
|
||||
mu sync.Mutex
|
||||
msg *types.Message
|
||||
pending map[string]map[string]any
|
||||
startedAt map[string]time.Time
|
||||
}
|
||||
|
||||
// registerQuickAnswerTimelineRecorder subscribes to the pipeline's tool events
|
||||
// for the lifetime of one fast-answer turn. Agent mode is excluded on purpose:
|
||||
// its own steps are written by the agent stream handler, which overwrites
|
||||
// AgentSteps wholesale.
|
||||
func registerQuickAnswerTimelineRecorder(bus *event.EventBus, msg *types.Message) {
|
||||
if bus == nil || msg == nil {
|
||||
return
|
||||
}
|
||||
rec := &quickAnswerTimelineRecorder{
|
||||
msg: msg,
|
||||
pending: make(map[string]map[string]any),
|
||||
startedAt: make(map[string]time.Time),
|
||||
}
|
||||
bus.On(event.EventAgentToolCall, rec.onToolCall)
|
||||
bus.On(event.EventAgentToolResult, rec.onToolResult)
|
||||
}
|
||||
|
||||
func (r *quickAnswerTimelineRecorder) onToolCall(_ context.Context, evt event.Event) error {
|
||||
data, ok := evt.Data.(event.AgentToolCallData)
|
||||
if !ok || !isQuickAnswerTimelineTool(data.ToolName) || data.ToolCallID == "" {
|
||||
return nil
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.pending[data.ToolCallID] = data.Arguments
|
||||
r.startedAt[data.ToolCallID] = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
// onToolResult is where the step is persisted. Waiting for the result keeps a
|
||||
// half-finished stage out of history: a stage still running when the turn is
|
||||
// stopped would come back from a reload looking like it had completed.
|
||||
func (r *quickAnswerTimelineRecorder) onToolResult(_ context.Context, evt event.Event) error {
|
||||
data, ok := evt.Data.(event.AgentToolResultData)
|
||||
if !ok || !isQuickAnswerTimelineTool(data.ToolName) || data.ToolCallID == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
args := r.pending[data.ToolCallID]
|
||||
delete(r.pending, data.ToolCallID)
|
||||
started, hasStart := r.startedAt[data.ToolCallID]
|
||||
delete(r.startedAt, data.ToolCallID)
|
||||
|
||||
duration := data.Duration
|
||||
if duration == 0 && hasStart {
|
||||
duration = time.Since(started).Milliseconds()
|
||||
}
|
||||
|
||||
appendQuickAnswerToolCall(r.msg, types.ToolCall{
|
||||
ID: types.PipelineToolCallIDPrefix + data.ToolCallID,
|
||||
Name: data.ToolName,
|
||||
Args: args,
|
||||
Result: &types.ToolResult{
|
||||
Success: data.Success,
|
||||
Output: data.Output,
|
||||
Error: data.Error,
|
||||
Data: data.Data,
|
||||
},
|
||||
Duration: duration,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func isQuickAnswerTimelineTool(name string) bool {
|
||||
_, ok := quickAnswerTimelineTools[name]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ensureQuickAnswerStep returns the single step a fast-answer turn records
|
||||
// into. There is no ReAct loop here, so everything belongs to iteration 0.
|
||||
func ensureQuickAnswerStep(msg *types.Message) *types.AgentStep {
|
||||
if len(msg.AgentSteps) == 0 {
|
||||
msg.AgentSteps = types.AgentSteps{{
|
||||
Iteration: 0,
|
||||
Timestamp: time.Now(),
|
||||
ToolCalls: make([]types.ToolCall, 0),
|
||||
}}
|
||||
}
|
||||
return &msg.AgentSteps[0]
|
||||
}
|
||||
|
||||
func appendQuickAnswerToolCall(msg *types.Message, call types.ToolCall) {
|
||||
step := ensureQuickAnswerStep(msg)
|
||||
for i := range step.ToolCalls {
|
||||
if step.ToolCalls[i].ID == call.ID {
|
||||
step.ToolCalls[i] = call
|
||||
return
|
||||
}
|
||||
}
|
||||
step.ToolCalls = append(step.ToolCalls, call)
|
||||
}
|
||||
Reference in New Issue
Block a user