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