feat(react): dynamic option during execution

Change-Id: I7c67d6b6244c3f37912faf35d64fa4c73bc04358
drew/english
shentong.martin 5 months ago committed by shentongmartin
parent 82cc9a471d
commit 9835e6abd6

@ -5,7 +5,7 @@ go 1.24.9
replace github.com/cloudwego/eino-examples => ../../.. replace github.com/cloudwego/eino-examples => ../../..
require ( require (
github.com/cloudwego/eino v0.7.8 github.com/cloudwego/eino v0.7.11
github.com/cloudwego/eino-examples v0.0.0-00010101000000-000000000000 github.com/cloudwego/eino-examples v0.0.0-00010101000000-000000000000
github.com/cloudwego/hertz v0.10.3 github.com/cloudwego/hertz v0.10.3
github.com/hertz-contrib/sse v0.1.0 github.com/hertz-contrib/sse v0.1.0

@ -106,6 +106,7 @@ github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cloudwego/eino v0.7.8 h1:3a2j1UKZZuQ3SzqDToOI5g6lrlJ7xZEtMlNQkTgIvaI= github.com/cloudwego/eino v0.7.8 h1:3a2j1UKZZuQ3SzqDToOI5g6lrlJ7xZEtMlNQkTgIvaI=
github.com/cloudwego/eino v0.7.8/go.mod h1:nA8Vacmuqv3pqKBQbTWENBLQ8MmGmPt/WqiyLeB8ohQ= github.com/cloudwego/eino v0.7.8/go.mod h1:nA8Vacmuqv3pqKBQbTWENBLQ8MmGmPt/WqiyLeB8ohQ=
github.com/cloudwego/eino v0.7.11/go.mod h1:nA8Vacmuqv3pqKBQbTWENBLQ8MmGmPt/WqiyLeB8ohQ=
github.com/cloudwego/eino-ext/components/model/ark v0.1.45 h1:LWvSHJVlvS1S/IxN9XUKNw/MI0I7YPePt3LMNxyCrZ0= github.com/cloudwego/eino-ext/components/model/ark v0.1.45 h1:LWvSHJVlvS1S/IxN9XUKNw/MI0I7YPePt3LMNxyCrZ0=
github.com/cloudwego/eino-ext/components/model/ark v0.1.45/go.mod h1:e8P5dGVI/JMQ1FYNgmu5EFRWA8fivBc6NwNJ9g8FBK8= github.com/cloudwego/eino-ext/components/model/ark v0.1.45/go.mod h1:e8P5dGVI/JMQ1FYNgmu5EFRWA8fivBc6NwNJ9g8FBK8=
github.com/cloudwego/eino-ext/components/model/openai v0.1.5 h1:+yvGbTPw93li9GSmdm6Rix88Yy8AXg5NNBcRbWx3CQU= github.com/cloudwego/eino-ext/components/model/openai v0.1.5 h1:+yvGbTPw93li9GSmdm6Rix88Yy8AXg5NNBcRbWx3CQU=

@ -0,0 +1,188 @@
# Dynamic Option Modification for ReAct Agent
This example demonstrates how to dynamically modify `model.Option` during ReAct agent execution. The key challenge is that options passed to `agent.Generate()` or `agent.Stream()` are fixed at call time, but we may want to change options based on the current iteration, previous tool calls, or other runtime conditions.
## Problem
When calling a ReAct agent, the option list is passed once and applied to all ChatModel calls during the loop:
```go
agent.Invoke(ctx, messages, opts...) // opts are fixed for all iterations
```
However, you may want to:
- Enable/disable extended thinking based on iteration
- Change `tool_choice` to force a final answer after N iterations
- Modify tool bindings dynamically
## Solution
We solve this by:
1. **Wrapping the ChatModel** with a `dynamic.ChatModel` that intercepts `Generate()`/`Stream()` calls
2. **Using Graph State** via `compose.ProcessState` to persist iteration count across the ReAct loop
3. **Wrapping the ReAct Agent** in a parent Graph that provides the state
4. **Using MessageFuture** to observe all intermediate results (reasoning, tool calls, tool results)
### Architecture
```
┌─────────────────────────────────────────────────────────┐
│ Parent Graph (with dynamic.State) │
│ ┌───────────────────────────────────────────────────┐ │
│ │ ReAct Agent (as sub-graph node) │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ dynamic.ChatModel │ │ │
│ │ │ ├─ Reads iteration from state │ │ │
│ │ │ ├─ Calls GetOptionFunc(ctx, input, state) │ │ │
│ │ │ ├─ Increments iteration in state │ │ │
│ │ │ └─ Calls inner ChatModel with merged opts │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
## Components
### State (`dynamic/state.go`)
Holds the iteration counter and optional data for decision making:
```go
type State struct {
Iteration int
LastToolCalls []*schema.ToolCall
CustomData map[string]any
}
type OptionFunc func(ctx context.Context, input []*schema.Message, state *State) []model.Option
```
### ChatModel (`dynamic/model.go`)
Wraps any `model.BaseChatModel` and uses `compose.ProcessState` to access the graph state:
```go
type ChatModel struct {
Model model.BaseChatModel
GetOptionFunc OptionFunc
}
```
## Usage
```go
// Wrap the ChatModel with dynamic option support
dynamicModel := &dynamic.ChatModel{
Model: arkChatModel,
GetOptionFunc: getDynamicOptions,
}
// Create ReAct agent with the dynamic model
rAgent, _ := react.NewAgent(ctx, &react.AgentConfig{
ToolCallingModel: dynamicModel,
ToolsConfig: toolsConfig,
})
// Create parent graph with local state
parentGraph := compose.NewGraph[[]*schema.Message, *schema.Message](
compose.WithGenLocalState(func(ctx context.Context) *dynamic.State {
return dynamic.NewState()
}),
)
// Export and add ReAct agent as sub-graph
agentGraph, agentOpts := rAgent.ExportGraph()
_ = parentGraph.AddGraphNode("react_agent", agentGraph, agentOpts...)
_ = parentGraph.AddEdge(compose.START, "react_agent")
_ = parentGraph.AddEdge("react_agent", compose.END)
runnable, _ := parentGraph.Compile(ctx)
// Use MessageFuture to observe intermediate results
msgFutureOpt, msgFuture := react.WithMessageFuture()
go func() {
// Process intermediate messages in a goroutine
iter := msgFuture.GetMessageStreams()
for {
sr, ok, _ := iter.Next()
if !ok {
break
}
// Read and print messages...
}
}()
// Use Invoke with DesignateNode to pass options to the sub-graph
runnable.Invoke(ctx, messages, agent.GetComposeOptions(msgFutureOpt)[0].DesignateNode("react_agent"))
```
## Example: Dynamic Option Function
```go
func getDynamicOptions(ctx context.Context, input []*schema.Message, state *dynamic.State) []model.Option {
var opts []model.Option
// Control thinking mode based on iteration
if state.Iteration >= 1 {
opts = append(opts, ark.WithThinking(&arkModel.Thinking{
Type: arkModel.ThinkingTypeDisabled,
}))
}
// Force final answer after first iteration
if state.Iteration >= 1 {
opts = append(opts, model.WithToolChoice(schema.ToolChoiceForbidden))
opts = append(opts, model.WithTools([]*schema.ToolInfo{}))
} else {
opts = append(opts, model.WithToolChoice(schema.ToolChoiceAllowed))
// Bind tools for first iteration
opts = append(opts, model.WithTools(toolInfos))
}
return opts
}
```
## Observing Intermediate Results with MessageFuture
The `react.WithMessageFuture()` function returns an option and a `MessageFuture` interface that allows you to observe all intermediate messages during agent execution:
- **Reasoning Content**: The model's thinking process (`msg.ReasoningContent`)
- **Tool Calls**: Function calls made by the model (`msg.ToolCalls`)
- **Tool Results**: Results returned from tool execution (`msg.Role == schema.Tool`)
- **Assistant Messages**: Text responses from the model
**Note**: When using `Invoke` instead of `Stream`, you must use `DesignateNode` to pass the MessageFuture option to the correct sub-graph node:
```go
runnable.Invoke(ctx, messages, agent.GetComposeOptions(msgFutureOpt)[0].DesignateNode("react_agent"))
```
## Quick Start
Environment variables:
- `ARK_API_KEY`
- `ARK_MODEL_NAME`
Build and run:
```bash
cd flow/agent/react/dynamic_option_example
go build -o dynamic_option_example .
./dynamic_option_example
```
## Key Design Decisions
1. **Graph State over Context**: Context values don't propagate back from `Generate()`, so we use `compose.ProcessState` to persist state across iterations.
2. **Wrapper Pattern**: Following the decorator pattern used elsewhere in the codebase.
3. **Simple Function Type**: Using `OptionFunc` instead of an interface keeps the API simple and easy to understand.
4. **Parent Graph**: The ReAct agent is wrapped as a sub-graph node in a parent graph that provides the state.
5. **MessageFuture for Observability**: Using `react.WithMessageFuture()` to capture and display all intermediate results including reasoning, tool calls, and tool results.
6. **Invoke with DesignateNode**: When using `Invoke` instead of `Stream`, use `DesignateNode` to ensure options are passed to the correct sub-graph node.

@ -0,0 +1,146 @@
/*
* 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 dynamic
import (
"context"
"fmt"
"time"
"github.com/cloudwego/eino/components"
"github.com/cloudwego/eino/components/model"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/schema"
)
// ChatModel wraps a BaseChatModel and enables dynamic option modification.
// Before each Generate() or Stream() call, it:
// 1. Reads the current iteration state from the parent graph via compose.ProcessState
// 2. Calls GetOptionFunc to get dynamic options based on the current state
// 3. Increments the iteration counter
// 4. Merges dynamic options with any static options and calls the inner model
type ChatModel struct {
// Model is the underlying ChatModel to wrap
Model model.BaseChatModel
// GetOptionFunc is called before each Generate()/Stream() call to get dynamic options
GetOptionFunc OptionFunc
}
// Generate implements model.BaseChatModel.
// It reads state, calls GetOptionFunc, increments iteration, and delegates to the inner model.
func (d *ChatModel) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) {
var dynamicOpts []model.Option
// Access the parent graph's state via compose.ProcessState.
// This is the key mechanism that allows state to persist across ReAct loop iterations.
// We are accessing parent graph's state here. Require eino version v0.7.11+
err := compose.ProcessState[*State](ctx, func(_ context.Context, state *State) error {
// Small delay to ensure log ordering (optional, for demo purposes)
time.Sleep(100 * time.Millisecond)
fmt.Printf("\n==================== Iteration %d ====================\n", state.Iteration)
// Get dynamic options based on current state
dynamicOpts = d.GetOptionFunc(ctx, input, state)
// Increment iteration for next call
state.Iteration++
return nil
})
if err != nil {
// If state access fails (e.g., not running in a graph), use no dynamic options
dynamicOpts = nil
}
// Merge dynamic options with static options (dynamic options take precedence)
mergedOpts := append(dynamicOpts, opts...)
resp, err := d.Model.Generate(ctx, input, mergedOpts...)
// Store tool calls in state for potential use in next iteration's decision
if err == nil && resp != nil && len(resp.ToolCalls) > 0 {
_ = compose.ProcessState[*State](ctx, func(_ context.Context, state *State) error {
toolCalls := make([]*schema.ToolCall, len(resp.ToolCalls))
for i := range resp.ToolCalls {
toolCalls[i] = &resp.ToolCalls[i]
}
state.LastToolCalls = toolCalls
return nil
})
}
return resp, err
}
// Stream implements model.BaseChatModel.
// Same logic as Generate but returns a stream reader.
func (d *ChatModel) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) {
var dynamicOpts []model.Option
// We are accessing parent graph's state here. Require eino version v0.7.11+
err := compose.ProcessState[*State](ctx, func(_ context.Context, state *State) error {
time.Sleep(100 * time.Millisecond)
fmt.Printf("\n==================== Iteration %d ====================\n", state.Iteration)
dynamicOpts = d.GetOptionFunc(ctx, input, state)
state.Iteration++
return nil
})
if err != nil {
dynamicOpts = nil
}
mergedOpts := append(dynamicOpts, opts...)
return d.Model.Stream(ctx, input, mergedOpts...)
}
// WithTools implements model.ToolCallingChatModel.
// It creates a new ChatModel wrapping the result of the inner model's WithTools.
func (d *ChatModel) WithTools(tools []*schema.ToolInfo) (model.ToolCallingChatModel, error) {
tcm, ok := d.Model.(model.ToolCallingChatModel)
if !ok {
return nil, nil
}
newModel, err := tcm.WithTools(tools)
if err != nil {
return nil, err
}
return &ChatModel{
Model: newModel,
GetOptionFunc: d.GetOptionFunc,
}, nil
}
// IsCallbacksEnabled implements components.Checker.
// Delegates to the inner model if it implements Checker.
func (d *ChatModel) IsCallbacksEnabled() bool {
checker, ok := d.Model.(components.Checker)
if ok {
return checker.IsCallbacksEnabled()
}
return false
}
// GetType returns the type name for this component.
func (d *ChatModel) GetType() string {
return "DynamicChatModel"
}
// Compile-time interface checks
var (
_ model.BaseChatModel = (*ChatModel)(nil)
_ model.ToolCallingChatModel = (*ChatModel)(nil)
_ components.Checker = (*ChatModel)(nil)
)

@ -0,0 +1,65 @@
/*
* 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 dynamic
import (
"context"
"github.com/cloudwego/eino/components/model"
"github.com/cloudwego/eino/schema"
)
// State holds the iteration state that persists across ReAct loop iterations.
// It is stored in the parent graph's local state and accessed via compose.ProcessState.
type State struct {
// Iteration is the current iteration number (0-indexed).
// It is incremented after each ChatModel.Generate() call.
Iteration int
// LastToolCalls stores the tool calls from the previous iteration.
// This can be used to make decisions based on what tools were called.
LastToolCalls []*schema.ToolCall
// CustomData allows storing arbitrary data for custom decision logic.
CustomData map[string]any
}
func init() {
// Register the State type for serialization support in compose graphs.
schema.RegisterName[State]("DynamicOptionState")
}
// NewState creates a new State with default values.
func NewState() *State {
return &State{
Iteration: 0,
CustomData: make(map[string]any),
}
}
// OptionFunc is the function signature for dynamic option generation.
// It is called before each ChatModel.Generate() call and returns options
// to be merged with any static options passed to the agent.
//
// Parameters:
// - ctx: The context from the current request
// - input: The input messages being sent to the ChatModel
// - state: The current iteration state (can be modified)
//
// Returns:
// - A slice of model.Option to be applied to this ChatModel call
type OptionFunc func(ctx context.Context, input []*schema.Message, state *State) []model.Option

@ -0,0 +1,308 @@
/*
* 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 main
import (
"context"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"sync"
"github.com/cloudwego/eino-ext/components/model/ark"
"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/flow/agent/react"
"github.com/cloudwego/eino/schema"
arkModel "github.com/volcengine/volcengine-go-sdk/service/arkruntime/model"
"github.com/cloudwego/eino-examples/components/model/httptransport"
"github.com/cloudwego/eino-examples/flow/agent/react/dynamic_option_example/dynamic"
"github.com/cloudwego/eino-examples/flow/agent/react/tools"
"github.com/cloudwego/eino-examples/internal/logs"
)
func main() {
arkApiKey := os.Getenv("ARK_API_KEY")
arkModelName := os.Getenv("ARK_MODEL_NAME")
ctx := context.Background()
// Create HTTP client with curl-style logging for debugging HTTP requests
client := &http.Client{Transport: httptransport.NewCurlRT(
http.DefaultTransport,
httptransport.WithLogger(log.Default()),
httptransport.WithCtxLogger(httptransport.IDCtxLogger{L: log.Default()}),
httptransport.WithPrintAuth(false),
httptransport.WithMaskHeaders([]string{"X-API-KEY", "API-KEY"}),
httptransport.WithStreamLogging(true),
httptransport.WithMaxStreamLogBytes(8192),
)}
// Create Ark ChatModel with custom HTTP client
config := &ark.ChatModelConfig{
APIKey: arkApiKey,
Model: arkModelName,
HTTPClient: client,
}
arkChatModel, err := ark.NewChatModel(ctx, config)
if err != nil {
logs.Errorf("failed to create chat model: %v", err)
return
}
restaurantTool := tools.GetRestaurantTool()
dishTool := tools.GetDishTool()
persona := `# Character:
`
// Wrap the ChatModel with dynamic.ChatModel to enable dynamic option modification.
// The GetOptionFunc will be called before each ChatModel.Generate() call,
// allowing us to modify options based on the current iteration state.
dynamicModel := &dynamic.ChatModel{
Model: arkChatModel,
GetOptionFunc: getDynamicOptions,
}
// Create ReAct agent with the dynamic model
rAgent, err := react.NewAgent(ctx, &react.AgentConfig{
ToolCallingModel: dynamicModel,
ToolsConfig: compose.ToolsNodeConfig{
Tools: []tool.BaseTool{restaurantTool, dishTool},
},
})
if err != nil {
logs.Errorf("failed to create agent: %v", err)
return
}
// Create a parent graph that wraps the ReAct agent.
// This parent graph provides the local state (dynamic.State) that persists
// across ReAct loop iterations. The state is accessed via compose.ProcessState
// inside the dynamic.ChatModel wrapper.
parentGraph := compose.NewGraph[[]*schema.Message, *schema.Message](
compose.WithGenLocalState(func(ctx context.Context) *dynamic.State {
return dynamic.NewState()
}),
)
// Export the ReAct agent as a sub-graph and add it to the parent graph
agentGraph, agentOpts := rAgent.ExportGraph()
err = parentGraph.AddGraphNode("react_agent", agentGraph, agentOpts...)
if err != nil {
logs.Errorf("failed to add graph node: %v", err)
return
}
_ = parentGraph.AddEdge(compose.START, "react_agent")
_ = parentGraph.AddEdge("react_agent", compose.END)
runnable, err := parentGraph.Compile(ctx, compose.WithGraphName("DynamicOptionReactAgent"))
if err != nil {
logs.Errorf("failed to compile graph: %v", err)
return
}
messages := []*schema.Message{
{
Role: schema.System,
Content: persona,
},
{
Role: schema.User,
Content: "我在北京,给我推荐一些菜,需要有口味辣一点的菜,至少推荐有 2 家餐厅",
},
}
// Create MessageFuture to observe intermediate results (reasoning, tool calls, tool results).
// This allows us to print the agent's thought process in real-time.
msgFutureOpt, msgFuture := react.WithMessageFuture()
// Process MessageFuture in a separate goroutine to print intermediate results
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
processMessageFuture(msgFuture)
}()
// Use Invoke instead of Stream. The MessageFuture still provides streaming
// access to intermediate messages even when using Invoke.
// Note: DesignateNode is used to pass the option to the specific sub-graph node.
_, err = runnable.Invoke(ctx, messages, agent.GetComposeOptions(msgFutureOpt)[0].DesignateNode("react_agent"))
if err != nil {
logs.Errorf("failed to invoke: %v", err)
return
}
wg.Wait()
fmt.Printf("\n==================== Finished ====================\n")
}
// processMessageFuture reads from the MessageFuture and prints intermediate results.
// Each iteration of the ReAct loop produces multiple message streams:
// - Assistant message with reasoning and tool calls
// - Tool result messages
// - Final assistant message with the answer
func processMessageFuture(msgFuture react.MessageFuture) {
iter := msgFuture.GetMessageStreams()
for {
sr, ok, err := iter.Next()
if err != nil {
logs.Errorf("failed to get next message stream: %v", err)
return
}
if !ok {
break
}
// Accumulate streaming chunks into complete content
var reasoningBuilder strings.Builder
var contentBuilder strings.Builder
var toolCallsMap = make(map[int]*strings.Builder)
var toolCallNames = make(map[int]string)
var toolResult *struct {
name string
content string
}
// Read all chunks from the stream
for {
msg, err := sr.Recv()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
logs.Errorf("failed to recv from message stream: %v", err)
return
}
// Accumulate reasoning content (thinking process)
if msg.ReasoningContent != "" {
reasoningBuilder.WriteString(msg.ReasoningContent)
}
// Accumulate tool calls (function name and arguments come in separate chunks)
if len(msg.ToolCalls) > 0 {
for _, tc := range msg.ToolCalls {
idx := 0
if tc.Index != nil {
idx = *tc.Index
}
if _, exists := toolCallsMap[idx]; !exists {
toolCallsMap[idx] = &strings.Builder{}
}
if tc.Function.Name != "" {
toolCallNames[idx] = tc.Function.Name
}
toolCallsMap[idx].WriteString(tc.Function.Arguments)
}
}
// Capture tool result
if msg.Role == schema.Tool && msg.Content != "" {
toolResult = &struct {
name string
content string
}{
name: msg.ToolName,
content: msg.Content,
}
}
// Accumulate assistant content (final answer)
if msg.Role == schema.Assistant && msg.Content != "" {
contentBuilder.WriteString(msg.Content)
}
}
// Print accumulated content
if reasoningBuilder.Len() > 0 {
fmt.Printf("\n[Reasoning]\n%s\n", reasoningBuilder.String())
}
if len(toolCallsMap) > 0 {
for idx := 0; idx < len(toolCallsMap); idx++ {
if builder, exists := toolCallsMap[idx]; exists {
name := toolCallNames[idx]
fmt.Printf("\n[ToolCall] %s(%s)\n", name, builder.String())
}
}
}
if toolResult != nil {
fmt.Printf("\n[ToolResult] %s:\n%s\n", toolResult.name, truncateString(toolResult.content, 300))
}
if contentBuilder.Len() > 0 && len(toolCallsMap) == 0 {
fmt.Printf("\n[FinalAnswer]\n%s\n", contentBuilder.String())
}
}
}
func truncateString(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
return s[:maxLen] + "..."
}
// getDynamicOptions is called before each ChatModel.Generate() call.
// It demonstrates how to dynamically modify options based on the current iteration:
// - Iteration 0: Enable thinking mode, allow tool calls
// - Iteration 1+: Disable thinking mode, forbid tool calls to force final answer
func getDynamicOptions(ctx context.Context, input []*schema.Message, state *dynamic.State) []model.Option {
var opts []model.Option
fmt.Printf("\n--- [DynamicOption] Preparing options for iteration %d ---\n", state.Iteration)
// Control thinking mode based on iteration
if state.Iteration >= 1 {
fmt.Printf(" -> Disabling thinking mode\n")
opts = append(opts, ark.WithThinking(&arkModel.Thinking{
Type: arkModel.ThinkingTypeDisabled,
}))
} else {
fmt.Printf(" -> Thinking mode enabled (first iteration)\n")
}
// Control tool choice based on iteration
// After the first iteration, forbid tool calls to force the model to give a final answer
if state.Iteration >= 1 {
fmt.Printf(" -> Forcing final answer (tool_choice=forbidden)\n")
opts = append(opts, model.WithToolChoice(schema.ToolChoiceForbidden))
opts = append(opts, model.WithTools([]*schema.ToolInfo{}))
} else {
fmt.Printf(" -> Tool choice: auto\n")
opts = append(opts, model.WithToolChoice(schema.ToolChoiceAllowed))
// Re-bind tools for the first iteration
restaurantTool := tools.GetRestaurantTool()
dishTool := tools.GetDishTool()
info1, _ := restaurantTool.Info(ctx)
info2, _ := dishTool.Info(ctx)
opts = append(opts, model.WithTools([]*schema.ToolInfo{info1, info2}))
}
return opts
}

@ -8,7 +8,7 @@ require (
github.com/alicebob/miniredis/v2 v2.35.0 github.com/alicebob/miniredis/v2 v2.35.0
github.com/bytedance/sonic v1.14.2 github.com/bytedance/sonic v1.14.2
github.com/chromedp/chromedp v0.9.5 github.com/chromedp/chromedp v0.9.5
github.com/cloudwego/eino v0.7.0 github.com/cloudwego/eino v0.7.11
github.com/cloudwego/eino-ext/callbacks/cozeloop v0.1.6 github.com/cloudwego/eino-ext/callbacks/cozeloop v0.1.6
github.com/cloudwego/eino-ext/components/document/parser/html v0.0.0-20251117090452-bd6375a0b3cf github.com/cloudwego/eino-ext/components/document/parser/html v0.0.0-20251117090452-bd6375a0b3cf
github.com/cloudwego/eino-ext/components/document/parser/pdf v0.0.0-20251117090452-bd6375a0b3cf github.com/cloudwego/eino-ext/components/document/parser/pdf v0.0.0-20251117090452-bd6375a0b3cf
@ -23,7 +23,7 @@ require (
github.com/cloudwego/eino-ext/devops v0.1.8 github.com/cloudwego/eino-ext/devops v0.1.8
github.com/coze-dev/cozeloop-go v0.1.11 github.com/coze-dev/cozeloop-go v0.1.11
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
github.com/eino-contrib/jsonschema v1.0.2 github.com/eino-contrib/jsonschema v1.0.3
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/json-iterator/go v1.1.12 github.com/json-iterator/go v1.1.12
github.com/kaptinlin/jsonrepair v0.2.4 github.com/kaptinlin/jsonrepair v0.2.4

@ -127,8 +127,8 @@ github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5P
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cloudwego/eino v0.7.0 h1:XDGdGMZCAVx+OC0IxiLlyNFELoLN+56THUhYYqEujuM= github.com/cloudwego/eino v0.7.11 h1:QQ3Ik4/nW1462CuvFsmH3gWAqNI/70BXRDmsYyvXyds=
github.com/cloudwego/eino v0.7.0/go.mod h1:JNapfU+QUrFFpboNDrNOFvmz0m9wjBFHHCr77RH6a50= github.com/cloudwego/eino v0.7.11/go.mod h1:nA8Vacmuqv3pqKBQbTWENBLQ8MmGmPt/WqiyLeB8ohQ=
github.com/cloudwego/eino-ext/callbacks/cozeloop v0.1.6 h1:gS4nAOpQQC5WItt1k32yjZt9O2UWMpnbgF6vkMQAWhg= github.com/cloudwego/eino-ext/callbacks/cozeloop v0.1.6 h1:gS4nAOpQQC5WItt1k32yjZt9O2UWMpnbgF6vkMQAWhg=
github.com/cloudwego/eino-ext/callbacks/cozeloop v0.1.6/go.mod h1:ZniRkgN+9FUFxtN60X7yzD6UOruqrKQusjrOiGcH4I8= github.com/cloudwego/eino-ext/callbacks/cozeloop v0.1.6/go.mod h1:ZniRkgN+9FUFxtN60X7yzD6UOruqrKQusjrOiGcH4I8=
github.com/cloudwego/eino-ext/components/document/parser/html v0.0.0-20251117090452-bd6375a0b3cf h1:Uwh3VT+xPrfDjM677dj1pSidCzBFoTrYlC274kEci5w= github.com/cloudwego/eino-ext/components/document/parser/html v0.0.0-20251117090452-bd6375a0b3cf h1:Uwh3VT+xPrfDjM677dj1pSidCzBFoTrYlC274kEci5w=
@ -188,8 +188,8 @@ github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5m
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/eino-contrib/jsonschema v1.0.2 h1:HaxruBMUdnXa7Lg/lX8g0Hk71ZIfdTZXmBQz0e3esr8= github.com/eino-contrib/jsonschema v1.0.3 h1:2Kfsm1xlMV0ssY2nuxshS4AwbLFuqmPmzIjLVJ1Fsp0=
github.com/eino-contrib/jsonschema v1.0.2/go.mod h1:cpnX4SyKjWjGC7iN2EbhxaTdLqGjCi0e9DxpLYxddD4= github.com/eino-contrib/jsonschema v1.0.3/go.mod h1:cpnX4SyKjWjGC7iN2EbhxaTdLqGjCi0e9DxpLYxddD4=
github.com/eino-contrib/ollama v0.1.0 h1:z1NaMdKW6X1ftP8g5xGGR5zDRPUtuTKFq35vBQgxsN4= github.com/eino-contrib/ollama v0.1.0 h1:z1NaMdKW6X1ftP8g5xGGR5zDRPUtuTKFq35vBQgxsN4=
github.com/eino-contrib/ollama v0.1.0/go.mod h1:mYsQ7b3DeqY8bHPuD3MZJYTqkgyL6LoemxoP/B7ZNhA= github.com/eino-contrib/ollama v0.1.0/go.mod h1:mYsQ7b3DeqY8bHPuD3MZJYTqkgyL6LoemxoP/B7ZNhA=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=

Loading…
Cancel
Save