Skip to content

Commit b382603

Browse files
committed
get the readfile tool whipped into shape
1 parent 4bb4785 commit b382603

2 files changed

Lines changed: 232 additions & 212 deletions

File tree

pkg/aiusechat/tools_readfile.go

Lines changed: 93 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,54 @@
44
package aiusechat
55

66
import (
7-
"bufio"
8-
"bytes"
97
"fmt"
108
"io"
119
"os"
1210
"strings"
1311

1412
"github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes"
13+
"github.com/wavetermdev/waveterm/pkg/util/readutil"
1514
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
1615
)
1716

17+
const StopReasonMaxBytes = "max_bytes"
18+
1819
type readTextFileParams struct {
19-
Filename string `json:"filename"`
20-
LineStart *int `json:"line_start"`
21-
LineEnd *int `json:"line_end"`
22-
MaxBytes *int `json:"max_bytes"`
23-
FromEnd bool `json:"from_end"`
20+
Filename string `json:"filename"`
21+
Origin *string `json:"origin"` // "start" or "end", defaults to "start"
22+
Offset *int `json:"offset"` // lines to skip, defaults to 0
23+
Count *int `json:"count"` // number of lines to read, defaults to DefaultLineCount
24+
MaxBytes *int `json:"max_bytes"`
25+
}
26+
27+
// truncateData truncates data to maxBytes while respecting line boundaries.
28+
// For origin "start", keeps the beginning and truncates at last newline before maxBytes.
29+
// For origin "end", keeps the end and truncates from beginning at first newline after removing excess.
30+
func truncateData(data string, origin string, maxBytes int) string {
31+
if len(data) <= maxBytes {
32+
return data
33+
}
34+
35+
if origin == "end" {
36+
excessBytes := len(data) - maxBytes
37+
truncateIdx := strings.Index(data[excessBytes:], "\n")
38+
if truncateIdx == -1 {
39+
return data[excessBytes:]
40+
}
41+
return data[excessBytes+truncateIdx+1:]
42+
}
43+
44+
truncateIdx := strings.LastIndex(data[:maxBytes], "\n")
45+
if truncateIdx == -1 {
46+
return data[:maxBytes]
47+
}
48+
return data[:truncateIdx+1]
2449
}
2550

