Skip to content

Commit f9ad0c4

Browse files
daemon365tonybase
andauthored
feat: blades bot (#213)
* feat: blades bot * fix bugs * fix blades * fix * fix * fix * fix * fix bug * fix bug * fix struct * fix bug * feat: add YAML parser and emitter implementation - Introduced a new YAML parser and emitter in `yamlh.go` and `yamlprivateh.go` files, providing comprehensive support for YAML document processing. - Implemented various data structures and constants for handling YAML tokens, events, nodes, and documents. - Added utility functions for character checks and buffer management to facilitate parsing and emitting processes. - Updated `modules.txt` to reflect new dependencies and versions, including updates to `github.com/go-kratos/blades` and `gopkg.in/yaml.v3`. * fix: update middleware import paths to use recipe package * fix * fix * fix: remove leftover merge conflict markers * chore: align non-cmd files with origin/main * refactor(app): update import paths and use unified middleware package * fix bug * fix bug * fix * fix * fix * add weixin * fix weixin * fix * feat: enhance tool descriptions and improve context handling in runner * feat: enhance error handling in agent tools and add context control option --------- Co-authored-by: chenzhihui <zhihui_chen@foxmail.com>
1 parent bc8158b commit f9ad0c4

File tree

407 files changed

+124654
-36
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

407 files changed

+124654
-36
lines changed

agent.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package blades
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"html/template"
78
"strings"
@@ -295,13 +296,18 @@ func (a *agent) handleTools(ctx context.Context, invocation *Invocation, part To
295296
if tool.Name() == part.Name {
296297
response, err := tool.Handle(ctx, part.Request)
297298
if err != nil {
298-
return part, err
299+
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
300+
return part, err
301+
}
302+
part.Response = "Tool error: " + err.Error()
303+
return part, nil
299304
}
300305
part.Response = response
301306
return part, nil
302307
}
303308
}
304-
return part, fmt.Errorf("agent: tool %s not found", part.Name)
309+
part.Response = fmt.Sprintf("Tool error: agent: tool %s not found", part.Name)
310+
return part, nil
305311
}
306312

307313
// executeTools executes the tools specified in the tool parts.
@@ -438,6 +444,9 @@ func (a *agent) handle(ctx context.Context, session Session, invocation *Invocat
438444
yield(nil, err)
439445
return
440446
}
447+
if _, ok := toolMessage.Actions[tools.ActionLoopExit]; ok {
448+
return
449+
}
441450
// In stateless mode, accumulate the tool result into the local slice
442451
// so the next iteration can supply it to the model without loading
443452
// session history. executeTools mutates finalMessage in-place and

agent_exit_tool_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package blades
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
bladestools "github.com/go-kratos/blades/tools"
8+
)
9+
10+
type exitToolLoopModel struct {
11+
calls int
12+
}
13+
14+
func (m *exitToolLoopModel) Name() string { return "exit-tool-loop" }
15+
16+
func (m *exitToolLoopModel) Generate(_ context.Context, _ *ModelRequest) (*ModelResponse, error) {
17+
m.calls++
18+
msg := NewAssistantMessage(StatusCompleted)
19+
msg.Role = RoleTool
20+
msg.Parts = append(msg.Parts, NewToolPart("call_exit", "exit", `{"reason":"done"}`))
21+
return &ModelResponse{Message: msg}, nil
22+
}
23+
24+
func (m *exitToolLoopModel) NewStreaming(context.Context, *ModelRequest) Generator[*ModelResponse, error] {
25+
return nil
26+
}
27+
28+
func TestAgentStopsImmediatelyAfterExitTool(t *testing.T) {
29+
t.Parallel()
30+
31+
model := &exitToolLoopModel{}
32+
agent, err := NewAgent(
33+
"review",
34+
WithModel(model),
35+
WithTools(bladestools.NewExitTool()),
36+
WithMaxIterations(5),
37+
)
38+
if err != nil {
39+
t.Fatalf("new agent: %v", err)
40+
}
41+
42+
msg, err := NewRunner(agent).Run(context.Background(), UserMessage("review this"))
43+
if err != nil {
44+
t.Fatalf("runner run: %v", err)
45+
}
46+
if got, want := model.calls, 1; got != want {
47+
t.Fatalf("model calls = %d, want %d", got, want)
48+
}
49+
if got, want := msg.Role, RoleTool; got != want {
50+
t.Fatalf("message role = %q, want %q", got, want)
51+
}
52+
if _, ok := msg.Actions[bladestools.ActionLoopExit]; !ok {
53+
t.Fatalf("expected %q action on final tool message", bladestools.ActionLoopExit)
54+
}
55+
}

agent_session_context_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package blades
22

