mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-09 12:54:47 +08:00
### What problem does this PR solve? - Tools management - Pregel engine wrapper for better usage - UT race - Coding style ### Type of change - [x] Refactoring
169 lines
4.5 KiB
Go
169 lines
4.5 KiB
Go
package core
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
|
|
"ragflow/internal/harness/core/schema"
|
|
)
|
|
|
|
// typedStateModelWrapper unifies message deep copy, ID injection, cancel checking,
|
|
// and event sending into a single wrapper layer for the model call.
|
|
//
|
|
// This is the central wrapper (typedStateModelWrapper) that sits between
|
|
// middlewares and the retry/failover chain, adding:
|
|
// - Message deep copy (prevent pointer-sharing in middleware chain)
|
|
// - Message ID auto-assignment
|
|
// - Cancel context checking before model call
|
|
// - Model output event emission
|
|
// - BeforeModelRewrite / AfterModelRewrite orchestration (via chatmodel.go loop)
|
|
type typedStateModelWrapper[M MessageType] struct {
|
|
inner Model[M]
|
|
cancelCtx *cancelContext
|
|
}
|
|
|
|
func newTypedStateModelWrapper[M MessageType](inner Model[M], cc *cancelContext) Model[M] {
|
|
return &typedStateModelWrapper[M]{inner: inner, cancelCtx: cc}
|
|
}
|
|
|
|
// copyMessage performs a deep copy of a Message or AgenticMessage to prevent
|
|
// pointer-sharing bugs when the same message flows through multiple wrappers.
|
|
//
|
|
// The Extra map uses JSON marshal/unmarshal for deep copy (same approach as
|
|
// checkpoint.deepCopy) so that nested maps/slices are fully independent.
|
|
// If JSON round-trip fails for a value, the original reference is kept as
|
|
// a fallback to avoid data loss.
|
|
func copyMessage[M MessageType](msg M) M {
|
|
switch v := any(msg).(type) {
|
|
case *schema.Message:
|
|
cp := &schema.Message{
|
|
Role: v.Role,
|
|
Content: v.Content,
|
|
Name: v.Name,
|
|
}
|
|
if len(v.ToolCalls) > 0 {
|
|
cp.ToolCalls = make([]schema.ToolCall, len(v.ToolCalls))
|
|
copy(cp.ToolCalls, v.ToolCalls)
|
|
}
|
|
if v.Extra != nil {
|
|
cp.Extra = make(map[string]any, len(v.Extra))
|
|
for k, val := range v.Extra {
|
|
cp.Extra[k] = deepCopyAny(val)
|
|
}
|
|
}
|
|
return any(cp).(M)
|
|
case *schema.AgenticMessage:
|
|
cp := &schema.AgenticMessage{
|
|
Role: v.Role,
|
|
Content: v.Content,
|
|
}
|
|
if len(v.ContentBlocks) > 0 {
|
|
cp.ContentBlocks = make([]schema.ContentBlock, len(v.ContentBlocks))
|
|
copy(cp.ContentBlocks, v.ContentBlocks)
|
|
}
|
|
return any(cp).(M)
|
|
}
|
|
return msg
|
|
}
|
|
|
|
// deepCopyAny performs a deep copy of an arbitrary value via JSON round-trip.
|
|
// Falls back to the original value if JSON marshal/unmarshal fails.
|
|
func deepCopyAny(v any) any {
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
data, err := json.Marshal(v)
|
|
if err != nil {
|
|
return v // fallback: keep original reference
|
|
}
|
|
var result any
|
|
if err := json.Unmarshal(data, &result); err != nil {
|
|
return v // fallback: keep original reference
|
|
}
|
|
return result
|
|
}
|
|
|
|
// preprocessInput performs cancel check, deep copy, and message ID injection.
|
|
// Returns nil if cancelled (caller should return ErrStreamCanceled immediately).
|
|
func (w *typedStateModelWrapper[M]) preprocessInput(msgs []M) []M {
|
|
if w.cancelCtx != nil && w.cancelCtx.isImmediate() {
|
|
return nil
|
|
}
|
|
copied := make([]M, len(msgs))
|
|
for i, m := range msgs {
|
|
copied[i] = copyMessage(m)
|
|
}
|
|
for _, m := range copied {
|
|
switch v := any(m).(type) {
|
|
case *schema.Message:
|
|
if v.Extra == nil {
|
|
v.Extra = make(map[string]any)
|
|
}
|
|
v.Extra = EnsureMessageID(v.Extra)
|
|
}
|
|
}
|
|
return copied
|
|
}
|
|
|
|
func (w *typedStateModelWrapper[M]) Generate(ctx context.Context, msgs []M, opts ...ModelOption) (M, error) {
|
|
copied := w.preprocessInput(msgs)
|
|
if copied == nil {
|
|
var zero M
|
|
return zero, ErrStreamCanceled
|
|
}
|
|
resp, err := w.inner.Generate(ctx, copied, opts...)
|
|
if err != nil {
|
|
return resp, err
|
|
}
|
|
return copyMessage(resp), nil
|
|
}
|
|
|
|
func (w *typedStateModelWrapper[M]) Stream(ctx context.Context, msgs []M, opts ...ModelOption) (*schema.StreamReader[M], error) {
|
|
// Cancel check before allocating any resources (returns error-embedded StreamReader)
|
|
if w.cancelCtx != nil && w.cancelCtx.isImmediate() {
|
|
r := schema.NewStreamReader[M]()
|
|
var zero M
|
|
r.Send(zero, ErrStreamCanceled)
|
|
r.Close()
|
|
return r, nil
|
|
}
|
|
|
|
copied := w.preprocessInput(msgs)
|
|
if copied == nil {
|
|
return nil, ErrStreamCanceled
|
|
}
|
|
|
|
s, err := w.inner.Stream(ctx, copied, opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
r := schema.NewStreamReader[M]()
|
|
go func() {
|
|
defer r.Close()
|
|
defer s.Close()
|
|
for {
|
|
if w.cancelCtx != nil && w.cancelCtx.isImmediate() {
|
|
var zero M
|
|
r.Send(zero, ErrStreamCanceled)
|
|
return
|
|
}
|
|
c, e := s.Recv()
|
|
if e == io.EOF {
|
|
break
|
|
}
|
|
if e != nil {
|
|
r.Send(c, e)
|
|
return
|
|
}
|
|
r.Send(copyMessage(c), nil)
|
|
}
|
|
}()
|
|
return r, nil
|
|
}
|
|
|
|
func (w *typedStateModelWrapper[M]) BindTools(tools []*schema.ToolInfo) error {
|
|
return w.inner.BindTools(tools)
|
|
}
|