From 56d152d41eaf44d9d729df94775ebd57bc16e735 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Sat, 1 Feb 2025 17:22:33 +0100 Subject: [PATCH 01/15] Updated --- agent.go | 4 ++++ pkg/agent/agent.go | 9 +++++++++ pkg/anthropic/client.go | 6 +++++- pkg/anthropic/model.go | 33 ++++++++++++++++++++++++++++++++- pkg/anthropic/session_test.go | 3 ++- pkg/ollama/chat_test.go | 2 +- pkg/ollama/model.go | 9 +++++++++ pkg/ollama/session_test.go | 2 +- 8 files changed, 63 insertions(+), 5 deletions(-) diff --git a/agent.go b/agent.go index b7658dd..1e75eea 100644 --- a/agent.go +++ b/agent.go @@ -11,4 +11,8 @@ type Agent interface { // Return the models Models(context.Context) ([]Model, error) + + // Return a model by name, or nil if not found. + // Panics on error. + Model(context.Context, string) Model } diff --git a/pkg/agent/agent.go b/pkg/agent/agent.go index 312c327..cd50c87 100644 --- a/pkg/agent/agent.go +++ b/pkg/agent/agent.go @@ -105,6 +105,15 @@ func (a *Agent) Models(ctx context.Context) ([]llm.Model, error) { return a.ListModels(ctx) } +// Return a model +func (a *Agent) Model(ctx context.Context, name string) llm.Model { + model, err := a.GetModel(ctx, name) + if err != nil { + panic(err) + } + return model +} + // Return the models from list of agents func (a *Agent) ListModels(ctx context.Context, names ...string) ([]llm.Model, error) { var result error diff --git a/pkg/anthropic/client.go b/pkg/anthropic/client.go index 6f4a13b..8bb617a 100644 --- a/pkg/anthropic/client.go +++ b/pkg/anthropic/client.go @@ -14,6 +14,7 @@ import ( type Client struct { *client.Client + cache map[string]llm.Model } var _ llm.Agent = (*Client)(nil) @@ -41,7 +42,10 @@ func New(ApiKey string, opts ...client.ClientOpt) (*Client, error) { } // Return the client - return &Client{client}, nil + return &Client{ + Client: client, + cache: make(map[string]llm.Model), + }, nil } /////////////////////////////////////////////////////////////////////////////// diff --git a/pkg/anthropic/model.go b/pkg/anthropic/model.go index 288cb47..04baa49 100644 --- a/pkg/anthropic/model.go +++ b/pkg/anthropic/model.go @@ -34,7 +34,38 @@ type ModelMeta struct { // Agent interface func (anthropic *Client) Models(ctx context.Context) ([]llm.Model, error) { - return anthropic.ListModels(ctx) + // Cache models + if len(anthropic.cache) == 0 { + models, err := anthropic.ListModels(ctx) + if err != nil { + return nil, err + } + for _, model := range models { + name := model.Name() + anthropic.cache[name] = model + } + } + + // Return models + result := make([]llm.Model, 0, len(anthropic.cache)) + for _, model := range anthropic.cache { + result = append(result, model) + } + return result, nil +} + +// Agent interface +func (anthropic *Client) Model(ctx context.Context, model string) llm.Model { + // Cache models + if len(anthropic.cache) == 0 { + _, err := anthropic.Models(ctx) + if err != nil { + panic(err) + } + } + + // Return model + return anthropic.cache[model] } // Get a model by name diff --git a/pkg/anthropic/session_test.go b/pkg/anthropic/session_test.go index 78c01de..e27c078 100644 --- a/pkg/anthropic/session_test.go +++ b/pkg/anthropic/session_test.go @@ -83,9 +83,10 @@ func Test_session_002(t *testing.T) { t.FailNow() } - err := toolkit.Run(context.TODO(), session.ToolCalls()...) + result, err := toolkit.Run(context.TODO(), session.ToolCalls()...) if !assert.NoError(err) { t.FailNow() } + assert.NotEmpty(result) }) } diff --git a/pkg/ollama/chat_test.go b/pkg/ollama/chat_test.go index cbc77c5..cc746dd 100644 --- a/pkg/ollama/chat_test.go +++ b/pkg/ollama/chat_test.go @@ -31,7 +31,7 @@ func Test_chat_001(t *testing.T) { t.Run("ChatStream", func(t *testing.T) { assert := assert.New(t) - response, err := client.Chat(context.TODO(), model.UserPrompt("why is the sky blue?"), llm.WithStream(func(stream llm.Context) { + response, err := client.Chat(context.TODO(), model.UserPrompt("why is the sky blue?"), llm.WithStream(func(stream llm.ContextContent) { t.Log(stream) })) if !assert.NoError(err) { diff --git a/pkg/ollama/model.go b/pkg/ollama/model.go index 246d68d..fa147e7 100644 --- a/pkg/ollama/model.go +++ b/pkg/ollama/model.go @@ -91,6 +91,15 @@ func (ollama *Client) Models(ctx context.Context) ([]llm.Model, error) { return ollama.ListModels(ctx) } +// Agent interface +func (ollama *Client) Model(ctx context.Context, name string) llm.Model { + model, err := ollama.GetModel(ctx, name) + if err != nil { + panic(err) + } + return model +} + // List models func (ollama *Client) ListModels(ctx context.Context) ([]llm.Model, error) { type respListModel struct { diff --git a/pkg/ollama/session_test.go b/pkg/ollama/session_test.go index e4df9d4..bc6e6a7 100644 --- a/pkg/ollama/session_test.go +++ b/pkg/ollama/session_test.go @@ -28,7 +28,7 @@ func Test_session_001(t *testing.T) { // Session with a single user prompt - streaming t.Run("stream", func(t *testing.T) { assert := assert.New(t) - session := model.Context(llm.WithStream(func(stream llm.Context) { + session := model.Context(llm.WithStream(func(stream llm.ContextContent) { t.Log("SESSION DELTA", stream) })) assert.NotNil(session) From 48e4c425f5abec84dbf94a2b93ff0fa37147331d Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Sat, 1 Feb 2025 17:51:28 +0100 Subject: [PATCH 02/15] Added mistral --- cmd/agent/main.go | 8 ++++ pkg/agent/opt.go | 12 +++++ pkg/mistral/client.go | 68 ++++++++++++++++++++++++++ pkg/mistral/client_test.go | 31 ++++++++++++ pkg/mistral/model.go | 98 ++++++++++++++++++++++++++++++++++++++ pkg/mistral/model_test.go | 26 ++++++++++ 6 files changed, 243 insertions(+) create mode 100644 pkg/mistral/client.go create mode 100644 pkg/mistral/client_test.go create mode 100644 pkg/mistral/model.go create mode 100644 pkg/mistral/model_test.go diff --git a/cmd/agent/main.go b/cmd/agent/main.go index 8d0970c..c7d5088 100644 --- a/cmd/agent/main.go +++ b/cmd/agent/main.go @@ -27,6 +27,7 @@ type Globals struct { // Agents Ollama `embed:"" help:"Ollama configuration"` Anthropic `embed:"" help:"Anthropic configuration"` + Mistral `embed:"" help:"Mistral configuration"` // Tools NewsAPI `embed:"" help:"NewsAPI configuration"` @@ -46,6 +47,10 @@ type Anthropic struct { AnthropicKey string `env:"ANTHROPIC_API_KEY" help:"Anthropic API Key"` } +type Mistral struct { + MistralKey string `env:"MISTRAL_API_KEY" help:"Mistral API Key"` +} + type NewsAPI struct { NewsKey string `env:"NEWSAPI_KEY" help:"News API Key"` } @@ -105,6 +110,9 @@ func main() { if cli.AnthropicKey != "" { opts = append(opts, agent.WithAnthropic(cli.AnthropicKey, clientopts...)) } + if cli.MistralKey != "" { + opts = append(opts, agent.WithMistral(cli.MistralKey, clientopts...)) + } // Make a toolkit toolkit := tool.NewToolKit() diff --git a/pkg/agent/opt.go b/pkg/agent/opt.go index a316881..12f45a7 100644 --- a/pkg/agent/opt.go +++ b/pkg/agent/opt.go @@ -5,6 +5,7 @@ import ( client "github.com/mutablelogic/go-client" llm "github.com/mutablelogic/go-llm" anthropic "github.com/mutablelogic/go-llm/pkg/anthropic" + mistral "github.com/mutablelogic/go-llm/pkg/mistral" ollama "github.com/mutablelogic/go-llm/pkg/ollama" ) @@ -32,3 +33,14 @@ func WithAnthropic(key string, opts ...client.ClientOpt) llm.Opt { } } } + +func WithMistral(key string, opts ...client.ClientOpt) llm.Opt { + return func(o *llm.Opts) error { + client, err := mistral.New(key, opts...) + if err != nil { + return err + } else { + return llm.WithAgent(client)(o) + } + } +} diff --git a/pkg/mistral/client.go b/pkg/mistral/client.go new file mode 100644 index 0000000..36e993f --- /dev/null +++ b/pkg/mistral/client.go @@ -0,0 +1,68 @@ +/* +mistral implements an API client for mistral (https://docs.mistral.ai/api/) +*/ +package mistral + +import ( + // Packages + "context" + + "github.com/mutablelogic/go-client" + "github.com/mutablelogic/go-llm" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +type Client struct { + *client.Client +} + +var _ llm.Agent = (*Client)(nil) + +/////////////////////////////////////////////////////////////////////////////// +// GLOBALS + +const ( + endPoint = "https://api.mistral.ai/v1" + defaultName = "mistral" +) + +/////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +// Create a new client +func New(ApiKey string, opts ...client.ClientOpt) (*Client, error) { + // Create client + opts = append(opts, client.OptEndpoint(endPoint)) + opts = append(opts, client.OptReqToken(client.Token{ + Scheme: client.Bearer, + Value: ApiKey, + })) + client, err := client.New(opts...) + if err != nil { + return nil, err + } + + // Return the client + return &Client{client}, nil +} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Return the name of the agent +func (Client) Name() string { + return defaultName +} + +// Return the models +func (c *Client) Models(ctx context.Context) ([]llm.Model, error) { + return c.ListModels(ctx) +} + +// Return a model by name, or nil if not found. +// Panics on error. +func (c *Client) Model(ctx context.Context, name string) llm.Model { + return nil +} diff --git a/pkg/mistral/client_test.go b/pkg/mistral/client_test.go new file mode 100644 index 0000000..7f4a9d6 --- /dev/null +++ b/pkg/mistral/client_test.go @@ -0,0 +1,31 @@ +package mistral_test + +import ( + "os" + "testing" + + // Packages + opts "github.com/mutablelogic/go-client" + mistral "github.com/mutablelogic/go-llm/pkg/mistral" + assert "github.com/stretchr/testify/assert" +) + +func Test_client_001(t *testing.T) { + assert := assert.New(t) + client, err := mistral.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) + assert.NoError(err) + assert.NotNil(client) + t.Log(client) +} + +/////////////////////////////////////////////////////////////////////////////// +// ENVIRONMENT + +func GetApiKey(t *testing.T) string { + key := os.Getenv("MISTRAL_API_KEY") + if key == "" { + t.Skip("MISTRAL_API_KEY not set") + t.SkipNow() + } + return key +} diff --git a/pkg/mistral/model.go b/pkg/mistral/model.go new file mode 100644 index 0000000..ea6b6d0 --- /dev/null +++ b/pkg/mistral/model.go @@ -0,0 +1,98 @@ +package mistral + +import ( + "context" + "encoding/json" + + "github.com/mutablelogic/go-client" + "github.com/mutablelogic/go-llm" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +type model struct { + meta Model +} + +type Model struct { + Name string `json:"id"` + Description string `json:"description,omitempty"` + Type string `json:"type,omitempty"` + CreatedAt *uint64 `json:"created,omitempty"` + OwnedBy string `json:"owned_by,omitempty"` + MaxContextLength uint64 `json:"max_context_length,omitempty"` + Aliases []string `json:"aliases,omitempty"` + Deprecation *string `json:"deprecation,omitempty"` + DefaultModelTemperature *float64 `json:"default_model_temperature,omitempty"` + Capabilities struct { + CompletionChat bool `json:"completion_chat,omitempty"` + CompletionFim bool `json:"completion_fim,omitempty"` + FunctionCalling bool `json:"function_calling,omitempty"` + FineTuning bool `json:"fine_tuning,omitempty"` + Vision bool `json:"vision,omitempty"` + } `json:"capabilities,omitempty"` +} + +/////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +func (m model) MarshalJSON() ([]byte, error) { + return json.Marshal(m.meta) +} + +func (m model) String() string { + data, err := json.MarshalIndent(m, "", " ") + if err != nil { + return err.Error() + } + return string(data) +} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS - API + +// ListModels returns all the models +func (c *Client) ListModels(ctx context.Context) ([]llm.Model, error) { + // Response + var response struct { + Data []Model `json:"data"` + } + if err := c.DoWithContext(ctx, nil, &response, client.OptPath("models")); err != nil { + return nil, err + } + + // Make models + result := make([]llm.Model, 0, len(response.Data)) + for _, meta := range response.Data { + result = append(result, &model{meta: meta}) + } + + // Return models + return result, nil +} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS - MODEL + +// Return the name of the model +func (m model) Name() string { + return m.meta.Name +} + +// Return am empty session context object for the model, +// setting session options +func (m model) Context(...llm.Opt) llm.Context { + return nil +} + +// Convenience method to create a session context object +// with a user prompt +func (m model) UserPrompt(string, ...llm.Opt) llm.Context { + return nil +} + +// Embedding vector generation +func (m model) Embedding(context.Context, string, ...llm.Opt) ([]float64, error) { + return nil, llm.ErrNotImplemented +} diff --git a/pkg/mistral/model_test.go b/pkg/mistral/model_test.go new file mode 100644 index 0000000..2a4048d --- /dev/null +++ b/pkg/mistral/model_test.go @@ -0,0 +1,26 @@ +package mistral_test + +import ( + "context" + "encoding/json" + "os" + "testing" + + // Packages + opts "github.com/mutablelogic/go-client" + mistral "github.com/mutablelogic/go-llm/pkg/mistral" + assert "github.com/stretchr/testify/assert" +) + +func Test_models_001(t *testing.T) { + assert := assert.New(t) + client, err := mistral.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) + assert.NoError(err) + assert.NotNil(client) + response, err := client.ListModels(context.TODO()) + assert.NoError(err) + assert.NotEmpty(response) + data, err := json.MarshalIndent(response, "", " ") + assert.NoError(err) + t.Log(string(data)) +} From b827ecf42a3e2af440be9e4da1f70623201cd540 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Sun, 2 Feb 2025 08:38:04 +0100 Subject: [PATCH 03/15] Updated mistral and docs --- README.md | 59 ++++++++++- opt.go | 61 +++++++++++- pkg/anthropic/opt.go | 14 --- pkg/mistral/chat_completion.go | 116 ++++++++++++++++++++++ pkg/mistral/chat_completion_test.go | 103 +++++++++++++++++++ pkg/mistral/client.go | 29 +++++- pkg/mistral/message.go | 75 ++++++++++++++ pkg/mistral/model.go | 12 --- pkg/mistral/opt.go | 140 ++++++++++++++++++++++++++ pkg/mistral/session.go | 149 ++++++++++++++++++++++++++++ 10 files changed, 725 insertions(+), 33 deletions(-) create mode 100644 pkg/mistral/chat_completion.go create mode 100644 pkg/mistral/chat_completion_test.go create mode 100644 pkg/mistral/message.go create mode 100644 pkg/mistral/opt.go create mode 100644 pkg/mistral/session.go diff --git a/README.md b/README.md index f8de370..8cf6c2d 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,14 @@ # go-llm Large Language Model API interface. This is a simple API interface for large language models -which run on [Ollama](https://github.com/ollama/ollama/blob/main/docs/api.md) -and [Anthopic](https://docs.anthropic.com/en/api/getting-started). +which run on [Ollama](https://github.com/ollama/ollama/blob/main/docs/api.md), +[Anthopic](https://docs.anthropic.com/en/api/getting-started) and [Mistral](https://docs.mistral.ai/). The module includes the ability to utilize: * Maintaining a session of messages * Tool calling support +* Creating embeddings from text * Streaming responses There is a command-line tool included in the module which can be used to interact with the API. @@ -28,7 +29,12 @@ docker run \ ## Programmatic Usage See the documentation [here](https://pkg.go.dev/github.com/mutablelogic/go-llm) -for integration into your own Go programs. To create an +for integration into your own Go programs. + +### Agent Instantiation + +For each LLM provider, you create an agent which can be used to interact with the API. +To create an [Ollama](https://pkg.go.dev/github.com/mutablelogic/go-llm/pkg/anthropic) agent, @@ -66,6 +72,25 @@ func main() { } ``` +For Mistral models, you can use: + +```go +import ( + "github.com/mutablelogic/go-llm/pkg/mistral" +) + +func main() { + // Create a new agent + agent, err := mistral.New(os.Getev("MISTRAL_API_KEY")) + if err != nil { + panic(err) + } + // ... +} +``` + +### Chat Sessions + You create a **chat session** with a model as follows, ```go @@ -90,6 +115,34 @@ func session(ctx context.Context, agent llm.Agent) error { } ``` +## Options + +You can add options to sessions, or to prompts. Different providers and models support +different options. + +| Option | Ollama | Anthropic | Mistral | OpenAI | Description | +|--------|--------|-----------|---------|--------|-------------| +| `llm.WithTemperature(float64)` | Yes | Yes | Yes | - | What sampling temperature to use, between 0.0 and 1.0. Higher values like 0.7 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. | +| `llm.WithTopP(float64)` | Yes | Yes | Yes | - | Nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. | +| `llm.WithTopK(uint64)` | Yes | Yes | No | - | Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. | +| `llm.WithMaxTokens(uint64)` | - | Yes | Yes | - | The maximum number of tokens to generate in the response. | +| `llm.WithStream(func(llm.ContextContent))` | Can be enabled when tools are not used | Yes | Yes | - | Stream the response to a function. | +| `llm.WithToolChoice(string, string, ...)` | No | Yes | Use `auto`, `any`, `none`, `required` or a function name. Only the first argument is used. | - | The tool to use for the model. | +| `llm.WithToolKit(llm.ToolKit)` | Cannot be combined with streaming | Yes | Yes | - | The set of tools to use. | +| `llm.WithStopSequence(string, string, ...)` | Yes | Yes | Yes | - | Stop generation if one of these tokens is detected. | +| `llm.WithSystemPrompt(string)` | No | Yes | Yes | - | Set the system prompt for the model. | +| `llm.WithSeed(uint64)` | No | Yes | Yes | - | The seed to use for random sampling. If set, different calls will generate deterministic results. | +| `llm.WithFormat(string)` | No | Yes | Use `json_format` or `text` | - | The format of the response. For Mistral, you must also instruct the model to produce JSON yourself with a system or a user message. | +| `mistral.WithPresencePenalty(float64)` | - | - | Yes | - | Determines how much the model penalizes the repetition of words or phrases. A higher presence penalty encourages the model to use a wider variety of words and phrases, making the output more diverse and creative. | +| `mistral.WithFequencyPenalty(float64)` | - | - | Yes | - | Penalizes the repetition of words based on their frequency in the generated text. A higher frequency penalty discourages the model from repeating words that have already appeared frequently in the output, promoting diversity and reducing repetition. | +| `mistral.WithPrediction(string)` | - | - | Yes | - | Enable users to specify expected results, optimizing response times by leveraging known or predictable content. This approach is especially effective for updating text documents or code files with minimal changes, reducing latency while maintaining high-quality results. | +| `llm.WithSafePrompt()` | - | - | Yes | - | Whether to inject a safety prompt before all conversations. | +| `llm.WithNumCompletions(uint64)` | - | - | Yes | - | Number of completions to return for each request. | +| `llm.WithAttachment(io.Reader)` | Yes | Yes | Yes | - | Attach a file to a user prompt. It is the responsibility of the caller to close the reader. | +| `antropic.WithEphemeral()` | No | Yes | No | - | Attachments should be cached server-side | +| `antropic.WithCitations()` | No | Yes | No | - | Attachments should be used in citations | +| `antropic.WithUser(string)` | No | Yes | No | - | Indicate the user name for the request, for debugging | + ## Contributing & Distribution *This module is currently in development and subject to change*. Please do file diff --git a/opt.go b/opt.go index df91705..c22382c 100644 --- a/opt.go +++ b/opt.go @@ -216,13 +216,21 @@ func WithTopP(v float64) Opt { // Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more // diverse answers, while a lower value (e.g. 10) will be more conservative. -func WithTopK(v uint) Opt { +func WithTopK(v uint64) Opt { return func(o *Opts) error { o.Set("top_k", v) return nil } } +// The maximum number of tokens to generate in the completion. +func WithMaxTokens(v uint64) Opt { + return func(o *Opts) error { + o.Set("max_tokens", v) + return nil + } +} + // Set system prompt func WithSystemPrompt(v string) Opt { return func(o *Opts) error { @@ -230,3 +238,54 @@ func WithSystemPrompt(v string) Opt { return nil } } + +// Set stop sequence +func WithStopSequence(v ...string) Opt { + return func(o *Opts) error { + o.Set("stop", v) + return nil + } +} + +// Set random seed for deterministic behavior +func WithSeed(v uint64) Opt { + return func(o *Opts) error { + o.Set("seed", v) + return nil + } +} + +// Set format +func WithFormat(v any) Opt { + return func(o *Opts) error { + o.Set("format", v) + return nil + } +} + +// Set tool choices: can be auto, none, required, any or a list of tool names +func WithToolChoice(v ...string) Opt { + return func(o *Opts) error { + o.Set("tool_choice", v) + return nil + } +} + +// Number of completions to return for each request +func WithNumCompletions(v uint64) Opt { + return func(o *Opts) error { + if v < 1 || v > 8 { + return ErrBadParameter.With("num_completions must be between 1 and 8") + } + o.Set("num_completions", v) + return nil + } +} + +// Inject a safety prompt before all conversations. +func WithSafePrompt() Opt { + return func(o *Opts) error { + o.Set("safe_prompt", true) + return nil + } +} diff --git a/pkg/anthropic/opt.go b/pkg/anthropic/opt.go index 3f64a42..5461b59 100644 --- a/pkg/anthropic/opt.go +++ b/pkg/anthropic/opt.go @@ -17,13 +17,6 @@ type optmetadata struct { //////////////////////////////////////////////////////////////////////////////// // OPTIONS -func WithMaxTokens(v uint) llm.Opt { - return func(o *llm.Opts) error { - o.Set("max_tokens", v) - return nil - } -} - func WithUser(v string) llm.Opt { return func(o *llm.Opts) error { o.Set("user", v) @@ -31,13 +24,6 @@ func WithUser(v string) llm.Opt { } } -func WithStopSequences(v ...string) llm.Opt { - return func(o *llm.Opts) error { - o.Set("stop", v) - return nil - } -} - func WithEphemeral() llm.Opt { return func(o *llm.Opts) error { o.Set("ephemeral", true) diff --git a/pkg/mistral/chat_completion.go b/pkg/mistral/chat_completion.go new file mode 100644 index 0000000..ab1ecae --- /dev/null +++ b/pkg/mistral/chat_completion.go @@ -0,0 +1,116 @@ +package mistral + +import ( + "context" + "encoding/json" + + "github.com/mutablelogic/go-client" + "github.com/mutablelogic/go-llm" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +// Chat Completion Response +type Response struct { + Id string `json:"id"` + Type string `json:"object"` + Created uint64 `json:"created"` + Model string `json:"model"` + Choices []Choice `json:"choices"` + Metrics `json:"usage,omitempty"` +} + +// Response variation +type Choice struct { + Index uint64 `json:"index"` + Message MessageMeta `json:"message"` + Reason string `json:"finish_reason,omitempty"` +} + +// Metrics +type Metrics struct { + InputTokens uint64 `json:"prompt_tokens,omitempty"` + OutputTokens uint `json:"completion_tokens,omitempty"` + TotalTokens uint `json:"total_tokens,omitempty"` +} + +/////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +func (r Response) String() string { + data, err := json.MarshalIndent(r, "", " ") + if err != nil { + return err.Error() + } + return string(data) +} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +type reqChatCompletion struct { + Model string `json:"model"` + Temperature float64 `json:"temperature,omitempty"` + TopP float64 `json:"top_p,omitempty"` + MaxTokens uint64 `json:"max_tokens,omitempty"` + Stream bool `json:"stream,omitempty"` + StopSequences []string `json:"stop,omitempty"` + Seed uint64 `json:"random_seed,omitempty"` + Messages []*MessageMeta `json:"messages"` + Format any `json:"response_format,omitempty"` + Tools []llm.Tool `json:"tools,omitempty"` + ToolChoice any `json:"tool_choice,omitempty"` + PresencePenalty float64 `json:"presence_penalty,omitempty"` + FrequencyPenalty float64 `json:"frequency_penalty,omitempty"` + NumChoices uint64 `json:"n,omitempty"` + Prediction *Content `json:"prediction,omitempty"` + SafePrompt bool `json:"safe_prompt,omitempty"` +} + +func (mistral *Client) ChatCompletion(ctx context.Context, context llm.Context, opts ...llm.Opt) (*Response, error) { + // Apply options + opt, err := llm.ApplyOpts(opts...) + if err != nil { + return nil, err + } + + // Append the system prompt at the beginning + seq := make([]*MessageMeta, 0, len(context.(*session).seq)+1) + if system := opt.SystemPrompt(); system != "" { + seq = append(seq, systemPrompt(system)) + } + seq = append(seq, context.(*session).seq...) + + // Request + req, err := client.NewJSONRequest(reqChatCompletion{ + Model: context.(*session).model.Name(), + Temperature: optTemperature(opt), + TopP: optTopP(opt), + MaxTokens: optMaxTokens(opt), + Stream: optStream(opt), + StopSequences: optStopSequences(opt), + Seed: optSeed(opt), + Messages: seq, + Format: optFormat(opt), + Tools: optTools(mistral, opt), + ToolChoice: optToolChoice(opt), + PresencePenalty: optPresencePenalty(opt), + FrequencyPenalty: optFrequencyPenalty(opt), + NumChoices: optNumCompletions(opt), + Prediction: optPrediction(opt), + SafePrompt: optSafePrompt(opt), + }) + if err != nil { + return nil, err + } + + // Response + var response Response + if err := mistral.DoWithContext(ctx, req, &response, client.OptPath("chat", "completions")); err != nil { + return nil, err + } + + // Return success + return &response, nil +} diff --git a/pkg/mistral/chat_completion_test.go b/pkg/mistral/chat_completion_test.go new file mode 100644 index 0000000..141dafd --- /dev/null +++ b/pkg/mistral/chat_completion_test.go @@ -0,0 +1,103 @@ +package mistral_test + +import ( + "context" + "os" + "testing" + + // Packages + opts "github.com/mutablelogic/go-client" + "github.com/mutablelogic/go-llm" + mistral "github.com/mutablelogic/go-llm/pkg/mistral" + assert "github.com/stretchr/testify/assert" +) + +func Test_chat_001(t *testing.T) { + assert := assert.New(t) + + client, err := mistral.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) + assert.NoError(err) + + model := client.Model(context.TODO(), "mistral-small-latest") + if assert.NotNil(model) { + response, err := client.ChatCompletion(context.TODO(), model.UserPrompt("Hello, how are you?")) + assert.NoError(err) + assert.NotEmpty(response) + t.Log(response) + } +} + +func Test_chat_002(t *testing.T) { + assert := assert.New(t) + + client, err := mistral.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) + assert.NoError(err) + + model := client.Model(context.TODO(), "mistral-large-latest") + if !assert.NotNil(model) { + t.FailNow() + } + + t.Run("Temperature", func(t *testing.T) { + _, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithTemperature(0.5)) + assert.NoError(err) + }) + t.Run("TopP", func(t *testing.T) { + _, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithTopP(0.5)) + assert.NoError(err) + }) + t.Run("MaxTokens", func(t *testing.T) { + _, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithMaxTokens(10)) + assert.NoError(err) + }) + t.Run("Stream", func(t *testing.T) { + _, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithStream(func(r llm.ContextContent) { + t.Log(r.Role(), "=>", r.Text()) + })) + assert.NoError(err) + }) + t.Run("Stop", func(t *testing.T) { + _, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithStopSequence("STOP")) + assert.NoError(err) + }) + t.Run("System", func(t *testing.T) { + _, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithSystemPrompt("You are shakespearian")) + assert.NoError(err) + }) + t.Run("Seed", func(t *testing.T) { + _, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithSeed(123)) + assert.NoError(err) + }) + t.Run("Format", func(t *testing.T) { + _, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithFormat("json_object"), llm.WithSystemPrompt("Return a JSON object")) + assert.NoError(err) + }) + t.Run("ToolChoiceAuto", func(t *testing.T) { + _, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithToolChoice("auto")) + assert.NoError(err) + }) + t.Run("ToolChoiceFunc", func(t *testing.T) { + _, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithToolChoice("get_weather")) + assert.NoError(err) + }) + t.Run("PresencePenalty", func(t *testing.T) { + _, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), mistral.WithPresencePenalty(-2)) + assert.NoError(err) + }) + t.Run("FrequencyPenalty", func(t *testing.T) { + _, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), mistral.WithFrequencyPenalty(-2)) + assert.NoError(err) + }) + t.Run("NumChoices", func(t *testing.T) { + _, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithNumCompletions(3)) + assert.NoError(err) + }) + t.Run("Prediction", func(t *testing.T) { + _, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), mistral.WithPrediction("The temperature in London today is")) + assert.NoError(err) + }) + t.Run("SafePrompt", func(t *testing.T) { + _, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithSafePrompt()) + assert.NoError(err) + }) +} diff --git a/pkg/mistral/client.go b/pkg/mistral/client.go index 36e993f..7e643dc 100644 --- a/pkg/mistral/client.go +++ b/pkg/mistral/client.go @@ -16,6 +16,7 @@ import ( type Client struct { *client.Client + cache map[string]llm.Model } var _ llm.Agent = (*Client)(nil) @@ -45,7 +46,7 @@ func New(ApiKey string, opts ...client.ClientOpt) (*Client, error) { } // Return the client - return &Client{client}, nil + return &Client{client, nil}, nil } /////////////////////////////////////////////////////////////////////////////// @@ -58,11 +59,33 @@ func (Client) Name() string { // Return the models func (c *Client) Models(ctx context.Context) ([]llm.Model, error) { - return c.ListModels(ctx) + // Cache models + if c.cache == nil { + models, err := c.ListModels(ctx) + if err != nil { + return nil, err + } + c.cache = make(map[string]llm.Model, len(models)) + for _, model := range models { + c.cache[model.Name()] = model + } + } + + // Return models + result := make([]llm.Model, 0, len(c.cache)) + for _, model := range c.cache { + result = append(result, model) + } + return result, nil } // Return a model by name, or nil if not found. // Panics on error. func (c *Client) Model(ctx context.Context, name string) llm.Model { - return nil + if c.cache == nil { + if _, err := c.Models(ctx); err != nil { + panic(err) + } + } + return c.cache[name] } diff --git a/pkg/mistral/message.go b/pkg/mistral/message.go new file mode 100644 index 0000000..84bfef5 --- /dev/null +++ b/pkg/mistral/message.go @@ -0,0 +1,75 @@ +package mistral + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +// Message with text or object content +type MessageMeta struct { + Role string `json:"role"` // assistant, user, tool, system + Prefix bool `json:"prefix,omitempty"` + Content any `json:"content,omitempty"` + // ContentTools +} + +type Content struct { + Type string `json:"type"` // text, reference, image_url + *Text `json:"text,omitempty"` // text content + *Prediction `json:"content,omitempty"` // prediction + *Image `json:"image_url,omitempty"` // image_url +} + +// text content +type Text string + +// text content +type Prediction string + +// either a URL or "data:image/png;base64," followed by the base64 encoded image +type Image string + +/////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +// Return a Content object with text content +func NewContent(t, v, p string) *Content { + content := new(Content) + content.Type = t + if v != "" { + content.Text = (*Text)(&v) + } + if p != "" { + content.Prediction = (*Prediction)(&p) + } + return content +} + +// Return a Content object with text content +func NewTextContent(v string) *Content { + return NewContent("text", v, "") +} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Return the text content +func (m MessageMeta) Text() string { + if text, ok := m.Content.(string); ok { + return text + } + return "" +} + +/* + if arr, ok := m.Content.([]Content); ok { + if len(m.Content) == 0 { + return "" + } + var text []string + for _, content := range m.Content { + if content.Type == "text" && content.Text != nil { + text = append(text, string(*content.Text)) + } + } + return strings.Join(text, "\n") +} +*/ diff --git a/pkg/mistral/model.go b/pkg/mistral/model.go index ea6b6d0..f981ee4 100644 --- a/pkg/mistral/model.go +++ b/pkg/mistral/model.go @@ -80,18 +80,6 @@ func (m model) Name() string { return m.meta.Name } -// Return am empty session context object for the model, -// setting session options -func (m model) Context(...llm.Opt) llm.Context { - return nil -} - -// Convenience method to create a session context object -// with a user prompt -func (m model) UserPrompt(string, ...llm.Opt) llm.Context { - return nil -} - // Embedding vector generation func (m model) Embedding(context.Context, string, ...llm.Opt) ([]float64, error) { return nil, llm.ErrNotImplemented diff --git a/pkg/mistral/opt.go b/pkg/mistral/opt.go new file mode 100644 index 0000000..b409470 --- /dev/null +++ b/pkg/mistral/opt.go @@ -0,0 +1,140 @@ +package mistral + +import ( + "strings" + + "github.com/mutablelogic/go-llm" +) + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +func WithPresencePenalty(v float64) llm.Opt { + return func(o *llm.Opts) error { + if v < -2 || v > 2 { + return llm.ErrBadParameter.With("presence_penalty") + } + o.Set("presence_penalty", v) + return nil + } +} + +func WithFrequencyPenalty(v float64) llm.Opt { + return func(o *llm.Opts) error { + if v < -2 || v > 2 { + return llm.ErrBadParameter.With("frequency_penalty") + } + o.Set("frequency_penalty", v) + return nil + } +} + +func WithPrediction(v string) llm.Opt { + return func(o *llm.Opts) error { + o.Set("prediction", v) + return nil + } +} + +/////////////////////////////////////////////////////////////////////////////// +// PRIVATE METHODS + +func optTemperature(opts *llm.Opts) float64 { + return opts.GetFloat64("temperature") +} + +func optTopP(opts *llm.Opts) float64 { + return opts.GetFloat64("top_p") +} + +func optMaxTokens(opts *llm.Opts) uint64 { + return opts.GetUint64("max_tokens") +} + +func optStream(opts *llm.Opts) bool { + return opts.StreamFn() != nil +} + +func optStopSequences(opts *llm.Opts) []string { + if opts.Has("stop") { + if stop, ok := opts.Get("stop").([]string); ok { + return stop + } + } + return nil +} + +func optSeed(opts *llm.Opts) uint64 { + return opts.GetUint64("seed") +} + +func optFormat(opts *llm.Opts) any { + var fmt struct { + Type string `json:"type"` + } + format := opts.GetString("format") + if format == "" { + return nil + } else { + fmt.Type = format + } + return fmt +} + +func optTools(agent llm.Agent, opts *llm.Opts) []llm.Tool { + toolkit := opts.ToolKit() + if toolkit == nil { + return nil + } + return toolkit.Tools(agent) +} + +func optToolChoice(opts *llm.Opts) any { + choices, ok := opts.Get("tool_choice").([]string) + if !ok || len(choices) == 0 { + return nil + } + + // We only support one choice + choice := strings.TrimSpace(strings.ToLower(choices[0])) + switch choice { + case "auto", "none", "any", "required": + return choice + case "": + return nil + default: + var fn struct { + Type string `json:"type"` + Function struct { + Name string `json:"name"` + } `json:"function"` + } + fn.Type = "function" + fn.Function.Name = choice + return fn + } +} + +func optPresencePenalty(opts *llm.Opts) float64 { + return opts.GetFloat64("presence_penalty") +} + +func optFrequencyPenalty(opts *llm.Opts) float64 { + return opts.GetFloat64("frequency_penalty") +} + +func optNumCompletions(opts *llm.Opts) uint64 { + return opts.GetUint64("num_completions") +} + +func optPrediction(opts *llm.Opts) *Content { + prediction := strings.TrimSpace(opts.GetString("prediction")) + if prediction == "" { + return nil + } + return NewContent("content", "", prediction) +} + +func optSafePrompt(opts *llm.Opts) bool { + return opts.GetBool("safe_prompt") +} diff --git a/pkg/mistral/session.go b/pkg/mistral/session.go new file mode 100644 index 0000000..6704af2 --- /dev/null +++ b/pkg/mistral/session.go @@ -0,0 +1,149 @@ +package mistral + +import ( + // Packages + "context" + "encoding/json" + + llm "github.com/mutablelogic/go-llm" +) + +////////////////////////////////////////////////////////////////// +// TYPES + +type session struct { + model *model // The model used for the session + opts []llm.Opt // Options to apply to the session + seq []*MessageMeta // Sequence of messages +} + +var _ llm.Context = (*session)(nil) + +/////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +// Return an empty session context object for the model, setting session options +func (model *model) Context(opts ...llm.Opt) llm.Context { + return &session{ + model: model, + opts: opts, + } +} + +// Convenience method to create a session context object with a user prompt, which +// panics on error +func (model *model) UserPrompt(prompt string, opts ...llm.Opt) llm.Context { + context := model.Context(opts...) + + meta, err := userPrompt(prompt, opts...) + if err != nil { + panic(err) + } + + // Add to the sequence + context.(*session).seq = append(context.(*session).seq, meta) + + // Return success + return context +} + +/////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +func (session session) String() string { + var data []byte + var err error + if len(session.seq) == 1 { + data, err = json.MarshalIndent(session.seq[0], "", " ") + } else { + data, err = json.MarshalIndent(session.seq, "", " ") + } + if err != nil { + return err.Error() + } + return string(data) +} + +////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Return the role of the last message +func (session *session) Role() string { + if len(session.seq) == 0 { + return "" + } + return session.seq[len(session.seq)-1].Role +} + +// Return the text of the last message +func (session *session) Text() string { + if len(session.seq) == 0 { + return "" + } + meta := session.seq[len(session.seq)-1] + return meta.Text() +} + +// Return the text of the last message +func (session *session) ToolCalls() []llm.ToolCall { + return nil +} + +// Generate a response from a user prompt (with attachments and +// other options) +func (session *session) FromUser(context.Context, string, ...llm.Opt) error { + return llm.ErrNotImplemented +} + +// Generate a response from a tool, passing the results +// from the tool call +func (session *session) FromTool(context.Context, ...llm.ToolResult) error { + return llm.ErrNotImplemented +} + +/////////////////////////////////////////////////////////////////////////////// +// PRIVATE METHODS + +func systemPrompt(prompt string) *MessageMeta { + return &MessageMeta{ + Role: "system", + Content: prompt, + } +} + +func userPrompt(prompt string, opts ...llm.Opt) (*MessageMeta, error) { + // Apply attachments + opt, err := llm.ApplyOpts(opts...) + if err != nil { + return nil, err + } + + // Get attachments + attachments := opt.Attachments() + + // Create user message + meta := MessageMeta{ + Role: "user", + Content: make([]*Content, 1, len(attachments)+1), + } + + // Append the text + meta.Content = []*Content{ + NewTextContent(prompt), + } + + // Append any additional data + // TODO + /* + for _, attachment := range attachments { + content, err := attachmentContent(attachment) + if err != nil { + return nil, err + } + meta.Content = append(meta.Content, content) + } + */ + + // Return success + return &meta, nil +} From 37ac3c7087e1e6106277b3cc923c4a6de44c0d6b Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Sun, 2 Feb 2025 08:51:41 +0100 Subject: [PATCH 04/15] Updating naming and support for multiple completions --- README.md | 33 +++++++++++++++++++++++++++------ context.go | 21 ++++++++++++++------- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 8cf6c2d..237871c 100644 --- a/README.md +++ b/README.md @@ -120,24 +120,45 @@ func session(ctx context.Context, agent llm.Agent) error { You can add options to sessions, or to prompts. Different providers and models support different options. +```go +type Model interface { + // Set session-wide options + Context(...Opt) Context + + // Add attachments (images, PDF's) to a user prompt + UserPrompt(string, ...Opt) Context + + // Set embedding options + Embedding(context.Context, string, ...Opt) ([]float64, error) +} + +type Context interface { + // Add single-use options when calling the model, which override + // session options. You can also attach files to a user prompt. + FromUser(context.Context, string, ...Opt) error +} +``` + +The options are as follows: + | Option | Ollama | Anthropic | Mistral | OpenAI | Description | |--------|--------|-----------|---------|--------|-------------| | `llm.WithTemperature(float64)` | Yes | Yes | Yes | - | What sampling temperature to use, between 0.0 and 1.0. Higher values like 0.7 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. | | `llm.WithTopP(float64)` | Yes | Yes | Yes | - | Nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. | | `llm.WithTopK(uint64)` | Yes | Yes | No | - | Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. | | `llm.WithMaxTokens(uint64)` | - | Yes | Yes | - | The maximum number of tokens to generate in the response. | -| `llm.WithStream(func(llm.ContextContent))` | Can be enabled when tools are not used | Yes | Yes | - | Stream the response to a function. | +| `llm.WithStream(func(llm.Completion))` | Can be enabled when tools are not used | Yes | Yes | - | Stream the response to a function. | | `llm.WithToolChoice(string, string, ...)` | No | Yes | Use `auto`, `any`, `none`, `required` or a function name. Only the first argument is used. | - | The tool to use for the model. | | `llm.WithToolKit(llm.ToolKit)` | Cannot be combined with streaming | Yes | Yes | - | The set of tools to use. | | `llm.WithStopSequence(string, string, ...)` | Yes | Yes | Yes | - | Stop generation if one of these tokens is detected. | | `llm.WithSystemPrompt(string)` | No | Yes | Yes | - | Set the system prompt for the model. | | `llm.WithSeed(uint64)` | No | Yes | Yes | - | The seed to use for random sampling. If set, different calls will generate deterministic results. | | `llm.WithFormat(string)` | No | Yes | Use `json_format` or `text` | - | The format of the response. For Mistral, you must also instruct the model to produce JSON yourself with a system or a user message. | -| `mistral.WithPresencePenalty(float64)` | - | - | Yes | - | Determines how much the model penalizes the repetition of words or phrases. A higher presence penalty encourages the model to use a wider variety of words and phrases, making the output more diverse and creative. | -| `mistral.WithFequencyPenalty(float64)` | - | - | Yes | - | Penalizes the repetition of words based on their frequency in the generated text. A higher frequency penalty discourages the model from repeating words that have already appeared frequently in the output, promoting diversity and reducing repetition. | -| `mistral.WithPrediction(string)` | - | - | Yes | - | Enable users to specify expected results, optimizing response times by leveraging known or predictable content. This approach is especially effective for updating text documents or code files with minimal changes, reducing latency while maintaining high-quality results. | -| `llm.WithSafePrompt()` | - | - | Yes | - | Whether to inject a safety prompt before all conversations. | -| `llm.WithNumCompletions(uint64)` | - | - | Yes | - | Number of completions to return for each request. | +| `mistral.WithPresencePenalty(float64)` | No | No | Yes | - | Determines how much the model penalizes the repetition of words or phrases. A higher presence penalty encourages the model to use a wider variety of words and phrases, making the output more diverse and creative. | +| `mistral.WithFequencyPenalty(float64)` | No | No | Yes | - | Penalizes the repetition of words based on their frequency in the generated text. A higher frequency penalty discourages the model from repeating words that have already appeared frequently in the output, promoting diversity and reducing repetition. | +| `mistral.WithPrediction(string)` | No | No | Yes | - | Enable users to specify expected results, optimizing response times by leveraging known or predictable content. This approach is especially effective for updating text documents or code files with minimal changes, reducing latency while maintaining high-quality results. | +| `llm.WithSafePrompt()` | No | No | Yes | - | Whether to inject a safety prompt before all conversations. | +| `llm.WithNumCompletions(uint64)` | No | No | Yes | - | Number of completions to return for each request. | | `llm.WithAttachment(io.Reader)` | Yes | Yes | Yes | - | Attach a file to a user prompt. It is the responsibility of the caller to close the reader. | | `antropic.WithEphemeral()` | No | Yes | No | - | Attachments should be cached server-side | | `antropic.WithCitations()` | No | Yes | No | - | Attachments should be used in citations | diff --git a/context.go b/context.go index cc8e83c..01a1411 100644 --- a/context.go +++ b/context.go @@ -5,21 +5,28 @@ import "context" ////////////////////////////////////////////////////////////////// // TYPES -// ContextContent is the content of the last context message -type ContextContent interface { +// Completion is the content of the last context message +type Completion interface { + // Return the number of completions, which is ususally 1 unless + // WithNumCompletions was used when calling the model + Num() int + // Return the current session role, which can be system, assistant, user, tool, tool_result, ... + // If this is a completion, the role is usually 'assistant' Role() string - // Return the current session text, or empty string if no text was returned - Text() string + // Return the text for the last completion. If multiple completions are not + // supported, the argument is ignored. + Text(int) string - // Return the current session tool calls, or empty if no tool calls were made - ToolCalls() []ToolCall + // Return the current session tool calls given the completion index. + // Will return nil if no tool calls were returned + ToolCalls(int) []ToolCall } // Context is fed to the agent to generate a response type Context interface { - ContextContent + Completion // Generate a response from a user prompt (with attachments and // other options) From 8b8f93dfe42669f2c19efb52d46e3efa4bcb69e4 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Sun, 2 Feb 2025 09:12:14 +0100 Subject: [PATCH 05/15] Updates --- README.md | 27 +++- pkg/mistral/chat_completion.go | 7 -- pkg/mistral/message.go | 48 ++++--- pkg/mistral/session.go | 22 +++- pkg/tool/old/tool.go_old | 220 --------------------------------- pkg/tool/old/tool.go_old_old | 216 -------------------------------- pkg/tool/old/tool_test.go_old | 29 ----- 7 files changed, 70 insertions(+), 499 deletions(-) delete mode 100644 pkg/tool/old/tool.go_old delete mode 100644 pkg/tool/old/tool.go_old_old delete mode 100644 pkg/tool/old/tool_test.go_old diff --git a/README.md b/README.md index 237871c..6c1c9b0 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ func main() { } ``` -For Mistral models, you can use: +For [Mistral](https://pkg.go.dev/github.com/mutablelogic/go-llm/pkg/mistral) models, you can use: ```go import ( @@ -89,6 +89,9 @@ func main() { } ``` +You can append options to the agent creation to set the client/server communication options, +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. + ### Chat Sessions You create a **chat session** with a model as follows, @@ -100,7 +103,7 @@ import ( func session(ctx context.Context, agent llm.Agent) error { // Create a new chat session - session := agent.Model("claude-3-5-haiku-20241022").Context() + session := agent.Model(context.TODO(), "claude-3-5-haiku-20241022").Context() // Repeat forever for { @@ -109,12 +112,28 @@ func session(ctx context.Context, agent llm.Agent) error { return err } - // Print the response - fmt.Println(session.Text()) + // Print the response for the zero'th completion + fmt.Println(session.Text(0)) } } ``` +### Embedding Generation + +TODO + +### Attachments & Image Caption Generation + +TODO + +### Streaming + +TODO + +### Tool Support + +TODO + ## Options You can add options to sessions, or to prompts. Different providers and models support diff --git a/pkg/mistral/chat_completion.go b/pkg/mistral/chat_completion.go index ab1ecae..5b4e271 100644 --- a/pkg/mistral/chat_completion.go +++ b/pkg/mistral/chat_completion.go @@ -21,13 +21,6 @@ type Response struct { Metrics `json:"usage,omitempty"` } -// Response variation -type Choice struct { - Index uint64 `json:"index"` - Message MessageMeta `json:"message"` - Reason string `json:"finish_reason,omitempty"` -} - // Metrics type Metrics struct { InputTokens uint64 `json:"prompt_tokens,omitempty"` diff --git a/pkg/mistral/message.go b/pkg/mistral/message.go index 84bfef5..17c103b 100644 --- a/pkg/mistral/message.go +++ b/pkg/mistral/message.go @@ -3,8 +3,18 @@ package mistral /////////////////////////////////////////////////////////////////////////////// // TYPES +// Possible completions +type Completions []Completion + +// Completion Variation +type Completion struct { + Index uint64 `json:"index"` + Message Message `json:"message"` + Reason string `json:"finish_reason,omitempty"` +} + // Message with text or object content -type MessageMeta struct { +type Message struct { Role string `json:"role"` // assistant, user, tool, system Prefix bool `json:"prefix,omitempty"` Content any `json:"content,omitempty"` @@ -30,7 +40,7 @@ type Image string /////////////////////////////////////////////////////////////////////////////// // LIFECYCLE -// Return a Content object with text content +// Return a Content object with text content (either in "text" or "prediction" field) func NewContent(t, v, p string) *Content { content := new(Content) content.Type = t @@ -51,25 +61,29 @@ func NewTextContent(v string) *Content { /////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS -// Return the text content -func (m MessageMeta) Text() string { - if text, ok := m.Content.(string); ok { - return text +// Return the number of completions +func (c Completions) Num() int { + return len(c) +} + +// Return the role of the completion +func (c Completions) Role() string { + // The role should be the same for all completions, let's use the first one + if len(c) == 0 { + return "" } - return "" + return c[0].Message.Role } -/* - if arr, ok := m.Content.([]Content); ok { - if len(m.Content) == 0 { +// Return the text content for a specific completion +func (c Completions) Text(index int) string { + if index < 0 || index >= len(c) { return "" } - var text []string - for _, content := range m.Content { - if content.Type == "text" && content.Text != nil { - text = append(text, string(*content.Text)) - } + completion := c[index].Message + if text, ok := completion.Content.(string); ok { + return text } - return strings.Join(text, "\n") + // Will the text be in other forms? + return "" } -*/ diff --git a/pkg/mistral/session.go b/pkg/mistral/session.go index 6704af2..69b17f2 100644 --- a/pkg/mistral/session.go +++ b/pkg/mistral/session.go @@ -1,10 +1,10 @@ package mistral import ( - // Packages "context" "encoding/json" + // Packages llm "github.com/mutablelogic/go-llm" ) @@ -12,9 +12,9 @@ import ( // TYPES type session struct { - model *model // The model used for the session - opts []llm.Opt // Options to apply to the session - seq []*MessageMeta // Sequence of messages + model *model // The model used for the session + opts []llm.Opt // Options to apply to the session + seq []*Message // Sequence of messages } var _ llm.Context = (*session)(nil) @@ -67,6 +67,16 @@ func (session session) String() string { ////////////////////////////////////////////////////////////////// // PUBLIC METHODS +// Return the number of completions +func (session *session) Num() int { + if len(session.seq) == 0 { + return 0 + } + message := session.seq[len(session.seq)-1] + message. + return session.seq[len(session.seq)-1].Role +} + // Return the role of the last message func (session *session) Role() string { if len(session.seq) == 0 { @@ -76,7 +86,7 @@ func (session *session) Role() string { } // Return the text of the last message -func (session *session) Text() string { +func (session *session) Text(index int) string { if len(session.seq) == 0 { return "" } @@ -85,7 +95,7 @@ func (session *session) Text() string { } // Return the text of the last message -func (session *session) ToolCalls() []llm.ToolCall { +func (session *session) ToolCalls(index int) []llm.ToolCall { return nil } diff --git a/pkg/tool/old/tool.go_old b/pkg/tool/old/tool.go_old deleted file mode 100644 index 2da2bee..0000000 --- a/pkg/tool/old/tool.go_old +++ /dev/null @@ -1,220 +0,0 @@ -package ollama - -import ( - "encoding/json" - "errors" - "fmt" - "reflect" - "strings" - - // Packages - llm "github.com/mutablelogic/go-llm" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -type Tool struct { - Type string `json:"type"` - Function ToolFunction `json:"function"` -} - -type ToolFunction struct { - Name string `json:"name"` - Description string `json:"description"` - Parameters struct { - Type string `json:"type,omitempty"` - Required []string `json:"required,omitempty"` - Properties map[string]ToolParameter `json:"properties,omitempty"` - } `json:"parameters"` - proto reflect.Type // Prototype for parameter return -} - -type ToolParameter struct { - Name string `json:"-"` - Type string `json:"type"` - Description string `json:"description,omitempty"` - Enum []string `json:"enum,omitempty"` - required bool - index []int // Field index into prototype for setting a field -} - -/////////////////////////////////////////////////////////////////////////////// -// LIFECYCLE - -// Return a tool, or panic if there is an error -func MustTool(name, description string, params any) *Tool { - tool, err := NewTool(name, description, params) - if err != nil { - panic(err) - } - return tool -} - -// Return a new tool definition -func NewTool(name, description string, params any) (*Tool, error) { - tool := Tool{ - Type: "function", - Function: ToolFunction{Name: name, Description: description, proto: reflect.TypeOf(params)}, - } - - // Add parameters - tool.Function.Parameters.Type = "object" - if params, err := paramsFor(params); err != nil { - return nil, err - } else { - tool.Function.Parameters.Required = make([]string, 0, len(params)) - tool.Function.Parameters.Properties = make(map[string]ToolParameter, len(params)) - for _, param := range params { - if _, exists := tool.Function.Parameters.Properties[param.Name]; exists { - return nil, llm.ErrConflict.Withf("parameter %q already exists", param.Name) - } else { - tool.Function.Parameters.Properties[param.Name] = param - } - if param.required { - tool.Function.Parameters.Required = append(tool.Function.Parameters.Required, param.Name) - } - } - } - - // Return success - return &tool, nil -} - -// Return a new tool call -func NewToolCall(v ToolCall) *ToolCallFunction { - return &v.Function -} - -/////////////////////////////////////////////////////////////////////////////// -// STRINGIFY - -func (t Tool) String() string { - data, err := json.MarshalIndent(t, "", " ") - if err != nil { - return err.Error() - } - return string(data) -} - -/////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - -func (t *Tool) Params(call ToolCall) (any, error) { - if call.Function.Name != t.Function.Name { - return nil, llm.ErrBadParameter.Withf("invalid function %q, expected %q", call.Function.Name, t.Function.Name) - } - - // Create parameters - params := reflect.New(t.Function.proto).Elem() - - // Iterate over arguments - var result error - for name, value := range call.Function.Arguments { - param, exists := t.Function.Parameters.Properties[name] - if !exists { - return nil, llm.ErrBadParameter.Withf("invalid argument %q", name) - } - result = errors.Join(result, paramSet(params.FieldByIndex(param.index), value)) - } - - // Return any errors - return params.Interface(), result -} - -/////////////////////////////////////////////////////////////////////////////// -// PRIVATE METHODS - -// Return tool parameters from a struct -func paramsFor(params any) ([]ToolParameter, error) { - if params == nil { - return []ToolParameter{}, nil - } - rt := reflect.TypeOf(params) - if rt.Kind() == reflect.Ptr { - rt = rt.Elem() - } - if rt.Kind() != reflect.Struct { - return nil, llm.ErrBadParameter.With("params must be a struct") - } - - // Iterate over fields - fields := reflect.VisibleFields(rt) - result := make([]ToolParameter, 0, len(fields)) - for _, field := range fields { - if param, err := paramFor(field); err != nil { - return nil, err - } else { - result = append(result, param) - } - } - - // Return success - return result, nil -} - -// Return tool parameters from a struct field -func paramFor(field reflect.StructField) (ToolParameter, error) { - // Name - name := field.Tag.Get("name") - if name == "" { - name = field.Name - } - - // Type - typ, err := paramType(field) - if err != nil { - return ToolParameter{}, err - } - - // Required - _, required := field.Tag.Lookup("required") - - // Enum - enum := []string{} - if enum_ := field.Tag.Get("enum"); enum_ != "" { - enum = strings.Split(enum_, ",") - } - - // Return success - return ToolParameter{ - Name: field.Name, - Type: typ, - Description: field.Tag.Get("help"), - Enum: enum, - required: required, - index: field.Index, - }, nil -} - -var ( - typeString = reflect.TypeOf("") - typeUint = reflect.TypeOf(uint(0)) - typeInt = reflect.TypeOf(int(0)) - typeFloat64 = reflect.TypeOf(float64(0)) - typeFloat32 = reflect.TypeOf(float32(0)) -) - -// Return parameter type from a struct field -func paramType(field reflect.StructField) (string, error) { - t := field.Type - if t.Kind() == reflect.Ptr { - t = t.Elem() - } - switch field.Type { - case typeString: - return "string", nil - case typeUint, typeInt: - return "integer", nil - case typeFloat64, typeFloat32: - return "number", nil - default: - return "", llm.ErrBadParameter.Withf("unsupported type %v for field %q", field.Type, field.Name) - } -} - -// Set a field parameter -func paramSet(field reflect.Value, v any) error { - fmt.Println("TODO", field, "=>", v) - return nil -} diff --git a/pkg/tool/old/tool.go_old_old b/pkg/tool/old/tool.go_old_old deleted file mode 100644 index 2d0a24b..0000000 --- a/pkg/tool/old/tool.go_old_old +++ /dev/null @@ -1,216 +0,0 @@ -package anthropic - -import ( - "encoding/json" - "reflect" - "strings" - - // Packages - llm "github.com/mutablelogic/go-llm" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -type Tool struct { - Name string `json:"name"` - Description string `json:"description"` - Parameters struct { - Type string `json:"type,omitempty"` - Required []string `json:"required,omitempty"` - Properties map[string]ToolParameter `json:"properties,omitempty"` - } `json:"input_schema"` - proto reflect.Type // Prototype for parameter return -} - -type ToolParameter struct { - Name string `json:"-"` - Type string `json:"type"` - Description string `json:"description,omitempty"` - Enum []string `json:"enum,omitempty"` - required bool - index []int // Field index into prototype for setting a field -} - -type toolcall struct { - ContentTool -} - -/////////////////////////////////////////////////////////////////////////////// -// LIFECYCLE - -// Return a tool, or panic if there is an error -func MustTool(name, description string, params any) *Tool { - tool, err := NewTool(name, description, params) - if err != nil { - panic(err) - } - return tool -} - -// Return a new tool definition -func NewTool(name, description string, params any) (*Tool, error) { - tool := Tool{ - Name: name, - Description: description, - proto: reflect.TypeOf(params), - } - - // Add parameters - tool.Parameters.Type = "object" - toolparams, err := paramsFor(params) - if err != nil { - return nil, err - } - - // Set parameters - tool.Parameters.Required = make([]string, 0, len(toolparams)) - tool.Parameters.Properties = make(map[string]ToolParameter, len(toolparams)) - for _, param := range toolparams { - if _, exists := tool.Parameters.Properties[param.Name]; exists { - return nil, llm.ErrConflict.Withf("parameter %q already exists", param.Name) - } else { - tool.Parameters.Properties[param.Name] = param - } - if param.required { - tool.Parameters.Required = append(tool.Parameters.Required, param.Name) - } - } - - // Return success - return &tool, nil -} - -// Return a new tool call from a content parameter -func NewToolCall(content *Content) *toolcall { - if content == nil || content.ContentTool.Id == "" || content.ContentTool.Name == "" { - return nil - } - return &toolcall{content.ContentTool} -} - -/////////////////////////////////////////////////////////////////////////////// -// STRINGIFY - -func (t Tool) String() string { - data, err := json.MarshalIndent(t, "", " ") - if err != nil { - return err.Error() - } - return string(data) -} - -func (t toolcall) String() string { - data, err := json.MarshalIndent(t, "", " ") - if err != nil { - return err.Error() - } - return string(data) -} - -/////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - -func (t *toolcall) Name() string { - return t.ContentTool.Name -} - -func (t *toolcall) Id() string { - return t.ContentTool.Id -} - -func (t *toolcall) Params() any { - // TODO: Convert - return t.ContentTool.Input -} - -/////////////////////////////////////////////////////////////////////////////// -// PRIVATE METHODS - -// Return tool parameters from a struct -func paramsFor(params any) ([]ToolParameter, error) { - if params == nil { - return []ToolParameter{}, nil - } - rt := reflect.TypeOf(params) - if rt.Kind() == reflect.Ptr { - rt = rt.Elem() - } - if rt.Kind() != reflect.Struct { - return nil, llm.ErrBadParameter.With("params must be a struct") - } - - // Iterate over fields - fields := reflect.VisibleFields(rt) - result := make([]ToolParameter, 0, len(fields)) - for _, field := range fields { - if param, err := paramFor(field); err != nil { - return nil, err - } else { - result = append(result, param) - } - } - - // Return success - return result, nil -} - -// Return tool parameters from a struct field -func paramFor(field reflect.StructField) (ToolParameter, error) { - // Name - name := field.Tag.Get("name") - if name == "" { - name = field.Name - } - - // Type - typ, err := paramType(field) - if err != nil { - return ToolParameter{}, err - } - - // Required - _, required := field.Tag.Lookup("required") - - // Enum - enum := []string{} - if enum_ := field.Tag.Get("enum"); enum_ != "" { - enum = strings.Split(enum_, ",") - } - - // Return success - return ToolParameter{ - Name: field.Name, - Type: typ, - Description: field.Tag.Get("help"), - Enum: enum, - required: required, - index: field.Index, - }, nil -} - -var ( - typeString = reflect.TypeOf("") - typeUint = reflect.TypeOf(uint(0)) - typeInt = reflect.TypeOf(int(0)) - typeFloat64 = reflect.TypeOf(float64(0)) - typeFloat32 = reflect.TypeOf(float32(0)) -) - -// Return parameter type from a struct field -func paramType(field reflect.StructField) (string, error) { - t := field.Type - if t.Kind() == reflect.Ptr { - t = t.Elem() - } - switch field.Type { - case typeString: - return "string", nil - case typeUint, typeInt: - return "integer", nil - case typeFloat64, typeFloat32: - return "number", nil - default: - return "", llm.ErrBadParameter.Withf("unsupported type %v for field %q", field.Type, field.Name) - } -} diff --git a/pkg/tool/old/tool_test.go_old b/pkg/tool/old/tool_test.go_old deleted file mode 100644 index f4d1d30..0000000 --- a/pkg/tool/old/tool_test.go_old +++ /dev/null @@ -1,29 +0,0 @@ -package ollama_test - -import ( - "testing" - - // Packagees - - ollama "github.com/mutablelogic/go-llm/pkg/ollama" -) - -func Test_tool_001(t *testing.T) { - tool, err := ollama.NewTool("test", "test_tool", struct{}{}) - if err != nil { - t.FailNow() - } - t.Log(tool) -} - -func Test_tool_002(t *testing.T) { - tool, err := ollama.NewTool("test", "test_tool", struct { - A string `help:"A string"` - B int `help:"An integer"` - C float64 `help:"A float" required:""` - }{}) - if err != nil { - t.FailNow() - } - t.Log(tool) -} From edcb55aee0f5d9a04dbcf93c15398edaefc767b1 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Sun, 2 Feb 2025 09:19:27 +0100 Subject: [PATCH 06/15] Updated --- README.md | 36 ++++++++++++++++++++++++++++++++++++ cmd/{agent => llm}/chat.go | 0 cmd/{agent => llm}/main.go | 0 cmd/{agent => llm}/models.go | 0 cmd/{agent => llm}/term.go | 0 context.go | 5 +++-- etc/docker/Dockerfile | 2 +- opt.go | 16 ++++++++-------- 8 files changed, 48 insertions(+), 11 deletions(-) rename cmd/{agent => llm}/chat.go (100%) rename cmd/{agent => llm}/main.go (100%) rename cmd/{agent => llm}/models.go (100%) rename cmd/{agent => llm}/term.go (100%) diff --git a/README.md b/README.md index 6c1c9b0..128da6e 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ docker run \ chat claude-3-5-haiku-20241022 ``` +See below for more information on how to use the command-line tool. + ## Programmatic Usage See the documentation [here](https://pkg.go.dev/github.com/mutablelogic/go-llm) @@ -183,6 +185,40 @@ The options are as follows: | `antropic.WithCitations()` | No | Yes | No | - | Attachments should be used in citations | | `antropic.WithUser(string)` | No | Yes | No | - | Indicate the user name for the request, for debugging | +## The Command Line Tool + +You can use the command-line tool to interact with the API. To build the tool, you can use the following command: + +```bash +go install github.com/mutablelogic/go-llm/cmd/llm@latest +llm --help +``` + +The output is something like: + +```text +Usage: llm [flags] + +LLM agent command line interface + +Flags: + -h, --help Show context-sensitive help. + --debug Enable debug output + --verbose Enable verbose output + --ollama-endpoint=STRING Ollama endpoint ($OLLAMA_URL) + --anthropic-key=STRING Anthropic API Key ($ANTHROPIC_API_KEY) + --news-key=STRING News API Key ($NEWSAPI_KEY) + +Commands: + agents Return a list of agents + models Return a list of models + tools Return a list of tools + download Download a model + chat Start a chat session + +Run "llm --help" for more information on a command. +``` + ## Contributing & Distribution *This module is currently in development and subject to change*. Please do file diff --git a/cmd/agent/chat.go b/cmd/llm/chat.go similarity index 100% rename from cmd/agent/chat.go rename to cmd/llm/chat.go diff --git a/cmd/agent/main.go b/cmd/llm/main.go similarity index 100% rename from cmd/agent/main.go rename to cmd/llm/main.go diff --git a/cmd/agent/models.go b/cmd/llm/models.go similarity index 100% rename from cmd/agent/models.go rename to cmd/llm/models.go diff --git a/cmd/agent/term.go b/cmd/llm/term.go similarity index 100% rename from cmd/agent/term.go rename to cmd/llm/term.go diff --git a/context.go b/context.go index 01a1411..0aad95d 100644 --- a/context.go +++ b/context.go @@ -15,12 +15,13 @@ type Completion interface { // If this is a completion, the role is usually 'assistant' Role() string - // Return the text for the last completion. If multiple completions are not + // Return the text for the last completion, with the argument as the + // completion index (usually 0). If multiple completions are not // supported, the argument is ignored. Text(int) string // Return the current session tool calls given the completion index. - // Will return nil if no tool calls were returned + // Will return nil if no tool calls were returned. ToolCalls(int) []ToolCall } diff --git a/etc/docker/Dockerfile b/etc/docker/Dockerfile index b612cda..3704fec 100644 --- a/etc/docker/Dockerfile +++ b/etc/docker/Dockerfile @@ -25,4 +25,4 @@ RUN apt update -y && apt install -y ca-certificates LABEL org.opencontainers.image.source=https://${SOURCE} # Entrypoint when running the server -ENTRYPOINT [ "/usr/local/bin/agent" ] +ENTRYPOINT [ "/usr/local/bin/llm" ] diff --git a/opt.go b/opt.go index c22382c..11b5625 100644 --- a/opt.go +++ b/opt.go @@ -13,12 +13,12 @@ type Opt func(*Opts) error // set of options type Opts struct { - agents map[string]Agent // Set of agents - toolkit ToolKit // Toolkit for tools - callback func(ContextContent) // Streaming callback - attachments []*Attachment // Attachments - system string // System prompt - options map[string]any // Additional options + agents map[string]Agent // Set of agents + toolkit ToolKit // Toolkit for tools + callback func(Completion) // Streaming callback + attachments []*Attachment // Attachments + system string // System prompt + options map[string]any // Additional options } //////////////////////////////////////////////////////////////////////////////// @@ -46,7 +46,7 @@ func (o *Opts) ToolKit() ToolKit { } // Return the stream function -func (o *Opts) StreamFn() func(ContextContent) { +func (o *Opts) StreamFn() func(Completion) { return o.callback } @@ -150,7 +150,7 @@ func WithToolKit(toolkit ToolKit) Opt { } // Set chat streaming function -func WithStream(fn func(ContextContent)) Opt { +func WithStream(fn func(Completion)) Opt { return func(o *Opts) error { o.callback = fn return nil From c6feba691b76541d3256885ea8bbc553ba723d55 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Sun, 2 Feb 2025 12:02:48 +0100 Subject: [PATCH 07/15] Added tool calling --- README.md | 57 ++++++-- attachment.go | 38 +++++ opt.go | 48 +++++++ pkg/mistral/chat_completion.go | 148 +++++++++++++++---- pkg/mistral/chat_completion_test.go | 211 +++++++++++++++++++++++----- pkg/mistral/message.go | 66 +++++++-- pkg/mistral/session.go | 68 ++++----- pkg/mistral/testdata/LICENSE | 201 ++++++++++++++++++++++++++ pkg/mistral/testdata/guggenheim.jpg | Bin 0 -> 139053 bytes pkg/mistral/tool.go | 56 ++++++++ pkg/tool/tool.go | 13 ++ pkg/tool/toolkit.go | 7 +- 12 files changed, 788 insertions(+), 125 deletions(-) create mode 100644 pkg/mistral/testdata/LICENSE create mode 100644 pkg/mistral/testdata/guggenheim.jpg create mode 100644 pkg/mistral/tool.go diff --git a/README.md b/README.md index 128da6e..094d990 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,20 @@ Large Language Model API interface. This is a simple API interface for large language models which run on [Ollama](https://github.com/ollama/ollama/blob/main/docs/api.md), -[Anthopic](https://docs.anthropic.com/en/api/getting-started) and [Mistral](https://docs.mistral.ai/). +[Anthopic](https://docs.anthropic.com/en/api/getting-started) and [Mistral](https://docs.mistral.ai/) +(OpenAI might be added later). The module includes the ability to utilize: * Maintaining a session of messages -* Tool calling support -* Creating embeddings from text +* Tool calling support, including using your own tools (aka Tool plugins) +* Creating embedding vectors from text * Streaming responses +* Multi-modal support (aka, Images and Attachments) There is a command-line tool included in the module which can be used to interact with the API. -For example, +If you have docker installed, you can use the following command to run the tool, without +installation: ```bash # Display help @@ -21,12 +24,13 @@ docker run ghcr.io/mutablelogic/go-llm:latest --help # Interact with Claude to retrieve news headlines, assuming # you have an API key for Anthropic and NewsAPI docker run \ - --interactive -e ANTHROPIC_API_KEY -e NEWSAPI_KEY \ + --interactive -e MISTRAL_API_KEY -e NEWSAPI_KEY \ ghcr.io/mutablelogic/go-llm:latest \ - chat claude-3-5-haiku-20241022 + chat claude-3-5-haiku-20241022 --prompt "What is the latest news?" ``` -See below for more information on how to use the command-line tool. +See below for more information on how to use the command-line tool (or how to install it +if you have a `go` compiler). ## Programmatic Usage @@ -46,7 +50,7 @@ import ( ) func main() { - // Create a new agent + // Create a new agent - replace the URL with the one to your Ollama instance agent, err := ollama.New("https://ollama.com/api/v1/") if err != nil { panic(err) @@ -57,7 +61,7 @@ func main() { To create an [Anthropic](https://pkg.go.dev/github.com/mutablelogic/go-llm/pkg/anthropic) -agent, +agent with an API key stored as an environment variable, ```go import ( @@ -66,7 +70,7 @@ import ( func main() { // Create a new agent - agent, err := anthropic.New(os.Getev("ANTHROPIC_API_KEY")) + agent, err := anthropic.New(os.Getenv("ANTHROPIC_API_KEY")) if err != nil { panic(err) } @@ -83,7 +87,7 @@ import ( func main() { // Create a new agent - agent, err := mistral.New(os.Getev("MISTRAL_API_KEY")) + agent, err := mistral.New(os.Getenv("MISTRAL_API_KEY")) if err != nil { panic(err) } @@ -94,6 +98,28 @@ func main() { You can append options to the agent creation to set the client/server communication options, 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. +There is also an _aggregated_ agent which can be used to interact with multiple providers at once. This is useful if you want +to use models from different providers simultaneously. + +```go +import ( + "github.com/mutablelogic/go-llm/pkg/agent" +) + +func main() { + // Create a new agent which aggregates multiple providers + agent, err := agent.New( + agent.WithAnthropic(os.Getenv("ANTHROPIC_API_KEY")), + agent.WithMistral(os.Getenv("MISTRAL_API_KEY")), + agent.WithOllama(os.Getenv("OLLAMA_URL")), + ) + if err != nil { + panic(err) + } + // ... +} +``` + ### Chat Sessions You create a **chat session** with a model as follows, @@ -120,6 +146,9 @@ func session(ctx context.Context, agent llm.Agent) error { } ``` +The `Context` object will continue to store the current session and options, and will +ensure the session is maintained across multiple calls. + ### Embedding Generation TODO @@ -146,16 +175,16 @@ type Model interface { // Set session-wide options Context(...Opt) Context - // Add attachments (images, PDF's) to a user prompt + // Add attachments (images, PDF's) to a user prompt for completion UserPrompt(string, ...Opt) Context - // Set embedding options + // Create an embedding vector with embedding options Embedding(context.Context, string, ...Opt) ([]float64, error) } type Context interface { // Add single-use options when calling the model, which override - // session options. You can also attach files to a user prompt. + // session options. You can attach files to a user prompt. FromUser(context.Context, string, ...Opt) error } ``` diff --git a/attachment.go b/attachment.go index c7733c4..5987a9d 100644 --- a/attachment.go +++ b/attachment.go @@ -1,8 +1,13 @@ package llm import ( + "encoding/base64" + "encoding/json" "io" + "mime" + "net/http" "os" + "path/filepath" ) /////////////////////////////////////////////////////////////////////////////// @@ -31,6 +36,25 @@ func ReadAttachment(r io.Reader) (*Attachment, error) { return &Attachment{filename: filename, data: data}, nil } +//////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +func (a *Attachment) String() string { + var j struct { + Filename string `json:"filename"` + Type string `json:"type"` + Bytes uint64 `json:"bytes"` + } + j.Filename = a.filename + j.Type = a.Type() + j.Bytes = uint64(len(a.data)) + data, err := json.MarshalIndent(j, "", " ") + if err != nil { + return err.Error() + } + return string(data) +} + //////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS @@ -41,3 +65,17 @@ func (a *Attachment) Filename() string { func (a *Attachment) Data() []byte { return a.data } + +func (a *Attachment) Type() string { + // Mimetype based on content + mimetype := http.DetectContentType(a.data) + if mimetype == "application/octet-stream" && a.filename != "" { + // Detect mimetype from extension + mimetype = mime.TypeByExtension(filepath.Ext(a.filename)) + } + return mimetype +} + +func (a *Attachment) Url() string { + return "data:" + a.Type() + ";base64," + base64.StdEncoding.EncodeToString(a.data) +} diff --git a/opt.go b/opt.go index 11b5625..f5179b1 100644 --- a/opt.go +++ b/opt.go @@ -1,6 +1,7 @@ package llm import ( + "encoding/json" "io" "time" ) @@ -13,6 +14,7 @@ type Opt func(*Opts) error // set of options type Opts struct { + prompt bool agents map[string]Agent // Set of agents toolkit ToolKit // Toolkit for tools callback func(Completion) // Streaming callback @@ -26,7 +28,22 @@ type Opts struct { // ApplyOpts returns a structure of options func ApplyOpts(opts ...Opt) (*Opts, error) { + return applyOpts(false, opts...) +} + +// ApplyPromptOpts returns a structure of options for a prompt +func ApplyPromptOpts(opts ...Opt) (*Opts, error) { + if opt, err := applyOpts(true, opts...); err != nil { + return nil, err + } else { + return opt, nil + } +} + +// ApplySessionOpts returns a structure of options +func applyOpts(prompt bool, opts ...Opt) (*Opts, error) { o := new(Opts) + o.prompt = prompt o.agents = make(map[string]Agent) o.options = make(map[string]any) for _, opt := range opts { @@ -37,6 +54,33 @@ func ApplyOpts(opts ...Opt) (*Opts, error) { return o, nil } +/////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +func (o Opts) MarshalJSON() ([]byte, error) { + var j struct { + ToolKit ToolKit `json:"toolkit,omitempty"` + Agents map[string]Agent `json:"agents,omitempty"` + System string `json:"system,omitempty"` + Attachments []*Attachment `json:"attachments,omitempty"` + Options map[string]any `json:"options,omitempty"` + } + j.ToolKit = o.toolkit + j.Agents = o.agents + j.Attachments = o.attachments + j.System = o.system + j.Options = o.options + return json.Marshal(j) +} + +func (o Opts) String() string { + data, err := json.Marshal(o) + if err != nil { + return err.Error() + } + return string(data) +} + /////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS - PROPERTIES @@ -182,6 +226,10 @@ func WithAgent(agent Agent) Opt { // Create an attachment func WithAttachment(r io.Reader) Opt { return func(o *Opts) error { + // Only attach if prompt is set + if !o.prompt { + return nil + } if attachment, err := ReadAttachment(r); err != nil { return err } else { diff --git a/pkg/mistral/chat_completion.go b/pkg/mistral/chat_completion.go index 5b4e271..91a081f 100644 --- a/pkg/mistral/chat_completion.go +++ b/pkg/mistral/chat_completion.go @@ -3,6 +3,7 @@ package mistral import ( "context" "encoding/json" + "strings" "github.com/mutablelogic/go-client" "github.com/mutablelogic/go-llm" @@ -13,12 +14,12 @@ import ( // Chat Completion Response type Response struct { - Id string `json:"id"` - Type string `json:"object"` - Created uint64 `json:"created"` - Model string `json:"model"` - Choices []Choice `json:"choices"` - Metrics `json:"usage,omitempty"` + Id string `json:"id"` + Type string `json:"object"` + Created uint64 `json:"created"` + Model string `json:"model"` + Completions `json:"choices"` + Metrics `json:"usage,omitempty"` } // Metrics @@ -28,6 +29,8 @@ type Metrics struct { TotalTokens uint `json:"total_tokens,omitempty"` } +var _ llm.Completion = (*Response)(nil) + /////////////////////////////////////////////////////////////////////////////// // STRINGIFY @@ -43,22 +46,22 @@ func (r Response) String() string { // PUBLIC METHODS type reqChatCompletion struct { - Model string `json:"model"` - Temperature float64 `json:"temperature,omitempty"` - TopP float64 `json:"top_p,omitempty"` - MaxTokens uint64 `json:"max_tokens,omitempty"` - Stream bool `json:"stream,omitempty"` - StopSequences []string `json:"stop,omitempty"` - Seed uint64 `json:"random_seed,omitempty"` - Messages []*MessageMeta `json:"messages"` - Format any `json:"response_format,omitempty"` - Tools []llm.Tool `json:"tools,omitempty"` - ToolChoice any `json:"tool_choice,omitempty"` - PresencePenalty float64 `json:"presence_penalty,omitempty"` - FrequencyPenalty float64 `json:"frequency_penalty,omitempty"` - NumChoices uint64 `json:"n,omitempty"` - Prediction *Content `json:"prediction,omitempty"` - SafePrompt bool `json:"safe_prompt,omitempty"` + Model string `json:"model"` + Temperature float64 `json:"temperature,omitempty"` + TopP float64 `json:"top_p,omitempty"` + MaxTokens uint64 `json:"max_tokens,omitempty"` + Stream bool `json:"stream,omitempty"` + StopSequences []string `json:"stop,omitempty"` + Seed uint64 `json:"random_seed,omitempty"` + Messages []*Message `json:"messages"` + Format any `json:"response_format,omitempty"` + Tools []llm.Tool `json:"tools,omitempty"` + ToolChoice any `json:"tool_choice,omitempty"` + PresencePenalty float64 `json:"presence_penalty,omitempty"` + FrequencyPenalty float64 `json:"frequency_penalty,omitempty"` + NumChoices uint64 `json:"n,omitempty"` + Prediction *Content `json:"prediction,omitempty"` + SafePrompt bool `json:"safe_prompt,omitempty"` } func (mistral *Client) ChatCompletion(ctx context.Context, context llm.Context, opts ...llm.Opt) (*Response, error) { @@ -69,11 +72,18 @@ func (mistral *Client) ChatCompletion(ctx context.Context, context llm.Context, } // Append the system prompt at the beginning - seq := make([]*MessageMeta, 0, len(context.(*session).seq)+1) + messages := make([]*Message, 0, len(context.(*session).seq)+1) if system := opt.SystemPrompt(); system != "" { - seq = append(seq, systemPrompt(system)) + messages = append(messages, systemPrompt(system)) + } + + // Always append the first message of each completion + for _, completion := range context.(*session).seq { + if completion.Num() == 0 { + continue + } + messages = append(messages, completion.Message(0)) } - seq = append(seq, context.(*session).seq...) // Request req, err := client.NewJSONRequest(reqChatCompletion{ @@ -84,7 +94,7 @@ func (mistral *Client) ChatCompletion(ctx context.Context, context llm.Context, Stream: optStream(opt), StopSequences: optStopSequences(opt), Seed: optSeed(opt), - Messages: seq, + Messages: messages, Format: optFormat(opt), Tools: optTools(mistral, opt), ToolChoice: optToolChoice(opt), @@ -98,12 +108,94 @@ func (mistral *Client) ChatCompletion(ctx context.Context, context llm.Context, return nil, err } - // Response var response Response - if err := mistral.DoWithContext(ctx, req, &response, client.OptPath("chat", "completions")); err != nil { + reqopts := []client.RequestOpt{ + client.OptPath("chat", "completions"), + } + if optStream(opt) { + reqopts = append(reqopts, client.OptTextStreamCallback(func(evt client.TextStreamEvent) error { + if err := streamEvent(&response, evt); err != nil { + return err + } + if fn := opt.StreamFn(); fn != nil { + fn(&response) + } + return nil + })) + } + + // Response + if err := mistral.DoWithContext(ctx, req, &response, reqopts...); err != nil { return nil, err } // Return success return &response, nil } + +/////////////////////////////////////////////////////////////////////////////// +// PRIVATE METHODS + +func streamEvent(response *Response, evt client.TextStreamEvent) error { + var delta Response + // If we are done, ignore + if strings.TrimSpace(evt.Data) == "[DONE]" { + return nil + } + // Decode the event + if err := evt.Json(&delta); err != nil { + return err + } + // Append the delta to the response + if delta.Id != "" { + response.Id = delta.Id + } + if delta.Created != 0 { + response.Created = delta.Created + } + if delta.Model != "" { + response.Model = delta.Model + } + for _, completion := range delta.Completions { + appendCompletion(response, &completion) + } + if delta.Metrics.InputTokens > 0 { + response.Metrics.InputTokens += delta.Metrics.InputTokens + } + if delta.Metrics.OutputTokens > 0 { + response.Metrics.OutputTokens += delta.Metrics.OutputTokens + } + if delta.Metrics.TotalTokens > 0 { + response.Metrics.TotalTokens += delta.Metrics.TotalTokens + } + return nil +} + +func appendCompletion(response *Response, c *Completion) { + for { + if c.Index < uint64(len(response.Completions)) { + break + } + response.Completions = append(response.Completions, Completion{ + Index: c.Index, + Message: &Message{ + Role: c.Delta.Role, + Content: "", + }, + }) + } + // Add the completion delta + if c.Reason != "" { + response.Completions[c.Index].Reason = c.Reason + } + if c.Delta.Role != "" { + response.Completions[c.Index].Message.Role = c.Delta.Role + } + + // TODO: We only allow deltas which are strings at the moment... + if str, ok := c.Delta.Content.(string); ok && str != "" { + if text, ok := response.Completions[c.Index].Message.Content.(string); ok { + response.Completions[c.Index].Message.Content = text + str + } + } +} diff --git a/pkg/mistral/chat_completion_test.go b/pkg/mistral/chat_completion_test.go index 141dafd..9730130 100644 --- a/pkg/mistral/chat_completion_test.go +++ b/pkg/mistral/chat_completion_test.go @@ -2,13 +2,16 @@ package mistral_test import ( "context" + "errors" "os" + "strings" "testing" // Packages opts "github.com/mutablelogic/go-client" "github.com/mutablelogic/go-llm" mistral "github.com/mutablelogic/go-llm/pkg/mistral" + "github.com/mutablelogic/go-llm/pkg/tool" assert "github.com/stretchr/testify/assert" ) @@ -29,75 +32,217 @@ func Test_chat_001(t *testing.T) { func Test_chat_002(t *testing.T) { assert := assert.New(t) - client, err := mistral.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) assert.NoError(err) - model := client.Model(context.TODO(), "mistral-large-latest") if !assert.NotNil(model) { t.FailNow() } t.Run("Temperature", func(t *testing.T) { - _, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithTemperature(0.5)) - assert.NoError(err) + r, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithTemperature(0.5)) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(1, r.Num()) + assert.NotEmpty(r.Text(0)) + t.Log(r) + } }) t.Run("TopP", func(t *testing.T) { - _, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithTopP(0.5)) - assert.NoError(err) + r, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithTopP(0.5)) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(1, r.Num()) + assert.NotEmpty(r.Text(0)) + t.Log(r) + } }) t.Run("MaxTokens", func(t *testing.T) { - _, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithMaxTokens(10)) - assert.NoError(err) + r, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithMaxTokens(10)) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(1, r.Num()) + assert.NotEmpty(r.Text(0)) + t.Log(r) + } }) t.Run("Stream", func(t *testing.T) { - _, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithStream(func(r llm.ContextContent) { - t.Log(r.Role(), "=>", r.Text()) + r, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithNumCompletions(2), llm.WithStream(func(r llm.Completion) { + t.Log(r.Role(), "=>", r.Text(0)) })) - assert.NoError(err) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(2, r.Num()) + assert.NotEmpty(r.Text(0)) + assert.NotEmpty(r.Text(1)) + t.Log(r) + } }) t.Run("Stop", func(t *testing.T) { - _, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithStopSequence("STOP")) - assert.NoError(err) + r, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithStopSequence("STOP")) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(1, r.Num()) + assert.NotEmpty(r.Text(0)) + t.Log(r) + } }) t.Run("System", func(t *testing.T) { - _, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithSystemPrompt("You are shakespearian")) - assert.NoError(err) + r, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithSystemPrompt("You are shakespearian")) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(1, r.Num()) + assert.NotEmpty(r.Text(0)) + t.Log(r) + } }) t.Run("Seed", func(t *testing.T) { - _, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithSeed(123)) - assert.NoError(err) + r, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithSeed(123)) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(1, r.Num()) + assert.NotEmpty(r.Text(0)) + t.Log(r) + } }) t.Run("Format", func(t *testing.T) { - _, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithFormat("json_object"), llm.WithSystemPrompt("Return a JSON object")) - assert.NoError(err) + r, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithFormat("json_object"), llm.WithSystemPrompt("Return a JSON object")) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(1, r.Num()) + assert.NotEmpty(r.Text(0)) + t.Log(r) + } }) t.Run("ToolChoiceAuto", func(t *testing.T) { - _, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithToolChoice("auto")) - assert.NoError(err) + r, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithToolChoice("auto")) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(1, r.Num()) + assert.NotEmpty(r.Text(0)) + t.Log(r) + } }) t.Run("ToolChoiceFunc", func(t *testing.T) { - _, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithToolChoice("get_weather")) - assert.NoError(err) + r, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithToolChoice("get_weather")) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(1, r.Num()) + assert.NotEmpty(r.Text(0)) + t.Log(r) + } }) t.Run("PresencePenalty", func(t *testing.T) { - _, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), mistral.WithPresencePenalty(-2)) - assert.NoError(err) + r, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), mistral.WithPresencePenalty(-2)) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(1, r.Num()) + assert.NotEmpty(r.Text(0)) + t.Log(r) + } }) t.Run("FrequencyPenalty", func(t *testing.T) { - _, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), mistral.WithFrequencyPenalty(-2)) - assert.NoError(err) + r, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), mistral.WithFrequencyPenalty(-2)) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(1, r.Num()) + assert.NotEmpty(r.Text(0)) + t.Log(r) + } }) t.Run("NumChoices", func(t *testing.T) { - _, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithNumCompletions(3)) - assert.NoError(err) + r, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithNumCompletions(3)) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(3, r.Num()) + assert.NotEmpty(r.Text(0)) + t.Log(r) + } }) t.Run("Prediction", func(t *testing.T) { - _, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), mistral.WithPrediction("The temperature in London today is")) - assert.NoError(err) + r, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), mistral.WithPrediction("The temperature in London today is")) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(1, r.Num()) + assert.NotEmpty(r.Text(0)) + t.Log(r) + } }) t.Run("SafePrompt", func(t *testing.T) { - _, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithSafePrompt()) - assert.NoError(err) + r, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithSafePrompt()) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(1, r.Num()) + assert.NotEmpty(r.Text(0)) + t.Log(r) + } }) } + +func Test_chat_003(t *testing.T) { + assert := assert.New(t) + client, err := mistral.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) + assert.NoError(err) + model := client.Model(context.TODO(), "pixtral-12b-2409") + if !assert.NotNil(model) { + t.FailNow() + } + + f, err := os.Open("testdata/guggenheim.jpg") + if !assert.NoError(err) { + t.FailNow() + } + defer f.Close() + + // Describe an image + r, err := client.ChatCompletion(context.TODO(), model.UserPrompt("Provide a short caption for this image", llm.WithAttachment(f))) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(1, r.Num()) + assert.NotEmpty(r.Text(0)) + t.Log(r.Text(0)) + } +} + +func Test_chat_004(t *testing.T) { + assert := assert.New(t) + client, err := mistral.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) + assert.NoError(err) + model := client.Model(context.TODO(), "mistral-small-latest") + if !assert.NotNil(model) { + t.FailNow() + } + + toolkit := tool.NewToolKit() + toolkit.Register(&weather{}) + + // Get the weather for a city + r, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the weather in the capital city of germany?"), llm.WithToolKit(toolkit)) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(1, r.Num()) + + calls := r.ToolCalls(0) + assert.NotEmpty(calls) + + var w weather + assert.NoError(calls[0].Decode(&w)) + assert.Equal("berlin", strings.ToLower(w.City)) + } +} + +type weather struct { + City string `json:"city" help:"The city to get the weather for"` +} + +func (weather) Name() string { + return "weather_in_city" +} + +func (weather) Description() string { + return "Get the weather for a city" +} + +func (weather) Run(ctx context.Context) (any, error) { + return nil, errors.New("I couldn't retrieve the weather for that city") +} diff --git a/pkg/mistral/message.go b/pkg/mistral/message.go index 17c103b..27acd90 100644 --- a/pkg/mistral/message.go +++ b/pkg/mistral/message.go @@ -1,24 +1,31 @@ package mistral +import ( + "github.com/mutablelogic/go-llm" +) + /////////////////////////////////////////////////////////////////////////////// // TYPES // Possible completions type Completions []Completion +var _ llm.Completion = Completions{} + // Completion Variation type Completion struct { - Index uint64 `json:"index"` - Message Message `json:"message"` - Reason string `json:"finish_reason,omitempty"` + Index uint64 `json:"index"` + Message *Message `json:"message"` + Delta *Message `json:"delta,omitempty"` // For streaming + Reason string `json:"finish_reason,omitempty"` } // Message with text or object content type Message struct { - Role string `json:"role"` // assistant, user, tool, system - Prefix bool `json:"prefix,omitempty"` - Content any `json:"content,omitempty"` - // ContentTools + Role string `json:"role,omitempty"` // assistant, user, tool, system + Prefix bool `json:"prefix,omitempty"` + Content any `json:"content,omitempty"` + ToolCalls `json:"tool_calls,omitempty"` } type Content struct { @@ -28,6 +35,9 @@ type Content struct { *Image `json:"image_url,omitempty"` // image_url } +// A set of tool calls +type ToolCalls []ToolCall + // text content type Text string @@ -58,6 +68,15 @@ func NewTextContent(v string) *Content { return NewContent("text", v, "") } +// Return an image attachment +func NewImageAttachment(a *llm.Attachment) *Content { + content := new(Content) + image := a.Url() + content.Type = "image_url" + content.Image = (*Image)(&image) + return content +} + /////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS @@ -84,6 +103,37 @@ func (c Completions) Text(index int) string { if text, ok := completion.Content.(string); ok { return text } - // Will the text be in other forms? + // TODO: Will the text be in other forms? return "" } + +// Return the current session tool calls given the completion index. +// Will return nil if no tool calls were returned. +func (c Completions) ToolCalls(index int) []llm.ToolCall { + if index < 0 || index >= len(c) { + return nil + } + + // Get the completion + completion := c[index].Message + if completion == nil { + return nil + } + + // Make the tool calls + calls := make([]llm.ToolCall, 0, len(completion.ToolCalls)) + for _, call := range completion.ToolCalls { + calls = append(calls, &toolcall{call}) + } + + // Return success + return calls +} + +// Return message for a specific completion +func (c Completions) Message(index int) *Message { + if index < 0 || index >= len(c) { + return nil + } + return c[index].Message +} diff --git a/pkg/mistral/session.go b/pkg/mistral/session.go index 69b17f2..82d6573 100644 --- a/pkg/mistral/session.go +++ b/pkg/mistral/session.go @@ -12,9 +12,9 @@ import ( // TYPES type session struct { - model *model // The model used for the session - opts []llm.Opt // Options to apply to the session - seq []*Message // Sequence of messages + model *model // The model used for the session + opts []llm.Opt // Options to apply to the session + seq []Completions // Sequence of messages } var _ llm.Context = (*session)(nil) @@ -27,6 +27,7 @@ func (model *model) Context(opts ...llm.Opt) llm.Context { return &session{ model: model, opts: opts, + seq: make([]Completions, 0, 10), } } @@ -35,13 +36,16 @@ func (model *model) Context(opts ...llm.Opt) llm.Context { func (model *model) UserPrompt(prompt string, opts ...llm.Opt) llm.Context { context := model.Context(opts...) - meta, err := userPrompt(prompt, opts...) + // Create a user prompt + message, err := userPrompt(prompt, opts...) if err != nil { panic(err) } // Add to the sequence - context.(*session).seq = append(context.(*session).seq, meta) + context.(*session).seq = append(context.(*session).seq, []Completion{ + {Message: message}, + }) // Return success return context @@ -72,9 +76,7 @@ func (session *session) Num() int { if len(session.seq) == 0 { return 0 } - message := session.seq[len(session.seq)-1] - message. - return session.seq[len(session.seq)-1].Role + return session.seq[len(session.seq)-1].Num() } // Return the role of the last message @@ -82,7 +84,7 @@ func (session *session) Role() string { if len(session.seq) == 0 { return "" } - return session.seq[len(session.seq)-1].Role + return session.seq[len(session.seq)-1].Role() } // Return the text of the last message @@ -90,11 +92,10 @@ func (session *session) Text(index int) string { if len(session.seq) == 0 { return "" } - meta := session.seq[len(session.seq)-1] - return meta.Text() + return session.seq[len(session.seq)-1].Text(index) } -// Return the text of the last message +// Return tool calls for the last message func (session *session) ToolCalls(index int) []llm.ToolCall { return nil } @@ -114,46 +115,33 @@ func (session *session) FromTool(context.Context, ...llm.ToolResult) error { /////////////////////////////////////////////////////////////////////////////// // PRIVATE METHODS -func systemPrompt(prompt string) *MessageMeta { - return &MessageMeta{ +func systemPrompt(prompt string) *Message { + return &Message{ Role: "system", Content: prompt, } } -func userPrompt(prompt string, opts ...llm.Opt) (*MessageMeta, error) { - // Apply attachments - opt, err := llm.ApplyOpts(opts...) +func userPrompt(prompt string, opts ...llm.Opt) (*Message, error) { + // Get attachments + opt, err := llm.ApplyPromptOpts(opts...) if err != nil { return nil, err } - // Get attachments + // Get attachments, allocate content attachments := opt.Attachments() + content := make([]*Content, 1, len(attachments)+1) - // Create user message - meta := MessageMeta{ - Role: "user", - Content: make([]*Content, 1, len(attachments)+1), - } - - // Append the text - meta.Content = []*Content{ - NewTextContent(prompt), + // Append the text and the attachments + content[0] = NewTextContent(prompt) + for _, attachment := range attachments { + content = append(content, NewImageAttachment(attachment)) } - // Append any additional data - // TODO - /* - for _, attachment := range attachments { - content, err := attachmentContent(attachment) - if err != nil { - return nil, err - } - meta.Content = append(meta.Content, content) - } - */ - // Return success - return &meta, nil + return &Message{ + Role: "user", + Content: content, + }, nil } diff --git a/pkg/mistral/testdata/LICENSE b/pkg/mistral/testdata/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/pkg/mistral/testdata/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/pkg/mistral/testdata/guggenheim.jpg b/pkg/mistral/testdata/guggenheim.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7e1651729c5008c35446d52049de346f86962ef3 GIT binary patch literal 139053 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 Date: Sun, 2 Feb 2025 14:24:13 +0100 Subject: [PATCH 08/15] Updated mistral --- pkg/mistral/chat_completion.go | 17 ++-- pkg/mistral/chat_completion_test.go | 6 +- pkg/mistral/message.go | 115 ++++++++++++++++++---------- pkg/mistral/model.go | 3 +- pkg/mistral/session.go | 110 +++++++++++++++++++++----- pkg/mistral/session_test.go | 66 ++++++++++++++++ pkg/mistral/tool.go | 22 +----- toolkit.go | 1 - 8 files changed, 246 insertions(+), 94 deletions(-) create mode 100644 pkg/mistral/session_test.go diff --git a/pkg/mistral/chat_completion.go b/pkg/mistral/chat_completion.go index 91a081f..b0e4bb0 100644 --- a/pkg/mistral/chat_completion.go +++ b/pkg/mistral/chat_completion.go @@ -78,11 +78,8 @@ func (mistral *Client) ChatCompletion(ctx context.Context, context llm.Context, } // Always append the first message of each completion - for _, completion := range context.(*session).seq { - if completion.Num() == 0 { - continue - } - messages = append(messages, completion.Message(0)) + for _, message := range context.(*session).seq { + messages = append(messages, message) } // Request @@ -179,8 +176,10 @@ func appendCompletion(response *Response, c *Completion) { response.Completions = append(response.Completions, Completion{ Index: c.Index, Message: &Message{ - Role: c.Delta.Role, - Content: "", + RoleContent: RoleContent{ + Role: c.Delta.Role(), + Content: "", + }, }, }) } @@ -188,8 +187,8 @@ func appendCompletion(response *Response, c *Completion) { if c.Reason != "" { response.Completions[c.Index].Reason = c.Reason } - if c.Delta.Role != "" { - response.Completions[c.Index].Message.Role = c.Delta.Role + if role := c.Delta.Role(); role != "" { + response.Completions[c.Index].Message.RoleContent.Role = role } // TODO: We only allow deltas which are strings at the moment... diff --git a/pkg/mistral/chat_completion_test.go b/pkg/mistral/chat_completion_test.go index 9730130..fe2697c 100644 --- a/pkg/mistral/chat_completion_test.go +++ b/pkg/mistral/chat_completion_test.go @@ -2,7 +2,7 @@ package mistral_test import ( "context" - "errors" + "fmt" "os" "strings" "testing" @@ -243,6 +243,6 @@ func (weather) Description() string { return "Get the weather for a city" } -func (weather) Run(ctx context.Context) (any, error) { - return nil, errors.New("I couldn't retrieve the weather for that city") +func (w weather) Run(ctx context.Context) (any, error) { + return fmt.Sprintf("The weather in %q is sunny and warm", w.City), nil } diff --git a/pkg/mistral/message.go b/pkg/mistral/message.go index 27acd90..b031171 100644 --- a/pkg/mistral/message.go +++ b/pkg/mistral/message.go @@ -1,7 +1,10 @@ package mistral import ( + "encoding/json" + "github.com/mutablelogic/go-llm" + "github.com/mutablelogic/go-llm/pkg/tool" ) /////////////////////////////////////////////////////////////////////////////// @@ -12,6 +15,21 @@ type Completions []Completion var _ llm.Completion = Completions{} +// Message with text or object content +type Message struct { + RoleContent + ToolCallArray `json:"tool_calls,omitempty"` +} + +type RoleContent struct { + Role string `json:"role,omitempty"` // assistant, user, tool, system + Id string `json:"tool_call_id,omitempty"` // tool call - when role is tool + Name string `json:"name,omitempty"` // function name - when role is tool + Content any `json:"content,omitempty"` // string or array of text, reference, image_url +} + +var _ llm.Completion = (*Message)(nil) + // Completion Variation type Completion struct { Index uint64 `json:"index"` @@ -20,23 +38,15 @@ type Completion struct { Reason string `json:"finish_reason,omitempty"` } -// Message with text or object content -type Message struct { - Role string `json:"role,omitempty"` // assistant, user, tool, system - Prefix bool `json:"prefix,omitempty"` - Content any `json:"content,omitempty"` - ToolCalls `json:"tool_calls,omitempty"` -} - type Content struct { - Type string `json:"type"` // text, reference, image_url + Type string `json:"type,omitempty"` // text, reference, image_url *Text `json:"text,omitempty"` // text content *Prediction `json:"content,omitempty"` // prediction *Image `json:"image_url,omitempty"` // image_url } // A set of tool calls -type ToolCalls []ToolCall +type ToolCallArray []ToolCall // text content type Text string @@ -78,58 +88,57 @@ func NewImageAttachment(a *llm.Attachment) *Content { } /////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS +// PUBLIC METHODS - MESSAGE -// Return the number of completions -func (c Completions) Num() int { - return len(c) +func (m Message) Num() int { + return 1 } -// Return the role of the completion -func (c Completions) Role() string { - // The role should be the same for all completions, let's use the first one - if len(c) == 0 { - return "" - } - return c[0].Message.Role +func (m Message) Role() string { + return m.RoleContent.Role } -// Return the text content for a specific completion -func (c Completions) Text(index int) string { - if index < 0 || index >= len(c) { +func (m Message) Text(index int) string { + if index != 0 { return "" } - completion := c[index].Message - if text, ok := completion.Content.(string); ok { + // If content is text, return it + if text, ok := m.Content.(string); ok { return text } - // TODO: Will the text be in other forms? + // For other kinds, return empty string for the moment return "" } -// Return the current session tool calls given the completion index. -// Will return nil if no tool calls were returned. -func (c Completions) ToolCalls(index int) []llm.ToolCall { - if index < 0 || index >= len(c) { - return nil - } - - // Get the completion - completion := c[index].Message - if completion == nil { +func (m Message) ToolCalls(index int) []llm.ToolCall { + if index != 0 { return nil } // Make the tool calls - calls := make([]llm.ToolCall, 0, len(completion.ToolCalls)) - for _, call := range completion.ToolCalls { - calls = append(calls, &toolcall{call}) + calls := make([]llm.ToolCall, 0, len(m.ToolCallArray)) + for _, call := range m.ToolCallArray { + var args map[string]any + if call.Function.Arguments != "" { + if err := json.Unmarshal([]byte(call.Function.Arguments), &args); err != nil { + return nil + } + } + calls = append(calls, tool.NewCall(call.Id, call.Function.Name, args)) } // Return success return calls } +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS - COMPLETIONS + +// Return the number of completions +func (c Completions) Num() int { + return len(c) +} + // Return message for a specific completion func (c Completions) Message(index int) *Message { if index < 0 || index >= len(c) { @@ -137,3 +146,29 @@ func (c Completions) Message(index int) *Message { } return c[index].Message } + +// Return the role of the completion +func (c Completions) Role() string { + // The role should be the same for all completions, let's use the first one + if len(c) == 0 { + return "" + } + return c[0].Message.Role() +} + +// Return the text content for a specific completion +func (c Completions) Text(index int) string { + if index < 0 || index >= len(c) { + return "" + } + return c[index].Message.Text(0) +} + +// Return the current session tool calls given the completion index. +// Will return nil if no tool calls were returned. +func (c Completions) ToolCalls(index int) []llm.ToolCall { + if index < 0 || index >= len(c) { + return nil + } + return c[index].Message.ToolCalls(0) +} diff --git a/pkg/mistral/model.go b/pkg/mistral/model.go index f981ee4..3bea206 100644 --- a/pkg/mistral/model.go +++ b/pkg/mistral/model.go @@ -12,6 +12,7 @@ import ( // TYPES type model struct { + *Client meta Model } @@ -65,7 +66,7 @@ func (c *Client) ListModels(ctx context.Context) ([]llm.Model, error) { // Make models result := make([]llm.Model, 0, len(response.Data)) for _, meta := range response.Data { - result = append(result, &model{meta: meta}) + result = append(result, &model{c, meta}) } // Return models diff --git a/pkg/mistral/session.go b/pkg/mistral/session.go index 82d6573..3b50539 100644 --- a/pkg/mistral/session.go +++ b/pkg/mistral/session.go @@ -12,9 +12,9 @@ import ( // TYPES type session struct { - model *model // The model used for the session - opts []llm.Opt // Options to apply to the session - seq []Completions // Sequence of messages + model *model // The model used for the session + opts []llm.Opt // Options to apply to the session + seq []*Message // Sequence of messages } var _ llm.Context = (*session)(nil) @@ -27,7 +27,7 @@ func (model *model) Context(opts ...llm.Opt) llm.Context { return &session{ model: model, opts: opts, - seq: make([]Completions, 0, 10), + seq: make([]*Message, 0, 10), } } @@ -43,9 +43,7 @@ func (model *model) UserPrompt(prompt string, opts ...llm.Opt) llm.Context { } // Add to the sequence - context.(*session).seq = append(context.(*session).seq, []Completion{ - {Message: message}, - }) + context.(*session).seq = append(context.(*session).seq, message) // Return success return context @@ -76,7 +74,7 @@ func (session *session) Num() int { if len(session.seq) == 0 { return 0 } - return session.seq[len(session.seq)-1].Num() + return 1 } // Return the role of the last message @@ -97,19 +95,62 @@ func (session *session) Text(index int) string { // Return tool calls for the last message func (session *session) ToolCalls(index int) []llm.ToolCall { - return nil + if len(session.seq) == 0 { + return nil + } + return session.seq[len(session.seq)-1].ToolCalls(index) } // Generate a response from a user prompt (with attachments and // other options) -func (session *session) FromUser(context.Context, string, ...llm.Opt) error { - return llm.ErrNotImplemented +func (session *session) FromUser(ctx context.Context, prompt string, opts ...llm.Opt) error { + message, err := userPrompt(prompt, opts...) + if err != nil { + return err + } + + // Append the user prompt to the sequence + session.seq = append(session.seq, message) + + // The options come from the session options and the user options + chatopts := make([]llm.Opt, 0, len(session.opts)+len(opts)) + chatopts = append(chatopts, session.opts...) + chatopts = append(chatopts, opts...) + + // Call the 'chat completion' method + r, err := session.model.ChatCompletion(ctx, session, chatopts...) + if err != nil { + return err + } + + // Append the first message from the set of completions + session.seq = append(session.seq, r.Completions.Message(0)) + + // Return success + return nil } -// Generate a response from a tool, passing the results -// from the tool call -func (session *session) FromTool(context.Context, ...llm.ToolResult) error { - return llm.ErrNotImplemented +// Generate a response from a tool, passing the results from the tool call +func (session *session) FromTool(ctx context.Context, results ...llm.ToolResult) error { + messages, err := toolResults(results...) + if err != nil { + return err + } + + // Append the tool results to the sequence + session.seq = append(session.seq, messages...) + + // Call the 'chat' method + r, err := session.model.ChatCompletion(ctx, session, session.opts...) + if err != nil { + return err + } + + // Append the first message from the set of completions + session.seq = append(session.seq, r.Completions.Message(0)) + + // Return success + return nil } /////////////////////////////////////////////////////////////////////////////// @@ -117,8 +158,10 @@ func (session *session) FromTool(context.Context, ...llm.ToolResult) error { func systemPrompt(prompt string) *Message { return &Message{ - Role: "system", - Content: prompt, + RoleContent: RoleContent{ + Role: "system", + Content: prompt, + }, } } @@ -141,7 +184,36 @@ func userPrompt(prompt string, opts ...llm.Opt) (*Message, error) { // Return success return &Message{ - Role: "user", - Content: content, + RoleContent: RoleContent{ + Role: "user", + Content: content, + }, }, nil } + +func toolResults(results ...llm.ToolResult) ([]*Message, error) { + // Check for no results + if len(results) == 0 { + return nil, llm.ErrBadParameter.Withf("No tool results") + } + + // Create results + messages := make([]*Message, 0, len(results)) + for _, result := range results { + value, err := json.Marshal(result.Value()) + if err != nil { + return nil, err + } + messages = append(messages, &Message{ + RoleContent: RoleContent{ + Role: "tool", + Id: result.Call().Id(), + Name: result.Call().Name(), + Content: string(value), + }, + }) + } + + // Return success + return messages, nil +} diff --git a/pkg/mistral/session_test.go b/pkg/mistral/session_test.go new file mode 100644 index 0000000..ac2a2f6 --- /dev/null +++ b/pkg/mistral/session_test.go @@ -0,0 +1,66 @@ +package mistral_test + +import ( + "context" + "os" + "testing" + + // Packages + opts "github.com/mutablelogic/go-client" + "github.com/mutablelogic/go-llm" + mistral "github.com/mutablelogic/go-llm/pkg/mistral" + "github.com/mutablelogic/go-llm/pkg/tool" + assert "github.com/stretchr/testify/assert" +) + +func Test_session_001(t *testing.T) { + assert := assert.New(t) + + client, err := mistral.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) + assert.NoError(err) + + model := client.Model(context.TODO(), "mistral-small-latest") + if !assert.NotNil(model) { + t.FailNow() + } + + session := model.Context() + if assert.NotNil(session) { + err := session.FromUser(context.TODO(), "Hello, how are you?") + assert.NoError(err) + t.Log(session) + } +} + +func Test_session_002(t *testing.T) { + assert := assert.New(t) + + client, err := mistral.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) + assert.NoError(err) + + model := client.Model(context.TODO(), "mistral-small-latest") + if !assert.NotNil(model) { + t.FailNow() + } + + toolkit := tool.NewToolKit() + toolkit.Register(&weather{}) + + session := model.Context(llm.WithToolKit(toolkit)) + if !assert.NotNil(session) { + t.FailNow() + } + + assert.NoError(session.FromUser(context.TODO(), "What is the weather like in London today?")) + calls := session.ToolCalls(0) + assert.Len(calls, 1) + assert.Equal("weather_in_city", calls[0].Name()) + + result, err := toolkit.Run(context.TODO(), calls...) + assert.NoError(err) + assert.Len(result, 1) + + assert.NoError(session.FromTool(context.TODO(), result...)) + + t.Log(session) +} diff --git a/pkg/mistral/tool.go b/pkg/mistral/tool.go index 163c881..255146e 100644 --- a/pkg/mistral/tool.go +++ b/pkg/mistral/tool.go @@ -1,7 +1,6 @@ package mistral import ( - "bytes" "encoding/json" ) @@ -14,7 +13,7 @@ type ToolCall struct { Function struct { Name string `json:"name,omitempty"` // tool name Arguments string `json:"arguments,omitempty"` // tool arguments - } + } `json:"function"` } type toolcall struct { @@ -35,22 +34,3 @@ func (t toolcall) String() string { } return string(data) } - -/////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - TOOL CALL - -func (t toolcall) Id() string { - return t.meta.Id -} - -// The tool name -func (t toolcall) Name() string { - return t.meta.Function.Name -} - -// Decode the calling parameters -func (t toolcall) Decode(v any) error { - var buf bytes.Buffer - buf.WriteString(t.meta.Function.Arguments) - return json.NewDecoder(&buf).Decode(v) -} diff --git a/toolkit.go b/toolkit.go index e61d98d..8b180e2 100644 --- a/toolkit.go +++ b/toolkit.go @@ -28,7 +28,6 @@ type Tool interface { Description() string // Run the tool with a deadline and return the result - // TODO: Change 'any' to ToolResult Run(context.Context) (any, error) } From 813e80883bb0c76e564514f68c78c6ce5914f608 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Sun, 2 Feb 2025 14:56:46 +0100 Subject: [PATCH 09/15] Updated mistral --- pkg/mistral/chat_completion_test.go | 13 +--- pkg/mistral/client_test.go | 55 +++++++++++---- pkg/mistral/embeddings.go | 101 ++++++++++++++++++++++++++++ pkg/mistral/embeddings_test.go | 20 ++++++ pkg/mistral/model.go | 5 -- pkg/mistral/model_test.go | 9 +-- pkg/mistral/session_test.go | 28 +++----- 7 files changed, 176 insertions(+), 55 deletions(-) create mode 100644 pkg/mistral/embeddings.go create mode 100644 pkg/mistral/embeddings_test.go diff --git a/pkg/mistral/chat_completion_test.go b/pkg/mistral/chat_completion_test.go index fe2697c..41afb88 100644 --- a/pkg/mistral/chat_completion_test.go +++ b/pkg/mistral/chat_completion_test.go @@ -8,7 +8,7 @@ import ( "testing" // Packages - opts "github.com/mutablelogic/go-client" + "github.com/mutablelogic/go-llm" mistral "github.com/mutablelogic/go-llm/pkg/mistral" "github.com/mutablelogic/go-llm/pkg/tool" @@ -17,11 +17,8 @@ import ( func Test_chat_001(t *testing.T) { assert := assert.New(t) - - client, err := mistral.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - model := client.Model(context.TODO(), "mistral-small-latest") + if assert.NotNil(model) { response, err := client.ChatCompletion(context.TODO(), model.UserPrompt("Hello, how are you?")) assert.NoError(err) @@ -32,8 +29,6 @@ func Test_chat_001(t *testing.T) { func Test_chat_002(t *testing.T) { assert := assert.New(t) - client, err := mistral.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) model := client.Model(context.TODO(), "mistral-large-latest") if !assert.NotNil(model) { t.FailNow() @@ -181,8 +176,6 @@ func Test_chat_002(t *testing.T) { func Test_chat_003(t *testing.T) { assert := assert.New(t) - client, err := mistral.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) model := client.Model(context.TODO(), "pixtral-12b-2409") if !assert.NotNil(model) { t.FailNow() @@ -206,8 +199,6 @@ func Test_chat_003(t *testing.T) { func Test_chat_004(t *testing.T) { assert := assert.New(t) - client, err := mistral.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) model := client.Model(context.TODO(), "mistral-small-latest") if !assert.NotNil(model) { t.FailNow() diff --git a/pkg/mistral/client_test.go b/pkg/mistral/client_test.go index 7f4a9d6..93fed56 100644 --- a/pkg/mistral/client_test.go +++ b/pkg/mistral/client_test.go @@ -1,7 +1,10 @@ package mistral_test import ( + "flag" + "log" "os" + "strconv" "testing" // Packages @@ -10,22 +13,46 @@ import ( assert "github.com/stretchr/testify/assert" ) -func Test_client_001(t *testing.T) { - assert := assert.New(t) - client, err := mistral.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - assert.NotNil(client) - t.Log(client) +/////////////////////////////////////////////////////////////////////////////// +// TEST SET-UP + +var ( + client *mistral.Client +) + +func TestMain(m *testing.M) { + var verbose bool + + // Verbose output + flag.Parse() + if f := flag.Lookup("test.v"); f != nil { + if v, err := strconv.ParseBool(f.Value.String()); err == nil { + verbose = v + } + } + + // API KEY + api_key := os.Getenv("MISTRAL_API_KEY") + if api_key == "" { + log.Print("MISTRAL_API_KEY not set") + os.Exit(0) + } + + // Create client + var err error + client, err = mistral.New(api_key, opts.OptTrace(os.Stderr, verbose)) + if err != nil { + log.Println(err) + os.Exit(-1) + } + os.Exit(m.Run()) } /////////////////////////////////////////////////////////////////////////////// -// ENVIRONMENT +// TESTS -func GetApiKey(t *testing.T) string { - key := os.Getenv("MISTRAL_API_KEY") - if key == "" { - t.Skip("MISTRAL_API_KEY not set") - t.SkipNow() - } - return key +func Test_client_001(t *testing.T) { + assert := assert.New(t) + assert.NotNil(client) + t.Log(client) } diff --git a/pkg/mistral/embeddings.go b/pkg/mistral/embeddings.go new file mode 100644 index 0000000..48555f3 --- /dev/null +++ b/pkg/mistral/embeddings.go @@ -0,0 +1,101 @@ +package mistral + +import ( + "context" + "encoding/json" + + // Packages + client "github.com/mutablelogic/go-client" + llm "github.com/mutablelogic/go-llm" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +// embeddings is the implementation of the llm.Embedding interface +type embeddings struct { + Embeddings +} + +// Embeddings is the metadata for a generated embedding vector +type Embeddings struct { + Id string `json:"id"` + Type string `json:"object"` + Model string `json:"model"` + Data []Embedding `json:"data"` + Metrics +} + +// Embedding is a single vector +type Embedding struct { + Type string `json:"object"` + Index uint64 `json:"index"` + Vector []float64 `json:"embedding"` +} + +/////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +func (m Embedding) MarshalJSON() ([]byte, error) { + return json.Marshal(m.Vector) +} + +func (m embeddings) MarshalJSON() ([]byte, error) { + return json.Marshal(m.Embeddings) +} + +func (m embeddings) String() string { + data, err := json.MarshalIndent(m, "", " ") + if err != nil { + return err.Error() + } + return string(data) +} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +type reqEmbedding struct { + Model string `json:"model"` + Input []string `json:"input"` + Format string `json:"encoding_format,omitempty"` +} + +func (mistral *Client) GenerateEmbedding(ctx context.Context, name string, prompt []string, _ ...llm.Opt) (*embeddings, error) { + // Options are currently ignored + + // Bail out is no prompt + if len(prompt) == 0 { + return nil, llm.ErrBadParameter.With("missing prompt") + } + + // Request + req, err := client.NewJSONRequest(reqEmbedding{ + Model: name, + Input: prompt, + }) + if err != nil { + return nil, err + } + + // Response + var response embeddings + if err := mistral.DoWithContext(ctx, req, &response, client.OptPath("embeddings")); err != nil { + return nil, err + } + + // Return success + return &response, nil +} + +// Generate one vector +func (model *model) Embedding(ctx context.Context, prompt string, opts ...llm.Opt) ([]float64, error) { + response, err := model.GenerateEmbedding(ctx, model.Name(), []string{prompt}, opts...) + if err != nil { + return nil, err + } + if len(response.Embeddings.Data) == 0 { + return nil, llm.ErrNotFound.With("no embeddings returned") + } + return response.Embeddings.Data[0].Vector, nil +} diff --git a/pkg/mistral/embeddings_test.go b/pkg/mistral/embeddings_test.go new file mode 100644 index 0000000..bc09454 --- /dev/null +++ b/pkg/mistral/embeddings_test.go @@ -0,0 +1,20 @@ +package mistral_test + +import ( + "context" + "testing" + + // Packages + assert "github.com/stretchr/testify/assert" +) + +func Test_embeddings_001(t *testing.T) { + assert := assert.New(t) + model := client.Model(context.TODO(), "mistral-embed") + if assert.NotNil(model) { + response, err := model.Embedding(context.TODO(), "Hello, how are you?") + assert.NoError(err) + assert.NotEmpty(response) + t.Log(response) + } +} diff --git a/pkg/mistral/model.go b/pkg/mistral/model.go index 3bea206..3093b91 100644 --- a/pkg/mistral/model.go +++ b/pkg/mistral/model.go @@ -80,8 +80,3 @@ func (c *Client) ListModels(ctx context.Context) ([]llm.Model, error) { func (m model) Name() string { return m.meta.Name } - -// Embedding vector generation -func (m model) Embedding(context.Context, string, ...llm.Opt) ([]float64, error) { - return nil, llm.ErrNotImplemented -} diff --git a/pkg/mistral/model_test.go b/pkg/mistral/model_test.go index 2a4048d..812be6e 100644 --- a/pkg/mistral/model_test.go +++ b/pkg/mistral/model_test.go @@ -3,23 +3,20 @@ package mistral_test import ( "context" "encoding/json" - "os" "testing" // Packages - opts "github.com/mutablelogic/go-client" - mistral "github.com/mutablelogic/go-llm/pkg/mistral" + assert "github.com/stretchr/testify/assert" ) func Test_models_001(t *testing.T) { assert := assert.New(t) - client, err := mistral.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - assert.NotNil(client) + response, err := client.ListModels(context.TODO()) assert.NoError(err) assert.NotEmpty(response) + data, err := json.MarshalIndent(response, "", " ") assert.NoError(err) t.Log(string(data)) diff --git a/pkg/mistral/session_test.go b/pkg/mistral/session_test.go index ac2a2f6..7fbcaa3 100644 --- a/pkg/mistral/session_test.go +++ b/pkg/mistral/session_test.go @@ -2,23 +2,16 @@ package mistral_test import ( "context" - "os" "testing" // Packages - opts "github.com/mutablelogic/go-client" - "github.com/mutablelogic/go-llm" - mistral "github.com/mutablelogic/go-llm/pkg/mistral" - "github.com/mutablelogic/go-llm/pkg/tool" + llm "github.com/mutablelogic/go-llm" + tool "github.com/mutablelogic/go-llm/pkg/tool" assert "github.com/stretchr/testify/assert" ) func Test_session_001(t *testing.T) { assert := assert.New(t) - - client, err := mistral.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - model := client.Model(context.TODO(), "mistral-small-latest") if !assert.NotNil(model) { t.FailNow() @@ -34,10 +27,6 @@ func Test_session_001(t *testing.T) { func Test_session_002(t *testing.T) { assert := assert.New(t) - - client, err := mistral.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - model := client.Model(context.TODO(), "mistral-small-latest") if !assert.NotNil(model) { t.FailNow() @@ -53,14 +42,15 @@ func Test_session_002(t *testing.T) { assert.NoError(session.FromUser(context.TODO(), "What is the weather like in London today?")) calls := session.ToolCalls(0) - assert.Len(calls, 1) - assert.Equal("weather_in_city", calls[0].Name()) + if assert.Len(calls, 1) { + assert.Equal("weather_in_city", calls[0].Name()) - result, err := toolkit.Run(context.TODO(), calls...) - assert.NoError(err) - assert.Len(result, 1) + result, err := toolkit.Run(context.TODO(), calls...) + assert.NoError(err) + assert.Len(result, 1) - assert.NoError(session.FromTool(context.TODO(), result...)) + assert.NoError(session.FromTool(context.TODO(), result...)) + } t.Log(session) } From b84adbc1b34b0a089b88f4dcfcdad79727e3164a Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Sun, 2 Feb 2025 20:15:52 +0100 Subject: [PATCH 10/15] Updated Ollama --- pkg/mistral/message.go | 7 +- pkg/ollama/chat.go | 113 ++++----- pkg/ollama/{chat_test.go => chat_test.go_old} | 0 pkg/ollama/client_test.go | 54 +++-- pkg/ollama/embedding_test.go | 11 +- pkg/ollama/message.go | 68 +++++- pkg/ollama/model.go | 66 ++++-- pkg/ollama/model_test.go | 44 ++-- pkg/ollama/opt.go | 16 +- pkg/ollama/session.go | 215 ++++++++++-------- .../{session_test.go => session_test.go_old} | 0 11 files changed, 362 insertions(+), 232 deletions(-) rename pkg/ollama/{chat_test.go => chat_test.go_old} (100%) rename pkg/ollama/{session_test.go => session_test.go_old} (100%) diff --git a/pkg/mistral/message.go b/pkg/mistral/message.go index b031171..6300b9e 100644 --- a/pkg/mistral/message.go +++ b/pkg/mistral/message.go @@ -3,6 +3,7 @@ package mistral import ( "encoding/json" + // Packages "github.com/mutablelogic/go-llm" "github.com/mutablelogic/go-llm/pkg/tool" ) @@ -23,13 +24,11 @@ type Message struct { type RoleContent struct { Role string `json:"role,omitempty"` // assistant, user, tool, system + Content any `json:"content,omitempty"` // string or array of text, reference, image_url Id string `json:"tool_call_id,omitempty"` // tool call - when role is tool Name string `json:"name,omitempty"` // function name - when role is tool - Content any `json:"content,omitempty"` // string or array of text, reference, image_url } -var _ llm.Completion = (*Message)(nil) - // Completion Variation type Completion struct { Index uint64 `json:"index"` @@ -38,6 +37,8 @@ type Completion struct { Reason string `json:"finish_reason,omitempty"` } +var _ llm.Completion = (*Message)(nil) + type Content struct { Type string `json:"type,omitempty"` // text, reference, image_url *Text `json:"text,omitempty"` // text content diff --git a/pkg/ollama/chat.go b/pkg/ollama/chat.go index 14bf5f5..888e84c 100644 --- a/pkg/ollama/chat.go +++ b/pkg/ollama/chat.go @@ -13,13 +13,13 @@ import ( /////////////////////////////////////////////////////////////////////////////// // TYPES -// Chat Response +// Chat Completion Response type Response struct { - Model string `json:"model"` - CreatedAt time.Time `json:"created_at"` - Message MessageMeta `json:"message"` - Done bool `json:"done"` - Reason string `json:"done_reason,omitempty"` + Model string `json:"model"` + CreatedAt time.Time `json:"created_at"` + Done bool `json:"done"` + Reason string `json:"done_reason,omitempty"` + Message `json:"message"` Metrics } @@ -33,6 +33,8 @@ type Metrics struct { EvalDuration time.Duration `json:"eval_duration,omitempty"` } +var _ llm.Completion = (*Response)(nil) + /////////////////////////////////////////////////////////////////////////////// // STRINGIFY @@ -49,34 +51,36 @@ func (r Response) String() string { type reqChat struct { Model string `json:"model"` - Messages []*MessageMeta `json:"messages"` - Tools []ToolFunction `json:"tools,omitempty"` + Messages []*Message `json:"messages"` + Tools []llm.Tool `json:"tools,omitempty"` Format string `json:"format,omitempty"` Options map[string]interface{} `json:"options,omitempty"` Stream bool `json:"stream"` KeepAlive *time.Duration `json:"keep_alive,omitempty"` } -func (ollama *Client) Chat(ctx context.Context, prompt llm.Context, opts ...llm.Opt) (*Response, error) { +func (ollama *Client) Chat(ctx context.Context, context llm.Context, opts ...llm.Opt) (*Response, error) { + // Apply options opt, err := llm.ApplyOpts(opts...) if err != nil { return nil, err } // Append the system prompt at the beginning - seq := make([]*MessageMeta, 0, len(prompt.(*session).seq)+1) - if system := opt.SystemPrompt(); system != "" { - seq = append(seq, &MessageMeta{ - Role: "system", - Content: opt.SystemPrompt(), - }) + messages := make([]*Message, 0, len(context.(*session).seq)+1) + //if system := opt.SystemPrompt(); system != "" { + // messages = append(messages, systemPrompt(system)) + //} + + // Always append the first message of each completion + for _, message := range context.(*session).seq { + messages = append(messages, message) } - seq = append(seq, prompt.(*session).seq...) // Request req, err := client.NewJSONRequest(reqChat{ - Model: prompt.(*session).model.Name(), - Messages: seq, + Model: context.(*session).model.Name(), + Messages: messages, Tools: optTools(ollama, opt), Format: optFormat(opt), Options: optOptions(opt), @@ -88,52 +92,53 @@ func (ollama *Client) Chat(ctx context.Context, prompt llm.Context, opts ...llm. } // Response - var response, delta Response - if err := ollama.DoWithContext(ctx, req, &delta, client.OptPath("chat"), client.OptJsonStreamCallback(func(v any) error { - if v, ok := v.(*Response); !ok || v == nil { - return llm.ErrConflict.Withf("Invalid stream response: %v", v) - } else { - response.Model = v.Model - response.CreatedAt = v.CreatedAt - response.Message.Role = v.Message.Role - response.Message.Content += v.Message.Content - if v.Done { - response.Done = v.Done - response.Metrics = v.Metrics - response.Reason = v.Reason + var response Response + reqopts := []client.RequestOpt{ + client.OptPath("chat"), + } + if optStream(ollama, opt) { + reqopts = append(reqopts, client.OptJsonStreamCallback(func(v any) error { + if v, ok := v.(*Response); !ok || v == nil { + return llm.ErrConflict.Withf("Invalid stream response: %v", v) + } else if err := streamEvent(&response, v); err != nil { + return err } - } - - //Call the chat callback - if optStream(ollama, opt) { if fn := opt.StreamFn(); fn != nil { fn(&response) } - } - return nil - })); err != nil { - return nil, err + return nil + })) } - // We return the delta or the response - if optStream(ollama, opt) { - return &response, nil - } else { - return &delta, nil + // Response + if err := ollama.DoWithContext(ctx, req, &response, reqopts...); err != nil { + return nil, err } -} -/////////////////////////////////////////////////////////////////////////////// -// INTERFACE - CONTEXT CONTENT - -func (response Response) Role() string { - return response.Message.Role + // Return success + return &response, nil } -func (response Response) Text() string { - return response.Message.Content -} +/////////////////////////////////////////////////////////////////////////////// +// PRIVATE METHODS -func (response Response) ToolCalls() []llm.ToolCall { +func streamEvent(response, delta *Response) error { + if delta.Model != "" { + response.Model = delta.Model + } + if !delta.CreatedAt.IsZero() { + response.CreatedAt = delta.CreatedAt + } + if delta.Message.RoleContent.Role != "" { + response.Message.RoleContent.Role = delta.Message.RoleContent.Role + } + if delta.Message.RoleContent.Content != "" { + response.Message.RoleContent.Content += delta.Message.RoleContent.Content + } + if delta.Done { + response.Done = delta.Done + response.Metrics = delta.Metrics + response.Reason = delta.Reason + } return nil } diff --git a/pkg/ollama/chat_test.go b/pkg/ollama/chat_test.go_old similarity index 100% rename from pkg/ollama/chat_test.go rename to pkg/ollama/chat_test.go_old diff --git a/pkg/ollama/client_test.go b/pkg/ollama/client_test.go index 851b98b..e1987c2 100644 --- a/pkg/ollama/client_test.go +++ b/pkg/ollama/client_test.go @@ -1,7 +1,10 @@ package ollama_test import ( + "flag" + "log" "os" + "strconv" "testing" // Packages @@ -10,23 +13,46 @@ import ( assert "github.com/stretchr/testify/assert" ) -func Test_client_001(t *testing.T) { - assert := assert.New(t) - client, err := ollama.New(GetEndpoint(t), opts.OptTrace(os.Stderr, true)) - if assert.NoError(err) { - assert.NotNil(client) - t.Log(client) +/////////////////////////////////////////////////////////////////////////////// +// TEST SET-UP + +var ( + client *ollama.Client +) + +func TestMain(m *testing.M) { + var verbose bool + + // Verbose output + flag.Parse() + if f := flag.Lookup("test.v"); f != nil { + if v, err := strconv.ParseBool(f.Value.String()); err == nil { + verbose = v + } } + + // Endpoint + endpoint_url := os.Getenv("OLLAMA_URL") + if endpoint_url == "" { + log.Print("OLLAMA_URL not set") + os.Exit(0) + } + + // Create client + var err error + client, err = ollama.New(endpoint_url, opts.OptTrace(os.Stderr, verbose)) + if err != nil { + log.Println(err) + os.Exit(-1) + } + os.Exit(m.Run()) } /////////////////////////////////////////////////////////////////////////////// -// ENVIRONMENT +// TESTS -func GetEndpoint(t *testing.T) string { - key := os.Getenv("OLLAMA_URL") - if key == "" { - t.Skip("OLLAMA_URL not set, skipping tests") - t.SkipNow() - } - return key +func Test_client_001(t *testing.T) { + assert := assert.New(t) + assert.NotNil(client) + t.Log(client) } diff --git a/pkg/ollama/embedding_test.go b/pkg/ollama/embedding_test.go index 77c854f..7e17989 100644 --- a/pkg/ollama/embedding_test.go +++ b/pkg/ollama/embedding_test.go @@ -2,24 +2,17 @@ package ollama_test import ( "context" - "os" "testing" // Packages - opts "github.com/mutablelogic/go-client" - ollama "github.com/mutablelogic/go-llm/pkg/ollama" + assert "github.com/stretchr/testify/assert" ) func Test_embed_001(t *testing.T) { - client, err := ollama.New(GetEndpoint(t), opts.OptTrace(os.Stderr, true)) - if err != nil { - t.FailNow() - } - t.Run("Embedding", func(t *testing.T) { assert := assert.New(t) - embedding, err := client.GenerateEmbedding(context.TODO(), "qwen:0.5b", []string{"world"}) + embedding, err := client.GenerateEmbedding(context.TODO(), "qwen:0.5b", []string{"hello, world"}) if !assert.NoError(err) { t.FailNow() } diff --git a/pkg/ollama/message.go b/pkg/ollama/message.go index 53efe76..d00c6b8 100644 --- a/pkg/ollama/message.go +++ b/pkg/ollama/message.go @@ -1,36 +1,80 @@ package ollama import ( + "fmt" + + // Packages llm "github.com/mutablelogic/go-llm" + tool "github.com/mutablelogic/go-llm/pkg/tool" ) /////////////////////////////////////////////////////////////////////////////// // TYPES -// Chat Message -type MessageMeta struct { - Role string `json:"role"` - Content string `json:"content,omitempty"` - FunctionName string `json:"name,omitempty"` // Function name for a tool result - Images []Data `json:"images,omitempty"` // Image attachments - ToolCalls []ToolCall `json:"tool_calls,omitempty"` // Tool calls from the assistant +// Message with text or object content +type Message struct { + RoleContent + ToolCallArray `json:"tool_calls,omitempty"` +} + +type RoleContent struct { + Role string `json:"role,omitempty"` // assistant, user, tool, system + Content string `json:"content,omitempty"` // string or array of text, reference, image_url + Images []Data `json:"images,omitempty"` // Image attachments + ToolResult } +// A set of tool calls +type ToolCallArray []ToolCall + type ToolCall struct { + Type string `json:"type"` // function Function ToolCallFunction `json:"function"` } type ToolCallFunction struct { Index int `json:"index,omitempty"` Name string `json:"name"` - Arguments map[string]any `json:"arguments"` + Arguments map[string]any `json:"arguments,omitempty"` } // Data represents the raw binary data of an image file. type Data []byte -// ToolFunction -type ToolFunction struct { - Type string `json:"type"` // function - Function llm.Tool `json:"function"` +// ToolResult +type ToolResult struct { + Name string `json:"name,omitempty"` // function name - when role is tool +} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS - MESSAGE + +func (m Message) Num() int { + return 1 +} + +func (m Message) Role() string { + return m.RoleContent.Role +} + +func (m Message) Text(index int) string { + if index != 0 { + return "" + } + return m.Content +} + +func (m Message) ToolCalls(index int) []llm.ToolCall { + if index != 0 { + return nil + } + + // Make the tool calls + calls := make([]llm.ToolCall, 0, len(m.ToolCallArray)) + for _, call := range m.ToolCallArray { + calls = append(calls, tool.NewCall(fmt.Sprint(call.Function.Index), call.Function.Name, call.Function.Arguments)) + } + + // Return success + return calls } diff --git a/pkg/ollama/model.go b/pkg/ollama/model.go index fa147e7..1d7900f 100644 --- a/pkg/ollama/model.go +++ b/pkg/ollama/model.go @@ -16,7 +16,7 @@ import ( // model is the implementation of the llm.Model interface type model struct { - client *Client + *Client ModelMeta } @@ -103,7 +103,7 @@ func (ollama *Client) Model(ctx context.Context, name string) llm.Model { // List models func (ollama *Client) ListModels(ctx context.Context) ([]llm.Model, error) { type respListModel struct { - Models []*model `json:"models"` + Models []ModelMeta `json:"models"` } // Send the request @@ -114,9 +114,8 @@ func (ollama *Client) ListModels(ctx context.Context) ([]llm.Model, error) { // Convert to llm.Model result := make([]llm.Model, 0, len(response.Models)) - for _, model := range response.Models { - model.client = ollama - result = append(result, model) + for _, meta := range response.Models { + result = append(result, &model{ollama, meta}) } // Return models @@ -126,7 +125,7 @@ func (ollama *Client) ListModels(ctx context.Context) ([]llm.Model, error) { // List running models func (ollama *Client) ListRunningModels(ctx context.Context) ([]llm.Model, error) { type respListModel struct { - Models []*model `json:"models"` + Models []ModelMeta `json:"models"` } // Send the request @@ -137,9 +136,8 @@ func (ollama *Client) ListRunningModels(ctx context.Context) ([]llm.Model, error // Convert to llm.Model result := make([]llm.Model, 0, len(response.Models)) - for _, model := range response.Models { - model.client = ollama - result = append(result, model) + for _, meta := range response.Models { + result = append(result, &model{ollama, meta}) } // Return models @@ -161,16 +159,13 @@ func (ollama *Client) GetModel(ctx context.Context, name string) (llm.Model, err } // Response - var response model + var response ModelMeta if err := ollama.DoWithContext(ctx, req, &response, client.OptPath("show")); err != nil { return nil, err - } else { - response.client = ollama - response.ModelMeta.Name = name } // Return success - return &response, nil + return &model{ollama, response}, nil } // Copy a local model by name @@ -251,3 +246,46 @@ func (ollama *Client) PullModel(ctx context.Context, name string, opts ...llm.Op // Return success return ollama.GetModel(ctx, name) } + +// Load a model into memory +func (ollama *Client) LoadModel(ctx context.Context, name string) (llm.Model, error) { + type reqLoadModel struct { + Model string `json:"model"` + } + + // Request + req, err := client.NewJSONRequest(reqLoadModel{ + Model: name, + }) + if err != nil { + return nil, err + } + + // Response + if err := ollama.DoWithContext(ctx, req, nil, client.OptPath("generate")); err != nil { + return nil, err + } + + // Return success + return ollama.GetModel(ctx, name) +} + +// Unload a model from memory +func (ollama *Client) UnloadModel(ctx context.Context, name string) error { + type reqLoadModel struct { + Model string `json:"model"` + KeepAlive uint `json:"keepalive"` + } + + // Request + req, err := client.NewJSONRequest(reqLoadModel{ + Model: name, + KeepAlive: 0, + }) + if err != nil { + return err + } + + // Response + return ollama.DoWithContext(ctx, req, nil, client.OptPath("generate")) +} diff --git a/pkg/ollama/model_test.go b/pkg/ollama/model_test.go index db14c9d..5a02d7f 100644 --- a/pkg/ollama/model_test.go +++ b/pkg/ollama/model_test.go @@ -2,23 +2,19 @@ package ollama_test import ( "context" - "os" "testing" // Packages - opts "github.com/mutablelogic/go-client" + ollama "github.com/mutablelogic/go-llm/pkg/ollama" assert "github.com/stretchr/testify/assert" ) func Test_model_001(t *testing.T) { - client, err := ollama.New(GetEndpoint(t), opts.OptTrace(os.Stderr, true)) - if err != nil { - t.FailNow() - } - var names []string + t.Run("Models", func(t *testing.T) { + // Get all models assert := assert.New(t) models, err := client.Models(context.TODO()) if !assert.NoError(err) { @@ -29,19 +25,19 @@ func Test_model_001(t *testing.T) { names = append(names, model.Name()) } }) - t.Run("Model", func(t *testing.T) { + // Get models one by one assert := assert.New(t) for _, name := range names { model, err := client.GetModel(context.TODO(), name) if !assert.NoError(err) { t.FailNow() } - t.Log(model) + assert.NotNil(model) } }) - t.Run("PullModel", func(t *testing.T) { + // Pull a model assert := assert.New(t) model, err := client.PullModel(context.TODO(), "qwen:0.5b", ollama.WithPullStatus(func(status *ollama.PullStatus) { t.Log(status) @@ -53,6 +49,7 @@ func Test_model_001(t *testing.T) { }) t.Run("CopyModel", func(t2 *testing.T) { + // Copy a model assert := assert.New(t) err := client.CopyModel(context.TODO(), "qwen:0.5b", t.Name()) if !assert.NoError(err) { @@ -60,15 +57,28 @@ func Test_model_001(t *testing.T) { } }) + t.Run("LoadModel", func(t2 *testing.T) { + // Load model into memory + assert := assert.New(t) + _, err := client.LoadModel(context.TODO(), t.Name()) + assert.NoError(err) + }) + + t.Run("UnloadModel", func(t2 *testing.T) { + // Unload model from memory + assert := assert.New(t) + err := client.UnloadModel(context.TODO(), t.Name()) + assert.NoError(err) + }) + t.Run("DeleteModel", func(t2 *testing.T) { + // Delete a model assert := assert.New(t) - _, err = client.GetModel(context.TODO(), t.Name()) - if !assert.NoError(err) { - t.FailNow() - } - err := client.DeleteModel(context.TODO(), t.Name()) - if !assert.NoError(err) { - t.FailNow() + _, err := client.GetModel(context.TODO(), t.Name()) + if assert.NoError(err) { + err = client.DeleteModel(context.TODO(), t.Name()) + assert.NoError(err) } }) + } diff --git a/pkg/ollama/opt.go b/pkg/ollama/opt.go index f5a28d0..5ed010d 100644 --- a/pkg/ollama/opt.go +++ b/pkg/ollama/opt.go @@ -79,24 +79,12 @@ func optPullStatus(opts *llm.Opts) func(*PullStatus) { return nil } -func optSystemPrompt(opts *llm.Opts) string { - return opts.SystemPrompt() -} - -func optTools(agent *Client, opts *llm.Opts) []ToolFunction { +func optTools(agent *Client, opts *llm.Opts) []llm.Tool { toolkit := opts.ToolKit() if toolkit == nil { return nil } - tools := toolkit.Tools(agent) - result := make([]ToolFunction, 0, len(tools)) - for _, tool := range tools { - result = append(result, ToolFunction{ - Type: "function", - Function: tool, - }) - } - return result + return toolkit.Tools(agent) } func optFormat(opts *llm.Opts) string { diff --git a/pkg/ollama/session.go b/pkg/ollama/session.go index 867f60a..50c702c 100644 --- a/pkg/ollama/session.go +++ b/pkg/ollama/session.go @@ -3,11 +3,9 @@ package ollama import ( "context" "encoding/json" - "fmt" // Packages llm "github.com/mutablelogic/go-llm" - "github.com/mutablelogic/go-llm/pkg/tool" ) /////////////////////////////////////////////////////////////////////////////// @@ -15,9 +13,9 @@ import ( // Implementation of a message session, which is a sequence of messages type session struct { - opts []llm.Opt - model *model - seq []*MessageMeta + model *model // The model used for the session + opts []llm.Opt // Options to apply to the session + seq []*Message // Sequence of messages } var _ llm.Context = (*session)(nil) @@ -25,21 +23,30 @@ var _ llm.Context = (*session)(nil) /////////////////////////////////////////////////////////////////////////////// // LIFECYCLE -// Create a new empty context +// Return an empty session context object for the model, setting session options func (model *model) Context(opts ...llm.Opt) llm.Context { return &session{ model: model, opts: opts, + seq: make([]*Message, 0, 10), } } -// Create a new context with a user prompt +// Convenience method to create a session context object with a user prompt, which +// panics on error func (model *model) UserPrompt(prompt string, opts ...llm.Opt) llm.Context { context := model.Context(opts...) - context.(*session).seq = append(context.(*session).seq, &MessageMeta{ - Role: "user", - Content: prompt, - }) + + // Create a user prompt + message, err := userPrompt(prompt, opts...) + if err != nil { + panic(err) + } + + // Add to the sequence + context.(*session).seq = append(context.(*session).seq, message) + + // Return success return context } @@ -60,137 +67,155 @@ func (session session) String() string { return string(data) } +////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Return the number of completions +func (session *session) Num() int { + if len(session.seq) == 0 { + return 0 + } + return 1 +} + +// Return the role of the last message +func (session *session) Role() string { + if len(session.seq) == 0 { + return "" + } + return session.seq[len(session.seq)-1].Role() +} + +// Return the text of the last message +func (session *session) Text(index int) string { + if len(session.seq) == 0 { + return "" + } + return session.seq[len(session.seq)-1].Text(index) +} + +// Return tool calls for the last message +func (session *session) ToolCalls(index int) []llm.ToolCall { + if len(session.seq) == 0 { + return nil + } + return session.seq[len(session.seq)-1].ToolCalls(index) +} + /////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS // Generate a response from a user prompt (with attachments) -func (s *session) FromUser(ctx context.Context, prompt string, opts ...llm.Opt) error { - // Append the user prompt - if user, err := userPrompt(prompt, opts...); err != nil { +func (session *session) FromUser(ctx context.Context, prompt string, opts ...llm.Opt) error { + message, err := userPrompt(prompt, opts...) + if err != nil { return err - } else { - s.seq = append(s.seq, user) } + // Append the user prompt to the sequence + session.seq = append(session.seq, message) + // The options come from the session options and the user options - chatopts := make([]llm.Opt, 0, len(s.opts)+len(opts)) - chatopts = append(chatopts, s.opts...) + chatopts := make([]llm.Opt, 0, len(session.opts)+len(opts)) + chatopts = append(chatopts, session.opts...) chatopts = append(chatopts, opts...) // Call the 'chat' method - client := s.model.client - r, err := client.Chat(ctx, s, chatopts...) + r, err := session.model.Chat(ctx, session, chatopts...) if err != nil { return err - } else { - s.seq = append(s.seq, &r.Message) } + // Append the message to the sequence + session.seq = append(session.seq, &r.Message) + // Return success return nil } // Generate a response from a tool calling result -func (s *session) FromTool(ctx context.Context, results ...llm.ToolResult) error { - if len(results) == 0 { - return llm.ErrConflict.Withf("No tool results") +func (session *session) FromTool(ctx context.Context, results ...llm.ToolResult) error { + messages, err := toolResults(results...) + if err != nil { + return err } - // Append the tool results - for _, result := range results { - if message, err := toolResult(result); err != nil { - return err - } else { - s.seq = append(s.seq, message) - } - } + // Append the tool results to the sequence + session.seq = append(session.seq, messages...) // Call the 'chat' method - r, err := s.model.client.Chat(ctx, s, s.opts...) + r, err := session.model.Chat(ctx, session, session.opts...) if err != nil { return err - } else { - s.seq = append(s.seq, &r.Message) } + // Append the first message from the set of completions + session.seq = append(session.seq, &r.Message) + // Return success return nil } -// Return the role of the last message -func (session *session) Role() string { - if len(session.seq) == 0 { - return "" - } - return session.seq[len(session.seq)-1].Role -} - -// Return the text of the last message -func (session *session) Text() string { - if len(session.seq) == 0 { - return "" - } - return session.seq[len(session.seq)-1].Content -} - -// Return the tool calls of the last message -func (session *session) ToolCalls() []llm.ToolCall { - // Sanity check for tool call - if len(session.seq) == 0 { - return nil - } - meta := session.seq[len(session.seq)-1] - if meta.Role != "assistant" { - return nil - } +/////////////////////////////////////////////////////////////////////////////// +// PRIVATE METHODS - // Gather tool calls - var result []llm.ToolCall - for _, call := range meta.ToolCalls { - result = append(result, tool.NewCall(fmt.Sprint(call.Function.Index), call.Function.Name, call.Function.Arguments)) +func systemPrompt(prompt string) *Message { + return &Message{ + RoleContent: RoleContent{ + Role: "system", + Content: prompt, + }, } - return result } -/////////////////////////////////////////////////////////////////////////////// -// PRIVATE METHODS - -func userPrompt(prompt string, opts ...llm.Opt) (*MessageMeta, error) { - // Apply options for attachments - opt, err := llm.ApplyOpts(opts...) +func userPrompt(prompt string, opts ...llm.Opt) (*Message, error) { + // Get attachments + opt, err := llm.ApplyPromptOpts(opts...) if err != nil { return nil, err } - // Create a new message - var meta MessageMeta - meta.Role = "user" - meta.Content = prompt - - if attachments := opt.Attachments(); len(attachments) > 0 { - meta.Images = make([]Data, len(attachments)) - for i, attachment := range attachments { - meta.Images[i] = attachment.Data() - } + // Get attachments, allocate content + attachments := opt.Attachments() + data := make([]Data, 0, len(attachments)) + for _, attachment := range attachments { + data = append(data, attachment.Data()) } // Return success - return &meta, nil + return &Message{ + RoleContent: RoleContent{ + Role: "user", + Content: prompt, + Images: data, + }, + }, nil } -func toolResult(result llm.ToolResult) (*MessageMeta, error) { - // Turn result into JSON - data, err := json.Marshal(result.Value()) - if err != nil { - return nil, err +func toolResults(results ...llm.ToolResult) ([]*Message, error) { + // Check for no results + if len(results) == 0 { + return nil, llm.ErrBadParameter.Withf("No tool results") } - // Create a new message - var meta MessageMeta - meta.Role = "tool" - meta.FunctionName = result.Call().Name() - meta.Content = string(data) + // Create results + messages := make([]*Message, 0, len(results)) + for _, result := range results { + value, err := json.Marshal(result.Value()) + if err != nil { + return nil, err + } + messages = append(messages, &Message{ + RoleContent: RoleContent{ + Role: "tool", + ToolResult: ToolResult{ + Name: result.Call().Name(), + }, + Content: string(value), + }, + }) + } // Return success - return &meta, nil + return messages, nil } diff --git a/pkg/ollama/session_test.go b/pkg/ollama/session_test.go_old similarity index 100% rename from pkg/ollama/session_test.go rename to pkg/ollama/session_test.go_old From ed2b92e848d97f31916d1b1a05c416de9fd9d13f Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Sun, 2 Feb 2025 20:43:43 +0100 Subject: [PATCH 11/15] Updated --- README.md | 8 ++--- opt.go | 20 +++++++++++ pkg/mistral/opt.go | 20 ----------- pkg/ollama/chat.go | 16 +++++---- pkg/ollama/chat_test.go | 68 ++++++++++++++++++++++++++++++++++++ pkg/ollama/embedding_test.go | 15 ++++++-- pkg/ollama/model.go | 2 ++ pkg/ollama/opt.go | 27 ++++++++++++-- 8 files changed, 140 insertions(+), 36 deletions(-) create mode 100644 pkg/ollama/chat_test.go diff --git a/README.md b/README.md index 094d990..95e265f 100644 --- a/README.md +++ b/README.md @@ -202,10 +202,10 @@ The options are as follows: | `llm.WithToolKit(llm.ToolKit)` | Cannot be combined with streaming | Yes | Yes | - | The set of tools to use. | | `llm.WithStopSequence(string, string, ...)` | Yes | Yes | Yes | - | Stop generation if one of these tokens is detected. | | `llm.WithSystemPrompt(string)` | No | Yes | Yes | - | Set the system prompt for the model. | -| `llm.WithSeed(uint64)` | No | Yes | Yes | - | The seed to use for random sampling. If set, different calls will generate deterministic results. | -| `llm.WithFormat(string)` | No | Yes | Use `json_format` or `text` | - | The format of the response. For Mistral, you must also instruct the model to produce JSON yourself with a system or a user message. | -| `mistral.WithPresencePenalty(float64)` | No | No | Yes | - | Determines how much the model penalizes the repetition of words or phrases. A higher presence penalty encourages the model to use a wider variety of words and phrases, making the output more diverse and creative. | -| `mistral.WithFequencyPenalty(float64)` | No | No | Yes | - | Penalizes the repetition of words based on their frequency in the generated text. A higher frequency penalty discourages the model from repeating words that have already appeared frequently in the output, promoting diversity and reducing repetition. | +| `llm.WithSeed(uint64)` | Yes | Yes | Yes | - | The seed to use for random sampling. If set, different calls will generate deterministic results. | +| `llm.WithFormat(string)` | Use `json` | Yes | Use `json_format` or `text` | - | The format of the response. For Mistral, you must also instruct the model to produce JSON yourself with a system or a user message. | +| `llm.WithPresencePenalty(float64)` | Yes | No | Yes | - | Determines how much the model penalizes the repetition of words or phrases. A higher presence penalty encourages the model to use a wider variety of words and phrases, making the output more diverse and creative. | +| `llm.WithFequencyPenalty(float64)` | Yes | No | Yes | - | Penalizes the repetition of words based on their frequency in the generated text. A higher frequency penalty discourages the model from repeating words that have already appeared frequently in the output, promoting diversity and reducing repetition. | | `mistral.WithPrediction(string)` | No | No | Yes | - | Enable users to specify expected results, optimizing response times by leveraging known or predictable content. This approach is especially effective for updating text documents or code files with minimal changes, reducing latency while maintaining high-quality results. | | `llm.WithSafePrompt()` | No | No | Yes | - | Whether to inject a safety prompt before all conversations. | | `llm.WithNumCompletions(uint64)` | No | No | Yes | - | Number of completions to return for each request. | diff --git a/opt.go b/opt.go index f5179b1..a378a91 100644 --- a/opt.go +++ b/opt.go @@ -271,6 +271,26 @@ func WithTopK(v uint64) Opt { } } +func WithPresencePenalty(v float64) Opt { + return func(o *Opts) error { + if v < -2 || v > 2 { + return ErrBadParameter.With("presence_penalty") + } + o.Set("presence_penalty", v) + return nil + } +} + +func WithFrequencyPenalty(v float64) Opt { + return func(o *Opts) error { + if v < -2 || v > 2 { + return ErrBadParameter.With("frequency_penalty") + } + o.Set("frequency_penalty", v) + return nil + } +} + // The maximum number of tokens to generate in the completion. func WithMaxTokens(v uint64) Opt { return func(o *Opts) error { diff --git a/pkg/mistral/opt.go b/pkg/mistral/opt.go index b409470..71e82da 100644 --- a/pkg/mistral/opt.go +++ b/pkg/mistral/opt.go @@ -9,26 +9,6 @@ import ( /////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS -func WithPresencePenalty(v float64) llm.Opt { - return func(o *llm.Opts) error { - if v < -2 || v > 2 { - return llm.ErrBadParameter.With("presence_penalty") - } - o.Set("presence_penalty", v) - return nil - } -} - -func WithFrequencyPenalty(v float64) llm.Opt { - return func(o *llm.Opts) error { - if v < -2 || v > 2 { - return llm.ErrBadParameter.With("frequency_penalty") - } - o.Set("frequency_penalty", v) - return nil - } -} - func WithPrediction(v string) llm.Opt { return func(o *llm.Opts) error { o.Set("prediction", v) diff --git a/pkg/ollama/chat.go b/pkg/ollama/chat.go index 888e84c..175e0b5 100644 --- a/pkg/ollama/chat.go +++ b/pkg/ollama/chat.go @@ -68,9 +68,9 @@ func (ollama *Client) Chat(ctx context.Context, context llm.Context, opts ...llm // Append the system prompt at the beginning messages := make([]*Message, 0, len(context.(*session).seq)+1) - //if system := opt.SystemPrompt(); system != "" { - // messages = append(messages, systemPrompt(system)) - //} + if system := opt.SystemPrompt(); system != "" { + messages = append(messages, systemPrompt(system)) + } // Always append the first message of each completion for _, message := range context.(*session).seq { @@ -92,7 +92,7 @@ func (ollama *Client) Chat(ctx context.Context, context llm.Context, opts ...llm } // Response - var response Response + var response, delta Response reqopts := []client.RequestOpt{ client.OptPath("chat"), } @@ -111,12 +111,16 @@ func (ollama *Client) Chat(ctx context.Context, context llm.Context, opts ...llm } // Response - if err := ollama.DoWithContext(ctx, req, &response, reqopts...); err != nil { + if err := ollama.DoWithContext(ctx, req, &delta, reqopts...); err != nil { return nil, err } // Return success - return &response, nil + if optStream(ollama, opt) { + return &response, nil + } else { + return &delta, nil + } } /////////////////////////////////////////////////////////////////////////////// diff --git a/pkg/ollama/chat_test.go b/pkg/ollama/chat_test.go new file mode 100644 index 0000000..989db79 --- /dev/null +++ b/pkg/ollama/chat_test.go @@ -0,0 +1,68 @@ +package ollama_test + +import ( + "context" + "testing" + + // Packages + + llm "github.com/mutablelogic/go-llm" + ollama "github.com/mutablelogic/go-llm/pkg/ollama" + assert "github.com/stretchr/testify/assert" +) + +func Test_chat_001(t *testing.T) { + // Pull the model + model, err := client.PullModel(context.TODO(), "qwen:0.5b", ollama.WithPullStatus(func(status *ollama.PullStatus) { + t.Log(status) + })) + if err != nil { + t.FailNow() + } + + t.Run("Temperature", func(t *testing.T) { + assert := assert.New(t) + response, err := client.Chat(context.TODO(), model.UserPrompt("why is the sky blue?"), llm.WithTemperature(0.5)) + if !assert.NoError(err) { + t.FailNow() + } + t.Log(response) + }) + + t.Run("TopP", func(t *testing.T) { + assert := assert.New(t) + response, err := client.Chat(context.TODO(), model.UserPrompt("why is the sky blue?"), llm.WithTopP(0.5)) + if !assert.NoError(err) { + t.FailNow() + } + t.Log(response) + }) + t.Run("TopK", func(t *testing.T) { + assert := assert.New(t) + response, err := client.Chat(context.TODO(), model.UserPrompt("why is the sky blue?"), llm.WithTopK(50)) + if !assert.NoError(err) { + t.FailNow() + } + t.Log(response) + }) + + t.Run("Stream", func(t *testing.T) { + assert := assert.New(t) + response, err := client.Chat(context.TODO(), model.UserPrompt("why is the sky blue?"), llm.WithStream(func(stream llm.Completion) { + t.Log(stream) + })) + if !assert.NoError(err) { + t.FailNow() + } + t.Log(response) + }) + + t.Run("Stop", func(t *testing.T) { + assert := assert.New(t) + response, err := client.Chat(context.TODO(), model.UserPrompt("why is the sky blue?"), llm.WithStopSequence("sky")) + if !assert.NoError(err) { + t.FailNow() + } + t.Log(response) + }) +} diff --git a/pkg/ollama/embedding_test.go b/pkg/ollama/embedding_test.go index 7e17989..2b0fc2c 100644 --- a/pkg/ollama/embedding_test.go +++ b/pkg/ollama/embedding_test.go @@ -9,13 +9,22 @@ import ( assert "github.com/stretchr/testify/assert" ) -func Test_embed_001(t *testing.T) { - t.Run("Embedding", func(t *testing.T) { +func Test_embeddings_001(t *testing.T) { + t.Run("Embedding1", func(t *testing.T) { assert := assert.New(t) embedding, err := client.GenerateEmbedding(context.TODO(), "qwen:0.5b", []string{"hello, world"}) if !assert.NoError(err) { t.FailNow() } - t.Log(embedding) + assert.Equal(1, len(embedding.Embeddings)) + }) + + t.Run("Embedding2", func(t *testing.T) { + assert := assert.New(t) + embedding, err := client.GenerateEmbedding(context.TODO(), "qwen:0.5b", []string{"hello, world", "goodbye cruel world"}) + if !assert.NoError(err) { + t.FailNow() + } + assert.Equal(2, len(embedding.Embeddings)) }) } diff --git a/pkg/ollama/model.go b/pkg/ollama/model.go index 1d7900f..b3c1db5 100644 --- a/pkg/ollama/model.go +++ b/pkg/ollama/model.go @@ -162,6 +162,8 @@ func (ollama *Client) GetModel(ctx context.Context, name string) (llm.Model, err var response ModelMeta if err := ollama.DoWithContext(ctx, req, &response, client.OptPath("show")); err != nil { return nil, err + } else { + response.Name = name } // Return success diff --git a/pkg/ollama/opt.go b/pkg/ollama/opt.go index 5ed010d..2b1afd4 100644 --- a/pkg/ollama/opt.go +++ b/pkg/ollama/opt.go @@ -91,6 +91,15 @@ func optFormat(opts *llm.Opts) string { return opts.GetString("format") } +func optStopSequence(opts *llm.Opts) []string { + if opts.Has("stop") { + if stop, ok := opts.Get("stop").([]string); ok { + return stop + } + } + return nil +} + func optOptions(opts *llm.Opts) map[string]any { result := make(map[string]any) if o, ok := opts.Get("options").(map[string]any); ok { @@ -101,13 +110,25 @@ func optOptions(opts *llm.Opts) map[string]any { // copy across temperature, top_p and top_k if opts.Has("temperature") { - result["temperature"] = opts.Get("temperature") + result["temperature"] = opts.Get("temperature").(float64) } if opts.Has("top_p") { - result["top_p"] = opts.Get("top_p") + result["top_p"] = opts.GetFloat64("top_p") } if opts.Has("top_k") { - result["top_k"] = opts.Get("top_k") + result["top_k"] = opts.GetUint64("top_k") + } + if opts.Has("stop") { + result["stop"] = opts.Get("stop").([]string) + } + if opts.Has("seed") { + result["seed"] = opts.GetUint64("seed") + } + if opts.Has("presence_penalty") { + result["presence_penalty"] = opts.GetFloat64("presence_penalty") + } + if opts.Has("frequency_penalty") { + result["frequency_penalty"] = opts.GetFloat64("frequency_penalty") } // Return result From 2b5ecded1a8d1b33aed9dfd7ae5e820cc4ed1870 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Sun, 2 Feb 2025 21:07:32 +0100 Subject: [PATCH 12/15] Updated --- README.md | 2 +- pkg/ollama/chat_test.go | 121 +++++++++++++++++++++++++++ pkg/ollama/chat_test.go_old | 146 --------------------------------- pkg/ollama/session_test.go | 58 +++++++++++++ pkg/ollama/session_test.go_old | 90 -------------------- 5 files changed, 180 insertions(+), 237 deletions(-) delete mode 100644 pkg/ollama/chat_test.go_old create mode 100644 pkg/ollama/session_test.go delete mode 100644 pkg/ollama/session_test.go_old diff --git a/README.md b/README.md index 95e265f..2cea2bd 100644 --- a/README.md +++ b/README.md @@ -196,7 +196,7 @@ The options are as follows: | `llm.WithTemperature(float64)` | Yes | Yes | Yes | - | What sampling temperature to use, between 0.0 and 1.0. Higher values like 0.7 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. | | `llm.WithTopP(float64)` | Yes | Yes | Yes | - | Nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. | | `llm.WithTopK(uint64)` | Yes | Yes | No | - | Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. | -| `llm.WithMaxTokens(uint64)` | - | Yes | Yes | - | The maximum number of tokens to generate in the response. | +| `llm.WithMaxTokens(uint64)` | No | Yes | Yes | - | The maximum number of tokens to generate in the response. | | `llm.WithStream(func(llm.Completion))` | Can be enabled when tools are not used | Yes | Yes | - | Stream the response to a function. | | `llm.WithToolChoice(string, string, ...)` | No | Yes | Use `auto`, `any`, `none`, `required` or a function name. Only the first argument is used. | - | The tool to use for the model. | | `llm.WithToolKit(llm.ToolKit)` | Cannot be combined with streaming | Yes | Yes | - | The set of tools to use. | diff --git a/pkg/ollama/chat_test.go b/pkg/ollama/chat_test.go index 989db79..b0ea189 100644 --- a/pkg/ollama/chat_test.go +++ b/pkg/ollama/chat_test.go @@ -2,12 +2,16 @@ package ollama_test import ( "context" + "fmt" + "os" + "strings" "testing" // Packages llm "github.com/mutablelogic/go-llm" ollama "github.com/mutablelogic/go-llm/pkg/ollama" + tool "github.com/mutablelogic/go-llm/pkg/tool" assert "github.com/stretchr/testify/assert" ) @@ -65,4 +69,121 @@ func Test_chat_001(t *testing.T) { } t.Log(response) }) + + t.Run("System", func(t *testing.T) { + assert := assert.New(t) + response, err := client.Chat(context.TODO(), model.UserPrompt("why is the sky blue?"), llm.WithSystemPrompt("reply as if you are shakespeare")) + if !assert.NoError(err) { + t.FailNow() + } + t.Log(response) + }) + + t.Run("Seed", func(t *testing.T) { + assert := assert.New(t) + response, err := client.Chat(context.TODO(), model.UserPrompt("why is the sky blue?"), llm.WithSeed(1234)) + if !assert.NoError(err) { + t.FailNow() + } + t.Log(response) + }) + + t.Run("Format", func(t *testing.T) { + assert := assert.New(t) + response, err := client.Chat(context.TODO(), model.UserPrompt("why is the sky blue? Reply in JSON format"), llm.WithFormat("json")) + if !assert.NoError(err) { + t.FailNow() + } + t.Log(response) + }) + + t.Run("PresencePenalty", func(t *testing.T) { + assert := assert.New(t) + response, err := client.Chat(context.TODO(), model.UserPrompt("why is the sky blue?t"), llm.WithPresencePenalty(-1.0)) + if !assert.NoError(err) { + t.FailNow() + } + t.Log(response) + }) + + t.Run("FrequencyPenalty", func(t *testing.T) { + assert := assert.New(t) + response, err := client.Chat(context.TODO(), model.UserPrompt("why is the sky blue?t"), llm.WithFrequencyPenalty(1.0)) + if !assert.NoError(err) { + t.FailNow() + } + t.Log(response) + }) +} + +func Test_chat_002(t *testing.T) { + assert := assert.New(t) + model, err := client.PullModel(context.TODO(), "llava:7b") + if !assert.NoError(err) { + t.FailNow() + } + assert.NotNil(model) + + f, err := os.Open("testdata/guggenheim.jpg") + if !assert.NoError(err) { + t.FailNow() + } + defer f.Close() + + // Describe an image + r, err := client.Chat(context.TODO(), model.UserPrompt("Provide a short caption for this image", llm.WithAttachment(f))) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(1, r.Num()) + assert.NotEmpty(r.Text(0)) + t.Log(r.Text(0)) + } +} + +func Test_chat_003(t *testing.T) { + assert := assert.New(t) + model, err := client.PullModel(context.TODO(), "llama3.2") + if !assert.NoError(err) { + t.FailNow() + } + assert.NotNil(model) + + toolkit := tool.NewToolKit() + toolkit.Register(&weather{}) + + // Get the weather for a city + r, err := client.Chat(context.TODO(), model.UserPrompt("What is the weather in the capital city of germany?"), llm.WithToolKit(toolkit)) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(1, r.Num()) + + calls := r.ToolCalls(0) + assert.NotEmpty(calls) + + var w weather + assert.NoError(calls[0].Decode(&w)) + assert.Equal("berlin", strings.ToLower(w.City)) + } +} + +type weather struct { + City string `json:"city" help:"The city to get the weather for"` +} + +func (weather) Name() string { + return "weather_in_city" +} + +func (weather) Description() string { + return "Get the weather for a city" +} + +func (w weather) Run(ctx context.Context) (any, error) { + var result struct { + City string `json:"city"` + Weather string `json:"weather"` + } + result.City = w.City + result.Weather = fmt.Sprintf("The weather in %q is sunny and warm", w.City) + return result, nil } diff --git a/pkg/ollama/chat_test.go_old b/pkg/ollama/chat_test.go_old deleted file mode 100644 index cc746dd..0000000 --- a/pkg/ollama/chat_test.go_old +++ /dev/null @@ -1,146 +0,0 @@ -package ollama_test - -import ( - "context" - "encoding/json" - "log" - "os" - "testing" - - // Packages - opts "github.com/mutablelogic/go-client" - llm "github.com/mutablelogic/go-llm" - ollama "github.com/mutablelogic/go-llm/pkg/ollama" - tool "github.com/mutablelogic/go-llm/pkg/tool" - assert "github.com/stretchr/testify/assert" -) - -func Test_chat_001(t *testing.T) { - client, err := ollama.New(GetEndpoint(t), opts.OptTrace(os.Stderr, true)) - if err != nil { - t.FailNow() - } - - // Pull the model - model, err := client.PullModel(context.TODO(), "qwen:0.5b", ollama.WithPullStatus(func(status *ollama.PullStatus) { - t.Log(status) - })) - if err != nil { - t.FailNow() - } - - t.Run("ChatStream", func(t *testing.T) { - assert := assert.New(t) - response, err := client.Chat(context.TODO(), model.UserPrompt("why is the sky blue?"), llm.WithStream(func(stream llm.ContextContent) { - t.Log(stream) - })) - if !assert.NoError(err) { - t.FailNow() - } - t.Log(response) - }) - - t.Run("ChatNoStream", func(t *testing.T) { - assert := assert.New(t) - response, err := client.Chat(context.TODO(), model.UserPrompt("why is the sky green?")) - if !assert.NoError(err) { - t.FailNow() - } - t.Log(response) - }) -} - -func Test_chat_002(t *testing.T) { - client, err := ollama.New(GetEndpoint(t), opts.OptTrace(os.Stderr, true)) - if err != nil { - t.FailNow() - } - - // Pull the model - model, err := client.PullModel(context.TODO(), "llama3.2:1b", ollama.WithPullStatus(func(status *ollama.PullStatus) { - t.Log(status) - })) - if err != nil { - t.FailNow() - } - - // Make a toolkit - toolkit := tool.NewToolKit() - if err := toolkit.Register(new(weather)); err != nil { - t.FailNow() - } - - t.Run("Tools", func(t *testing.T) { - assert := assert.New(t) - response, err := client.Chat(context.TODO(), - model.UserPrompt("what is the weather in berlin?"), - llm.WithToolKit(toolkit), - ) - if !assert.NoError(err) { - t.FailNow() - } - t.Log(response) - }) -} - -func Test_chat_003(t *testing.T) { - client, err := ollama.New(GetEndpoint(t), opts.OptTrace(os.Stderr, false)) - if err != nil { - t.FailNow() - } - - // Pull the model - model, err := client.PullModel(context.TODO(), "llava", ollama.WithPullStatus(func(status *ollama.PullStatus) { - t.Log(status) - })) - if err != nil { - t.FailNow() - } - - // Explain the content of an image - t.Run("Image", func(t *testing.T) { - assert := assert.New(t) - - f, err := os.Open("testdata/guggenheim.jpg") - if !assert.NoError(err) { - t.FailNow() - } - defer f.Close() - - response, err := client.Chat(context.TODO(), - model.UserPrompt("describe this photo to me", llm.WithAttachment(f)), - ) - if !assert.NoError(err) { - t.FailNow() - } - t.Log(response) - }) -} - -//////////////////////////////////////////////////////////////////////////////// -// TOOLS - -type weather struct { - Location string `json:"location" name:"location" help:"The location to get the weather for" required:"true"` -} - -func (*weather) Name() string { - return "weather_in_location" -} - -func (*weather) Description() string { - return "Get the weather in a location" -} - -func (weather *weather) String() string { - data, err := json.MarshalIndent(weather, "", " ") - if err != nil { - return err.Error() - } - return string(data) -} - -func (weather *weather) Run(ctx context.Context) (any, error) { - log.Println("weather_in_location", "=>", weather) - return "very sunny today", nil -} diff --git a/pkg/ollama/session_test.go b/pkg/ollama/session_test.go new file mode 100644 index 0000000..e343eff --- /dev/null +++ b/pkg/ollama/session_test.go @@ -0,0 +1,58 @@ +package ollama_test + +import ( + "context" + "testing" + + // Packages + llm "github.com/mutablelogic/go-llm" + tool "github.com/mutablelogic/go-llm/pkg/tool" + assert "github.com/stretchr/testify/assert" +) + +func Test_session_001(t *testing.T) { + assert := assert.New(t) + model, err := client.PullModel(context.TODO(), "llama3.2") + if !assert.NoError(err) { + t.FailNow() + } + assert.NotNil(model) + + session := model.Context() + if assert.NotNil(session) { + err := session.FromUser(context.TODO(), "Hello, how are you?") + assert.NoError(err) + t.Log(session) + } +} + +func Test_session_002(t *testing.T) { + assert := assert.New(t) + model, err := client.PullModel(context.TODO(), "llama3.2") + if !assert.NoError(err) { + t.FailNow() + } + assert.NotNil(model) + + toolkit := tool.NewToolKit() + toolkit.Register(&weather{}) + + session := model.Context(llm.WithToolKit(toolkit)) + if !assert.NotNil(session) { + t.FailNow() + } + + assert.NoError(session.FromUser(context.TODO(), "What is the weather like in London today?")) + calls := session.ToolCalls(0) + if assert.Len(calls, 1) { + assert.Equal("weather_in_city", calls[0].Name()) + + result, err := toolkit.Run(context.TODO(), calls...) + assert.NoError(err) + assert.Len(result, 1) + + assert.NoError(session.FromTool(context.TODO(), result...)) + } + + t.Log(session) +} diff --git a/pkg/ollama/session_test.go_old b/pkg/ollama/session_test.go_old deleted file mode 100644 index bc6e6a7..0000000 --- a/pkg/ollama/session_test.go_old +++ /dev/null @@ -1,90 +0,0 @@ -package ollama_test - -import ( - "context" - "os" - "testing" - - // Packages - opts "github.com/mutablelogic/go-client" - llm "github.com/mutablelogic/go-llm" - ollama "github.com/mutablelogic/go-llm/pkg/ollama" - "github.com/mutablelogic/go-llm/pkg/tool" - assert "github.com/stretchr/testify/assert" -) - -func Test_session_001(t *testing.T) { - client, err := ollama.New(GetEndpoint(t), opts.OptTrace(os.Stderr, true)) - if err != nil { - t.FailNow() - } - - // Pull the model - model, err := client.PullModel(context.TODO(), "qwen:0.5b") - if err != nil { - t.FailNow() - } - - // Session with a single user prompt - streaming - t.Run("stream", func(t *testing.T) { - assert := assert.New(t) - session := model.Context(llm.WithStream(func(stream llm.ContextContent) { - t.Log("SESSION DELTA", stream) - })) - assert.NotNil(session) - - err := session.FromUser(context.TODO(), "Why is the grass green?") - if !assert.NoError(err) { - t.FailNow() - } - assert.Equal("assistant", session.Role()) - assert.NotEmpty(session.Text()) - }) - - // Session with a single user prompt - not streaming - t.Run("nostream", func(t *testing.T) { - assert := assert.New(t) - session := model.Context() - assert.NotNil(session) - - err := session.FromUser(context.TODO(), "Why is the sky blue?") - if !assert.NoError(err) { - t.FailNow() - } - assert.Equal("assistant", session.Role()) - assert.NotEmpty(session.Text()) - }) -} - -func Test_session_002(t *testing.T) { - client, err := ollama.New(GetEndpoint(t), opts.OptTrace(os.Stderr, true)) - if err != nil { - t.FailNow() - } - - // Pull the model - model, err := client.PullModel(context.TODO(), "llama3.2") - if err != nil { - t.FailNow() - } - - // Make a toolkit - toolkit := tool.NewToolKit() - if err := toolkit.Register(new(weather)); err != nil { - t.FailNow() - } - - // Session with a tool call - t.Run("toolcall", func(t *testing.T) { - assert := assert.New(t) - - session := model.Context(llm.WithToolKit(toolkit)) - assert.NotNil(session) - - err = session.FromUser(context.TODO(), "What is today's weather in Berlin?", llm.WithTemperature(0.5)) - if !assert.NoError(err) { - t.FailNow() - } - t.Log(session) - }) -} From 63b692b0e9343aa8ff149eafa24dbf885df9044f Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Sun, 2 Feb 2025 21:17:14 +0100 Subject: [PATCH 13/15] Temporarily removed anthropic --- cmd/llm/chat.go | 18 ++++++++++-------- cmd/llm/main.go | 12 +++++++----- pkg/agent/opt.go | 20 ++++++++++---------- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/cmd/llm/chat.go b/cmd/llm/chat.go index 4067d75..203af95 100644 --- a/cmd/llm/chat.go +++ b/cmd/llm/chat.go @@ -18,6 +18,7 @@ import ( type ChatCmd struct { Model string `arg:"" help:"Model name"` NoStream bool `flag:"nostream" help:"Disable streaming"` + NoTools bool `flag:"nostream" help:"Disable tool calling"` System string `flag:"system" help:"Set the system prompt"` } @@ -39,16 +40,17 @@ func (cmd *ChatCmd) Run(globals *Globals) error { // Set the options opts := []llm.Opt{} if !cmd.NoStream { - opts = append(opts, llm.WithStream(func(cc llm.ContextContent) { - if text := cc.Text(); text != "" { - fmt.Println(text) + opts = append(opts, llm.WithStream(func(cc llm.Completion) { + if text := cc.Text(0); text != "" { + text = strings.ReplaceAll(text, "\n", " ") + fmt.Print("\r" + text) } })) } if cmd.System != "" { opts = append(opts, llm.WithSystemPrompt(cmd.System)) } - if globals.toolkit != nil { + if globals.toolkit != nil && !cmd.NoTools { opts = append(opts, llm.WithToolKit(globals.toolkit)) } @@ -77,12 +79,12 @@ func (cmd *ChatCmd) Run(globals *Globals) error { // Repeat call tools until no more calls are made for { - calls := session.ToolCalls() + calls := session.ToolCalls(0) if len(calls) == 0 { break } - if session.Text() != "" { - globals.term.Println(session.Text()) + if session.Text(0) != "" { + globals.term.Println(session.Text(0)) } else { var names []string for _, call := range calls { @@ -98,7 +100,7 @@ func (cmd *ChatCmd) Run(globals *Globals) error { } // Print the response - globals.term.Println("\n" + session.Text() + "\n") + globals.term.Println("\n" + session.Text(0) + "\n") } }) } diff --git a/cmd/llm/main.go b/cmd/llm/main.go index c7d5088..205ad86 100644 --- a/cmd/llm/main.go +++ b/cmd/llm/main.go @@ -12,8 +12,8 @@ import ( client "github.com/mutablelogic/go-client" llm "github.com/mutablelogic/go-llm" agent "github.com/mutablelogic/go-llm/pkg/agent" - "github.com/mutablelogic/go-llm/pkg/newsapi" - "github.com/mutablelogic/go-llm/pkg/tool" + newsapi "github.com/mutablelogic/go-llm/pkg/newsapi" + tool "github.com/mutablelogic/go-llm/pkg/tool" ) //////////////////////////////////////////////////////////////////////////////// @@ -107,9 +107,11 @@ func main() { if cli.OllamaEndpoint != "" { opts = append(opts, agent.WithOllama(cli.OllamaEndpoint, clientopts...)) } - if cli.AnthropicKey != "" { - opts = append(opts, agent.WithAnthropic(cli.AnthropicKey, clientopts...)) - } + /* + if cli.AnthropicKey != "" { + opts = append(opts, agent.WithAnthropic(cli.AnthropicKey, clientopts...)) + } + */ if cli.MistralKey != "" { opts = append(opts, agent.WithMistral(cli.MistralKey, clientopts...)) } diff --git a/pkg/agent/opt.go b/pkg/agent/opt.go index 12f45a7..36fe662 100644 --- a/pkg/agent/opt.go +++ b/pkg/agent/opt.go @@ -4,7 +4,6 @@ import ( // Packages client "github.com/mutablelogic/go-client" llm "github.com/mutablelogic/go-llm" - anthropic "github.com/mutablelogic/go-llm/pkg/anthropic" mistral "github.com/mutablelogic/go-llm/pkg/mistral" ollama "github.com/mutablelogic/go-llm/pkg/ollama" ) @@ -23,17 +22,18 @@ func WithOllama(endpoint string, opts ...client.ClientOpt) llm.Opt { } } -func WithAnthropic(key string, opts ...client.ClientOpt) llm.Opt { - return func(o *llm.Opts) error { - client, err := anthropic.New(key, opts...) - if err != nil { - return err - } else { - return llm.WithAgent(client)(o) +/* + func WithAnthropic(key string, opts ...client.ClientOpt) llm.Opt { + return func(o *llm.Opts) error { + client, err := anthropic.New(key, opts...) + if err != nil { + return err + } else { + return llm.WithAgent(client)(o) + } } } -} - +*/ func WithMistral(key string, opts ...client.ClientOpt) llm.Opt { return func(o *llm.Opts) error { client, err := mistral.New(key, opts...) From e559fb29d57f0734a0be1591f2d5db89a08f06fe Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Sun, 2 Feb 2025 21:37:34 +0100 Subject: [PATCH 14/15] Updates --- cmd/llm/chat.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cmd/llm/chat.go b/cmd/llm/chat.go index 203af95..42abf47 100644 --- a/cmd/llm/chat.go +++ b/cmd/llm/chat.go @@ -42,8 +42,9 @@ func (cmd *ChatCmd) Run(globals *Globals) error { if !cmd.NoStream { opts = append(opts, llm.WithStream(func(cc llm.Completion) { if text := cc.Text(0); text != "" { - text = strings.ReplaceAll(text, "\n", " ") - fmt.Print("\r" + text) + count := strings.Count(text, "\n") + fmt.Print(strings.Repeat("\033[F", count) + strings.Repeat(" ", count) + "\r") + fmt.Print(text) } })) } From 9e30bd377a9a084687d14c71b9e0e56b56dd3c8f Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Sun, 2 Feb 2025 21:47:04 +0100 Subject: [PATCH 15/15] Updates --- cmd/llm/chat.go | 17 ++++++++++++----- cmd/llm/models.go | 2 +- pkg/mistral/model.go | 4 ++-- pkg/ollama/model.go | 8 ++++++-- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/cmd/llm/chat.go b/cmd/llm/chat.go index 42abf47..bd14f6e 100644 --- a/cmd/llm/chat.go +++ b/cmd/llm/chat.go @@ -19,6 +19,7 @@ type ChatCmd struct { Model string `arg:"" help:"Model name"` NoStream bool `flag:"nostream" help:"Disable streaming"` NoTools bool `flag:"nostream" help:"Disable tool calling"` + Prompt string `flag:"prompt" help:"Set the initial user prompt"` System string `flag:"system" help:"Set the system prompt"` } @@ -60,11 +61,17 @@ func (cmd *ChatCmd) Run(globals *Globals) error { // Continue looping until end of input for { - input, err := globals.term.ReadLine(model.Name() + "> ") - if errors.Is(err, io.EOF) { - return nil - } else if err != nil { - return err + var input string + if cmd.Prompt != "" { + input = cmd.Prompt + cmd.Prompt = "" + } else { + input, err = globals.term.ReadLine(model.Name() + "> ") + if errors.Is(err, io.EOF) { + return nil + } else if err != nil { + return err + } } // Ignore empty input diff --git a/cmd/llm/models.go b/cmd/llm/models.go index 1bb96ee..e304507 100644 --- a/cmd/llm/models.go +++ b/cmd/llm/models.go @@ -60,7 +60,7 @@ func (*ListAgentsCmd) Run(globals *Globals) error { return fmt.Errorf("No agents found") } - var agents []string + agents := make([]string, 0, len(agent.Agents())) for _, agent := range agent.Agents() { agents = append(agents, agent.Name()) } diff --git a/pkg/mistral/model.go b/pkg/mistral/model.go index 3093b91..24420b6 100644 --- a/pkg/mistral/model.go +++ b/pkg/mistral/model.go @@ -12,8 +12,8 @@ import ( // TYPES type model struct { - *Client - meta Model + *Client `json:"-"` + meta Model } type Model struct { diff --git a/pkg/ollama/model.go b/pkg/ollama/model.go index b3c1db5..94fb7d4 100644 --- a/pkg/ollama/model.go +++ b/pkg/ollama/model.go @@ -16,7 +16,7 @@ import ( // model is the implementation of the llm.Model interface type model struct { - *Client + *Client `json:"-"` ModelMeta } @@ -60,8 +60,12 @@ type PullStatus struct { /////////////////////////////////////////////////////////////////////////////// // STRINGIFY +func (m model) MarshalJSON() ([]byte, error) { + return json.Marshal(m.ModelMeta) +} + func (m model) String() string { - data, err := json.MarshalIndent(m.ModelMeta, "", " ") + data, err := json.MarshalIndent(m, "", " ") if err != nil { return err.Error() }