You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
189 lines
7.2 KiB
Markdown
189 lines
7.2 KiB
Markdown
# 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.
|