Files
2026-07-13 12:49:10 +08:00

402 lines
13 KiB
Go

/*
* Copyright 2025 CloudWeGo Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package react
import (
"context"
"github.com/cloudwego/eino/callbacks"
"github.com/cloudwego/eino/components/model"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/flow/agent"
"github.com/cloudwego/eino/internal"
"github.com/cloudwego/eino/schema"
ub "github.com/cloudwego/eino/utils/callbacks"
)
// WithToolOptions returns an agent option that specifies tool.Option for the tools in agent.
func WithToolOptions(opts ...tool.Option) agent.AgentOption {
return agent.WithComposeOptions(compose.WithToolsNodeOption(compose.WithToolOption(opts...)))
}
// WithChatModelOptions returns an agent option that specifies model.Option for the chat model in agent.
func WithChatModelOptions(opts ...model.Option) agent.AgentOption {
return agent.WithComposeOptions(compose.WithChatModelOption(opts...))
}
// WithToolList returns an agent option that specifies compose.ToolsNodeOption for ToolsNode in agent.
// If you also need to pass ToolInfo to the chat model, use WithTools instead.
// Deprecated: This changes tool list for ToolsNode ONLY.
func WithToolList(tools ...tool.BaseTool) agent.AgentOption {
return agent.WithComposeOptions(compose.WithToolsNodeOption(compose.WithToolList(tools...)))
}
// WithTools is a convenience function that configures a React agent with a list of tools.
// It performs two essential operations:
// 1. Extracts tool information for the chat model to understand available tools
// 2. Registers the actual tool implementations for execution
//
// Parameters:
// - ctx: The context for the operation, used when calling Info() on each tool
// - tools: A variadic list of tools that must implement either InvokableTool or StreamableTool interfaces
//
// Returns:
// - []agent.AgentOption: A slice containing exactly 2 agent options:
// - Option 1: Configures the chat model with tool schemas via model.WithTools(toolInfos)
// - Option 2: Registers the tool implementations via compose.WithToolList(tools...)
// - error: Returns an error if any tool's Info() method fails
//
// Usage Example:
//
// ctx := context.Background()
// agentOptions, err := WithTools(ctx, myTool1, myTool2, myTool3)
// if err != nil {
// return fmt.Errorf("failed to configure tools: %w", err)
// }
//
// agent, err := react.NewAgent(ctx, &react.AgentConfig{
// ToolCallingModel: myModel,
// // other config...
// })
// if err != nil {
// return fmt.Errorf("failed to create agent: %w", err)
// }
//
// // Use the tool options with Generate or Stream methods
// msg, err := agent.Generate(ctx, messages, agentOptions...)
// // or
// stream, err := agent.Stream(ctx, messages, agentOptions...)
//
// Comparison with Related Functions:
// - WithToolList: Only registers tool implementations, doesn't configure the chat model
// - WithTools: Comprehensive setup that handles both chat model configuration and tool registration
//
// Notes:
// - The function always returns exactly 2 options when successful
// - Both returned options should be applied to the agent for proper tool functionality
func WithTools(ctx context.Context, tools ...tool.BaseTool) ([]agent.AgentOption, error) {
toolInfos := make([]*schema.ToolInfo, 0, len(tools))
for _, tl := range tools {
info, err := tl.Info(ctx)
if err != nil {
return nil, err
}
toolInfos = append(toolInfos, info)
}
opts := make([]agent.AgentOption, 2)
opts[0] = agent.WithComposeOptions(compose.WithChatModelOption(model.WithTools(toolInfos)))
opts[1] = agent.WithComposeOptions(compose.WithToolsNodeOption(compose.WithToolList(tools...)))
return opts, nil
}
// Iterator provides a lightweight FIFO stream of values and errors
// produced during agent execution.
type Iterator[T any] struct {
ch *internal.UnboundedChan[item[T]]
}
// Next retrieves the next value from the iterator.
// It returns the zero value and false when the stream is exhausted.
func (iter *Iterator[T]) Next() (T, bool, error) {
ch := iter.ch
if ch == nil {
var zero T
return zero, false, nil
}
i, ok := ch.Receive()
if !ok {
var zero T
return zero, false, nil
}
return i.v, true, i.err
}
// MessageFuture exposes asynchronous accessors for messages produced
// by Generate and Stream calls.
type MessageFuture interface {
// GetMessages returns an iterator for retrieving messages generated during "agent.Generate" calls.
GetMessages() *Iterator[*schema.Message]
// GetMessageStreams returns an iterator for retrieving streaming messages generated during "agent.Stream" calls.
GetMessageStreams() *Iterator[*schema.StreamReader[*schema.Message]]
}
// WithMessageFuture returns an agent option and a MessageFuture interface instance.
// The option configures the agent to collect messages generated during execution,
// while the MessageFuture interface allows users to asynchronously retrieve these messages.
//
// This function works correctly both when the agent is used directly and when it is
// embedded as a subgraph within another graph. Graph callbacks are filtered using
// the address in the context: only callbacks whose address contains a runnable segment
// matching the agent's configured graph name are processed.
func WithMessageFuture() (agent.AgentOption, MessageFuture) {
h := &cbHandler{started: make(chan struct{}), graphName: GraphName}
cmHandler := &ub.ModelCallbackHandler{
OnEnd: h.onChatModelEnd,
OnEndWithStreamOutput: h.onChatModelEndWithStreamOutput,
}
createToolResultSender := func() toolResultSender {
return func(toolName, callID, result string) {
msg := schema.ToolMessage(result, callID, schema.WithToolName(toolName))
h.sendMessage(msg)
}
}
createStreamToolResultSender := func() streamToolResultSender {
return func(toolName, callID string, resultStream *schema.StreamReader[string]) {
cvt := func(in string) (*schema.Message, error) {
return schema.ToolMessage(in, callID, schema.WithToolName(toolName)), nil
}
msgStream := schema.StreamReaderWithConvert(resultStream, cvt)
h.sendMessageStream(msgStream)
}
}
createEnhancedToolResultSender := func() enhancedToolResultSender {
return func(toolName, callID string, result *schema.ToolResult) {
var err error
msg := schema.ToolMessage("", callID, schema.WithToolName(toolName))
msg.UserInputMultiContent, err = result.ToMessageInputParts()
if err != nil {
return
}
h.sendMessage(msg)
}
}
createEnhancedStreamToolResultSender := func() enhancedStreamToolResultSender {
return func(toolName, callID string, resultStream *schema.StreamReader[*schema.ToolResult]) {
cvt := func(result *schema.ToolResult) (*schema.Message, error) {
var err error
msg := schema.ToolMessage("", callID, schema.WithToolName(toolName))
msg.UserInputMultiContent, err = result.ToMessageInputParts()
if err != nil {
return nil, err
}
return msg, nil
}
msgStream := schema.StreamReaderWithConvert(resultStream, cvt)
h.sendMessageStream(msgStream)
}
}
graphHandler := callbacks.NewHandlerBuilder().
OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context {
ctx = h.onGraphStart(ctx, info, input)
return setToolResultSendersToCtx(ctx, &toolResultSenders{
sender: createToolResultSender(),
streamSender: createStreamToolResultSender(),
enhancedResultSender: createEnhancedToolResultSender(),
enhancedStreamToolResultSender: createEnhancedStreamToolResultSender(),
})
}).
OnStartWithStreamInputFn(func(ctx context.Context, info *callbacks.RunInfo, input *schema.StreamReader[callbacks.CallbackInput]) context.Context {
ctx = h.onGraphStartWithStreamInput(ctx, info, input)
return setToolResultSendersToCtx(ctx, &toolResultSenders{
sender: createToolResultSender(),
streamSender: createStreamToolResultSender(),
enhancedResultSender: createEnhancedToolResultSender(),
enhancedStreamToolResultSender: createEnhancedStreamToolResultSender(),
})
}).
OnEndFn(h.onGraphEnd).
OnEndWithStreamOutputFn(h.onGraphEndWithStreamOutput).
OnErrorFn(h.onGraphError).Build()
cb := ub.NewHandlerHelper().ChatModel(cmHandler).Graph(graphHandler).Handler()
option := agent.WithComposeOptions(compose.WithCallbacks(cb))
return option, h
}
type item[T any] struct {
v T
err error
}
type cbHandler struct {
graphName string
ownAddress compose.Address
ownClaimed bool
msgs *internal.UnboundedChan[item[*schema.Message]]
sMsgs *internal.UnboundedChan[item[*schema.StreamReader[*schema.Message]]]
started chan struct{}
}
func (h *cbHandler) GetMessages() *Iterator[*schema.Message] {
<-h.started
return &Iterator[*schema.Message]{ch: h.msgs}
}
func (h *cbHandler) GetMessageStreams() *Iterator[*schema.StreamReader[*schema.Message]] {
<-h.started
return &Iterator[*schema.StreamReader[*schema.Message]]{ch: h.sMsgs}
}
// isOwnGraph reports whether the callback is being invoked for the React agent's own graph.
//
// After the first onGraphStart call records h.ownAddress, subsequent calls compare the
// current context address against that exact recorded address. This handles nested React
// agents correctly: each cbHandler instance records its own unique full address path, so
// two React agents at different nesting depths (e.g., an outer agent whose tool invokes
// another React agent) will never interfere with each other even if they share the same
// graph name.
func (h *cbHandler) isOwnGraph(ctx context.Context) bool {
if !h.ownClaimed {
return false
}
return compose.GetCurrentAddress(ctx).Equals(h.ownAddress)
}
// claimOwnership is called exclusively from onGraphStart / onGraphStartWithStreamInput
// to record this handler's address on first invocation. It returns true if the handler
// has not yet been initialised, records the current address, and returns false on all
// subsequent calls.
func (h *cbHandler) claimOwnership(ctx context.Context) bool {
if h.ownClaimed {
return false
}
h.ownAddress = compose.GetCurrentAddress(ctx)
h.ownClaimed = true
return true
}
func (h *cbHandler) onChatModelEnd(ctx context.Context,
_ *callbacks.RunInfo, input *model.CallbackOutput) context.Context {
h.sendMessage(input.Message)
return ctx
}
func (h *cbHandler) onChatModelEndWithStreamOutput(ctx context.Context,
_ *callbacks.RunInfo, input *schema.StreamReader[*model.CallbackOutput]) context.Context {
c := func(output *model.CallbackOutput) (*schema.Message, error) {
return output.Message, nil
}
s := schema.StreamReaderWithConvert(input, c)
h.sendMessageStream(s)
return ctx
}
func (h *cbHandler) onGraphError(ctx context.Context,
_ *callbacks.RunInfo, err error) context.Context {
if !h.isOwnGraph(ctx) {
return ctx
}
if h.msgs != nil {
h.msgs.Send(item[*schema.Message]{err: err})
} else if h.sMsgs != nil {
h.sMsgs.Send(item[*schema.StreamReader[*schema.Message]]{err: err})
}
return ctx
}
func (h *cbHandler) onGraphEnd(ctx context.Context,
_ *callbacks.RunInfo, _ callbacks.CallbackOutput) context.Context {
if !h.isOwnGraph(ctx) {
return ctx
}
if h.msgs != nil {
h.msgs.Close()
}
return ctx
}
func (h *cbHandler) onGraphEndWithStreamOutput(ctx context.Context,
_ *callbacks.RunInfo, _ *schema.StreamReader[callbacks.CallbackOutput]) context.Context {
if !h.isOwnGraph(ctx) {
return ctx
}
if h.sMsgs != nil {
h.sMsgs.Close()
}
return ctx
}
func (h *cbHandler) onGraphStart(ctx context.Context,
_ *callbacks.RunInfo, _ callbacks.CallbackInput) context.Context {
if !h.claimOwnership(ctx) {
return ctx
}
h.msgs = internal.NewUnboundedChan[item[*schema.Message]]()
close(h.started)
return ctx
}
func (h *cbHandler) onGraphStartWithStreamInput(ctx context.Context, _ *callbacks.RunInfo,
input *schema.StreamReader[callbacks.CallbackInput]) context.Context {
input.Close()
if !h.claimOwnership(ctx) {
return ctx
}
h.sMsgs = internal.NewUnboundedChan[item[*schema.StreamReader[*schema.Message]]]()
close(h.started)
return ctx
}
func (h *cbHandler) sendMessage(msg *schema.Message) {
if h.msgs != nil {
h.msgs.Send(item[*schema.Message]{v: msg})
} else {
sMsg := schema.StreamReaderFromArray([]*schema.Message{msg})
h.sMsgs.Send(item[*schema.StreamReader[*schema.Message]]{v: sMsg})
}
}
func (h *cbHandler) sendMessageStream(sMsg *schema.StreamReader[*schema.Message]) {
if h.sMsgs != nil {
h.sMsgs.Send(item[*schema.StreamReader[*schema.Message]]{v: sMsg})
} else {
// concat
msg, err := schema.ConcatMessageStream(sMsg)
if err != nil {
h.msgs.Send(item[*schema.Message]{err: err})
} else {
h.msgs.Send(item[*schema.Message]{v: msg})
}
}
}