Skip to content

Commit c6feba6

Browse files
committed
Added tool calling
1 parent edcb55a commit c6feba6

File tree

12 files changed

+788
-125
lines changed

12 files changed

+788
-125
lines changed

README.md

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,20 @@
22

33
Large Language Model API interface. This is a simple API interface for large language models
44
which run on [Ollama](https://github.com/ollama/ollama/blob/main/docs/api.md),
5-
[Anthopic](https://docs.anthropic.com/en/api/getting-started) and [Mistral](https://docs.mistral.ai/).
5+
[Anthopic](https://docs.anthropic.com/en/api/getting-started) and [Mistral](https://docs.mistral.ai/)
6+
(OpenAI might be added later).
67

78
The module includes the ability to utilize:
89

910
* Maintaining a session of messages
10-
* Tool calling support
11-
* Creating embeddings from text
11+
* Tool calling support, including using your own tools (aka Tool plugins)
12+
* Creating embedding vectors from text
1213
* Streaming responses
14+
* Multi-modal support (aka, Images and Attachments)
1315

1416
There is a command-line tool included in the module which can be used to interact with the API.
15-
For example,
17+
If you have docker installed, you can use the following command to run the tool, without
18+
installation:
1619

1720
```bash
1821
# Display help
@@ -21,12 +24,13 @@ docker run ghcr.io/mutablelogic/go-llm:latest --help
2124
# Interact with Claude to retrieve news headlines, assuming
2225
# you have an API key for Anthropic and NewsAPI
2326
docker run \
24-
--interactive -e ANTHROPIC_API_KEY -e NEWSAPI_KEY \
27+
--interactive -e MISTRAL_API_KEY -e NEWSAPI_KEY \
2528
ghcr.io/mutablelogic/go-llm:latest \
26-
chat claude-3-5-haiku-20241022
29+
chat claude-3-5-haiku-20241022 --prompt "What is the latest news?"
2730
```
2831

29-
See below for more information on how to use the command-line tool.
32+
See below for more information on how to use the command-line tool (or how to install it
33+
if you have a `go` compiler).
3034

3135
## Programmatic Usage
3236

@@ -46,7 +50,7 @@ import (
4650
)
4751

4852
func main() {
49-
// Create a new agent
53+
// Create a new agent - replace the URL with the one to your Ollama instance
5054
agent, err := ollama.New("https://ollama.com/api/v1/")
5155
if err != nil {
5256
panic(err)
@@ -57,7 +61,7 @@ func main() {
5761

5862
To create an
5963
[Anthropic](https://pkg.go.dev/github.com/mutablelogic/go-llm/pkg/anthropic)
60-
agent,
64+
agent with an API key stored as an environment variable,
6165

6266
```go
6367
import (
@@ -66,7 +70,7 @@ import (
6670

6771
func main() {
6872
// Create a new agent
69-
agent, err := anthropic.New(os.Getev("ANTHROPIC_API_KEY"))
73+
agent, err := anthropic.New(os.Getenv("ANTHROPIC_API_KEY"))
7074
if err != nil {
7175
panic(err)
7276
}
@@ -83,7 +87,7 @@ import (
8387

8488
func main() {
8589
// Create a new agent
86-
agent, err := mistral.New(os.Getev("MISTRAL_API_KEY"))
90+
agent, err := mistral.New(os.Getenv("MISTRAL_API_KEY"))
8791
if err != nil {
8892
panic(err)
8993
}
@@ -94,6 +98,28 @@ func main() {
9498
You can append options to the agent creation to set the client/server communication options,
9599
such as user agent strings, timeouts, debugging, rate limiting, adding custom headers, etc. See [here](https://pkg.go.dev/github.com/mutablelogic/go-client#readme-basic-usage) for more information.
96100

101+
There is also an _aggregated_ agent which can be used to interact with multiple providers at once. This is useful if you want
102+
to use models from different providers simultaneously.
103+
104+
```go
105+
import (
106+
"github.com/mutablelogic/go-llm/pkg/agent"
107+
)
108+
109+
func main() {
110+
// Create a new agent which aggregates multiple providers
111+
agent, err := agent.New(
112+
agent.WithAnthropic(os.Getenv("ANTHROPIC_API_KEY")),
113+
agent.WithMistral(os.Getenv("MISTRAL_API_KEY")),
114+
agent.WithOllama(os.Getenv("OLLAMA_URL")),
115+
)
116+
if err != nil {
117+
panic(err)
118+
}
119+
// ...
120+
}
121+
```
122+
97123
### Chat Sessions
98124

99125
You create a **chat session** with a model as follows,
@@ -120,6 +146,9 @@ func session(ctx context.Context, agent llm.Agent) error {
120146
}
121147
```
122148

149+
The `Context` object will continue to store the current session and options, and will
150+
ensure the session is maintained across multiple calls.
151+
123152
### Embedding Generation
124153

125154
TODO
@@ -146,16 +175,16 @@ type Model interface {
146175
// Set session-wide options
147176
Context(...Opt) Context
148177

149-
// Add attachments (images, PDF's) to a user prompt
178+
// Add attachments (images, PDF's) to a user prompt for completion
150179
UserPrompt(string, ...Opt) Context
151180

152-
// Set embedding options
181+
// Create an embedding vector with embedding options
153182
Embedding(context.Context, string, ...Opt) ([]float64, error)
154183
}
155184

156185
type Context interface {
157186
// Add single-use options when calling the model, which override
158-
// session options. You can also attach files to a user prompt.
187+
// session options. You can attach files to a user prompt.
159188
FromUser(context.Context, string, ...Opt) error
160189
}
161190
```

attachment.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
package llm
22

33
import (
4+
"encoding/base64"
5+
"encoding/json"
46
"io"
7+
"mime"
8+
"net/http"
59
"os"
10+
"path/filepath"
611
)
712

813
///////////////////////////////////////////////////////////////////////////////
@@ -31,6 +36,25 @@ func ReadAttachment(r io.Reader) (*Attachment, error) {
3136
return &Attachment{filename: filename, data: data}, nil
3237
}
3338

39+
////////////////////////////////////////////////////////////////////////////////
40+
// STRINGIFY
41+
42+
func (a *Attachment) String() string {
43+
var j struct {
44+
Filename string `json:"filename"`
45+
Type string `json:"type"`
46+
Bytes uint64 `json:"bytes"`
47+
}
48+
j.Filename = a.filename
49+
j.Type = a.Type()
50+
j.Bytes = uint64(len(a.data))
51+
data, err := json.MarshalIndent(j, "", " ")
52+
if err != nil {
53+
return err.Error()
54+
}
55+
return string(data)
56+
}
57+
3458
////////////////////////////////////////////////////////////////////////////////
3559
// PUBLIC METHODS
3660

@@ -41,3 +65,17 @@ func (a *Attachment) Filename() string {
4165
func (a *Attachment) Data() []byte {
4266
return a.data
4367
}
68+
69+
func (a *Attachment) Type() string {
70+
// Mimetype based on content
71+
mimetype := http.DetectContentType(a.data)
72+
if mimetype == "application/octet-stream" && a.filename != "" {
73+
// Detect mimetype from extension
74+
mimetype = mime.TypeByExtension(filepath.Ext(a.filename))
75+
}
76+
return mimetype
77+
}
78+
79+
func (a *Attachment) Url() string {
80+
return "data:" + a.Type() + ";base64," + base64.StdEncoding.EncodeToString(a.data)
81+
}

opt.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package llm
22

33
import (
4+
"encoding/json"
45
"io"
56
"time"
67
)
@@ -13,6 +14,7 @@ type Opt func(*Opts) error
1314

1415
// set of options
1516
type Opts struct {
17+
prompt bool
1618
agents map[string]Agent // Set of agents
1719
toolkit ToolKit // Toolkit for tools
1820
callback func(Completion) // Streaming callback
@@ -26,7 +28,22 @@ type Opts struct {
2628

2729
// ApplyOpts returns a structure of options
2830
func ApplyOpts(opts ...Opt) (*Opts, error) {
31+
return applyOpts(false, opts...)
32+
}
33+
34+
// ApplyPromptOpts returns a structure of options for a prompt
35+
func ApplyPromptOpts(opts ...Opt) (*Opts, error) {
36+
if opt, err := applyOpts(true, opts...); err != nil {
37+
return nil, err
38+
} else {
39+
return opt, nil
40+
}
41+
}
42+
43+
// ApplySessionOpts returns a structure of options
44+
func applyOpts(prompt bool, opts ...Opt) (*Opts, error) {
2945
o := new(Opts)
46+
o.prompt = prompt
3047
o.agents = make(map[string]Agent)
3148
o.options = make(map[string]any)
3249
for _, opt := range opts {
@@ -37,6 +54,33 @@ func ApplyOpts(opts ...Opt) (*Opts, error) {
3754
return o, nil
3855
}
3956

57+
///////////////////////////////////////////////////////////////////////////////
58+
// STRINGIFY
59+
60+
func (o Opts) MarshalJSON() ([]byte, error) {
61+
var j struct {
62+
ToolKit ToolKit `json:"toolkit,omitempty"`
63+
Agents map[string]Agent `json:"agents,omitempty"`
64+
System string `json:"system,omitempty"`
65+
Attachments []*Attachment `json:"attachments,omitempty"`
66+
Options map[string]any `json:"options,omitempty"`
67+
}
68+
j.ToolKit = o.toolkit
69+
j.Agents = o.agents
70+
j.Attachments = o.attachments
71+
j.System = o.system
72+
j.Options = o.options
73+
return json.Marshal(j)
74+
}
75+
76+
func (o Opts) String() string {
77+
data, err := json.Marshal(o)
78+
if err != nil {
79+
return err.Error()
80+
}
81+
return string(data)
82+
}
83+
4084
///////////////////////////////////////////////////////////////////////////////
4185
// PUBLIC METHODS - PROPERTIES
4286

@@ -182,6 +226,10 @@ func WithAgent(agent Agent) Opt {
182226
// Create an attachment
183227
func WithAttachment(r io.Reader) Opt {
184228
return func(o *Opts) error {
229+
// Only attach if prompt is set
230+
if !o.prompt {
231+
return nil
232+
}
185233
if attachment, err := ReadAttachment(r); err != nil {
186234
return err
187235
} else {

0 commit comments

Comments
 (0)