Files
ragflow/internal/harness/core/state_guard.go
Yingfeng 706e0d2d06 Refactor harness framework (#16271)
### What problem does this PR solve?

- Tools management
- Pregel engine wrapper for better usage
- UT race
- Coding style

### Type of change

- [x] Refactoring
2026-06-23 20:18:04 +08:00

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)
}