Skip to content

Commit 4aef84f

Browse files
authored
refactor(adk): modify truncation / clear replacement message (#770)
1 parent 5b434eb commit 4aef84f

File tree

3 files changed

+264
-81
lines changed

3 files changed

+264
-81
lines changed

adk/middlewares/reduction/consts.go

Lines changed: 48 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -19,53 +19,71 @@ package reduction
1919

2020
import "github.com/cloudwego/eino/adk/internal"
2121

22-
func getTruncWithOffloadingFmt() string {
22+
const (
23+
truncFmt = `<persisted-output>
24+
Output too large ({original_size}). Full output saved to: {file_path}
25+
Preview (first {preview_size}):
26+
{preview_first}
27+
28+
Preview (last {preview_size}):
29+
{preview_last}
30+
31+
</persisted-output>`
32+
truncFmtZh = `<persisted-output>
33+
输出结果过大 ({original_size}). 完整输出保存到: {file_path}
34+
预览 (前 {preview_size}):
35+
{preview_first}
36+
37+
预览 (后 {preview_size}):
38+
{preview_last}
39+
40+
</persisted-output>`
41+
)
42+
43+
const (
44+
clearWithOffloadingFmt = `<persisted-output>Tool result saved to: {file_path}
45+
Use {read_tool_name} to view</persisted-output>`
46+
clearWithOffloadingFmtZh = `<persisted-output>工具结果已保存至: {file_path}
47+
使用 {read_tool_name} 进行查看</persisted-output>`
48+
49+
clearWithoutOffloadingFmt = `[Old tool result content cleared]`
50+
clearWithoutOffloadingFmtZh = `[工具输出结果已清理]`
51+
)
52+
53+
const (
54+
msgReducedFlag = "_reduction_mw_processed"
55+
msgReducedTokens = "_reduction_mw_tokens"
56+
)
57+
58+
func getTruncFmt() string {
2359
s, _ := internal.SelectPrompt(internal.I18nPrompts{
24-
English: truncWithOffloadingFmt,
25-
Chinese: truncWithOffloadingFmtZh,
60+
English: truncFmt,
61+
Chinese: truncFmtZh,
2662
})
2763
if s == "" {
28-
return truncWithOffloadingFmt
64+
return truncFmt
2965
}
3066
return s
3167
}
3268

33-
func getTruncWithoutOffloadingFmt() string {
69+
func getClearWithOffloadingFmt() string {
3470
s, _ := internal.SelectPrompt(internal.I18nPrompts{
35-
English: truncWithoutOffloadingFmt,
36-
Chinese: truncWithoutOffloadingFmtZh,
71+
English: clearWithOffloadingFmt,
72+
Chinese: clearWithOffloadingFmtZh,
3773
})
3874
if s == "" {
39-
return truncWithoutOffloadingFmt
75+
return clearWithOffloadingFmt
4076
}
4177
return s
4278
}
4379

44-
func getToolOffloadResultFmt() string {
80+
func getClearWithoutOffloadingFmt() string {
4581
s, _ := internal.SelectPrompt(internal.I18nPrompts{
46-
English: toolOffloadResultFmt,
47-
Chinese: toolOffloadResultFmtZh,
82+
English: clearWithoutOffloadingFmt,
83+
Chinese: clearWithoutOffloadingFmtZh,
4884
})
4985
if s == "" {
50-
return toolOffloadResultFmt
86+
return clearWithoutOffloadingFmt
5187
}
5288
return s
5389
}
54-
55-
const (
56-
truncWithOffloadingFmt = `...({removed_count} chars truncated, full result saved to {file_path}, use {read_file_tool_name} tool to retrieve if needed)`
57-
truncWithOffloadingFmtZh = `...(后续 {removed_count} 个字符被截断, 完整内容保存在 {file_path}, 需要时使用 {read_file_tool_name} 工具导入)`
58-
59-
truncWithoutOffloadingFmt = `...({removed_count} chars truncated)`
60-
truncWithoutOffloadingFmtZh = `...(后续 {removed_count} 个字符被截断)`
61-
)
62-
63-
const (
64-
toolOffloadResultFmt = `Tool result is too large, retrieve from %s if needed`
65-
toolOffloadResultFmtZh = `工具输出结果过长, 需要时从 %s 中导入`
66-
)
67-
68-
const (
69-
msgReducedFlag = "_reduction_mw_processed"
70-
msgReducedTokens = "_reduction_mw_tokens"
71-
)

adk/middlewares/reduction/reduction.go

Lines changed: 77 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,15 @@ import (
5050
// important information. After all, ClearPostProcess will be called, which you could save or notify current state.
5151
type Config struct {
5252
// Backend is the storage backend where truncated content will be saved.
53-
// Optional. When Backend not set, truncated / cleared content will be discarded.
53+
// Required.
5454
Backend Backend
5555

56+
// SkipTruncation skip truncating.
57+
SkipTruncation bool
58+
59+
// SkipClear skip clearing.
60+
SkipClear bool
61+
5662
// ReadFileToolName is tool name used to retrieve from file.
5763
// After offloading content to file, you should give agent the same tool to retrieve content.
5864
// Required. Default is "read_file".
@@ -94,7 +100,7 @@ type Config struct {
94100

95101
type ToolReductionConfig struct {
96102
// Backend is the storage backend where truncated content will be saved.
97-
// Optional. When Backend not set, truncated / cleared content will be discarded.
103+
// Required.
98104
Backend Backend
99105

100106
// SkipTruncation skip truncating this tool.
@@ -104,7 +110,7 @@ type ToolReductionConfig struct {
104110
SkipClear bool
105111

106112
// ReadFileToolName is tool name used to retrieve from file.
107-
// Optional. Default is "read_file".
113+
// Required. Default is "read_file".
108114
ReadFileToolName string
109115

110116
// RootDir root dir to save truncated content, file name is {tool_call_id}.
@@ -134,6 +140,9 @@ type ToolDetail struct {
134140

135141
// OffloadInfo contains the result of the Handler's decision.
136142
type OffloadInfo struct {
143+
// NeedClear indicates whether the tool output should be clear.
144+
NeedClear bool
145+
137146
// NeedOffload indicates whether the tool output should be offloaded.
138147
NeedOffload bool
139148

@@ -145,28 +154,32 @@ type OffloadInfo struct {
145154
OffloadContent string
146155
}
147156

148-
func (t *ToolReductionConfig) fillDefaults() *ToolReductionConfig {
157+
func (t *ToolReductionConfig) fillDefaults() (*ToolReductionConfig, error) {
158+
if t.Backend == nil && !t.SkipTruncation {
159+
return nil, fmt.Errorf("backend must be set when not skipping truncation")
160+
}
149161
if t.ReadFileToolName == "" {
150162
t.ReadFileToolName = "read_file"
151163
}
152164
if t.RootDir == "" {
153165
t.RootDir = "/tmp"
154166
if !t.SkipClear && t.ClearHandler == nil {
155-
t.ClearHandler = defaultClearHandler("/tmp/clear")
167+
t.ClearHandler = defaultClearHandler("/tmp/clear", t.Backend != nil, t.ReadFileToolName)
156168
}
157169
} else {
158170
if !t.SkipClear && t.ClearHandler == nil {
159-
t.ClearHandler = defaultClearHandler(filepath.Join(t.RootDir, "clear"))
171+
t.ClearHandler = defaultClearHandler(filepath.Join(t.RootDir, "clear"), t.Backend != nil, t.ReadFileToolName)
160172
}
161173
}
162174
if t.MaxLengthForTrunc == 0 {
163175
t.MaxLengthForTrunc = 50000
164176
}
165-
return t
177+
return t, nil
166178
}
167179

168180
// New creates tool reduction middleware from config
169181
func New(_ context.Context, config *Config) (adk.ChatModelAgentMiddleware, error) {
182+
var err error
170183
if config == nil {
171184
return nil, fmt.Errorf("config must not be nil")
172185
}
@@ -178,19 +191,25 @@ func New(_ context.Context, config *Config) (adk.ChatModelAgentMiddleware, error
178191
}
179192
defaultReductionConfig := &ToolReductionConfig{
180193
Backend: config.Backend,
181-
SkipTruncation: false,
182-
SkipClear: false,
194+
SkipTruncation: config.SkipTruncation,
195+
SkipClear: config.SkipClear,
183196
ReadFileToolName: config.ReadFileToolName,
184197
RootDir: config.RootDir,
185198
MaxLengthForTrunc: config.MaxLengthForTrunc,
186199
}
187-
defaultReductionConfig = defaultReductionConfig.fillDefaults()
200+
defaultReductionConfig, err = defaultReductionConfig.fillDefaults()
201+
if err != nil {
202+
return nil, err
203+
}
188204

189205
for _, reductionConfig := range config.ToolConfig {
190206
if reductionConfig == nil {
191207
continue
192208
}
193-
reductionConfig = reductionConfig.fillDefaults()
209+
reductionConfig, err = reductionConfig.fillDefaults()
210+
if err != nil {
211+
return nil, err
212+
}
194213
}
195214

196215
return &toolReductionMiddleware{
@@ -303,33 +322,25 @@ func (t *toolReductionMiddleware) toolTruncationHandler(ctx context.Context, con
303322
}
304323

305324
filePath := filepath.Join(config.RootDir, "trunc", detail.ToolContext.CallID)
306-
var truncatedMsg string
307-
if config.Backend != nil {
308-
truncatedMsg, err = pyfmt.Fmt(getTruncWithOffloadingFmt(), map[string]any{
309-
"removed_count": len(resultText) - config.MaxLengthForTrunc,
310-
"file_path": filePath,
311-
"read_file_tool_name": config.ReadFileToolName,
312-
})
313-
314-
} else {
315-
truncatedMsg, err = pyfmt.Fmt(getTruncWithoutOffloadingFmt(), map[string]any{
316-
"removed_count": len(resultText) - config.MaxLengthForTrunc,
317-
})
318-
}
325+
previewSize := config.MaxLengthForTrunc / 2
326+
truncatedMsg, err := pyfmt.Fmt(getTruncFmt(), map[string]any{
327+
"original_size": len(resultText),
328+
"file_path": filePath,
329+
"preview_size": previewSize,
330+
"preview_first": resultText[:previewSize],
331+
"preview_last": resultText[len(resultText)-previewSize:],
332+
})
319333
if err != nil {
320334
return "", err
321335
}
322336

323337
truncResult = resultText[:config.MaxLengthForTrunc] + truncatedMsg
324-
325-
if config.Backend != nil {
326-
err = config.Backend.Write(ctx, &filesystem.WriteRequest{
327-
FilePath: filePath,
328-
Content: resultText,
329-
})
330-
if err != nil {
331-
return "", err
332-
}
338+
err = config.Backend.Write(ctx, &filesystem.WriteRequest{
339+
FilePath: filePath,
340+
Content: resultText,
341+
})
342+
if err != nil {
343+
return "", err
333344
}
334345

335346
return truncResult, nil
@@ -426,10 +437,10 @@ func (t *toolReductionMiddleware) BeforeModelRewriteState(ctx context.Context, s
426437
if offloadErr != nil {
427438
return ctx, state, offloadErr
428439
}
429-
if !offloadInfo.NeedOffload {
440+
if !offloadInfo.NeedClear {
430441
continue
431442
}
432-
if cfg.Backend != nil {
443+
if offloadInfo.NeedOffload && cfg.Backend != nil {
433444
writeErr := cfg.Backend.Write(ctx, &filesystem.WriteRequest{
434445
FilePath: offloadInfo.FilePath,
435446
Content: offloadInfo.OffloadContent,
@@ -513,23 +524,42 @@ func defaultTokenCounter(_ context.Context, msgs []*schema.Message, tools []*sch
513524
return tokens, nil
514525
}
515526

516-
func defaultClearHandler(rootDir string) func(ctx context.Context, detail *ToolDetail) (*OffloadInfo, error) {
517-
return func(ctx context.Context, detail *ToolDetail) (*OffloadInfo, error) {
518-
fileName := detail.ToolContext.CallID
519-
if fileName == "" {
520-
fileName = uuid.NewString()
521-
}
522-
filePath := filepath.Join(rootDir, fileName)
523-
nResult := fmt.Sprintf(getToolOffloadResultFmt(), filePath)
527+
func defaultClearHandler(rootDir string, needOffload bool, readFileToolName string) func(ctx context.Context, detail *ToolDetail) (*OffloadInfo, error) {
528+
return func(ctx context.Context, detail *ToolDetail) (offloadInfo *OffloadInfo, err error) {
524529
if len(detail.ToolResult.Parts) == 0 || detail.ToolResult.Parts[0].Type != schema.ToolPartTypeText {
525530
// brutal judge
526531
return nil, fmt.Errorf("default offload currently not support multimodal content")
527532
}
528-
offloadInfo := &OffloadInfo{
529-
NeedOffload: true,
530-
FilePath: filePath,
531-
OffloadContent: detail.ToolResult.Parts[0].Text,
533+
534+
fileName := detail.ToolContext.CallID
535+
if fileName == "" {
536+
fileName = uuid.NewString()
537+
}
538+
539+
var nResult string
540+
if needOffload {
541+
filePath := filepath.Join(rootDir, fileName)
542+
nResult, err = pyfmt.Fmt(getClearWithOffloadingFmt(), map[string]any{
543+
"file_path": filePath,
544+
"read_tool_name": readFileToolName,
545+
})
546+
if err != nil {
547+
return nil, err
548+
}
549+
offloadInfo = &OffloadInfo{
550+
NeedClear: true,
551+
NeedOffload: true,
552+
FilePath: filePath,
553+
OffloadContent: detail.ToolResult.Parts[0].Text,
554+
}
555+
} else {
556+
nResult = getClearWithoutOffloadingFmt()
557+
offloadInfo = &OffloadInfo{
558+
NeedClear: true,
559+
NeedOffload: false,
560+
}
532561
}
562+
533563
detail.ToolResult.Parts[0].Text = nResult
534564

535565
return offloadInfo, nil

0 commit comments

Comments
 (0)