33
import (
44
"context"
5+
"fmt"
56
"testing"
67

78
bladestools "github.com/go-kratos/blades/tools"
@@ -192,3 +193,70 @@ func TestAgentRunWithSessionAndNilPreparedContextKeepsInvocationMessagesAcrossTo
192193
t.Fatalf("tool response = %q, want %q", got, want)
193194
}
194195
}
196+
197+
type recoverableToolErrorModel struct {
198+
calls int
199+
secondInput []*Message
200+
}
201+
202+
func (m *recoverableToolErrorModel) Name() string { return "recoverable-tool-error" }
203+
204+
func (m *recoverableToolErrorModel) Generate(_ context.Context, req *ModelRequest) (*ModelResponse, error) {
205+
m.calls++
206+
if m.calls == 2 {
207+
m.secondInput = append(m.secondInput[:0], req.Messages...)
208+
}
209+
210+
msg := NewAssistantMessage(StatusCompleted)
211+
if m.calls == 1 {
212+
msg.Role = RoleTool
213+
msg.Parts = append(msg.Parts, NewToolPart("call_1", "edit", `{"path":"IDENTITY.md"}`))
214+
return &ModelResponse{Message: msg}, nil
215+
}
216+
217+
msg.Parts = append(msg.Parts, TextPart{Text: "recovered"})
218+
return &ModelResponse{Message: msg}, nil
219+
}
220+
221+
func (m *recoverableToolErrorModel) NewStreaming(context.Context, *ModelRequest) Generator[*ModelResponse, error] {
222+
return nil
223+
}
224+
225+
func TestAgentRunKeepsGoingAfterRecoverableToolError(t *testing.T) {
226+
t.Parallel()
227+
228+
model := &recoverableToolErrorModel{}
229+
tool := bladestools.NewTool("edit", "edit", bladestools.HandleFunc(func(context.Context, string) (string, error) {
230+
return "", fmt.Errorf("edit: edits[0] target not found")
231+
}))
232+
agent, err := NewAgent("tool-agent", WithModel(model), WithTools(tool))
233+
if err != nil {
234+
t.Fatalf("new agent: %v", err)
235+
}
236+
237+
runner := NewRunner(agent)
238+
session := NewSession()
239+
output, err := runner.Run(context.Background(), UserMessage("set persona"), WithSession(session))
240+
if err != nil {
241+
t.Fatalf("runner run: %v", err)
242+
}
243+
if got, want := output.Text(), "recovered"; got != want {
244+
t.Fatalf("output text = %q, want %q", got, want)
245+
}
246+
if got, want := model.calls, 2; got != want {
247+
t.Fatalf("model calls = %d, want %d", got, want)
248+
}
249+
if got, want := len(model.secondInput), 2; got != want {
250+
t.Fatalf("second input len = %d, want %d", got, want)
251+
}
252+
part, ok := model.secondInput[1].Parts[0].(ToolPart)
253+
if !ok {
254+
t.Fatalf("second input second part type = %T, want ToolPart", model.secondInput[1].Parts[0])
255+
}
256+
if got, want := part.Completed, true; got != want {
257+
t.Fatalf("tool completed = %t, want %t", got, want)
258+
}
259+
if got, want := part.Response, "Tool error: edit: edits[0] target not found"; got != want {
260+
t.Fatalf("tool response = %q, want %q", got, want)
261+
}
262+
}

agent_tool_status_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package blades
22

33
import (
44
"context"
5+
"errors"
6+
"fmt"
57
"testing"
68

79
bladestools "github.com/go-kratos/blades/tools"
@@ -106,3 +108,48 @@ func TestAgentExecuteToolsSkipsCompletedToolPart(t *testing.T) {
106108
t.Fatalf("tool response = %q, want %q", gotResp, want)
107109
}
108110
}
111+
112+
func TestAgentExecuteToolsCapturesToolErrorsInResponse(t *testing.T) {
113+
t.Parallel()
114+
115+
tool := bladestools.NewTool("edit", "edit", bladestools.HandleFunc(func(context.Context, string) (string, error) {
116+
return "", context.DeadlineExceeded
117+
}))
118+
invocation := &Invocation{Tools: []bladestools.Tool{tool}}
119+
message := NewAssistantMessage(StatusCompleted)
120+
message.Role = RoleTool
121+
message.Parts = append(message.Parts, NewToolPart("call_1", "edit", `{"path":"IDENTITY.md"}`))
122+
123+
_, err := (&agent{}).executeTools(context.Background(), invocation, message)
124+
if !errors.Is(err, context.DeadlineExceeded) {
125+
t.Fatalf("executeTools error = %v, want deadline exceeded", err)
126+
}
127+
}
128+
129+
func TestAgentExecuteToolsTurnsRecoverableErrorsIntoCompletedToolResults(t *testing.T) {
130+
t.Parallel()
131+
132+
tool := bladestools.NewTool("edit", "edit", bladestools.HandleFunc(func(context.Context, string) (string, error) {
133+
return "", fmt.Errorf("edit: edits[0] target not found")
134+
}))
135+
invocation := &Invocation{Tools: []bladestools.Tool{tool}}
136+
message := NewAssistantMessage(StatusCompleted)
137+
message.Role = RoleTool
138+
message.Parts = append(message.Parts, NewToolPart("call_1", "edit", `{"path":"IDENTITY.md"}`))
139+
140+
got, err := (&agent{}).executeTools(context.Background(), invocation, message)
141+
if err != nil {
142+
t.Fatalf("executeTools returned error: %v", err)
143+
}
144+
145+
toolPart, ok := got.Parts[0].(ToolPart)
146+
if !ok {
147+
t.Fatalf("part type = %T, want ToolPart", got.Parts[0])
148+
}
149+
if got, want := toolPart.Completed, true; got != want {
150+
t.Fatalf("tool completed = %t, want %t", got, want)
151+
}
152+
if got, want := toolPart.Response, "Tool error: edit: edits[0] target not found"; got != want {
153+
t.Fatalf("tool response = %q, want %q", got, want)
154+
}
155+
}

0 commit comments

Comments
 (0)