2651
func readTextFileCallback(input any) (any, error) {
27-
const DEFAULT_LINE_COUNT = 100
28-
const DEFAULT_MAX_BYTES = 50 * 1024
52+
const DefaultLineCount = 100
53+
const DefaultMaxBytes = 50 * 1024
54+
const ReadLimit = 1024 * 1024 * 1024
2955

3056
var params readTextFileParams
3157
if err := utilfn.ReUnmarshal(&params, input); err != nil {
@@ -36,7 +62,7 @@ func readTextFileCallback(input any) (any, error) {
3662
return nil, fmt.Errorf("missing filename parameter")
3763
}
3864

39-
maxBytes := DEFAULT_MAX_BYTES
65+
maxBytes := DefaultMaxBytes
4066
if params.MaxBytes != nil {
4167
maxBytes = *params.MaxBytes
4268
}
@@ -66,90 +92,70 @@ func readTextFileCallback(input any) (any, error) {
6692
return nil, fmt.Errorf("file appears to be binary content")
6793
}
6894

69-
file.Seek(0, 0)
70-
71-
var lines []string
72-
scanner := bufio.NewScanner(file)
73-
buf := make([]byte, 0, 64*1024)
74-
scanner.Buffer(buf, maxBytes)
75-
76-
for scanner.Scan() {
77-
lines = append(lines, scanner.Text())
95+
origin := "start"
96+
if params.Origin != nil {
97+
origin = *params.Origin
7898
}
7999

80-
if err := scanner.Err(); err != nil {
81-
return nil, fmt.Errorf("error reading file: %w", err)
100+
if origin != "start" && origin != "end" {
101+
return nil, fmt.Errorf("invalid origin value '%s': must be 'start' or 'end'", origin)
82102
}
83103

84-
totalLines := len(lines)
104+
offset := 0
105+
if params.Offset != nil {
106+
offset = *params.Offset
107+
}
85108

86-
if params.FromEnd {
87-
for i, j := 0, len(lines)-1; i < j; i, j = i+1, j-1 {
88-
lines[i], lines[j] = lines[j], lines[i]
109+
count := DefaultLineCount
110+
if params.Count != nil {
111+
count = *params.Count
112+
if count < 1 {
113+
return nil, fmt.Errorf("count must be at least 1, got %d", count)
89114
}
90115
}
91116

92-
start := 0
93-
count := DEFAULT_LINE_COUNT
94-
if params.LineStart != nil {
95-
start = *params.LineStart
96-
}
97-
if params.LineEnd != nil {
98-
count = *params.LineEnd - start
117+
if offset < 0 {
118+
offset = 0
99119
}
100120

101-
if start < 0 {
102-
start = 0
103-
}
104-
if start > len(lines) {
105-
start = len(lines)
106-
}
121+
var lines []string
122+
var stopReason string
107123

108-
end := start + count
109-
if end > len(lines) {
110-
end = len(lines)
124+
if _, err := file.Seek(0, 0); err != nil {
125+
return nil, fmt.Errorf("failed to seek to start of file: %w", err)
111126
}
112-
if end < start {
113-
end = start
114-
}
115-
116-
selectedLines := lines[start:end]
117127

118-
var dataBuilder strings.Builder
119-
for i, line := range selectedLines {
120-
if i > 0 {
121-
dataBuilder.WriteString("\n")
128+
if origin == "end" {
129+
lines, stopReason, err = readutil.ReadTailLines(file, count, offset, int64(ReadLimit))
130+
if err != nil {
131+
return nil, fmt.Errorf("error reading file from end: %w", err)
122132
}
123-
dataBuilder.WriteString(line)
124-
}
125-
data := dataBuilder.String()
126-
127-
truncated := ""
128-
currentBytes := len(data)
129-
if currentBytes >= maxBytes {
130-
truncated = "max_bytes"
131-
lastNewline := bytes.LastIndexByte([]byte(data[:maxBytes]), '\n')
132-
if lastNewline > 0 {
133-
data = data[:lastNewline]
134-
} else {
135-
data = data[:maxBytes]
133+
} else {
134+
lines, stopReason, err = readutil.ReadLines(file, count, offset, ReadLimit)
135+
if err != nil {
136+
return nil, fmt.Errorf("error reading file: %w", err)
136137
}
137-
} else if end >= len(lines) {
138-
truncated = "eof"
139138
}
140139

141-
if truncated == "" {
142-
truncated = "null"
140+
data := strings.Join(lines, "")
141+
data = strings.TrimSuffix(data, "\n")
142+
143+
if len(data) > maxBytes {
144+
data = truncateData(data, origin, maxBytes)
145+
stopReason = StopReasonMaxBytes
143146
}
144147

145-
return map[string]any{
148+
result := map[string]any{
146149
"total_size": totalSize,
147-
"line_count": totalLines,
148150
"data": data,
149151
"modified": utilfn.FormatRelativeTime(modTime),
150152
"modified_time": modTime.UTC().Format("2006-01-02 15:04:05 UTC"),
151-
"truncated": truncated,
152-
}, nil
153+
}
154+
if stopReason != "" {
155+
result["truncated"] = stopReason
156+
}
157+
158+
return result, nil
153159
}
154160

155161
func GetReadTextFileToolDefinition() uctypes.ToolDefinition {
@@ -165,29 +171,34 @@ func GetReadTextFileToolDefinition() uctypes.ToolDefinition {
165171
"type": "string",
166172
"description": "Path to the file to read",
167173
},
168-
"line_start": map[string]any{
174+
"origin": map[string]any{
175+
"type": "string",
176+
"enum": []string{"start", "end"},
177+
"default": "start",
178+
"description": "Where to read from: 'start' (default) or 'end' of file",
179+
},
180+
"offset": map[string]any{
169181
"type": "integer",
170182
"minimum": 0,
171-
"description": "Starting line number (0-based). If from_end is true, this is lines from the end.",
183+
"default": 0,
184+
"description": "Lines to skip. From 'start': 0-based line index. From 'end': lines to skip from the end (0 = very last line)",
172185
},
173-
"line_end": map[string]any{
186+
"count": map[string]any{
174187
"type": "integer",
175-
"minimum": 0,
176-
"description": "Ending line number (exclusive). If from_end is true, this is lines from the end.",
188+
"minimum": 1,
189+
"default": 100,
190+
"description": "Number of lines to return",
177191
},
178192
"max_bytes": map[string]any{
179193
"type": "integer",
180194
"minimum": 1,
181-
"description": "Maximum bytes to return (default: 51200). Data will be truncated if it exceeds this.",
182-
},
183-
"from_end": map[string]any{
184-
"type": "boolean",
185-
"description": "If true, read lines from the end of the file instead of the beginning (default: false)",
195+
"default": 51200,
196+
"description": "Maximum bytes to return. If the result exceeds this, it will be truncated at line boundaries",
186197
},
187198
},
188199
"required": []string{"filename"},
189200
"additionalProperties": false,
190201
},
191202
ToolAnyCallback: readTextFileCallback,
192203
}
193-
}
204+
}

0 commit comments

Comments
 (0)