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.
eino-examples/adk/common/tool/review_edit_wrapper.go

117 lines
3.6 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 tool
import (
"context"
"fmt"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/schema"
)
// ReviewEditInfo is presented to the user for editing.
type ReviewEditInfo struct {
ToolName string
ArgumentsInJSON string
ToolCallID string
ReviewResult *ReviewEditResult
}
// ReviewEditResult is the result of the user's review.
type ReviewEditResult struct {
EditedArgumentsInJSON *string
NoNeedToEdit bool
Disapproved bool
DisapproveReason *string
}
func (re *ReviewEditInfo) String() string {
return fmt.Sprintf("Tool '%s' is about to be called with the following arguments:\n`\n%s\n`\n\n"+
"Please review and either provide edited arguments in JSON format, "+
"reply with 'no need to edit', or reply with 'N' to disapprove the tool call.",
re.ToolName, re.ArgumentsInJSON)
}
func init() {
schema.Register[*ReviewEditInfo]()
}
// InvokableReviewEditTool is a wrapper that enforces a review-and-edit step.
type InvokableReviewEditTool struct {
tool.InvokableTool
}
func (i InvokableReviewEditTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
return i.InvokableTool.Info(ctx)
}
func (i InvokableReviewEditTool) InvokableRun(ctx context.Context, argumentsInJSON string,
opts ...tool.Option) (string, error) {
toolInfo, err := i.Info(ctx)
if err != nil {
return "", err
}
wasInterrupted, _, storedArguments := compose.GetInterruptState[string](ctx)
if !wasInterrupted { // Initial invocation, interrupt for review.
return "", compose.StatefulInterrupt(ctx, &ReviewEditInfo{
ToolName: toolInfo.Name,
ArgumentsInJSON: argumentsInJSON,
ToolCallID: compose.GetToolCallID(ctx),
}, argumentsInJSON)
}
isResumeTarget, hasData, data := compose.GetResumeContext[*ReviewEditInfo](ctx)
if !isResumeTarget { // Not for us, re-interrupt.
return "", compose.StatefulInterrupt(ctx, &ReviewEditInfo{
ToolName: toolInfo.Name,
ArgumentsInJSON: storedArguments,
ToolCallID: compose.GetToolCallID(ctx),
}, storedArguments)
}
if !hasData || data.ReviewResult == nil {
return "", fmt.Errorf("tool '%s' resumed with no review data", toolInfo.Name)
}
result := data.ReviewResult
if result.Disapproved {
if result.DisapproveReason != nil {
return fmt.Sprintf("tool '%s' disapproved, reason: %s", toolInfo.Name, *result.DisapproveReason), nil
}
return fmt.Sprintf("tool '%s' disapproved", toolInfo.Name), nil
}
if result.NoNeedToEdit {
return i.InvokableTool.InvokableRun(ctx, storedArguments, opts...)
}
if result.EditedArgumentsInJSON != nil {
res, err := i.InvokableTool.InvokableRun(ctx, *result.EditedArgumentsInJSON, opts...)
if err != nil {
return "", err
}
return fmt.Sprintf("after presenting the tool call info to the user, the user explilcitly changed tool call arguments to %s. Tool called, final result: %s",
*result.EditedArgumentsInJSON, res), nil
}
return "", fmt.Errorf("invalid review result for tool '%s'", toolInfo.Name)
}