Skip to content

Commit 8b1151e

Browse files
committed
Tool migration
1 parent 0d1dadd commit 8b1151e

File tree

17 files changed

+542
-106
lines changed

17 files changed

+542
-106
lines changed

.DS_Store

6 KB
Binary file not shown.

context.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,20 @@ import "context"
77

88
// Context is fed to the agent to generate a response
99
type Context interface {
10-
// Return the role, which can be system, assistant, user, tool, tool_result, ...
10+
// Return the current session role, which can be system, assistant, user, tool, tool_result, ...
1111
Role() string
1212

13-
// Return the text of the context
13+
// Return the current session text, or empty string if no text was returned
1414
Text() string
1515

16+
// Return the current session tool calls, or empty if no tool calls were made
17+
ToolCalls() []ToolCall
18+
1619
// Generate a response from a user prompt (with attachments and
1720
// other empheral options
18-
FromUser(context.Context, string, ...Opt) (Context, error)
21+
FromUser(context.Context, string, ...Opt) error
1922

2023
// Generate a response from a tool, passing the call identifier or
2124
// function name, and the result
22-
FromTool(context.Context, string, any, ...Opt) (Context, error)
25+
FromTool(context.Context, string, any, ...Opt) error
2326
}

pkg/anthropic/messages.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ func (r opt) String() string {
5858
type reqMessages struct {
5959
Model string `json:"model"`
6060
Messages []*MessageMeta `json:"messages"`
61+
Tools []llm.Tool `json:"tools,omitempty"`
6162
opt
6263
}
6364

@@ -77,6 +78,7 @@ func (anthropic *Client) Messages(ctx context.Context, context llm.Context, opts
7778
req, err := client.NewJSONRequest(reqMessages{
7879
Model: context.(*session).model.Name(),
7980
Messages: context.(*session).seq,
81+
Tools: opt.Tools(),
8082
opt: *opt,
8183
})
8284
if err != nil {

pkg/anthropic/model.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515

1616
// model is the implementation of the llm.Model interface
1717
type model struct {
18+
client *Client
1819
ModelMeta
1920
}
2021

@@ -38,14 +39,13 @@ func (anthropic *Client) Models(ctx context.Context) ([]llm.Model, error) {
3839

3940
// Get a model by name
4041
func (anthropic *Client) GetModel(ctx context.Context, name string) (llm.Model, error) {
41-
4242
var response ModelMeta
4343
if err := anthropic.DoWithContext(ctx, nil, &response, client.OptPath("models", name)); err != nil {
4444
return nil, err
4545
}
4646

4747
// Return success
48-
return &model{ModelMeta: response}, nil
48+
return &model{client: anthropic, ModelMeta: response}, nil
4949
}
5050

5151
// List models
@@ -68,6 +68,7 @@ func (anthropic *Client) ListModels(ctx context.Context) ([]llm.Model, error) {
6868
// Convert to llm.Model
6969
for _, meta := range response.Body {
7070
result = append(result, &model{
71+
client: anthropic,
7172
ModelMeta: meta,
7273
})
7374
}

pkg/anthropic/opt.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
// Packages
77
llm "github.com/mutablelogic/go-llm"
8+
tool "github.com/mutablelogic/go-llm/pkg/tool"
89
)
910

1011
////////////////////////////////////////////////////////////////////////////////
@@ -19,10 +20,10 @@ type opt struct {
1920
Temperature float64 `json:"temperature,omitempty"`
2021
TopK uint `json:"top_k,omitempty"`
2122
TopP float64 `json:"top_p,omitempty"`
22-
Tools []*Tool `json:"tools,omitempty"`
2323

2424
data []*Content // Additional message content
2525
callback func(*Response) // Streaming callback
26+
toolkit *tool.ToolKit // Toolkit for tools
2627
}
2728

2829
type optmetadata struct {
@@ -42,6 +43,17 @@ func apply(opts ...llm.Opt) (*opt, error) {
4243
return o, nil
4344
}
4445

46+
////////////////////////////////////////////////////////////////////////////////
47+
// PUBLIC METHODS
48+
49+
func (o *opt) Tools() []llm.Tool {
50+
if o.toolkit == nil {
51+
return nil
52+
} else {
53+
return o.toolkit.Tools()
54+
}
55+
}
56+
4557
////////////////////////////////////////////////////////////////////////////////
4658
// OPTIONS
4759

@@ -122,11 +134,11 @@ func WithTopK(v uint) llm.Opt {
122134
}
123135
}
124136

125-
// Messages: Append a tool to the request.
126-
func WithTool(v *Tool) llm.Opt {
137+
// Messages: Append a toolkit to the request
138+
func WithToolKit(v *tool.ToolKit) llm.Opt {
127139
return func(o any) error {
128140
if v != nil {
129-
o.(*opt).Tools = append(o.(*opt).Tools, v)
141+
o.(*opt).toolkit = v
130142
}
131143
return nil
132144
}

pkg/anthropic/session.go

Lines changed: 85 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,39 +22,29 @@ var _ llm.Context = (*session)(nil)
2222
///////////////////////////////////////////////////////////////////////////////
2323
// LIFECYCLE
2424

25-
// Return am empty session context object for the model,
26-
// setting session options
25+
// Return an empty session context object for the model, setting session options
2726
func (model *model) Context(opts ...llm.Opt) llm.Context {
2827
return &session{
2928
model: model,
3029
opts: opts,
3130
}
3231
}
3332

34-
// Convenience method to create a session context object
35-
// with a user prompt, which panics on error
33+
// Convenience method to create a session context object with a user prompt, which
34+
// panics on error
3635
func (model *model) UserPrompt(prompt string, opts ...llm.Opt) llm.Context {
37-
// Apply attachments
38-
opt, err := apply(opts...)
36+
context := model.Context(opts...)
37+
38+
meta, err := userPrompt(prompt, opts...)
3939
if err != nil {
4040
panic(err)
4141
}
4242

43-
meta := MessageMeta{
44-
Role: "user",
45-
Content: make([]*Content, 1, len(opt.data)+1),
46-
}
47-
48-
// Append the text
49-
meta.Content[0] = NewTextContent(prompt)
50-
51-
// Append any additional data
52-
for _, data := range opt.data {
53-
meta.Content = append(meta.Content, data)
54-
}
43+
// Add to the sequence
44+
context.(*session).seq = append(context.(*session).seq, meta)
5545

5646
// Return success
57-
return nil
47+
return context
5848
}
5949

6050
///////////////////////////////////////////////////////////////////////////////
@@ -98,14 +88,85 @@ func (session *session) Text() string {
9888
return string(data)
9989
}
10090

101-
// Generate a response from a user prompt (with attachments and
91+
// Return the current session tool calls, or empty if no tool calls were made
92+
func (session *session) ToolCalls() []llm.ToolCall {
93+
// Sanity check for tool call
94+
if len(session.seq) == 0 {
95+
return nil
96+
}
97+
meta := session.seq[len(session.seq)-1]
98+
if meta.Role != "assistant" {
99+
return nil
100+
}
101+
102+
// Gather tool calls
103+
var result []llm.ToolCall
104+
for _, content := range meta.Content {
105+
if content.Type == "tool_use" {
106+
result = append(result, NewToolCall(content))
107+
}
108+
}
109+
return result
110+
}
111+
112+
// Generate a response from a user prompt (with attachments) and
102113
// other empheral options
103-
func (session *session) FromUser(context.Context, string, ...llm.Opt) (llm.Context, error) {
104-
return nil, llm.ErrNotImplemented
114+
func (session *session) FromUser(ctx context.Context, prompt string, opts ...llm.Opt) error {
115+
// Append the user prompt to the sequence
116+
meta, err := userPrompt(prompt, opts...)
117+
if err != nil {
118+
return err
119+
} else {
120+
session.seq = append(session.seq, meta)
121+
}
122+
123+
// The options come from the session options and the user options
124+
chatopts := make([]llm.Opt, 0, len(session.opts)+len(opts))
125+
chatopts = append(chatopts, session.opts...)
126+
chatopts = append(chatopts, opts...)
127+
128+
// Call the 'chat' method
129+
client := session.model.client
130+
r, err := client.Messages(ctx, session, chatopts...)
131+
if err != nil {
132+
return err
133+
} else {
134+
session.seq = append(session.seq, &r.MessageMeta)
135+
}
136+
137+
// Return success
138+
return nil
105139
}
106140

107141
// Generate a response from a tool, passing the call identifier or
108142
// function name, and the result
109-
func (session *session) FromTool(context.Context, string, any, ...llm.Opt) (llm.Context, error) {
110-
return nil, llm.ErrNotImplemented
143+
func (session *session) FromTool(context.Context, string, any, ...llm.Opt) error {
144+
return llm.ErrNotImplemented
145+
}
146+
147+
///////////////////////////////////////////////////////////////////////////////
148+
// PRIVATE METHODS
149+
150+
func userPrompt(prompt string, opts ...llm.Opt) (*MessageMeta, error) {
151+
// Apply attachments
152+
opt, err := apply(opts...)
153+
if err != nil {
154+
return nil, err
155+
}
156+
157+
meta := MessageMeta{
158+
Role: "user",
159+
Content: make([]*Content, 1, len(opt.data)+1),
160+
}
161+
162+
// Append the text
163+
meta.Content[0] = NewTextContent(prompt)
164+
165+
// Append any additional data
166+
for _, data := range opt.data {
167+
meta.Content = append(meta.Content, data)
168+
}
169+
170+
// Return success
171+
return &meta, nil
111172
}

pkg/anthropic/session_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package anthropic_test
2+
3+
import (
4+
"context"
5+
"os"
6+
"testing"
7+
8+
// Packages
9+
opts "github.com/mutablelogic/go-client"
10+
anthropic "github.com/mutablelogic/go-llm/pkg/anthropic"
11+
assert "github.com/stretchr/testify/assert"
12+
)
13+
14+
func Test_session_001(t *testing.T) {
15+
client, err := anthropic.New(GetApiKey(t), opts.OptTrace(os.Stderr, true))
16+
if err != nil {
17+
t.FailNow()
18+
}
19+
20+
model, err := client.GetModel(context.TODO(), "claude-3-haiku-20240307")
21+
if err != nil {
22+
t.FailNow()
23+
}
24+
25+
// Session with a single user prompt - streaming
26+
t.Run("stream", func(t *testing.T) {
27+
assert := assert.New(t)
28+
session := model.Context(anthropic.WithStream(func(stream *anthropic.Response) {
29+
t.Log("SESSION DELTA", stream)
30+
}))
31+
assert.NotNil(session)
32+
33+
err := session.FromUser(context.TODO(), "Why is the grass green?")
34+
if !assert.NoError(err) {
35+
t.FailNow()
36+
}
37+
assert.Equal("assistant", session.Role())
38+
assert.NotEmpty(session.Text())
39+
})
40+
41+
// Session with a single user prompt - not streaming
42+
t.Run("nostream", func(t *testing.T) {
43+
assert := assert.New(t)
44+
session := model.Context()
45+
assert.NotNil(session)
46+
47+
err := session.FromUser(context.TODO(), "Why is the sky blue?")
48+
if !assert.NoError(err) {
49+
t.FailNow()
50+
}
51+
assert.Equal("assistant", session.Role())
52+
assert.NotEmpty(session.Text())
53+
})
54+
}
55+
56+
func Test_session_002(t *testing.T) {
57+
client, err := anthropic.New(GetApiKey(t), opts.OptTrace(os.Stderr, true))
58+
if err != nil {
59+
t.FailNow()
60+
}
61+
62+
model, err := client.GetModel(context.TODO(), "claude-3-haiku-20240307")
63+
if err != nil {
64+
t.FailNow()
65+
}
66+
67+
// Session with a tool call
68+
t.Run("toolcall", func(t *testing.T) {
69+
assert := assert.New(t)
70+
71+
tool, err := anthropic.NewTool("get_weather", "Return the current weather", nil)
72+
if !assert.NoError(err) {
73+
t.FailNow()
74+
}
75+
76+
session := model.Context(anthropic.WithTool(tool))
77+
assert.NotNil(session)
78+
79+
err = session.FromUser(context.TODO(), "What is today's weather?")
80+
if !assert.NoError(err) {
81+
t.FailNow()
82+
}
83+
84+
toolcalls := session.ToolCalls()
85+
assert.NotEmpty(toolcalls)
86+
t.Log(toolcalls)
87+
})
88+
}

0 commit comments

Comments
 (0)