From edbf7cf8888b8e9ea82af092ac5fcb9b0cbe94b3 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Tue, 4 Feb 2025 07:16:29 +0100 Subject: [PATCH 01/25] Adding OpenAI --- cmd/llm/main.go | 8 +++ pkg/agent/opt.go | 14 +++- pkg/openai/client.go | 56 ++++++++++++++++ pkg/openai/client_test.go | 58 ++++++++++++++++ pkg/openai/embeddings.go | 115 +++++++++++++++++++++++++++++++ pkg/openai/embeddings_test.go | 20 ++++++ pkg/openai/model.go | 123 ++++++++++++++++++++++++++++++++++ pkg/openai/model_test.go | 43 ++++++++++++ pkg/openai/opt.go | 41 ++++++++++++ pkg/openai/session.go | 19 ++++++ 10 files changed, 496 insertions(+), 1 deletion(-) create mode 100644 pkg/openai/client.go create mode 100644 pkg/openai/client_test.go create mode 100644 pkg/openai/embeddings.go create mode 100644 pkg/openai/embeddings_test.go create mode 100644 pkg/openai/model.go create mode 100644 pkg/openai/model_test.go create mode 100644 pkg/openai/opt.go create mode 100644 pkg/openai/session.go diff --git a/cmd/llm/main.go b/cmd/llm/main.go index ff82c92..cca49f5 100644 --- a/cmd/llm/main.go +++ b/cmd/llm/main.go @@ -28,6 +28,7 @@ type Globals struct { Ollama `embed:"" help:"Ollama configuration"` Anthropic `embed:"" help:"Anthropic configuration"` Mistral `embed:"" help:"Mistral configuration"` + OpenAI `embed:"" help:"OpenAI configuration"` // Tools NewsAPI `embed:"" help:"NewsAPI configuration"` @@ -51,6 +52,10 @@ type Mistral struct { MistralKey string `env:"MISTRAL_API_KEY" help:"Mistral API Key"` } +type OpenAI struct { + OpenAIKey string `env:"OPENAI_API_KEY" help:"OpenAI API Key"` +} + type NewsAPI struct { NewsKey string `env:"NEWSAPI_KEY" help:"News API Key"` } @@ -113,6 +118,9 @@ func main() { if cli.MistralKey != "" { opts = append(opts, agent.WithMistral(cli.MistralKey, clientopts...)) } + if cli.OpenAIKey != "" { + opts = append(opts, agent.WithOpenAI(cli.OpenAIKey, clientopts...)) + } // Make a toolkit toolkit := tool.NewToolKit() diff --git a/pkg/agent/opt.go b/pkg/agent/opt.go index 9e54647..e007fbf 100644 --- a/pkg/agent/opt.go +++ b/pkg/agent/opt.go @@ -4,9 +4,10 @@ import ( // Packages client "github.com/mutablelogic/go-client" llm "github.com/mutablelogic/go-llm" - "github.com/mutablelogic/go-llm/pkg/anthropic" + anthropic "github.com/mutablelogic/go-llm/pkg/anthropic" mistral "github.com/mutablelogic/go-llm/pkg/mistral" ollama "github.com/mutablelogic/go-llm/pkg/ollama" + openai "github.com/mutablelogic/go-llm/pkg/openai" ) //////////////////////////////////////////////////////////////////////////////// @@ -44,3 +45,14 @@ func WithMistral(key string, opts ...client.ClientOpt) llm.Opt { } } } + +func WithOpenAI(key string, opts ...client.ClientOpt) llm.Opt { + return func(o *llm.Opts) error { + client, err := openai.New(key, opts...) + if err != nil { + return err + } else { + return llm.WithAgent(client)(o) + } + } +} diff --git a/pkg/openai/client.go b/pkg/openai/client.go new file mode 100644 index 0000000..1494902 --- /dev/null +++ b/pkg/openai/client.go @@ -0,0 +1,56 @@ +/* +openai implements an API client for OpenAI +https://platform.openai.com/docs/api-reference +*/ +package openai + +import ( + + // Packages + client "github.com/mutablelogic/go-client" + llm "github.com/mutablelogic/go-llm" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +type Client struct { + *client.Client + cache map[string]llm.Model +} + +var _ llm.Agent = (*Client)(nil) + +/////////////////////////////////////////////////////////////////////////////// +// GLOBALS + +const ( + endPoint = "https://api.openai.com/v1" + defaultName = "openai" +) + +/////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +// Create a new client +func New(ApiKey string, opts ...client.ClientOpt) (*Client, error) { + // Create client + client, err := client.New(append(opts, client.OptEndpoint(endPoint), client.OptReqToken(client.Token{ + Scheme: client.Bearer, + Value: ApiKey, + }))...) + if err != nil { + return nil, err + } + + // Return the client + return &Client{client, nil}, nil +} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Return the name of the agent +func (*Client) Name() string { + return defaultName +} diff --git a/pkg/openai/client_test.go b/pkg/openai/client_test.go new file mode 100644 index 0000000..6b3b890 --- /dev/null +++ b/pkg/openai/client_test.go @@ -0,0 +1,58 @@ +package openai_test + +import ( + "flag" + "log" + "os" + "strconv" + "testing" + + // Packages + opts "github.com/mutablelogic/go-client" + openai "github.com/mutablelogic/go-llm/pkg/openai" + assert "github.com/stretchr/testify/assert" +) + +/////////////////////////////////////////////////////////////////////////////// +// TEST SET-UP + +var ( + client *openai.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("OPENAI_API_KEY") + if api_key == "" { + log.Print("OPENAI_API_KEY not set") + os.Exit(0) + } + + // Create client + var err error + client, err = openai.New(api_key, opts.OptTrace(os.Stderr, verbose)) + if err != nil { + log.Println(err) + os.Exit(-1) + } + os.Exit(m.Run()) +} + +/////////////////////////////////////////////////////////////////////////////// +// TESTS + +func Test_client_001(t *testing.T) { + assert := assert.New(t) + assert.NotNil(client) + t.Log(client) +} diff --git a/pkg/openai/embeddings.go b/pkg/openai/embeddings.go new file mode 100644 index 0000000..28d377b --- /dev/null +++ b/pkg/openai/embeddings.go @@ -0,0 +1,115 @@ +package openai + +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 { + 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"` +} + +// Metrics +type Metrics struct { + PromptTokens uint64 `json:"prompt_tokens,omitempty"` + TotalTokens uint64 `json:"total_tokens,omitempty"` +} + +/////////////////////////////////////////////////////////////////////////////// +// 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"` + Dimensions uint64 `json:"dimensions,omitempty"` + User string `json:"user,omitempty"` +} + +func (openai *Client) GenerateEmbedding(ctx context.Context, model string, prompt []string, opts ...llm.Opt) (*embeddings, error) { + // Bail out is no prompt + if len(prompt) == 0 { + return nil, llm.ErrBadParameter.With("missing prompt") + } + + // Apply options + opt, err := llm.ApplyOpts(opts...) + if err != nil { + return nil, err + } + + // Request + req, err := client.NewJSONRequest(reqEmbedding{ + Model: model, + Input: prompt, + Format: optFormat(opt), + Dimensions: optDimensions(opt), + User: optUser(opt), + }) + if err != nil { + return nil, err + } + + // Response + var response embeddings + if err := openai.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/openai/embeddings_test.go b/pkg/openai/embeddings_test.go new file mode 100644 index 0000000..d0cd29a --- /dev/null +++ b/pkg/openai/embeddings_test.go @@ -0,0 +1,20 @@ +package openai_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(), "text-embedding-ada-002") + 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/openai/model.go b/pkg/openai/model.go new file mode 100644 index 0000000..bf548d2 --- /dev/null +++ b/pkg/openai/model.go @@ -0,0 +1,123 @@ +package openai + +import ( + "context" + "encoding/json" + + // Packages + client "github.com/mutablelogic/go-client" + llm "github.com/mutablelogic/go-llm" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +type model struct { + *Client `json:"-"` + meta Model +} + +var _ llm.Model = (*model)(nil) + +type Model struct { + Name string `json:"id"` + Type string `json:"object,omitempty"` + CreatedAt uint64 `json:"created,omitempty"` + OwnedBy string `json:"owned_by,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 - llm.Model implementation + +// Return model name +func (m model) Name() string { + return m.meta.Name +} + +// Return the models +func (openai *Client) Models(ctx context.Context) ([]llm.Model, error) { + // Cache models + if openai.cache == nil { + models, err := openai.ListModels(ctx) + if err != nil { + return nil, err + } + openai.cache = make(map[string]llm.Model, len(models)) + for _, m := range models { + openai.cache[m.Name] = &model{openai, m} + } + } + + // Return models + result := make([]llm.Model, 0, len(openai.cache)) + for _, model := range openai.cache { + result = append(result, model) + } + return result, nil +} + +// Return a model by name, or nil if not found. +// Panics on error. +func (openai *Client) Model(ctx context.Context, name string) llm.Model { + if openai.cache == nil { + if _, err := openai.Models(ctx); err != nil { + panic(err) + } + } + return openai.cache[name] +} + +/////////////////////////////////////////////////////////////////////////////// +// API CALLS + +// ListModels returns all the models +func (c *Client) ListModels(ctx context.Context) ([]Model, error) { + // Return the response + var response struct { + Data []Model `json:"data"` + } + if err := c.DoWithContext(ctx, nil, &response, client.OptPath("models")); err != nil { + return nil, err + } + + // Return success + return response.Data, nil +} + +// GetModel returns one model +func (c *Client) GetModel(ctx context.Context, model string) (*Model, error) { + // Return the response + var response Model + if err := c.DoWithContext(ctx, nil, &response, client.OptPath("models", model)); err != nil { + return nil, err + } + + // Return success + return &response, nil +} + +// Delete a fine-tuned model. You must have the Owner role in your organization +// to delete a model. +func (c *Client) DeleteModel(ctx context.Context, model string) error { + if err := c.DoWithContext(ctx, client.MethodDelete, nil, client.OptPath("models", model)); err != nil { + return err + } + + // Return success + return nil +} diff --git a/pkg/openai/model_test.go b/pkg/openai/model_test.go new file mode 100644 index 0000000..c4b6425 --- /dev/null +++ b/pkg/openai/model_test.go @@ -0,0 +1,43 @@ +package openai_test + +import ( + "context" + "testing" + + // Packages + assert "github.com/stretchr/testify/assert" +) + +func Test_models_001(t *testing.T) { + assert := assert.New(t) + + response, err := client.ListModels(context.TODO()) + assert.NoError(err) + assert.NotEmpty(response) + + t.Run("models", func(t *testing.T) { + for _, model := range response { + model_, err := client.GetModel(context.TODO(), model.Name) + if assert.NoError(err) { + assert.NotNil(model_) + assert.Equal(*model_, model) + } + } + }) +} + +func Test_models_002(t *testing.T) { + assert := assert.New(t) + + response, err := client.Models(context.TODO()) + assert.NoError(err) + assert.NotEmpty(response) + + t.Run("models", func(t *testing.T) { + for _, model := range response { + model_ := client.Model(context.TODO(), model.Name()) + assert.NotNil(model_) + assert.Equal(model_, model) + } + }) +} diff --git a/pkg/openai/opt.go b/pkg/openai/opt.go new file mode 100644 index 0000000..7815c6a --- /dev/null +++ b/pkg/openai/opt.go @@ -0,0 +1,41 @@ +package openai + +import ( + // Packages + "github.com/mutablelogic/go-llm" +) + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Embeddings: The number of dimensions the resulting output embeddings +// should have. Only supported in text-embedding-3 and later models. +func WithDimensions(v uint64) llm.Opt { + return func(o *llm.Opts) error { + o.Set("dimensions", v) + return nil + } +} + +// A unique identifier representing your end-user +func WithUser(v string) llm.Opt { + return func(o *llm.Opts) error { + o.Set("user", v) + return nil + } +} + +/////////////////////////////////////////////////////////////////////////////// +// PRIVATE METHODS + +func optFormat(opts *llm.Opts) string { + return opts.GetString("format") +} + +func optDimensions(opts *llm.Opts) uint64 { + return opts.GetUint64("dimensions") +} + +func optUser(opts *llm.Opts) string { + return opts.GetString("user") +} diff --git a/pkg/openai/session.go b/pkg/openai/session.go new file mode 100644 index 0000000..b04d770 --- /dev/null +++ b/pkg/openai/session.go @@ -0,0 +1,19 @@ +package openai + +import ( + // Packages + llm "github.com/mutablelogic/go-llm" +) + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +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 +} From 93a25a6324df79c2ca72259f9af621bf7e78bd82 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Tue, 4 Feb 2025 08:28:54 +0100 Subject: [PATCH 02/25] Adding OpenAI interface --- pkg/anthropic/message.go | 2 +- pkg/openai/completion.go | 226 +++++++++++++++++++++++++++++++++++++++ pkg/openai/embeddings.go | 6 -- pkg/openai/message.go | 121 +++++++++++++++++++++ pkg/openai/session.go | 18 ++-- pkg/session/session.go | 110 +++++++++++++++++++ 6 files changed, 470 insertions(+), 13 deletions(-) create mode 100644 pkg/openai/completion.go create mode 100644 pkg/openai/message.go create mode 100644 pkg/session/session.go diff --git a/pkg/anthropic/message.go b/pkg/anthropic/message.go index 33aecb3..dae3c48 100644 --- a/pkg/anthropic/message.go +++ b/pkg/anthropic/message.go @@ -6,7 +6,7 @@ import ( // Packages llm "github.com/mutablelogic/go-llm" - "github.com/mutablelogic/go-llm/pkg/tool" + tool "github.com/mutablelogic/go-llm/pkg/tool" ) /////////////////////////////////////////////////////////////////////////////// diff --git a/pkg/openai/completion.go b/pkg/openai/completion.go new file mode 100644 index 0000000..061341d --- /dev/null +++ b/pkg/openai/completion.go @@ -0,0 +1,226 @@ +package openai + +import ( + "context" + "encoding/json" + "strings" + + // Packages + client "github.com/mutablelogic/go-client" + llm "github.com/mutablelogic/go-llm" + session "github.com/mutablelogic/go-llm/pkg/session" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +// Completion Response +type Response struct { + Type string `json:"object"` + Created uint64 `json:"created"` + Model string `json:"model"` + SystemFingerprint string `json:"system_fingerprint"` + ServiceTier string `json:"service_tier"` + Completions `json:"choices"` + Metrics `json:"usage,omitempty"` +} + +// Metrics +type Metrics struct { + PromptTokens uint64 `json:"prompt_tokens,omitempty"` + CompletionTokens uint64 `json:"completion_tokens,omitempty"` + TotalTokens uint64 `json:"total_tokens,omitempty"` + CompletionTokenDetails struct { + ReasoningTokens uint64 `json:"reasoning_tokens,omitempty"` + AcceptedPredictionTokens uint64 `json:"accepted_prediction_tokens,omitempty"` + RejectedPredictionTokens uint64 `json:"rejected_prediction_tokens,omitempty"` + } `json:"completion_token_details,omitempty"` +} + +var _ llm.Completion = (*Response)(nil) + +/////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +func (r Response) String() string { + data, err := json.MarshalIndent(r, "", " ") + if err != nil { + return err.Error() + } + return string(data) +} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +type reqCompletion struct { + Model string `json:"model"` + Store *bool `json:"store,omitempty"` + ReasoningEffort string `json:"reasoning_effort,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` + FrequencyPenalty float64 `json:"frequency_penalty,omitempty"` + LogitBias map[uint64]int64 `json:"logit_bias,omitempty"` + LogProbs *bool `json:"logprobs,omitempty"` + TopLogProbs uint64 `json:"top_logprobs,omitempty"` + MaxTokens uint64 `json:"max_completion_tokens,omitempty"` + NumChoices uint64 `json:"n,omitempty"` + Modalties []string `json:"modalities,omitempty"` + Prediction *Content `json:"prediction,omitempty"` + Audio *Audio `json:"audio,omitempty"` + PresencePenalty float64 `json:"presence_penalty,omitempty"` + Format *Format `json:"response_format,omitempty"` + Seed uint64 `json:"random_seed,omitempty"` + ServiceTier string `json:"service_tier,omitempty"` + StopSequences []string `json:"stop,omitempty"` + Stream *bool `json:"stream,omitempty"` + StreamOptions *StreamOptions `json:"stream_options,omitempty"` + Temperature float64 `json:"temperature,omitempty"` + TopP float64 `json:"top_p,omitempty"` + Tools []llm.Tool `json:"tools,omitempty"` + ToolChoice any `json:"tool_choice,omitempty"` + ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty"` + User string `json:"user,omitempty"` + Messages []llm.Completion `json:"messages"` +} + +func (model *model) Completion(ctx context.Context, session llm.Context, opts ...llm.Opt) (*Response, error) { + // Apply options + opt, err := llm.ApplyOpts(opts...) + if err != nil { + return nil, err + } + + // Request + req, err := client.NewJSONRequest(reqCompletion{ + Model: model.Name(), + Temperature: optTemperature(opt), + TopP: optTopP(opt), + MaxTokens: optMaxTokens(opt), + Stream: optStream(opt), + StopSequences: optStopSequences(opt), + Seed: optSeed(opt), + Messages: messages, + 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 + } + + var response Response + 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{ + RoleContent: RoleContent{ + Role: c.Delta.Role(), + Content: "", + }, + }, + }) + } + // Add the completion delta + if c.Reason != "" { + response.Completions[c.Index].Reason = c.Reason + } + 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... + 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 + } + } +} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Generate a completion from a prompt without any history +func (model *model) Completion(prompt string, opts ...llm.Opt) (llm.Completion, error) { + // Create a new session + session := session.NewSession(model, &messagefactory{}, opts...) + + // Append a user prompt + message, err := messagefactory{}.UserPrompt(prompt, opts...) + if err != nil { + panic(err) + } + session.Append(message) + + return session +} diff --git a/pkg/openai/embeddings.go b/pkg/openai/embeddings.go index 28d377b..8d85ad3 100644 --- a/pkg/openai/embeddings.go +++ b/pkg/openai/embeddings.go @@ -32,12 +32,6 @@ type Embedding struct { Vector []float64 `json:"embedding"` } -// Metrics -type Metrics struct { - PromptTokens uint64 `json:"prompt_tokens,omitempty"` - TotalTokens uint64 `json:"total_tokens,omitempty"` -} - /////////////////////////////////////////////////////////////////////////////// // STRINGIFY diff --git a/pkg/openai/message.go b/pkg/openai/message.go new file mode 100644 index 0000000..dfdfd35 --- /dev/null +++ b/pkg/openai/message.go @@ -0,0 +1,121 @@ +package openai + +import ( + "encoding/json" + + // Packages + llm "github.com/mutablelogic/go-llm" + session "github.com/mutablelogic/go-llm/pkg/session" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +type messagefactory struct{} + +// Message with text or object content +type Message struct { + RoleContent +} + +// Completion choices +type Completions []Completion + +// Completion Variation +type Completion struct { + Index uint64 `json:"index"` + Message *Message `json:"message"` + Delta *Message `json:"delta,omitempty"` // For streaming + Reason string `json:"finish_reason,omitempty"` +} + +var _ llm.Completion = (*Message)(nil) + +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 +} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS - MESSAGE FACTORY + +func (messagefactory) SystemPrompt(prompt string) session.Message { + return &Message{ + RoleContent: RoleContent{ + Role: "system", + Content: prompt, + }, + } +} + +func (messagefactory) UserPrompt(prompt string, opts ...llm.Opt) (session.Message, error) { + // TODO: Attachments + // Return success + return &Message{ + RoleContent: RoleContent{ + Role: "user", + Content: prompt, + }, + }, nil +} + +func (messagefactory) ToolResults(results ...llm.ToolResult) ([]session.Message, error) { + // Check for no results + if len(results) == 0 { + return nil, llm.ErrBadParameter.Withf("No tool results") + } + + // Create results + messages := make([]session.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", + Content: string(value), + }, + }) + } + + // Return success + return messages, nil +} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS - COMPLETION + +// Return the number of completions +func (message *Message) Num() int { + return 1 +} + +// Return the current session role +func (message *Message) Role() string { + return message.RoleContent.Role +} + +// Return the text for the last completion +func (message *Message) Text(index int) string { + if index != 0 { + return "" + } + // If content is text, return it + if text, ok := message.Content.(string); ok { + return text + } + // 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 (message *Message) ToolCalls(index int) []llm.ToolCall { + if index != 0 { + return nil + } + // TODO + return nil +} diff --git a/pkg/openai/session.go b/pkg/openai/session.go index b04d770..2bb8b09 100644 --- a/pkg/openai/session.go +++ b/pkg/openai/session.go @@ -3,17 +3,23 @@ package openai import ( // Packages llm "github.com/mutablelogic/go-llm" + session "github.com/mutablelogic/go-llm/pkg/session" ) /////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS -func (m *model) Context(...llm.Opt) llm.Context { - return nil +func (model *model) Context(opts ...llm.Opt) llm.Context { + return session.NewSession(model, &messagefactory{}, opts...) } -// Convenience method to create a session context object -// with a user prompt -func (m *model) UserPrompt(string, ...llm.Opt) llm.Context { - return nil +// Convenience method to create a session context object with a user prompt +func (model *model) UserPrompt(prompt string, opts ...llm.Opt) llm.Context { + session := session.NewSession(model, &messagefactory{}, opts...) + message, err := messagefactory{}.UserPrompt(prompt, opts...) + if err != nil { + panic(err) + } + session.Append(message) + return session } diff --git a/pkg/session/session.go b/pkg/session/session.go new file mode 100644 index 0000000..5ceef7f --- /dev/null +++ b/pkg/session/session.go @@ -0,0 +1,110 @@ +package session + +import ( + "context" + + // Packages + "github.com/mutablelogic/go-llm" +) + +/////////////////////////////////////////////////////////////////////////////// +// INTERFACE + +// Abstract interface for a message factory +type MessageFactory interface { + // Generate a system prompt + SystemPrompt(prompt string) Message + + // Generate a user prompt, with attachments and other options + UserPrompt(string, ...llm.Opt) (Message, error) + + // Generate an array of results from calling tools + ToolResults(...llm.ToolResult) ([]Message, error) +} + +// Abstract interface for a message +type Message interface { +} + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +// A chat session with history +type session struct { + model llm.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) + +/////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +func NewSession(model llm.Model, factory MessageFactory, opts ...llm.Opt) *session { + return &session{ + model: model, + opts: opts, + seq: make([]Message, 0, 10), + } +} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Return an array of messages in the session with system prompt. If the +// prompt is empty, no system prompt is prepended +func (session *session) WithSystemPrompt(prompt string) []Message { + // TODO +} + +// Append a message to the session +func (session *session) Append(messages ...Message) { + session.seq = append(session.seq, messages...) +} + +// 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 +} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS - COMPLETION + +// Return the number of completions +func (session *session) Num() int { + if len(session.seq) == 0 { + return 0 + } + return session.seq[len(session.seq)-1].Num() +} + +// Return the current session role +func (session *session) Role() string { + if len(session.seq) == 0 { + return "" + } + return session.seq[len(session.seq)-1].Role() +} + +// Return the text for the last completion +func (session *session) Text(index int) string { + if len(session.seq) == 0 { + return "" + } + return session.seq[len(session.seq)-1].Text(index) +} + +// Return the current session tool calls given the completion index. +// Will return nil if no tool calls were returned. +func (session *session) ToolCalls(index int) []llm.ToolCall { + if len(session.seq) == 0 { + return nil + } + return session.seq[len(session.seq)-1].ToolCalls(index) +} From 174703cf8065bcada1247517b872bc97e9d40dd1 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Wed, 5 Feb 2025 10:05:52 +0100 Subject: [PATCH 03/25] Updated OpenAI --- model.go | 9 +- opt.go | 9 ++ pkg/anthropic/completion.go | 5 + pkg/mistral/completion.go | 5 + pkg/mistral/opt.go | 11 +- pkg/ollama/chat.go | 4 + pkg/openai/completion.go | 186 +++++++++------------- pkg/openai/completion_test.go | 22 +++ pkg/openai/content.go | 16 ++ pkg/openai/message.go | 11 -- pkg/openai/opt.go | 291 ++++++++++++++++++++++++++++++++++ pkg/openai/opt_audio.go | 26 +++ pkg/openai/opt_format.go | 25 +++ pkg/openai/opt_stream.go | 15 ++ pkg/openai/opt_toolchoice.go | 23 +++ pkg/session/session.go | 3 + 16 files changed, 530 insertions(+), 131 deletions(-) create mode 100644 pkg/openai/completion_test.go create mode 100644 pkg/openai/content.go create mode 100644 pkg/openai/opt_audio.go create mode 100644 pkg/openai/opt_format.go create mode 100644 pkg/openai/opt_stream.go create mode 100644 pkg/openai/opt_toolchoice.go diff --git a/model.go b/model.go index 221105b..453ec82 100644 --- a/model.go +++ b/model.go @@ -1,6 +1,8 @@ package llm -import "context" +import ( + "context" +) // An Model can be used to generate a response to a user prompt, // which is passed to an agent. The interaction occurs through @@ -15,7 +17,10 @@ type Model interface { // Convenience method to create a session context object // with a user prompt - UserPrompt(string, ...Opt) Context + //UserPrompt(string, ...Opt) Context + + // Create a completion from a text prompt + Completion(context.Context, string, ...Opt) (Completion, error) // Embedding vector generation Embedding(context.Context, string, ...Opt) ([]float64, error) diff --git a/opt.go b/opt.go index a378a91..f34d5d7 100644 --- a/opt.go +++ b/opt.go @@ -357,3 +357,12 @@ func WithSafePrompt() Opt { return nil } } + +// Predicted output, which is most common when you are regenerating a file +// with only minor changes to most of the content. +func WithPrediction(v string) Opt { + return func(o *Opts) error { + o.Set("prediction", v) + return nil + } +} diff --git a/pkg/anthropic/completion.go b/pkg/anthropic/completion.go index 6c25a11..109a063 100644 --- a/pkg/anthropic/completion.go +++ b/pkg/anthropic/completion.go @@ -61,6 +61,11 @@ type reqMessages struct { ToolChoice any `json:"tool_choice,omitempty"` } +func (model *model) Completion(ctx context.Context, prompt string, opts ...llm.Opt) (llm.Completion, error) { + // TODO + return nil, llm.ErrNotImplemented +} + func (anthropic *Client) Messages(ctx context.Context, context llm.Context, opts ...llm.Opt) (*Response, error) { // Apply options opt, err := llm.ApplyOpts(opts...) diff --git a/pkg/mistral/completion.go b/pkg/mistral/completion.go index b0e4bb0..f008138 100644 --- a/pkg/mistral/completion.go +++ b/pkg/mistral/completion.go @@ -64,6 +64,11 @@ type reqChatCompletion struct { SafePrompt bool `json:"safe_prompt,omitempty"` } +func (model *model) Completion(ctx context.Context, prompt string, opts ...llm.Opt) (llm.Completion, error) { + // TODO + return nil, llm.ErrNotImplemented +} + func (mistral *Client) ChatCompletion(ctx context.Context, context llm.Context, opts ...llm.Opt) (*Response, error) { // Apply options opt, err := llm.ApplyOpts(opts...) diff --git a/pkg/mistral/opt.go b/pkg/mistral/opt.go index 71e82da..e76bc5d 100644 --- a/pkg/mistral/opt.go +++ b/pkg/mistral/opt.go @@ -3,19 +3,10 @@ package mistral import ( "strings" + // Packages "github.com/mutablelogic/go-llm" ) -/////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - -func WithPrediction(v string) llm.Opt { - return func(o *llm.Opts) error { - o.Set("prediction", v) - return nil - } -} - /////////////////////////////////////////////////////////////////////////////// // PRIVATE METHODS diff --git a/pkg/ollama/chat.go b/pkg/ollama/chat.go index 175e0b5..41eb086 100644 --- a/pkg/ollama/chat.go +++ b/pkg/ollama/chat.go @@ -59,6 +59,10 @@ type reqChat struct { KeepAlive *time.Duration `json:"keep_alive,omitempty"` } +func (model *model) Completion(ctx context.Context, prompt string, opts ...llm.Opt) (llm.Completion, error) { + // TODO + return nil, llm.ErrNotImplemented +} func (ollama *Client) Chat(ctx context.Context, context llm.Context, opts ...llm.Opt) (*Response, error) { // Apply options opt, err := llm.ApplyOpts(opts...) diff --git a/pkg/openai/completion.go b/pkg/openai/completion.go index 061341d..a114ac7 100644 --- a/pkg/openai/completion.go +++ b/pkg/openai/completion.go @@ -3,12 +3,10 @@ package openai import ( "context" "encoding/json" - "strings" // Packages client "github.com/mutablelogic/go-client" llm "github.com/mutablelogic/go-llm" - session "github.com/mutablelogic/go-llm/pkg/session" ) /////////////////////////////////////////////////////////////////////////////// @@ -25,6 +23,17 @@ type Response struct { Metrics `json:"usage,omitempty"` } +// Completion choices +type Completions []Completion + +// Completion Variation +type Completion struct { + Index uint64 `json:"index"` + Message *Message `json:"message"` + Delta *Message `json:"delta,omitempty"` // For streaming + Reason string `json:"finish_reason,omitempty"` +} + // Metrics type Metrics struct { PromptTokens uint64 `json:"prompt_tokens,omitempty"` @@ -60,19 +69,19 @@ type reqCompletion struct { Metadata map[string]string `json:"metadata,omitempty"` FrequencyPenalty float64 `json:"frequency_penalty,omitempty"` LogitBias map[uint64]int64 `json:"logit_bias,omitempty"` - LogProbs *bool `json:"logprobs,omitempty"` + LogProbs bool `json:"logprobs,omitempty"` TopLogProbs uint64 `json:"top_logprobs,omitempty"` MaxTokens uint64 `json:"max_completion_tokens,omitempty"` - NumChoices uint64 `json:"n,omitempty"` + NumCompletions uint64 `json:"n,omitempty"` Modalties []string `json:"modalities,omitempty"` Prediction *Content `json:"prediction,omitempty"` Audio *Audio `json:"audio,omitempty"` PresencePenalty float64 `json:"presence_penalty,omitempty"` - Format *Format `json:"response_format,omitempty"` + ResponseFormat *Format `json:"response_format,omitempty"` Seed uint64 `json:"random_seed,omitempty"` ServiceTier string `json:"service_tier,omitempty"` StopSequences []string `json:"stop,omitempty"` - Stream *bool `json:"stream,omitempty"` + Stream bool `json:"stream,omitempty"` StreamOptions *StreamOptions `json:"stream_options,omitempty"` Temperature float64 `json:"temperature,omitempty"` TopP float64 `json:"top_p,omitempty"` @@ -83,31 +92,48 @@ type reqCompletion struct { Messages []llm.Completion `json:"messages"` } -func (model *model) Completion(ctx context.Context, session llm.Context, opts ...llm.Opt) (*Response, error) { +func (model *model) Completion(ctx context.Context, prompt string, opts ...llm.Opt) (llm.Completion, error) { // Apply options opt, err := llm.ApplyOpts(opts...) if err != nil { return nil, err } + // Create a message + message, err := messagefactory{}.UserPrompt(prompt, opts...) + if err != nil { + return nil, err + } + // Request req, err := client.NewJSONRequest(reqCompletion{ - Model: model.Name(), - Temperature: optTemperature(opt), - TopP: optTopP(opt), - MaxTokens: optMaxTokens(opt), - Stream: optStream(opt), - StopSequences: optStopSequences(opt), - Seed: optSeed(opt), - Messages: messages, - 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), + Model: model.Name(), + Store: optStore(opt), + ReasoningEffort: optReasoningEffort(opt), + Metadata: optMetadata(opt), + FrequencyPenalty: optFrequencyPenalty(opt), + LogitBias: optLogitBias(opt), + LogProbs: optLogProbs(opt), + TopLogProbs: optTopLogProbs(opt), + MaxTokens: optMaxTokens(opt), + NumCompletions: optNumCompletions(opt), + Modalties: optModalities(opt), + Prediction: optPrediction(opt), + Audio: optAudio(opt), + PresencePenalty: optPresencePenalty(opt), + ResponseFormat: optResponseFormat(opt), + Seed: optSeed(opt), + ServiceTier: optServiceTier(opt), + StreamOptions: optStreamOptions(opt), + Temperature: optTemperature(opt), + TopP: optTopP(opt), + Stream: optStream(opt), + StopSequences: optStopSequences(opt), + Tools: optTools(model, opt), + ToolChoice: optToolChoice(opt), + ParallelToolCalls: optParallelToolCalls(opt), + User: optUser(opt), + Messages: []llm.Completion{message}, }) if err != nil { return nil, err @@ -117,20 +143,9 @@ func (model *model) Completion(ctx context.Context, session llm.Context, opts .. 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 { + if err := model.DoWithContext(ctx, req, &response, reqopts...); err != nil { return nil, err } @@ -139,88 +154,43 @@ func (model *model) Completion(ctx context.Context, session llm.Context, opts .. } /////////////////////////////////////////////////////////////////////////////// -// PRIVATE METHODS +// COMPLETIONS + +// Return the number of completions +func (c Completions) Num() int { + return len(c) +} -func streamEvent(response *Response, evt client.TextStreamEvent) error { - var delta Response - // If we are done, ignore - if strings.TrimSpace(evt.Data) == "[DONE]" { +// Return message for a specific completion +func (c Completions) Message(index int) *Message { + if index < 0 || index >= len(c) { 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 + return c[index].Message } -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{ - RoleContent: RoleContent{ - Role: c.Delta.Role(), - Content: "", - }, - }, - }) - } - // Add the completion delta - if c.Reason != "" { - response.Completions[c.Index].Reason = c.Reason - } - if role := c.Delta.Role(); role != "" { - response.Completions[c.Index].Message.RoleContent.Role = role +// 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() +} - // 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 - } +// 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) } -/////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - -// Generate a completion from a prompt without any history -func (model *model) Completion(prompt string, opts ...llm.Opt) (llm.Completion, error) { - // Create a new session - session := session.NewSession(model, &messagefactory{}, opts...) - - // Append a user prompt - message, err := messagefactory{}.UserPrompt(prompt, opts...) - if err != nil { - panic(err) +// 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 } - session.Append(message) - - return session + return c[index].Message.ToolCalls(0) } diff --git a/pkg/openai/completion_test.go b/pkg/openai/completion_test.go new file mode 100644 index 0000000..72e1720 --- /dev/null +++ b/pkg/openai/completion_test.go @@ -0,0 +1,22 @@ +package openai_test + +import ( + "context" + "testing" + + assert "github.com/stretchr/testify/assert" +) + +func Test_completion_001(t *testing.T) { + assert := assert.New(t) + model := client.Model(context.TODO(), "gpt-4o-mini") + if !assert.NotNil(model) { + t.FailNow() + } + + response, err := model.Completion(context.TODO(), "Hello, how are you?") + if assert.NoError(err) { + assert.NotEmpty(response) + t.Log(response) + } +} diff --git a/pkg/openai/content.go b/pkg/openai/content.go new file mode 100644 index 0000000..bff4ff2 --- /dev/null +++ b/pkg/openai/content.go @@ -0,0 +1,16 @@ +package openai + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +type Content struct { + Type string `json:"type"` // text or content + Content string `json:"content,omitempty"` // text content +} + +/////////////////////////////////////////////////////////////////////////////// +// LICECYCLE + +func NewContentString(typ, content string) *Content { + return &Content{Type: typ, Content: content} +} diff --git a/pkg/openai/message.go b/pkg/openai/message.go index dfdfd35..2758365 100644 --- a/pkg/openai/message.go +++ b/pkg/openai/message.go @@ -18,17 +18,6 @@ type Message struct { RoleContent } -// Completion choices -type Completions []Completion - -// Completion Variation -type Completion struct { - Index uint64 `json:"index"` - Message *Message `json:"message"` - Delta *Message `json:"delta,omitempty"` // For streaming - Reason string `json:"finish_reason,omitempty"` -} - var _ llm.Completion = (*Message)(nil) type RoleContent struct { diff --git a/pkg/openai/opt.go b/pkg/openai/opt.go index 7815c6a..eddcbb2 100644 --- a/pkg/openai/opt.go +++ b/pkg/openai/opt.go @@ -2,6 +2,9 @@ package openai import ( // Packages + "slices" + "strings" + "github.com/mutablelogic/go-llm" ) @@ -25,17 +28,305 @@ func WithUser(v string) llm.Opt { } } +// Whether or not to store the output of this chat completion request for use in +// model distillation or evals products. +func WithStore(v bool) llm.Opt { + return func(o *llm.Opts) error { + o.Set("store", v) + return nil + } +} + +// Constrains effort on reasoning for reasoning models. Currently supported values are +// low, medium, and high. Reducing reasoning effort can result in faster responses +// and fewer tokens used on reasoning in a response. +func WithReasoningEffort(v string) llm.Opt { + return func(o *llm.Opts) error { + o.Set("reasoning_effort", v) + return nil + } +} + +// Key-value pair that can be attached to an object. This can be useful for storing +// additional information about the object in a structured format, and querying for objects +// via API or the dashboard. +func WithMetadata(k, v string) llm.Opt { + return func(o *llm.Opts) error { + metadata, ok := o.Get("metadata").(map[string]string) + if !ok { + metadata = make(map[string]string, 16) + } + metadata[k] = v + o.Set("metadata", metadata) + return nil + } +} + +// Tokens (specified by their token ID in the tokenizer) to an associated bias +// value from -100 to 100. Mathematically, the bias is added to the logits +// generated by the model prior to sampling. The exact effect will vary per model, +// but values between -1 and 1 should decrease or increase likelihood of selection; +// values like -100 or 100 should result in a ban or exclusive selection of the +// relevant token. +func WithLogitBias(token uint64, bias int64) llm.Opt { + return func(o *llm.Opts) error { + logit_bias, ok := o.Get("logit_bias").(map[uint64]int64) + if !ok { + logit_bias = make(map[uint64]int64, 16) + } + logit_bias[token] = bias + o.Set("logit_bias", logit_bias) + return nil + } +} + +// Whether to return log probabilities of the output tokens or not. +func WithLogProbs() llm.Opt { + return func(o *llm.Opts) error { + o.Set("logprobs", true) + return nil + } +} + +// An integer between 0 and 20 specifying the number of most likely tokens +// to return at each token position, each with an associated log probability. +func WithTopLogProbs(v uint64) llm.Opt { + return func(o *llm.Opts) error { + if v > 20 { + return llm.ErrBadParameter.With("top_logprobs") + } + o.Set("logprobs", true) + o.Set("top_logprobs", v) + return nil + } +} + +// Output types that you would like the model to generate for this request. +// Supported values are: "text", "audio" +func WithModalities(v ...string) llm.Opt { + return func(o *llm.Opts) error { + arr, ok := o.Get("modalities").([]string) + if !ok { + arr = make([]string, 0, 16) + } + for _, v := range v { + v = strings.ToLower(strings.TrimSpace(v)) + if !slices.Contains(arr, v) { + arr = append(arr, v) + } + } + o.Set("modalities", arr) + return nil + } +} + +// Parameters for audio output +func WithAudio(voice, format string) llm.Opt { + return func(o *llm.Opts) error { + if err := WithModalities("audio")(o); err != nil { + return err + } + if audio := NewAudio(voice, format); audio != nil { + o.Set("audio", audio) + } else { + return llm.ErrBadParameter.With("audio") + } + return nil + } +} + +// Specifies the latency tier to use for processing the request. Values +// can be auto or default +func WithServiceTier(v string) llm.Opt { + return func(o *llm.Opts) error { + o.Set("service_tier", v) + return nil + } +} + +// Enable streaming and include usage information in the streaming response +func WithStreamOptions(fn func(llm.Completion), include_usage bool) llm.Opt { + return func(o *llm.Opts) error { + if err := llm.WithStream(fn)(o); err != nil { + return err + } + o.Set("stream_options_include_usage", include_usage) + return nil + } +} + +// Disable parallel tool calling +func WithDisableParallelToolCalls() llm.Opt { + return func(o *llm.Opts) error { + o.Set("parallel_tool_calls", false) + return nil + } +} + /////////////////////////////////////////////////////////////////////////////// // PRIVATE METHODS +// For embedding func optFormat(opts *llm.Opts) string { return opts.GetString("format") } +// For embedding func optDimensions(opts *llm.Opts) uint64 { return opts.GetUint64("dimensions") } +// For embedding and completions func optUser(opts *llm.Opts) string { return opts.GetString("user") } + +func optStore(opts *llm.Opts) *bool { + if v, ok := opts.Get("store").(bool); ok { + return &v + } + return nil +} + +func optReasoningEffort(opts *llm.Opts) string { + return opts.GetString("reasoning_effort") +} + +func optMetadata(opts *llm.Opts) map[string]string { + if metadata, ok := opts.Get("metadata").(map[string]string); ok { + return metadata + } + return nil +} + +func optFrequencyPenalty(opts *llm.Opts) float64 { + return opts.GetFloat64("frequency_penalty") +} + +func optLogitBias(opts *llm.Opts) map[uint64]int64 { + if logit_bias, ok := opts.Get("logit_bias").(map[uint64]int64); ok { + return logit_bias + } + return nil +} + +func optLogProbs(opts *llm.Opts) bool { + return opts.GetBool("logprobs") +} + +func optTopLogProbs(opts *llm.Opts) uint64 { + return opts.GetUint64("top_logprobs") +} + +func optMaxTokens(opts *llm.Opts) uint64 { + return opts.GetUint64("max_tokens") +} + +func optNumCompletions(opts *llm.Opts) uint64 { + return opts.GetUint64("num_choices") +} + +func optModalities(opts *llm.Opts) []string { + if v, ok := opts.Get("modalities").([]string); ok { + return v + } + return nil +} + +func optPrediction(opts *llm.Opts) *Content { + v := strings.TrimSpace(opts.GetString("prediction")) + if v != "" { + return NewContentString("content", v) + } + return nil +} + +func optAudio(opts *llm.Opts) *Audio { + if v, ok := opts.Get("audio").(*Audio); ok { + return v + } + return nil +} + +func optPresencePenalty(opts *llm.Opts) float64 { + return opts.GetFloat64("presence_penalty") +} + +func optResponseFormat(opts *llm.Opts) *Format { + if format := NewFormat(optFormat(opts)); format != nil { + return format + } else { + return nil + } +} + +func optSeed(opts *llm.Opts) uint64 { + return opts.GetUint64("seed") +} + +func optServiceTier(opts *llm.Opts) string { + return opts.GetString("service_tier") +} + +func optStreamOptions(opts *llm.Opts) *StreamOptions { + if opts.Has("stream_options_include_usage") { + return NewStreamOptions(opts.GetBool("stream_options_include_usage")) + } else { + return nil + } +} + +func optStream(opts *llm.Opts) bool { + return opts.StreamFn() != nil +} + +func optTemperature(opts *llm.Opts) float64 { + return opts.GetFloat64("temperature") +} + +func optTopP(opts *llm.Opts) float64 { + return opts.GetFloat64("top_p") +} + +func optStopSequences(opts *llm.Opts) []string { + if opts.Has("stop") { + if stop, ok := opts.Get("stop").([]string); ok { + return stop + } + } + return nil +} + +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", "required": + return choice + case "": + return nil + default: + return NewToolChoice(choice) + } +} + +func optParallelToolCalls(opts *llm.Opts) *bool { + if opts.Has("parallel_tool_calls") { + v := opts.GetBool("parallel_tool_calls") + return &v + } + return nil +} diff --git a/pkg/openai/opt_audio.go b/pkg/openai/opt_audio.go new file mode 100644 index 0000000..ed230bc --- /dev/null +++ b/pkg/openai/opt_audio.go @@ -0,0 +1,26 @@ +package openai + +import "strings" + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +type Audio struct { + // Supported voices include ash, ballad, coral, sage, and verse + Voice string `json:"voice"` + + // Supported formats: wav, mp3, flac, opus, or pcm16 + Format string `json:"format"` +} + +/////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +func NewAudio(voice, format string) *Audio { + voice = strings.TrimSpace(strings.ToLower(voice)) + format = strings.TrimSpace(strings.ToLower(format)) + if voice == "" || format == "" { + return nil + } + return &Audio{Voice: voice, Format: format} +} diff --git a/pkg/openai/opt_format.go b/pkg/openai/opt_format.go new file mode 100644 index 0000000..34e4623 --- /dev/null +++ b/pkg/openai/opt_format.go @@ -0,0 +1,25 @@ +package openai + +import "strings" + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +type Format struct { + // Supported response format types are text, json_object or json_schema + Type string `json:"type"` +} + +/////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +func NewFormat(format string) *Format { + format = strings.TrimSpace(strings.ToLower(format)) + switch format { + case "text", "json_object": + return &Format{Type: format} + default: + // json_schema is not yet supported + return nil + } +} diff --git a/pkg/openai/opt_stream.go b/pkg/openai/opt_stream.go new file mode 100644 index 0000000..c82da5e --- /dev/null +++ b/pkg/openai/opt_stream.go @@ -0,0 +1,15 @@ +package openai + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +type StreamOptions struct { + IncludeUsage bool `json:"include_usage"` +} + +/////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +func NewStreamOptions(include_usage bool) *StreamOptions { + return &StreamOptions{IncludeUsage: include_usage} +} diff --git a/pkg/openai/opt_toolchoice.go b/pkg/openai/opt_toolchoice.go new file mode 100644 index 0000000..c89c5c9 --- /dev/null +++ b/pkg/openai/opt_toolchoice.go @@ -0,0 +1,23 @@ +package openai + +import "strings" + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +type ToolChoice struct { + Type string `json:"type"` + Function struct { + Name string `json:"name"` + } `json:"function"` +} + +/////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +func NewToolChoice(function string) *ToolChoice { + choice := new(ToolChoice) + choice.Type = "function" + choice.Function.Name = strings.TrimSpace(strings.ToLower(function)) + return choice +} diff --git a/pkg/session/session.go b/pkg/session/session.go index 5ceef7f..71e856a 100644 --- a/pkg/session/session.go +++ b/pkg/session/session.go @@ -24,6 +24,7 @@ type MessageFactory interface { // Abstract interface for a message type Message interface { + llm.Completion } /////////////////////////////////////////////////////////////////////////////// @@ -41,6 +42,7 @@ var _ llm.Context = (*session)(nil) /////////////////////////////////////////////////////////////////////////////// // LIFECYCLE +// Create a new empty session with a capacity for 10 messages in the history func NewSession(model llm.Model, factory MessageFactory, opts ...llm.Opt) *session { return &session{ model: model, @@ -56,6 +58,7 @@ func NewSession(model llm.Model, factory MessageFactory, opts ...llm.Opt) *sessi // prompt is empty, no system prompt is prepended func (session *session) WithSystemPrompt(prompt string) []Message { // TODO + return nil } // Append a message to the session From 36bcca7cc58280a33d4c3d7d48ff6d2c47595614 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Wed, 5 Feb 2025 12:16:44 +0100 Subject: [PATCH 04/25] Updated OpenAI --- README.md | 46 +++--- attachment.go | 57 +++++-- context.go | 12 +- opt.go | 8 + pkg/anthropic/completion_test.go | 3 +- pkg/anthropic/opt.go | 7 - pkg/openai/completion.go | 25 ++- pkg/openai/completion_test.go | 251 +++++++++++++++++++++++++++++++ pkg/openai/content.go | 7 +- pkg/openai/message.go | 16 +- pkg/openai/opt.go | 18 +-- pkg/session/session.go | 8 + 12 files changed, 396 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index ad60a12..9bc9318 100644 --- a/README.md +++ b/README.md @@ -263,26 +263,38 @@ 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)` | 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 | Use `auto`, `any` or a function name. Only the first argument is used. | 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)` | Yes | No | Yes | - | The seed to use for random sampling. If set, different calls will generate deterministic results. | -| `llm.WithFormat(string)` | Use `json` | No | 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. | +| `llm.WithTemperature(float64)` | Yes | 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 | 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 | 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)` | No | Yes | 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 | Yes | Stream the response to a function. | +| `llm.WithToolChoice(string, string, ...)` | No | Use `auto`, `any` or a function name. Only the first argument is used. | Use `auto`, `any`, `none`, `required` or a function name. Only the first argument is used. | Use `auto`, `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 | Yes | The set of tools to use. | +| `llm.WithStopSequence(string, string, ...)` | Yes | Yes | Yes | Yes | Stop generation if one of these tokens is detected. | +| `llm.WithSystemPrompt(string)` | No | Yes | Yes | Yes | Set the system prompt for the model. | +| `llm.WithSeed(uint64)` | Yes | No | Yes | Yes | The seed to use for random sampling. If set, different calls will generate deterministic results. | +| `llm.WithFormat(string)` | Use `json` | No | Use `json_format` or `text` | 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 | 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 | 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.WithPrediction(string)` | No | No | Yes | 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 | No | Whether to inject a safety prompt before all conversations. | +| `llm.WithNumCompletions(uint64)` | No | No | Yes | 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 for the request, for debugging | +| `llm.WithUser(string)` | No | Yes | No | Yes | A unique identifier representing your end-user, | +| `openai.WithStore(bool)` | No | No | No | Yes | Whether or not to store the output of this chat completion request | +| `openai.WithDimensions(uint64)` | No | No | No | Yes | The number of dimensions the resulting output embeddings should have. Only supported in text-embedding-3 and later models | +| `openai.WithReasoningEffort(string)` | No | No | No | Yes | The level of effort model should put into reasoning. | +| `openai.WithMetadata(string, string)` | No | No | No | Yes | Metadata to be logged with the completion. | +| `openai.WithLogitBias(uint64, int64)` | No | No | No | Yes | A token and their logit bias value. Call multiple times to add additional tokens | +| `openai.WithLogProbs()` | No | No | No | Yes | Include the log probabilities on the completion. | +| `openai.WithLogProbs()` | No | No | No | Yes | Include the log probabilities on the completion. | +| `openai.WithTopLogProbs(uint64)` | No | No | No | Yes | An integer between 0 and 20 specifying the number of most likely tokens to return at each token position. | +| `openai.WithAudio(string, string)` | No | No | No | Yes | Output audio (voice, format) for the completion. Can be used with certain models. | +| `openai.WithServiceTier(string)` | No | No | No | Yes | Specifies the latency tier to use for processing the request. | +| `openai.WithStreamOptions(func(llm.Completion), bool)` | No | No | No | Yes | Include usage information in the stream response | +| `openai.WithDisableParallelToolCalls()` | No | No | No | Yes | Call tools in serial, rather than in parallel | ## The Command Line Tool diff --git a/attachment.go b/attachment.go index 5987a9d..a43aa78 100644 --- a/attachment.go +++ b/attachment.go @@ -13,10 +13,17 @@ import ( /////////////////////////////////////////////////////////////////////////////// // TYPES +type AttachmentMeta struct { + Id string `json:"id,omitempty"` + Filename string `json:"filename"` + Data []byte `json:"data"` + ExpiresAt uint64 `json:"expires_at,omitempty"` + Caption string `json:"transcript,omitempty"` +} + // Attachment for messages type Attachment struct { - filename string - data []byte + meta AttachmentMeta } //////////////////////////////////////////////////////////////////////////////// @@ -33,22 +40,40 @@ func ReadAttachment(r io.Reader) (*Attachment, error) { if f, ok := r.(*os.File); ok { filename = f.Name() } - return &Attachment{filename: filename, data: data}, nil + return &Attachment{ + meta: AttachmentMeta{ + Filename: filename, + Data: data, + }, + }, nil } //////////////////////////////////////////////////////////////////////////////// // STRINGIFY -func (a *Attachment) String() string { +func (a *Attachment) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &a.meta) +} + +func (a *Attachment) MarshalJSON() ([]byte, error) { + // Create a JSON representation var j struct { - Filename string `json:"filename"` + Id string `json:"id,omitempty"` + Filename string `json:"filename,omitempty"` Type string `json:"type"` Bytes uint64 `json:"bytes"` + Caption string `json:"transcript,omitempty"` } - j.Filename = a.filename + j.Id = a.meta.Id + j.Filename = a.meta.Filename j.Type = a.Type() - j.Bytes = uint64(len(a.data)) - data, err := json.MarshalIndent(j, "", " ") + j.Bytes = uint64(len(a.meta.Data)) + j.Caption = a.meta.Caption + return json.Marshal(j) +} + +func (a *Attachment) String() string { + data, err := json.MarshalIndent(a.meta, "", " ") if err != nil { return err.Error() } @@ -59,23 +84,27 @@ func (a *Attachment) String() string { // PUBLIC METHODS func (a *Attachment) Filename() string { - return a.filename + return a.meta.Filename } func (a *Attachment) Data() []byte { - return a.data + return a.meta.Data +} + +func (a *Attachment) Caption() string { + return a.meta.Caption } func (a *Attachment) Type() string { // Mimetype based on content - mimetype := http.DetectContentType(a.data) - if mimetype == "application/octet-stream" && a.filename != "" { + mimetype := http.DetectContentType(a.meta.Data) + if mimetype == "application/octet-stream" && a.meta.Filename != "" { // Detect mimetype from extension - mimetype = mime.TypeByExtension(filepath.Ext(a.filename)) + mimetype = mime.TypeByExtension(filepath.Ext(a.meta.Filename)) } return mimetype } func (a *Attachment) Url() string { - return "data:" + a.Type() + ";base64," + base64.StdEncoding.EncodeToString(a.data) + return "data:" + a.Type() + ";base64," + base64.StdEncoding.EncodeToString(a.meta.Data) } diff --git a/context.go b/context.go index 0aad95d..303366d 100644 --- a/context.go +++ b/context.go @@ -8,18 +8,22 @@ import "context" // 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 + // WithNumCompletions was used Num() int - // Return the current session role, which can be system, assistant, user, tool, tool_result, ... + // 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 text for the last completion, with the argument as the - // completion index (usually 0). If multiple completions are not - // supported, the argument is ignored. + // completion index (usually 0). Text(int) string + // Return audio for the last completion, with the argument as the + // completion index (usually 0). + Audio(int) *Attachment + // Return the current session tool calls given the completion index. // Will return nil if no tool calls were returned. ToolCalls(int) []ToolCall diff --git a/opt.go b/opt.go index f34d5d7..f526146 100644 --- a/opt.go +++ b/opt.go @@ -366,3 +366,11 @@ func WithPrediction(v string) Opt { return nil } } + +// A unique identifier representing your end-user +func WithUser(v string) Opt { + return func(o *Opts) error { + o.Set("user", v) + return nil + } +} diff --git a/pkg/anthropic/completion_test.go b/pkg/anthropic/completion_test.go index 2726c7e..a5db6a4 100644 --- a/pkg/anthropic/completion_test.go +++ b/pkg/anthropic/completion_test.go @@ -10,7 +10,6 @@ import ( // Packages llm "github.com/mutablelogic/go-llm" - anthropic "github.com/mutablelogic/go-llm/pkg/anthropic" "github.com/mutablelogic/go-llm/pkg/tool" assert "github.com/stretchr/testify/assert" ) @@ -101,7 +100,7 @@ func Test_chat_002(t *testing.T) { } }) t.Run("User", func(t *testing.T) { - r, err := client.Messages(context.TODO(), model.UserPrompt("What is the temperature in London?"), anthropic.WithUser("username")) + r, err := client.Messages(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithUser("username")) if assert.NoError(err) { assert.Equal("assistant", r.Role()) assert.Equal(1, r.Num()) diff --git a/pkg/anthropic/opt.go b/pkg/anthropic/opt.go index 862721b..b77fcdd 100644 --- a/pkg/anthropic/opt.go +++ b/pkg/anthropic/opt.go @@ -17,13 +17,6 @@ type optmetadata struct { //////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS -func WithUser(v string) llm.Opt { - return func(o *llm.Opts) error { - o.Set("user", v) - return nil - } -} - func WithEphemeral() llm.Opt { return func(o *llm.Opts) error { o.Set("ephemeral", true) diff --git a/pkg/openai/completion.go b/pkg/openai/completion.go index a114ac7..e48271e 100644 --- a/pkg/openai/completion.go +++ b/pkg/openai/completion.go @@ -14,6 +14,7 @@ import ( // Completion Response type Response struct { + Id string `json:"id"` Type string `json:"object"` Created uint64 `json:"created"` Model string `json:"model"` @@ -36,14 +37,18 @@ type Completion struct { // Metrics type Metrics struct { - PromptTokens uint64 `json:"prompt_tokens,omitempty"` - CompletionTokens uint64 `json:"completion_tokens,omitempty"` - TotalTokens uint64 `json:"total_tokens,omitempty"` + PromptTokens uint64 `json:"prompt_tokens,omitempty"` + CompletionTokens uint64 `json:"completion_tokens,omitempty"` + TotalTokens uint64 `json:"total_tokens,omitempty"` + PromptTokenDetails struct { + CachedTokens uint64 `json:"cached_tokens,omitempty"` + AudioTokens uint64 `json:"audio_tokens,omitempty"` + } `json:"prompt_tokens_details,omitempty"` CompletionTokenDetails struct { ReasoningTokens uint64 `json:"reasoning_tokens,omitempty"` AcceptedPredictionTokens uint64 `json:"accepted_prediction_tokens,omitempty"` RejectedPredictionTokens uint64 `json:"rejected_prediction_tokens,omitempty"` - } `json:"completion_token_details,omitempty"` + } `json:"completion_tokens_details,omitempty"` } var _ llm.Completion = (*Response)(nil) @@ -78,7 +83,7 @@ type reqCompletion struct { Audio *Audio `json:"audio,omitempty"` PresencePenalty float64 `json:"presence_penalty,omitempty"` ResponseFormat *Format `json:"response_format,omitempty"` - Seed uint64 `json:"random_seed,omitempty"` + Seed uint64 `json:"seed,omitempty"` ServiceTier string `json:"service_tier,omitempty"` StopSequences []string `json:"stop,omitempty"` Stream bool `json:"stream,omitempty"` @@ -99,6 +104,8 @@ func (model *model) Completion(ctx context.Context, prompt string, opts ...llm.O return nil, err } + // TODO: Add a system message + // Create a message message, err := messagefactory{}.UserPrompt(prompt, opts...) if err != nil { @@ -186,6 +193,14 @@ func (c Completions) Text(index int) string { return c[index].Message.Text(0) } +// Return audio content for a specific completion +func (c Completions) Audio(index int) *llm.Attachment { + if index < 0 || index >= len(c) { + return nil + } + return c[index].Message.Audio(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 { diff --git a/pkg/openai/completion_test.go b/pkg/openai/completion_test.go index 72e1720..1fd6626 100644 --- a/pkg/openai/completion_test.go +++ b/pkg/openai/completion_test.go @@ -4,6 +4,8 @@ import ( "context" "testing" + llm "github.com/mutablelogic/go-llm" + openai "github.com/mutablelogic/go-llm/pkg/openai" assert "github.com/stretchr/testify/assert" ) @@ -20,3 +22,252 @@ func Test_completion_001(t *testing.T) { t.Log(response) } } + +func Test_completion_002(t *testing.T) { + assert := assert.New(t) + + model := client.Model(context.TODO(), "gpt-4o-mini") + if !assert.NotNil(model) { + t.FailNow() + } + + o3_model := client.Model(context.TODO(), "o3-mini") + if !assert.NotNil(o3_model) { + t.FailNow() + } + + audio_model := client.Model(context.TODO(), "gpt-4o-audio-preview") + if !assert.NotNil(audio_model) { + t.FailNow() + } + + t.Run("Store", func(t *testing.T) { + r, err := model.Completion(context.TODO(), "What is the temperature in London?", openai.WithStore(true)) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(1, r.Num()) + assert.NotEmpty(r.Text(0)) + t.Log(r) + } + }) + + t.Run("ReasoningEffort", func(t *testing.T) { + r, err := o3_model.Completion(context.TODO(), "What is the temperature in London?", openai.WithReasoningEffort("low")) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(1, r.Num()) + assert.NotEmpty(r.Text(0)) + t.Log(r) + } + }) + + t.Run("Metadata", func(t *testing.T) { + r, err := model.Completion(context.TODO(), "What is the temperature in London?", openai.WithMetadata("a", "b")) + 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) { + r, err := model.Completion(context.TODO(), "What is the temperature in London?", llm.WithFrequencyPenalty(-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("LogitBias", func(t *testing.T) { + r, err := model.Completion(context.TODO(), "What is the temperature in London?", openai.WithLogitBias(56, 22)) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(1, r.Num()) + assert.NotEmpty(r.Text(0)) + t.Log(r) + } + }) + + t.Run("LogProbs", func(t *testing.T) { + r, err := model.Completion(context.TODO(), "What is the temperature in London?", openai.WithLogProbs()) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(1, r.Num()) + assert.NotEmpty(r.Text(0)) + t.Log(r) + } + }) + + t.Run("TopLogProbs", func(t *testing.T) { + r, err := model.Completion(context.TODO(), "What is the temperature in London?", openai.WithTopLogProbs(3)) + 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) { + r, err := model.Completion(context.TODO(), "What is the temperature in London?", llm.WithMaxTokens(20)) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(1, r.Num()) + assert.NotEmpty(r.Text(0)) + t.Log(r) + } + }) + + t.Run("Completions", func(t *testing.T) { + r, err := model.Completion(context.TODO(), "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)) + assert.NotEmpty(r.Text(1)) + assert.NotEmpty(r.Text(2)) + t.Log(r) + } + }) + + t.Run("Modalties", func(t *testing.T) { + r, err := model.Completion(context.TODO(), "What is the temperature in London?", openai.WithModalities("text")) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(1, r.Num()) + assert.NotEmpty(r.Text(0)) + t.Log(r) + } + }) + + t.Run("Prediction", func(t *testing.T) { + r, err := model.Completion(context.TODO(), "Why is the sky blue", llm.WithPrediction("The sky is blue due to Rayleigh scattering")) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(1, r.Num()) + assert.NotEmpty(r.Text(0)) + t.Log(r) + } + }) + + t.Run("Audio", func(t *testing.T) { + r, err := audio_model.Completion( + context.TODO(), + "Tell me in no more than ten words why is the sky blue", + openai.WithAudio("ash", "mp3"), + ) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(1, r.Num()) + assert.NotEmpty(r.Text(0)) // Returns the audio transcript + assert.NotEmpty(r.Audio(0)) + t.Log(r) + } + }) + + t.Run("PresencePenalty", func(t *testing.T) { + r, err := model.Completion( + context.TODO(), + "Tell me in no more than ten words why is the sky blue", + llm.WithPresencePenalty(1.0), + ) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(1, r.Num()) + assert.NotEmpty(r.Text(0)) + t.Log(r) + } + }) + + t.Run("ResponseFormat", func(t *testing.T) { + r, err := model.Completion( + context.TODO(), + "Tell me in no more than ten words why is the sky blue, and response in JSON format", + llm.WithFormat("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("Seed", func(t *testing.T) { + r, err := model.Completion( + context.TODO(), + "Tell me in no more than ten words why is the sky blue", + 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("ServiceTier", func(t *testing.T) { + r, err := model.Completion( + context.TODO(), + "Tell me in no more than ten words why is the sky blue", + openai.WithServiceTier("default"), + ) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(1, r.Num()) + assert.NotEmpty(r.Text(0)) + t.Log(r) + } + }) + + t.Run("Stop", func(t *testing.T) { + r, err := model.Completion( + context.TODO(), + "Tell me in no more than ten words why is the sky blue", + llm.WithStopSequence("sky", "blue"), + ) + 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) { + r, err := model.Completion( + context.TODO(), + "Tell me in no more than ten words why is the sky blue", + llm.WithTopP(0.1), + ) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(1, r.Num()) + assert.NotEmpty(r.Text(0)) + t.Log(r) + } + }) + + t.Run("User", func(t *testing.T) { + r, err := model.Completion( + context.TODO(), + "Tell me in no more than ten words why is the sky blue", + openai.WithUser("test_user"), + ) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(1, r.Num()) + assert.NotEmpty(r.Text(0)) + t.Log(r) + } + }) + +} + +// TODO: Streaming + +// TODO: Tools diff --git a/pkg/openai/content.go b/pkg/openai/content.go index bff4ff2..746adc2 100644 --- a/pkg/openai/content.go +++ b/pkg/openai/content.go @@ -1,11 +1,14 @@ package openai +import "github.com/mutablelogic/go-llm" + /////////////////////////////////////////////////////////////////////////////// // TYPES type Content struct { - Type string `json:"type"` // text or content - Content string `json:"content,omitempty"` // text content + Type string `json:"type"` // text or content + Content string `json:"content,omitempty"` // text content + Audio llm.Attachment `json:"audio,omitempty"` // audio content } /////////////////////////////////////////////////////////////////////////////// diff --git a/pkg/openai/message.go b/pkg/openai/message.go index 2758365..fab559f 100644 --- a/pkg/openai/message.go +++ b/pkg/openai/message.go @@ -16,6 +16,7 @@ type messagefactory struct{} // Message with text or object content type Message struct { RoleContent + Media *llm.Attachment `json:"audio,omitempty"` } var _ llm.Completion = (*Message)(nil) @@ -92,13 +93,26 @@ func (message *Message) Text(index int) string { return "" } // If content is text, return it - if text, ok := message.Content.(string); ok { + if text, ok := message.Content.(string); ok && text != "" { return text } + // If content is audio, and there is a caption, return it + if audio := message.Audio(0); audio != nil && audio.Caption() != "" { + return audio.Caption() + } + // For other kinds, return empty string for the moment return "" } +// Return the audio +func (message *Message) Audio(index int) *llm.Attachment { + if index != 0 { + return nil + } + return message.Media +} + // Return the current session tool calls given the completion index. // Will return nil if no tool calls were returned. func (message *Message) ToolCalls(index int) []llm.ToolCall { diff --git a/pkg/openai/opt.go b/pkg/openai/opt.go index eddcbb2..73efa36 100644 --- a/pkg/openai/opt.go +++ b/pkg/openai/opt.go @@ -20,14 +20,6 @@ func WithDimensions(v uint64) llm.Opt { } } -// A unique identifier representing your end-user -func WithUser(v string) llm.Opt { - return func(o *llm.Opts) error { - o.Set("user", v) - return nil - } -} - // Whether or not to store the output of this chat completion request for use in // model distillation or evals products. func WithStore(v bool) llm.Opt { @@ -52,6 +44,12 @@ func WithReasoningEffort(v string) llm.Opt { // via API or the dashboard. func WithMetadata(k, v string) llm.Opt { return func(o *llm.Opts) error { + // Set store to true + if err := WithStore(true)(o); err != nil { + return err + } + + // Add metadata metadata, ok := o.Get("metadata").(map[string]string) if !ok { metadata = make(map[string]string, 16) @@ -123,7 +121,7 @@ func WithModalities(v ...string) llm.Opt { // Parameters for audio output func WithAudio(voice, format string) llm.Opt { return func(o *llm.Opts) error { - if err := WithModalities("audio")(o); err != nil { + if err := WithModalities("text", "audio")(o); err != nil { return err } if audio := NewAudio(voice, format); audio != nil { @@ -223,7 +221,7 @@ func optMaxTokens(opts *llm.Opts) uint64 { } func optNumCompletions(opts *llm.Opts) uint64 { - return opts.GetUint64("num_choices") + return opts.GetUint64("num_completions") } func optModalities(opts *llm.Opts) []string { diff --git a/pkg/session/session.go b/pkg/session/session.go index 71e856a..ad42b76 100644 --- a/pkg/session/session.go +++ b/pkg/session/session.go @@ -103,6 +103,14 @@ func (session *session) Text(index int) string { return session.seq[len(session.seq)-1].Text(index) } +// Return audio for the last completion +func (session *session) Audio(index int) *llm.Attachment { + if len(session.seq) == 0 { + return nil + } + return session.seq[len(session.seq)-1].Audio(index) +} + // Return the current session tool calls given the completion index. // Will return nil if no tool calls were returned. func (session *session) ToolCalls(index int) []llm.ToolCall { From 9b5f43fc95d0c2facb38d991c7b03603b52b3ac3 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Wed, 5 Feb 2025 12:21:14 +0100 Subject: [PATCH 05/25] Updated docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9bc9318..3c9d9a0 100644 --- a/README.md +++ b/README.md @@ -280,9 +280,9 @@ The options are as follows: | `llm.WithSafePrompt()` | No | No | Yes | No | Whether to inject a safety prompt before all conversations. | | `llm.WithNumCompletions(uint64)` | No | No | Yes | 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. | +| `llm.WithUser(string)` | No | Yes | No | Yes | A unique identifier representing your end-user | | `antropic.WithEphemeral()` | No | Yes | No | - | Attachments should be cached server-side | | `antropic.WithCitations()` | No | Yes | No | - | Attachments should be used in citations | -| `llm.WithUser(string)` | No | Yes | No | Yes | A unique identifier representing your end-user, | | `openai.WithStore(bool)` | No | No | No | Yes | Whether or not to store the output of this chat completion request | | `openai.WithDimensions(uint64)` | No | No | No | Yes | The number of dimensions the resulting output embeddings should have. Only supported in text-embedding-3 and later models | | `openai.WithReasoningEffort(string)` | No | No | No | Yes | The level of effort model should put into reasoning. | From cc3ea57a0b10e3ae2c41546652a8c5fd2bab7b0a Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Thu, 6 Feb 2025 10:11:52 +0100 Subject: [PATCH 06/25] Updated OpenAI --- attachment.go | 32 ++++- context.go | 5 +- model.go | 7 +- opt.go | 4 + pkg/openai/completion.go | 163 +++++++++++++++++++++-- pkg/openai/completion_test.go | 158 ++++++++++++++++++++++- pkg/openai/content.go | 40 +++++- pkg/openai/message.go | 38 ++++-- pkg/openai/session.go | 11 -- pkg/openai/session_test.go | 56 ++++++++ pkg/openai/testdata/LICENSE | 201 +++++++++++++++++++++++++++++ pkg/openai/testdata/guggenheim.jpg | Bin 0 -> 139053 bytes pkg/openai/tool.go | 59 +++++++++ pkg/session/session.go | 61 ++++++--- pkg/tool/toolkit.go | 8 +- 15 files changed, 775 insertions(+), 68 deletions(-) create mode 100644 pkg/openai/session_test.go create mode 100644 pkg/openai/testdata/LICENSE create mode 100644 pkg/openai/testdata/guggenheim.jpg create mode 100644 pkg/openai/tool.go diff --git a/attachment.go b/attachment.go index a43aa78..be3bd77 100644 --- a/attachment.go +++ b/attachment.go @@ -15,10 +15,10 @@ import ( type AttachmentMeta struct { Id string `json:"id,omitempty"` - Filename string `json:"filename"` - Data []byte `json:"data"` + Filename string `json:"filename,omitempty"` ExpiresAt uint64 `json:"expires_at,omitempty"` Caption string `json:"transcript,omitempty"` + Data []byte `json:"data"` } // Attachment for messages @@ -29,6 +29,11 @@ type Attachment struct { //////////////////////////////////////////////////////////////////////////////// // LIFECYCLE +// NewAttachment creates a new, empty attachment +func NewAttachment() *Attachment { + return new(Attachment) +} + // ReadAttachment returns an attachment from a reader object. // It is the responsibility of the caller to close the reader. func ReadAttachment(r io.Reader) (*Attachment, error) { @@ -96,6 +101,10 @@ func (a *Attachment) Caption() string { } func (a *Attachment) Type() string { + // If there's no data, return empty + if len(a.meta.Data) == 0 { + return "" + } // Mimetype based on content mimetype := http.DetectContentType(a.meta.Data) if mimetype == "application/octet-stream" && a.meta.Filename != "" { @@ -108,3 +117,22 @@ func (a *Attachment) Type() string { func (a *Attachment) Url() string { return "data:" + a.Type() + ";base64," + base64.StdEncoding.EncodeToString(a.meta.Data) } + +// Streaming includes the ability to append data +func (a *Attachment) Append(other *Attachment) { + if other.meta.Id != "" { + a.meta.Id = other.meta.Id + } + if other.meta.Filename != "" { + a.meta.Filename = other.meta.Filename + } + if other.meta.ExpiresAt != 0 { + a.meta.ExpiresAt = other.meta.ExpiresAt + } + if other.meta.Caption != "" { + a.meta.Caption += other.meta.Caption + } + if len(other.meta.Data) > 0 { + a.meta.Data = append(a.meta.Data, other.meta.Data...) + } +} diff --git a/context.go b/context.go index 303366d..a7e8d53 100644 --- a/context.go +++ b/context.go @@ -5,7 +5,7 @@ import "context" ////////////////////////////////////////////////////////////////// // TYPES -// Completion is the content of the last context message +// Completion is the content of the last message type Completion interface { // Return the number of completions, which is ususally 1 unless // WithNumCompletions was used @@ -29,7 +29,8 @@ type Completion interface { ToolCalls(int) []ToolCall } -// Context is fed to the agent to generate a response +// Context is a context window fed to the agent to generate a response, +// with the ability to create the next completion type Context interface { Completion diff --git a/model.go b/model.go index 453ec82..456ecf2 100644 --- a/model.go +++ b/model.go @@ -15,13 +15,12 @@ type Model interface { // setting session options Context(...Opt) Context - // Convenience method to create a session context object - // with a user prompt - //UserPrompt(string, ...Opt) Context - // Create a completion from a text prompt Completion(context.Context, string, ...Opt) (Completion, error) + // Create a completion from a chat session + Chat(context.Context, []Completion, ...Opt) (Completion, error) + // Embedding vector generation Embedding(context.Context, string, ...Opt) ([]float64, error) } diff --git a/opt.go b/opt.go index f526146..eb1a356 100644 --- a/opt.go +++ b/opt.go @@ -197,6 +197,10 @@ func WithToolKit(toolkit ToolKit) Opt { func WithStream(fn func(Completion)) Opt { return func(o *Opts) error { o.callback = fn + + // We include usage metrics in the streaming response for openai + o.Set("stream_options_include_usage", true) + return nil } } diff --git a/pkg/openai/completion.go b/pkg/openai/completion.go index e48271e..fe68e1b 100644 --- a/pkg/openai/completion.go +++ b/pkg/openai/completion.go @@ -3,6 +3,8 @@ package openai import ( "context" "encoding/json" + "fmt" + "strings" // Packages client "github.com/mutablelogic/go-client" @@ -21,7 +23,7 @@ type Response struct { SystemFingerprint string `json:"system_fingerprint"` ServiceTier string `json:"service_tier"` Completions `json:"choices"` - Metrics `json:"usage,omitempty"` + *Metrics `json:"usage,omitempty"` } // Completion choices @@ -64,6 +66,22 @@ func (r Response) String() string { return string(data) } +func (c Completion) String() string { + data, err := json.MarshalIndent(c, "", " ") + if err != nil { + return err.Error() + } + return string(data) +} + +func (m Metrics) String() string { + data, err := json.MarshalIndent(m, "", " ") + if err != nil { + return err.Error() + } + return string(data) +} + /////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS @@ -97,21 +115,30 @@ type reqCompletion struct { Messages []llm.Completion `json:"messages"` } +// Send a completion request with a single prompt, and return the next completion func (model *model) Completion(ctx context.Context, prompt string, opts ...llm.Opt) (llm.Completion, error) { - // Apply options - opt, err := llm.ApplyOpts(opts...) + message, err := messagefactory{}.UserPrompt(prompt, opts...) if err != nil { return nil, err } + return model.Chat(ctx, []llm.Completion{message}, opts...) +} - // TODO: Add a system message - - // Create a message - message, err := messagefactory{}.UserPrompt(prompt, opts...) +// Send a completion request with multiple completions, and return the next completion +func (model *model) Chat(ctx context.Context, completions []llm.Completion, opts ...llm.Opt) (llm.Completion, error) { + // Apply options + opt, err := llm.ApplyOpts(opts...) if err != nil { return nil, err } + // Create the completions including the system prompt + messages := make([]llm.Completion, 0, len(completions)+1) + if system := opt.SystemPrompt(); system != "" { + messages = append(messages, messagefactory{}.SystemPrompt(system)) + } + messages = append(messages, completions...) + // Request req, err := client.NewJSONRequest(reqCompletion{ Model: model.Name(), @@ -140,17 +167,31 @@ func (model *model) Completion(ctx context.Context, prompt string, opts ...llm.O ToolChoice: optToolChoice(opt), ParallelToolCalls: optParallelToolCalls(opt), User: optUser(opt), - Messages: []llm.Completion{message}, + Messages: messages, }) if err != nil { return nil, err } + // Response options var response Response reqopts := []client.RequestOpt{ client.OptPath("chat", "completions"), } + // Streaming + 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 := model.DoWithContext(ctx, req, &response, reqopts...); err != nil { return nil, err @@ -160,6 +201,112 @@ func (model *model) Completion(ctx context.Context, prompt string, opts ...llm.O return &response, nil } +/////////////////////////////////////////////////////////////////////////////// +// PRIVATE METHODS - STREAMING + +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.Type != "" { + response.Type = delta.Type + } + if delta.Created != 0 { + response.Created = delta.Created + } + if delta.Model != "" { + response.Model = delta.Model + } + if delta.SystemFingerprint != "" { + response.SystemFingerprint = delta.SystemFingerprint + } + if delta.ServiceTier != "" { + response.ServiceTier = delta.ServiceTier + } + + // Append the delta to the response + for _, completion := range delta.Completions { + if err := appendCompletion(response, &completion); err != nil { + return err + } + } + + // Apend the metrics to the response + if delta.Metrics != nil { + response.Metrics = delta.Metrics + } + return nil +} + +func appendCompletion(response *Response, c *Completion) error { + fmt.Println(c) + // Append a new completion + for { + if c.Index < uint64(len(response.Completions)) { + break + } + response.Completions = append(response.Completions, Completion{ + Index: c.Index, + Message: &Message{ + RoleContent: RoleContent{ + Role: c.Delta.Role(), + Content: "", + }, + }, + }) + } + + // Add the reason + if c.Reason != "" { + response.Completions[c.Index].Reason = c.Reason + } + + // Get the completion + message := response.Completions[c.Index].Message + if message == nil { + return llm.ErrBadParameter + } + + // Add the role + if role := c.Delta.Role(); role != "" { + message.RoleContent.Role = role + } + + // We only allow deltas which are strings at the moment + if c.Delta.Content != nil { + if str, ok := c.Delta.Content.(string); ok { + if text, ok := message.Content.(string); ok { + message.Content = text + str + } else { + message.Content = str + } + } else { + return llm.ErrNotImplemented.Withf("appendCompletion not implemented: %T", c.Delta.Content) + } + } + + // Append audio data + if c.Delta.Media != nil { + if message.Media == nil { + message.Media = llm.NewAttachment() + } + message.Media.Append(c.Delta.Media) + } + + // Return success + return nil +} + /////////////////////////////////////////////////////////////////////////////// // COMPLETIONS diff --git a/pkg/openai/completion_test.go b/pkg/openai/completion_test.go index 1fd6626..c4bbaa8 100644 --- a/pkg/openai/completion_test.go +++ b/pkg/openai/completion_test.go @@ -2,10 +2,13 @@ package openai_test import ( "context" + "fmt" + "os" "testing" llm "github.com/mutablelogic/go-llm" openai "github.com/mutablelogic/go-llm/pkg/openai" + "github.com/mutablelogic/go-llm/pkg/tool" assert "github.com/stretchr/testify/assert" ) @@ -26,6 +29,7 @@ func Test_completion_001(t *testing.T) { func Test_completion_002(t *testing.T) { assert := assert.New(t) + // Test options model := client.Model(context.TODO(), "gpt-4o-mini") if !assert.NotNil(model) { t.FailNow() @@ -256,7 +260,7 @@ func Test_completion_002(t *testing.T) { r, err := model.Completion( context.TODO(), "Tell me in no more than ten words why is the sky blue", - openai.WithUser("test_user"), + llm.WithUser("test_user"), ) if assert.NoError(err) { assert.Equal("assistant", r.Role()) @@ -268,6 +272,154 @@ func Test_completion_002(t *testing.T) { } -// TODO: Streaming +func Test_completion_003(t *testing.T) { + assert := assert.New(t) + + model := client.Model(context.TODO(), "gpt-4o-mini") + if !assert.NotNil(model) { + t.FailNow() + } + + audio_model := client.Model(context.TODO(), "gpt-4o-audio-preview") + if !assert.NotNil(audio_model) { + t.FailNow() + } + + // Test streaming + t.Run("Streaming", func(t *testing.T) { + r, err := model.Completion( + context.TODO(), + "Tell me in no more than ten words why is the sky blue", + llm.WithStream(func(message llm.Completion) { + // TODO + }), + ) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(1, r.Num()) + assert.NotEmpty(r.Text(0)) + } + }) + + t.Run("StreamingCompletions", func(t *testing.T) { + r, err := model.Completion( + context.TODO(), + "Tell me in no more than ten words why is the sky blue", + llm.WithNumCompletions(2), + llm.WithStream(func(message llm.Completion) { + // TODO + }), + ) + 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.Run("StreamingUsage", func(t *testing.T) { + r, err := model.Completion( + context.TODO(), + "Tell me in no more than ten words why is the sky blue", + openai.WithStreamOptions(func(message llm.Completion) { + // TODO + }, true), + ) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(1, r.Num()) + assert.NotEmpty(r.Text(0)) + } + }) + + t.Run("StreamingAudio", func(t *testing.T) { + r, err := audio_model.Completion( + context.TODO(), + "Tell me in exactly three words why is the sky blue", + openai.WithStreamOptions(func(message llm.Completion) { + // TODO + }, true), + openai.WithAudio("ash", "pcm16"), + ) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(1, r.Num()) + assert.NotEmpty(r.Text(0)) + assert.NotEmpty(r.Audio(0)) + } + }) + +} + +func Test_completion_004(t *testing.T) { + assert := assert.New(t) + + model := client.Model(context.TODO(), "gpt-4o-mini") + if !assert.NotNil(model) { + t.FailNow() + } + toolkit := tool.NewToolKit() + toolkit.Register(weather{}) + + // Test tool support + t.Run("Toolkit", func(t *testing.T) { + r, err := model.Completion( + context.TODO(), + "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()) + assert.NotEmpty(r.ToolCalls(0)) + + toolcalls := r.ToolCalls(0) + assert.Len(toolcalls, 1) + assert.Equal("weather_in_city", toolcalls[0].Name()) + } + }) +} -// TODO: Tools +type weather struct { + City string `json:"city" help:"The city to get the weather for" required:"true"` +} + +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) { + return fmt.Sprintf("The weather in %q is sunny and warm", w.City), nil +} + +func Test_completion_005(t *testing.T) { + assert := assert.New(t) + model := client.Model(context.TODO(), "gpt-4o-mini") + if !assert.NotNil(model) { + t.FailNow() + } + + // Test image captioning + t.Run("ImageCaption", func(t *testing.T) { + f, err := os.Open("testdata/guggenheim.jpg") + if !assert.NoError(err) { + t.FailNow() + } + defer f.Close() + + r, err := model.Completion( + context.TODO(), + "Describe this picture", + llm.WithAttachment(f), + ) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(1, r.Num()) + } + }) +} diff --git a/pkg/openai/content.go b/pkg/openai/content.go index 746adc2..2b71a6d 100644 --- a/pkg/openai/content.go +++ b/pkg/openai/content.go @@ -1,14 +1,34 @@ package openai -import "github.com/mutablelogic/go-llm" +import ( + "net/url" + + "github.com/mutablelogic/go-llm" +) /////////////////////////////////////////////////////////////////////////////// // TYPES type Content struct { - Type string `json:"type"` // text or content - Content string `json:"content,omitempty"` // text content - Audio llm.Attachment `json:"audio,omitempty"` // audio content + Type string `json:"type"` // text or content + Content string `json:"content,omitempty"` // content content ;-) + Text string `json:"text,omitempty"` // text content + Audio *llm.Attachment `json:"audio,omitempty"` // audio content + Image *Image `json:"image_url,omitempty"` // image content +} + +// A set of tool calls +type ToolCallArray []ToolCall + +// 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 struct { + Url string `json:"url,omitempty"` } /////////////////////////////////////////////////////////////////////////////// @@ -17,3 +37,15 @@ type Content struct { func NewContentString(typ, content string) *Content { return &Content{Type: typ, Content: content} } + +func NewTextContext(content string) *Content { + return &Content{Type: "text", Text: content} +} + +func NewImageData(image *llm.Attachment) *Content { + return &Content{Type: "image_url", Image: &Image{Url: image.Url()}} +} + +func NewImageUrl(url *url.URL) *Content { + return &Content{Type: "image_url", Image: &Image{Url: url.String()}} +} diff --git a/pkg/openai/message.go b/pkg/openai/message.go index fab559f..939f8a6 100644 --- a/pkg/openai/message.go +++ b/pkg/openai/message.go @@ -5,7 +5,6 @@ import ( // Packages llm "github.com/mutablelogic/go-llm" - session "github.com/mutablelogic/go-llm/pkg/session" ) /////////////////////////////////////////////////////////////////////////////// @@ -17,6 +16,7 @@ type messagefactory struct{} type Message struct { RoleContent Media *llm.Attachment `json:"audio,omitempty"` + Calls ToolCalls `json:"tool_calls,omitempty"` } var _ llm.Completion = (*Message)(nil) @@ -26,10 +26,12 @@ type RoleContent struct { Content any `json:"content,omitempty"` // string or array of text, reference, image_url } +type ToolCalls []toolcall + /////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS - MESSAGE FACTORY -func (messagefactory) SystemPrompt(prompt string) session.Message { +func (messagefactory) SystemPrompt(prompt string) llm.Completion { return &Message{ RoleContent: RoleContent{ Role: "system", @@ -38,25 +40,40 @@ func (messagefactory) SystemPrompt(prompt string) session.Message { } } -func (messagefactory) UserPrompt(prompt string, opts ...llm.Opt) (session.Message, error) { - // TODO: Attachments +func (messagefactory) UserPrompt(prompt string, opts ...llm.Opt) (llm.Completion, error) { + // Get attachments + opt, err := llm.ApplyPromptOpts(opts...) + if err != nil { + return nil, err + } + + // Get attachments, allocate content + attachments := opt.Attachments() + content := make([]*Content, 1, len(attachments)+1) + + // Append the text and the attachments + content[0] = NewTextContext(prompt) + for _, attachment := range attachments { + content = append(content, NewImageData(attachment)) + } + // Return success return &Message{ RoleContent: RoleContent{ Role: "user", - Content: prompt, + Content: content, }, }, nil } -func (messagefactory) ToolResults(results ...llm.ToolResult) ([]session.Message, error) { +func (messagefactory) ToolResults(results ...llm.ToolResult) ([]llm.Completion, error) { // Check for no results if len(results) == 0 { return nil, llm.ErrBadParameter.Withf("No tool results") } // Create results - messages := make([]session.Message, 0, len(results)) + messages := make([]llm.Completion, 0, len(results)) for _, result := range results { value, err := json.Marshal(result.Value()) if err != nil { @@ -119,6 +136,9 @@ func (message *Message) ToolCalls(index int) []llm.ToolCall { if index != 0 { return nil } - // TODO - return nil + calls := make([]llm.ToolCall, 0, len(message.Calls)) + for _, call := range message.Calls { + calls = append(calls, call) + } + return calls } diff --git a/pkg/openai/session.go b/pkg/openai/session.go index 2bb8b09..d81515b 100644 --- a/pkg/openai/session.go +++ b/pkg/openai/session.go @@ -12,14 +12,3 @@ import ( func (model *model) Context(opts ...llm.Opt) llm.Context { return session.NewSession(model, &messagefactory{}, opts...) } - -// Convenience method to create a session context object with a user prompt -func (model *model) UserPrompt(prompt string, opts ...llm.Opt) llm.Context { - session := session.NewSession(model, &messagefactory{}, opts...) - message, err := messagefactory{}.UserPrompt(prompt, opts...) - if err != nil { - panic(err) - } - session.Append(message) - return session -} diff --git a/pkg/openai/session_test.go b/pkg/openai/session_test.go new file mode 100644 index 0000000..0f1004c --- /dev/null +++ b/pkg/openai/session_test.go @@ -0,0 +1,56 @@ +package openai_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 := client.Model(context.TODO(), "gpt-4o-mini") + 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) + model := client.Model(context.TODO(), "gpt-4o-mini") + 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) + 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/openai/testdata/LICENSE b/pkg/openai/testdata/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/pkg/openai/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/openai/testdata/guggenheim.jpg b/pkg/openai/testdata/guggenheim.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7e1651729c5008c35446d52049de346f86962ef3 GIT binary patch literal 139053 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 Date: Thu, 6 Feb 2025 11:15:06 +0100 Subject: [PATCH 07/25] Updated OpenAI --- context.go | 3 +++ pkg/openai/completion.go | 2 +- pkg/openai/message.go | 16 ++++++++++++ pkg/openai/model.go | 6 +++++ pkg/openai/session.go | 14 ---------- pkg/session/session.go | 55 +++++++++++++++++++++++++++++++++------- 6 files changed, 72 insertions(+), 24 deletions(-) delete mode 100644 pkg/openai/session.go diff --git a/context.go b/context.go index a7e8d53..db5dd12 100644 --- a/context.go +++ b/context.go @@ -11,6 +11,9 @@ type Completion interface { // WithNumCompletions was used Num() int + // Return a specific completion + Choice(int) Completion + // Return the current session role, which can be system, assistant, // user, tool, tool_result, ... // If this is a completion, the role is usually 'assistant' diff --git a/pkg/openai/completion.go b/pkg/openai/completion.go index fe68e1b..3c2ab3e 100644 --- a/pkg/openai/completion.go +++ b/pkg/openai/completion.go @@ -316,7 +316,7 @@ func (c Completions) Num() int { } // Return message for a specific completion -func (c Completions) Message(index int) *Message { +func (c Completions) Choice(index int) llm.Completion { if index < 0 || index >= len(c) { return nil } diff --git a/pkg/openai/message.go b/pkg/openai/message.go index 939f8a6..e0c4cee 100644 --- a/pkg/openai/message.go +++ b/pkg/openai/message.go @@ -17,6 +17,7 @@ type Message struct { RoleContent Media *llm.Attachment `json:"audio,omitempty"` Calls ToolCalls `json:"tool_calls,omitempty"` + *ToolResults } var _ llm.Completion = (*Message)(nil) @@ -28,6 +29,10 @@ type RoleContent struct { type ToolCalls []toolcall +type ToolResults struct { + Id string `json:"tool_call_id,omitempty"` +} + /////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS - MESSAGE FACTORY @@ -84,6 +89,9 @@ func (messagefactory) ToolResults(results ...llm.ToolResult) ([]llm.Completion, Role: "tool", Content: string(value), }, + ToolResults: &ToolResults{ + Id: result.Call().Id(), + }, }) } @@ -104,6 +112,14 @@ func (message *Message) Role() string { return message.RoleContent.Role } +// Return the completion +func (message *Message) Choice(index int) llm.Completion { + if index != 0 { + return nil + } + return message +} + // Return the text for the last completion func (message *Message) Text(index int) string { if index != 0 { diff --git a/pkg/openai/model.go b/pkg/openai/model.go index bf548d2..df75b5f 100644 --- a/pkg/openai/model.go +++ b/pkg/openai/model.go @@ -7,6 +7,7 @@ import ( // Packages client "github.com/mutablelogic/go-client" llm "github.com/mutablelogic/go-llm" + "github.com/mutablelogic/go-llm/pkg/session" ) /////////////////////////////////////////////////////////////////////////////// @@ -82,6 +83,11 @@ func (openai *Client) Model(ctx context.Context, name string) llm.Model { return openai.cache[name] } +// Return a new empty session +func (model *model) Context(opts ...llm.Opt) llm.Context { + return session.NewSession(model, &messagefactory{}, opts...) +} + /////////////////////////////////////////////////////////////////////////////// // API CALLS diff --git a/pkg/openai/session.go b/pkg/openai/session.go deleted file mode 100644 index d81515b..0000000 --- a/pkg/openai/session.go +++ /dev/null @@ -1,14 +0,0 @@ -package openai - -import ( - // Packages - llm "github.com/mutablelogic/go-llm" - session "github.com/mutablelogic/go-llm/pkg/session" -) - -/////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - -func (model *model) Context(opts ...llm.Opt) llm.Context { - return session.NewSession(model, &messagefactory{}, opts...) -} diff --git a/pkg/session/session.go b/pkg/session/session.go index 25dd77a..f557190 100644 --- a/pkg/session/session.go +++ b/pkg/session/session.go @@ -2,6 +2,7 @@ package session import ( "context" + "encoding/json" // Packages "github.com/mutablelogic/go-llm" @@ -48,6 +49,21 @@ func NewSession(model llm.Model, factory MessageFactory, opts ...llm.Opt) *sessi } } +/////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +func (session session) MarshalJSON() ([]byte, error) { + return json.Marshal(session.seq) +} + +func (session session) String() string { + data, err := json.MarshalIndent(session, "", " ") + if err != nil { + return err.Error() + } + return string(data) +} + /////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS @@ -74,27 +90,40 @@ func (session *session) FromUser(ctx context.Context, prompt string, opts ...llm if err != nil { return err } else { - session.Append(message) + return session.chat(ctx, message) + } +} + +// Generate a response from a tool, passing the results from the tool call +func (session *session) FromTool(ctx context.Context, results ...llm.ToolResult) error { + // Append the tool results + if results, err := session.factory.ToolResults(results...); err != nil { + return err + } else { + return session.chat(ctx, results...) } +} - // Generate the completion +func (session *session) chat(ctx context.Context, messages ...llm.Completion) error { + // Append the messages to the chat + session.Append(messages...) + + // Generate the completion, and append the first choice of the completion + // TODO: Use Opts to select which completion choice to use completion, err := session.model.Chat(ctx, session.seq, session.opts...) if err != nil { return err + } else if completion.Num() == 0 { + return llm.ErrBadParameter.With("No completion choices returned") } - // Append the completion - session.Append(completion) + // Append the first choice + session.Append(completion.Choice(0)) // 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 -} - /////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS - COMPLETION @@ -114,6 +143,14 @@ func (session *session) Role() string { return session.seq[len(session.seq)-1].Role() } +// Return the current session choice +func (session *session) Choice(index int) llm.Completion { + if len(session.seq) == 0 { + return nil + } + return session.seq[len(session.seq)-1].Choice(index) +} + // Return the text for the last completion func (session *session) Text(index int) string { if len(session.seq) == 0 { From 4064255cd394a7ccaf45e99ba5505cfbe26a2e81 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Thu, 6 Feb 2025 11:25:46 +0100 Subject: [PATCH 08/25] Updated attachment detection --- attachment.go | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/attachment.go b/attachment.go index be3bd77..ab35210 100644 --- a/attachment.go +++ b/attachment.go @@ -26,6 +26,10 @@ type Attachment struct { meta AttachmentMeta } +const ( + defaultMimetype = "application/octet-stream" +) + //////////////////////////////////////////////////////////////////////////////// // LIFECYCLE @@ -56,10 +60,12 @@ func ReadAttachment(r io.Reader) (*Attachment, error) { //////////////////////////////////////////////////////////////////////////////// // STRINGIFY +// Convert JSON into an attachment func (a *Attachment) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, &a.meta) } +// Convert an attachment into JSON func (a *Attachment) MarshalJSON() ([]byte, error) { // Create a JSON representation var j struct { @@ -77,6 +83,7 @@ func (a *Attachment) MarshalJSON() ([]byte, error) { return json.Marshal(j) } +// Stringify an attachment func (a *Attachment) String() string { data, err := json.MarshalIndent(a.meta, "", " ") if err != nil { @@ -88,29 +95,46 @@ func (a *Attachment) String() string { //////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS +// Return the filename of an attachment func (a *Attachment) Filename() string { return a.meta.Filename } +// Return the raw attachment data func (a *Attachment) Data() []byte { return a.meta.Data } +// Return the caption for the attachment func (a *Attachment) Caption() string { return a.meta.Caption } +// Return the mime media type for the attachment, based +// on the data and/or filename extension. Returns an empty string if +// there is no data or filename func (a *Attachment) Type() string { - // If there's no data, return empty - if len(a.meta.Data) == 0 { + // If there's no data or filename, return empty + if len(a.meta.Data) == 0 && a.meta.Filename == "" { return "" } + // Mimetype based on content - mimetype := http.DetectContentType(a.meta.Data) - if mimetype == "application/octet-stream" && a.meta.Filename != "" { + mimetype := defaultMimetype + if len(a.meta.Data) > 0 { + mimetype = http.DetectContentType(a.meta.Data) + if mimetype != defaultMimetype { + return mimetype + } + } + + // Mimetype based on filename + if a.meta.Filename != "" { // Detect mimetype from extension mimetype = mime.TypeByExtension(filepath.Ext(a.meta.Filename)) } + + // Return the default mimetype return mimetype } From 86c6ff6770911e62be8daf4410518318a2519490 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Thu, 6 Feb 2025 12:04:36 +0100 Subject: [PATCH 09/25] Updated docs --- README.md | 212 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 171 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 3c9d9a0..cec04a5 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,16 @@ # go-llm -Large Language Model API interface. This is a simple API interface for large language models +The module implements 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/) -(OpenAI might be added later). +[Anthopic](https://docs.anthropic.com/en/api/getting-started), [Mistral](https://docs.mistral.ai/) +and [OpenAI](https://platform.openai.com/docs/api-reference). The module implements the ability to: -The module includes the ability to utilize: - -* Maintaining a session of messages -* 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) +* Maintain a session of messages; +* Tool calling support, including using your own tools (aka Tool plugins); +* Create embedding vectors from text; +* Stream responses; +* Multi-modal support (aka, Images, Audio and Attachments); +* Text-to-speech (OpenAI only) for completions There is a command-line tool included in the module which can be used to interact with the API. If you have docker installed, you can use the following command to run the tool, without @@ -24,7 +23,8 @@ 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 \ - -e OLLAMA_URL -e MISTRAL_API_KEY -e NEWSAPI_KEY \ + -e OLLAMA_URL -e MISTRAL_API_KEY -e ANTHROPIC_API_KEY -e OPENAI_API_KEY \ + -e NEWSAPI_KEY \ ghcr.io/mutablelogic/go-llm:latest \ chat mistral-small-latest --prompt "What is the latest news?" --no-stream ``` @@ -35,7 +35,7 @@ install it if you have a `go` compiler). ## Programmatic Usage See the documentation [here](https://pkg.go.dev/github.com/mutablelogic/go-llm) -for integration into your own Go programs. +for integration into your own code. ### Agent Instantiation @@ -95,6 +95,24 @@ func main() { } ``` +Similarly for [OpenAI](https://pkg.go.dev/github.com/mutablelogic/go-llm/pkg/openai) +models, you can use: + +```go +import ( + "github.com/mutablelogic/go-llm/pkg/openai" +) + +func main() { + // Create a new agent + agent, err := openai.New(os.Getenv("OPENAI_API_KEY")) + if err != nil { + panic(err) + } + // ... +} +``` + 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. @@ -111,6 +129,7 @@ func main() { agent, err := agent.New( agent.WithAnthropic(os.Getenv("ANTHROPIC_API_KEY")), agent.WithMistral(os.Getenv("MISTRAL_API_KEY")), + agent.WithOpenAI(os.Getenv("OPENAI_API_KEY")), agent.WithOllama(os.Getenv("OLLAMA_URL")), ) if err != nil { @@ -120,6 +139,30 @@ func main() { } ``` +### Completion + +You can generate a **completion** as follows, + +```go +import ( + "github.com/mutablelogic/go-llm" +) + +func completion(ctx context.Context, agent llm.Agent) (string, error) { + completion, err := agent. + Model(ctx, "claude-3-5-haiku-20241022"). + Completion((ctx, "Why is the sky blue?") + if err != nil { + return "", err + } else { + return completion.Text(0), nil + } +} +``` + +The zero index argument on `completion.Text(int)` indicates you want the text from the zero'th completion +choice, for providers who can generate serveral different choices simultaneously. + ### Chat Sessions You create a **chat session** with a model as follows, @@ -131,7 +174,9 @@ import ( func session(ctx context.Context, agent llm.Agent) error { // Create a new chat session - session := agent.Model(context.TODO(), "claude-3-5-haiku-20241022").Context() + session := agent. + Model(ctx, "claude-3-5-haiku-20241022"). + Context() // Repeat forever for { @@ -147,11 +192,11 @@ 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. +ensure the session is maintained across multiple completion calls. ### Embedding Generation -You can generate embedding vectors using an appropriate model with Ollama or Mistral models: +You can generate embedding vectors using an appropriate model with Ollama, OpenAI and Mistral models: ```go import ( @@ -159,8 +204,9 @@ import ( ) func embedding(ctx context.Context, agent llm.Agent) error { - // Create a new chat session - vector, err := agent.Model(ctx, "mistral-embed").Embedding(ctx, "hello") + vector, err := agent. + Model(ctx, "mistral-embed"). + Embedding(ctx, "hello") // ... } ``` @@ -182,21 +228,19 @@ func generate_image_caption(ctx context.Context, agent llm.Agent, path string) ( } defer f.Close() - // Describe an image - r, err := agent.Model("claude-3-5-sonnet-20241022").UserPrompt( - ctx, model.UserPrompt("Provide a short caption for this image", llm.WithAttachment(f)) - ) + completion, err := agent. + Model(ctx, "claude-3-5-sonnet-20241022"). + Completion((ctx, "Provide a short caption for this image", llm.WithAttachment(f)) if err != nil { - return "", err - } + return "", err + } - // Return success - return r.Text(0), err + return completion.Text(0), nil } ``` -To summarize a text or PDF docment is exactly the same using an Anthropic model, but maybe with a -different prompt. +To summarize a text or PDF document is exactly the same using an Anthropic model, but maybe +with a different prompt. ### Streaming @@ -210,16 +254,14 @@ import ( ) func generate_completion(ctx context.Context, agent llm.Agent, prompt string) (string, error) { - r, err := agent.Model("claude-3-5-sonnet-20241022").UserPrompt( - ctx, model.UserPrompt("What is the weather in London?"), - llm.WithStream(stream_callback), - ) + completion, err := agent. + Model(ctx, "claude-3-5-haiku-20241022"). + Completion((ctx, "Why is the sky blue?", llm.WithStream(stream_callback)) if err != nil { return "", err + } else { + return completion.Text(0), nil } - - // Return success - return r.Text(0), err } func stream_callback(completion llm.Completion) { @@ -231,9 +273,92 @@ func stream_callback(completion llm.Completion) { ### Tool Support -All providers support tools, but not all models. +All providers support tools, but not all models. Your own tools should implement the +following interface: + +```go +package llm + +// Definition of a tool +type Tool interface { + Name() string // The name of the tool + Description() string // The description of the tool + Run(context.Context) (any, error) // Run the tool with a deadline and + // return the result +} +``` + +For example, if you want to implement a tool which adds to numbers, + +```go +package addition + +type Adder struct { + A float64 `name:"a" help:"The first number" required:"true"` + B float64 `name:"b" help:"The second number" required:"true"` +} + +func (Adder) Name() string { + return "add_two_numbers" +} + +func (Adder) Description() string { + return "Add two numbers together and return the result" +} + +func (a Adder) Run(context.Context) (any, error) { + return a.A + a.B, nil +} +``` + +Then you can include your tool as part of the completion. It's possible that a +completion will continue to call additional tools, in which case you should +actually loop through completions until no tool calls are made. + +```go +import ( + "github.com/mutablelogic/go-llm" + "github.com/mutablelogic/go-llm/pkg/tool" +) + +func add_two_numbers(ctx context.Context, agent llm.Agent) (string, error) { + context := agent.Model(ctx, "claude-3-5-haiku-20241022").Context() + toolkit := tool.NewToolKit() + toolkit.Register(Adder{}) + + // Get the tool call + if err := context.FromUser(ctx, "What is five plus seven?", llm.WithToolKit(toolkit)); err != nil { + return "", err + } + + // Call tools + for { + calls := context.ToolCalls(0) + if len(calls) == 0 { + break + } -TODO + // Print out any intermediate messages + if context.Text(0) != "" { + fmt.Println(context.Text(0)) + } + + // Get the results from the toolkit + results, err := toolkit.Run(ctx, calls...) + if err != nil { + return "", err + } + + // Get another tool call or a user response + if err := context.FromTool(ctx, results...); err != nil { + return "", err + } + } + + // Return the result + return context.Text(0) +} +``` ## Options @@ -241,20 +366,25 @@ You can add options to sessions, or to prompts. Different providers and models s different options. ```go +package llm + type Model interface { // Set session-wide options Context(...Opt) Context - // Add attachments (images, PDF's) to a user prompt for completion - UserPrompt(string, ...Opt) Context + // Create a completion from a text prompt + Completion(context.Context, string, ...Opt) (Completion, error) + + // Create a completion from a chat session + Chat(context.Context, []Completion, ...Opt) (Completion, error) - // Create an embedding vector with embedding options + // Embedding vector generation Embedding(context.Context, string, ...Opt) ([]float64, error) } type Context interface { - // Add single-use options when calling the model, which override - // session options. You can attach files to a user prompt. + // Generate a response from a user prompt (with attachments and + // other options) FromUser(context.Context, string, ...Opt) error } ``` From 5844acfd16c551b02d8d7315809150c52d2b83e8 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Thu, 6 Feb 2025 12:12:28 +0100 Subject: [PATCH 10/25] Updated docs --- README.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cec04a5..1f3e7fc 100644 --- a/README.md +++ b/README.md @@ -288,7 +288,7 @@ type Tool interface { } ``` -For example, if you want to implement a tool which adds to numbers, +For example, if you want to implement a tool which adds two numbers, ```go package addition @@ -360,6 +360,20 @@ func add_two_numbers(ctx context.Context, agent llm.Agent) (string, error) { } ``` +Parameters are implemented as struct fields, with tags. The tags you can include are: + +* `name:""` - Set the name for the parameter +* `json:""` - If `name` is not used, then the name is set from the `json` tag +* `help:":` - Set the description for the parameter +* `required:""` - The parameter is required as part of the tool call +* `enum:"a,b,c"` - The parameter value should be one of these comma-separated options + +The transation of field types is as follows: + +* `string` - Translates as JSON `string` +* `uint`, `int` - Translates to JSON `integer` +* `float32`, `float64` - Translates to JSON `number` + ## Options You can add options to sessions, or to prompts. Different providers and models support From 17f86038e7478b90dce575a5911d4e0ca0f926db Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Thu, 6 Feb 2025 13:15:26 +0100 Subject: [PATCH 11/25] Fixed streaming responses --- cmd/llm/chat.go | 21 ++++++++++++++------- cmd/llm/main.go | 20 +++++++++++--------- cmd/llm/models.go | 15 ++++++++------- pkg/agent/opt.go | 6 ++---- pkg/openai/completion.go | 27 +++++++++++++++++++++++++-- 5 files changed, 60 insertions(+), 29 deletions(-) diff --git a/cmd/llm/chat.go b/cmd/llm/chat.go index bd14f6e..4368318 100644 --- a/cmd/llm/chat.go +++ b/cmd/llm/chat.go @@ -38,15 +38,16 @@ func (cmd *ChatCmd) Run(globals *Globals) error { return err } + // Current buffer + var buf string + // Set the options opts := []llm.Opt{} if !cmd.NoStream { opts = append(opts, llm.WithStream(func(cc llm.Completion) { - if text := cc.Text(0); text != "" { - count := strings.Count(text, "\n") - fmt.Print(strings.Repeat("\033[F", count) + strings.Repeat(" ", count) + "\r") - fmt.Print(text) - } + text := cc.Text(0) + fmt.Print(strings.TrimPrefix(text, buf)) + buf = text })) } if cmd.System != "" { @@ -91,6 +92,7 @@ func (cmd *ChatCmd) Run(globals *Globals) error { if len(calls) == 0 { break } + if session.Text(0) != "" { globals.term.Println(session.Text(0)) } else { @@ -100,6 +102,7 @@ func (cmd *ChatCmd) Run(globals *Globals) error { } globals.term.Println("Calling ", strings.Join(names, ", ")) } + if results, err := globals.toolkit.Run(ctx, calls...); err != nil { return err } else if err := session.FromTool(ctx, results...); err != nil { @@ -107,8 +110,12 @@ func (cmd *ChatCmd) Run(globals *Globals) error { } } - // Print the response - globals.term.Println("\n" + session.Text(0) + "\n") + // Print the response, if not streaming + if cmd.NoStream { + globals.term.Println("\n" + session.Text(0) + "\n") + } else { + globals.term.Println() + } } }) } diff --git a/cmd/llm/main.go b/cmd/llm/main.go index cca49f5..ab7aef6 100644 --- a/cmd/llm/main.go +++ b/cmd/llm/main.go @@ -109,15 +109,17 @@ func main() { // Create an agent opts := []llm.Opt{} - if cli.OllamaEndpoint != "" { - opts = append(opts, agent.WithOllama(cli.OllamaEndpoint, clientopts...)) - } - if cli.AnthropicKey != "" { - opts = append(opts, agent.WithAnthropic(cli.AnthropicKey, clientopts...)) - } - if cli.MistralKey != "" { - opts = append(opts, agent.WithMistral(cli.MistralKey, clientopts...)) - } + /* + if cli.OllamaEndpoint != "" { + opts = append(opts, agent.WithOllama(cli.OllamaEndpoint, clientopts...)) + } + if cli.AnthropicKey != "" { + opts = append(opts, agent.WithAnthropic(cli.AnthropicKey, clientopts...)) + } + if cli.MistralKey != "" { + opts = append(opts, agent.WithMistral(cli.MistralKey, clientopts...)) + } + */ if cli.OpenAIKey != "" { opts = append(opts, agent.WithOpenAI(cli.OpenAIKey, clientopts...)) } diff --git a/cmd/llm/models.go b/cmd/llm/models.go index e304507..2bf0072 100644 --- a/cmd/llm/models.go +++ b/cmd/llm/models.go @@ -8,7 +8,6 @@ import ( // Packages llm "github.com/mutablelogic/go-llm" agent "github.com/mutablelogic/go-llm/pkg/agent" - ollama "github.com/mutablelogic/go-llm/pkg/ollama" ) //////////////////////////////////////////////////////////////////////////////// @@ -83,12 +82,14 @@ func (cmd *DownloadModelCmd) Run(globals *Globals) error { } // Download the model switch agent.Name() { - case "ollama": - model, err := agent.(*ollama.Client).PullModel(ctx, cmd.Model) - if err != nil { - return err - } - fmt.Println(model) + /* + case "ollama": + model, err := agent.(*ollama.Client).PullModel(ctx, cmd.Model) + if err != nil { + return err + } + fmt.Println(model) + */ default: return fmt.Errorf("Agent %q does not support model download", agent.Name()) } diff --git a/pkg/agent/opt.go b/pkg/agent/opt.go index e007fbf..6179f5c 100644 --- a/pkg/agent/opt.go +++ b/pkg/agent/opt.go @@ -4,15 +4,12 @@ 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" openai "github.com/mutablelogic/go-llm/pkg/openai" ) //////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS - +/* func WithOllama(endpoint string, opts ...client.ClientOpt) llm.Opt { return func(o *llm.Opts) error { client, err := ollama.New(endpoint, opts...) @@ -45,6 +42,7 @@ func WithMistral(key string, opts ...client.ClientOpt) llm.Opt { } } } +*/ func WithOpenAI(key string, opts ...client.ClientOpt) llm.Opt { return func(o *llm.Opts) error { diff --git a/pkg/openai/completion.go b/pkg/openai/completion.go index 3c2ab3e..a4f4315 100644 --- a/pkg/openai/completion.go +++ b/pkg/openai/completion.go @@ -3,7 +3,6 @@ package openai import ( "context" "encoding/json" - "fmt" "strings" // Packages @@ -249,7 +248,6 @@ func streamEvent(response *Response, evt client.TextStreamEvent) error { } func appendCompletion(response *Response, c *Completion) error { - fmt.Println(c) // Append a new completion for { if c.Index < uint64(len(response.Completions)) { @@ -303,6 +301,31 @@ func appendCompletion(response *Response, c *Completion) error { message.Media.Append(c.Delta.Media) } + // Append tool calls + for i := range c.Delta.Calls { + if i >= len(message.Calls) { + message.Calls = append(message.Calls, toolcall{}) + } + } + + for i, call := range c.Delta.Calls { + if call.meta.Id != "" { + message.Calls[i].meta.Id = call.meta.Id + } + if call.meta.Index != 0 { + message.Calls[i].meta.Index = call.meta.Index + } + if call.meta.Type != "" { + message.Calls[i].meta.Type = call.meta.Type + } + if call.meta.Function.Name != "" { + message.Calls[i].meta.Function.Name = call.meta.Function.Name + } + if call.meta.Function.Arguments != "" { + message.Calls[i].meta.Function.Arguments += call.meta.Function.Arguments + } + } + // Return success return nil } From 4f4f6d6acd3273baacc1ef6467f6b013bf6724cd Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Thu, 6 Feb 2025 21:35:01 +0100 Subject: [PATCH 12/25] Updated llm --- cmd/llm/complete.go | 102 +++++++ cmd/llm/main.go | 16 +- error.go | 3 + pkg/agent/opt.go | 4 +- pkg/ollama/chat.go | 152 ---------- pkg/ollama/client.go | 2 - pkg/ollama/completion.go | 281 ++++++++++++++++++ .../{chat_test.go => completion_test.go_old} | 0 pkg/ollama/message.go | 107 ++++++- pkg/ollama/model.go | 23 +- pkg/ollama/opt.go | 22 +- pkg/ollama/session.go | 221 -------------- .../{session_test.go => session_test.go_old} | 0 pkg/openai/client.go | 1 - pkg/openai/model.go | 2 +- 15 files changed, 533 insertions(+), 403 deletions(-) create mode 100644 cmd/llm/complete.go delete mode 100644 pkg/ollama/chat.go create mode 100644 pkg/ollama/completion.go rename pkg/ollama/{chat_test.go => completion_test.go_old} (100%) delete mode 100644 pkg/ollama/session.go rename pkg/ollama/{session_test.go => session_test.go_old} (100%) diff --git a/cmd/llm/complete.go b/cmd/llm/complete.go new file mode 100644 index 0000000..d950933 --- /dev/null +++ b/cmd/llm/complete.go @@ -0,0 +1,102 @@ +package main + +import ( + "context" + "fmt" + "io" + "os" + "strings" + + // Packages + llm "github.com/mutablelogic/go-llm" + agent "github.com/mutablelogic/go-llm/pkg/agent" +) + +//////////////////////////////////////////////////////////////////////////////// +// TYPES + +type CompleteCmd struct { + Model string `arg:"" help:"Model name"` + Prompt string `arg:"" optional:"" help:"Prompt"` + File []string `type:"file" help:"Files to attach"` + System string `flag:"system" help:"Set the system prompt"` + NoStream bool `flag:"no-stream" help:"Do not stream output"` +} + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +func (cmd *CompleteCmd) Run(globals *Globals) error { + return runagent(globals, func(ctx context.Context, client llm.Agent) error { + var prompt []byte + + // Load the model + model, err := client.(*agent.Agent).GetModel(ctx, cmd.Model) + if err != nil { + return err + } + + // If we are pipeline content in via stdin + fileInfo, err := os.Stdin.Stat() + if err != nil { + return llm.ErrInternalServerError.Withf("Failed to get stdin stat: %v", err) + } + if (fileInfo.Mode() & os.ModeCharDevice) == 0 { + if data, err := io.ReadAll(os.Stdin); err != nil { + return err + } else if len(data) > 0 { + prompt = data + } + } + + // Append any further prompt + if len(cmd.Prompt) > 0 { + prompt = append(prompt, []byte("\n\n")...) + prompt = append(prompt, []byte(cmd.Prompt)...) + } + + opts := cmd.opts() + if !cmd.NoStream { + // Add streaming callback + var buf string + opts = append(opts, llm.WithStream(func(c llm.Completion) { + fmt.Print(strings.TrimPrefix(c.Text(0), buf)) + buf = c.Text(0) + })) + } + + // Add attachments + for _, file := range cmd.File { + f, err := os.Open(file) + if err != nil { + return err + } + defer f.Close() + opts = append(opts, llm.WithAttachment(f)) + } + + // Make the completion + completion, err := model.Completion(ctx, string(prompt), opts...) + if err != nil { + return err + } + + // Print the completion + if cmd.NoStream { + fmt.Println(completion.Text(0)) + } else { + fmt.Println("") + } + + // Return success + return nil + }) +} + +func (cmd *CompleteCmd) opts() []llm.Opt { + opts := []llm.Opt{} + if cmd.System != "" { + opts = append(opts, llm.WithSystemPrompt(cmd.System)) + } + return opts +} diff --git a/cmd/llm/main.go b/cmd/llm/main.go index ab7aef6..e87039f 100644 --- a/cmd/llm/main.go +++ b/cmd/llm/main.go @@ -6,6 +6,7 @@ import ( "os/signal" "path/filepath" "syscall" + "time" // Packages kong "github.com/alecthomas/kong" @@ -21,8 +22,9 @@ import ( type Globals struct { // Debugging - Debug bool `name:"debug" help:"Enable debug output"` - Verbose bool `name:"verbose" help:"Enable verbose output"` + Debug bool `name:"debug" help:"Enable debug output"` + Verbose bool `name:"verbose" help:"Enable verbose output"` + Timeout time.Duration `name:"timeout" help:"Timeout for the command"` // Agents Ollama `embed:"" help:"Ollama configuration"` @@ -71,6 +73,7 @@ type CLI struct { // Commands Download DownloadModelCmd `cmd:"" help:"Download a model"` Chat ChatCmd `cmd:"" help:"Start a chat session"` + Complete CompleteCmd `cmd:"" help:"Complete a prompt"` } //////////////////////////////////////////////////////////////////////////////// @@ -106,13 +109,16 @@ func main() { if cli.Debug || cli.Verbose { clientopts = append(clientopts, client.OptTrace(os.Stderr, cli.Verbose)) } + if cli.Timeout > 0 { + clientopts = append(clientopts, client.OptTimeout(cli.Timeout)) + } // Create an agent opts := []llm.Opt{} + if cli.OllamaEndpoint != "" { + opts = append(opts, agent.WithOllama(cli.OllamaEndpoint, clientopts...)) + } /* - if cli.OllamaEndpoint != "" { - opts = append(opts, agent.WithOllama(cli.OllamaEndpoint, clientopts...)) - } if cli.AnthropicKey != "" { opts = append(opts, agent.WithAnthropic(cli.AnthropicKey, clientopts...)) } diff --git a/error.go b/error.go index 4bb0c9d..bd7a9c0 100644 --- a/error.go +++ b/error.go @@ -13,6 +13,7 @@ const ( ErrBadParameter ErrNotImplemented ErrConflict + ErrInternalServerError ) //////////////////////////////////////////////////////////////////////////////// @@ -36,6 +37,8 @@ func (e Err) Error() string { return "not implemented" case ErrConflict: return "conflict" + case ErrInternalServerError: + return "internal server error" } return fmt.Sprintf("error code %d", int(e)) } diff --git a/pkg/agent/opt.go b/pkg/agent/opt.go index 6179f5c..36f637b 100644 --- a/pkg/agent/opt.go +++ b/pkg/agent/opt.go @@ -4,12 +4,13 @@ import ( // Packages client "github.com/mutablelogic/go-client" llm "github.com/mutablelogic/go-llm" + "github.com/mutablelogic/go-llm/pkg/ollama" openai "github.com/mutablelogic/go-llm/pkg/openai" ) //////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS -/* + func WithOllama(endpoint string, opts ...client.ClientOpt) llm.Opt { return func(o *llm.Opts) error { client, err := ollama.New(endpoint, opts...) @@ -21,6 +22,7 @@ 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...) diff --git a/pkg/ollama/chat.go b/pkg/ollama/chat.go deleted file mode 100644 index 41eb086..0000000 --- a/pkg/ollama/chat.go +++ /dev/null @@ -1,152 +0,0 @@ -package ollama - -import ( - "context" - "encoding/json" - "time" - - // Packages - client "github.com/mutablelogic/go-client" - llm "github.com/mutablelogic/go-llm" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -// Chat Completion Response -type Response struct { - Model string `json:"model"` - CreatedAt time.Time `json:"created_at"` - Done bool `json:"done"` - Reason string `json:"done_reason,omitempty"` - Message `json:"message"` - Metrics -} - -// Metrics -type Metrics struct { - TotalDuration time.Duration `json:"total_duration,omitempty"` - LoadDuration time.Duration `json:"load_duration,omitempty"` - PromptEvalCount int `json:"prompt_eval_count,omitempty"` - PromptEvalDuration time.Duration `json:"prompt_eval_duration,omitempty"` - EvalCount int `json:"eval_count,omitempty"` - EvalDuration time.Duration `json:"eval_duration,omitempty"` -} - -var _ llm.Completion = (*Response)(nil) - -/////////////////////////////////////////////////////////////////////////////// -// STRINGIFY - -func (r Response) String() string { - data, err := json.MarshalIndent(r, "", " ") - if err != nil { - return err.Error() - } - return string(data) -} - -/////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - -type reqChat struct { - Model string `json:"model"` - 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 (model *model) Completion(ctx context.Context, prompt string, opts ...llm.Opt) (llm.Completion, error) { - // TODO - return nil, llm.ErrNotImplemented -} -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 - 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) - } - - // Request - req, err := client.NewJSONRequest(reqChat{ - Model: context.(*session).model.Name(), - Messages: messages, - Tools: optTools(ollama, opt), - Format: optFormat(opt), - Options: optOptions(opt), - Stream: optStream(ollama, opt), - KeepAlive: optKeepAlive(opt), - }) - if err != nil { - return nil, err - } - - // Response - var response, delta 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 - } - if fn := opt.StreamFn(); fn != nil { - fn(&response) - } - return nil - })) - } - - // Response - if err := ollama.DoWithContext(ctx, req, &delta, reqopts...); err != nil { - return nil, err - } - - // Return success - if optStream(ollama, opt) { - return &response, nil - } else { - return &delta, nil - } -} - -/////////////////////////////////////////////////////////////////////////////// -// PRIVATE METHODS - -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/client.go b/pkg/ollama/client.go index 56d9c62..f68ca32 100644 --- a/pkg/ollama/client.go +++ b/pkg/ollama/client.go @@ -1,7 +1,6 @@ package ollama import ( - // Packages client "github.com/mutablelogic/go-client" llm "github.com/mutablelogic/go-llm" @@ -14,7 +13,6 @@ type Client struct { *client.Client } -// Ensure it satisfies the agent.Agent interface var _ llm.Agent = (*Client)(nil) /////////////////////////////////////////////////////////////////////////////// diff --git a/pkg/ollama/completion.go b/pkg/ollama/completion.go new file mode 100644 index 0000000..12c18aa --- /dev/null +++ b/pkg/ollama/completion.go @@ -0,0 +1,281 @@ +package ollama + +import ( + "context" + "encoding/json" + "strings" + "time" + + // Packages + client "github.com/mutablelogic/go-client" + llm "github.com/mutablelogic/go-llm" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +// Chat Response +type Response struct { + Model string `json:"model"` + CreatedAt time.Time `json:"created_at"` + Done bool `json:"done"` + Reason string `json:"done_reason,omitempty"` + Response *string `json:"response"` // For completion + Message `json:"message"` // For chat + Metrics +} + +var _ llm.Completion = (*Response)(nil) + +// Metrics +type Metrics struct { + TotalDuration time.Duration `json:"total_duration,omitempty"` + LoadDuration time.Duration `json:"load_duration,omitempty"` + PromptEvalCount int `json:"prompt_eval_count,omitempty"` + PromptEvalDuration time.Duration `json:"prompt_eval_duration,omitempty"` + EvalCount int `json:"eval_count,omitempty"` + EvalDuration time.Duration `json:"eval_duration,omitempty"` +} + +/////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +func (r Response) String() string { + data, err := json.MarshalIndent(r, "", " ") + if err != nil { + return err.Error() + } + return string(data) +} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// https://github.com/ollama/ollama/blob/main/api/types.go +type reqCompletion struct { + // Model name + Model string `json:"model"` + + // Prompt is the textual prompt to send to the model. + Prompt string `json:"prompt"` + + // Suffix is the text that comes after the inserted text. + Suffix string `json:"suffix,omitempty"` + + // System overrides the model's default system message/prompt. + System string `json:"system,omitempty"` + + // Template overrides the model's default prompt template. + Template string `json:"template,omitempty"` + + // Stream specifies whether the response is streaming; it is true by default. + Stream *bool `json:"stream,omitempty"` + + // Raw set to true means that no formatting will be applied to the prompt. + Raw bool `json:"raw,omitempty"` + + // Format specifies the format to return a response in. + Format json.RawMessage `json:"format,omitempty"` + + // KeepAlive controls how long the model will stay loaded in memory following + // this request. + KeepAlive *time.Duration `json:"keep_alive,omitempty"` + + // Images is an optional list of base64-encoded images accompanying this + // request, for multimodal models. + Images []ImageData `json:"images,omitempty"` + + // Options lists model-specific options. For example, temperature can be + // set through this field, if the model supports it. + Options map[string]any `json:"options,omitempty"` +} + +func (model *model) Completion(ctx context.Context, prompt string, opts ...llm.Opt) (llm.Completion, error) { + // Apply options - including prompt options + opt, err := llm.ApplyPromptOpts(opts...) + if err != nil { + return nil, err + } + + // Make images + images := make([]ImageData, 0, len(opt.Attachments())) + for _, attachment := range opt.Attachments() { + if !strings.HasPrefix(attachment.Type(), "image/") { + return nil, llm.ErrBadParameter.Withf("Attachment is not an image: %v", attachment.Filename()) + } + images = append(images, attachment.Data()) + } + + // Request + req, err := client.NewJSONRequest(reqCompletion{ + Model: model.Name(), + Prompt: prompt, + System: opt.SystemPrompt(), + Stream: optStream(model.Client, opt), + Format: json.RawMessage(optFormat(opt)), + KeepAlive: optKeepAlive(opt), + Images: images, + Options: optOptions(opt), + }) + if err != nil { + return nil, err + } + + // Make the request + return model.request(ctx, req, opt.StreamFn(), client.OptPath("generate")) +} + +func (model *model) request(ctx context.Context, req client.Payload, streamfn func(llm.Completion), opts ...client.RequestOpt) (*Response, error) { + var delta, response Response + if streamfn != nil { + opts = append(opts, client.OptJsonStreamCallback(func(v any) error { + if v, ok := v.(*Response); !ok || v == nil { + return llm.ErrConflict.Withf("Invalid stream response: %v", delta) + } else if err := streamEvent(&response, v); err != nil { + return err + } + if fn := streamfn; fn != nil { + fn(&response) + } + return nil + })) + } + + // Response + if err := model.DoWithContext(ctx, req, &delta, opts...); err != nil { + return nil, err + } + + // Return success + if streamfn != nil { + return &response, nil + } else if delta.Response != nil { + delta.Message = Message{ + RoleContent: RoleContent{ + Role: "user", + Content: *delta.Response, + }, + } + return &delta, nil + } else { + return nil, llm.ErrInternalServerError.Withf("No response") + } +} + +// Create a completion from a chat session +func (model *model) Chat(context.Context, []llm.Completion, ...llm.Opt) (llm.Completion, error) { + return nil, llm.ErrNotImplemented +} + +/* +type reqChat struct { + Model string `json:"model"` + 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, 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 + 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) + } + + // Request + req, err := client.NewJSONRequest(reqChat{ + Model: context.(*session).model.Name(), + Messages: messages, + Tools: optTools(ollama, opt), + Format: optFormat(opt), + Options: optOptions(opt), + Stream: optStream(ollama, opt), + KeepAlive: optKeepAlive(opt), + }) + if err != nil { + return nil, err + } + + // Response + var response, delta 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 + } + if fn := opt.StreamFn(); fn != nil { + fn(&response) + } + return nil + })) + } + + // Response + if err := ollama.DoWithContext(ctx, req, &delta, reqopts...); err != nil { + return nil, err + } + + // Return success + if optStream(ollama, opt) { + return &response, nil + } else { + return &delta, nil + } +} +*/ + +/////////////////////////////////////////////////////////////////////////////// +// PRIVATE METHODS + +func streamEvent(response, delta *Response) error { + // Completion instead of chat + if delta.Response != nil { + delta.Message = Message{ + RoleContent: RoleContent{ + Role: "user", + Content: *delta.Response, + }, + } + } + + // Update response from the delta + 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 success + return nil +} diff --git a/pkg/ollama/chat_test.go b/pkg/ollama/completion_test.go_old similarity index 100% rename from pkg/ollama/chat_test.go rename to pkg/ollama/completion_test.go_old diff --git a/pkg/ollama/message.go b/pkg/ollama/message.go index d00c6b8..4ef8432 100644 --- a/pkg/ollama/message.go +++ b/pkg/ollama/message.go @@ -1,6 +1,7 @@ package ollama import ( + "encoding/json" "fmt" // Packages @@ -11,21 +12,28 @@ import ( /////////////////////////////////////////////////////////////////////////////// // TYPES +type messagefactory struct{} + // Message with text or object content type Message struct { RoleContent - ToolCallArray `json:"tool_calls,omitempty"` + Images []ImageData `json:"images,omitempty"` + Calls ToolCalls `json:"tool_calls,omitempty"` + *ToolResults } +var _ llm.Completion = (*Message)(nil) + 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 ToolCalls []ToolCall + +type ToolResults struct { + Name string `json:"name,omitempty"` // function name - when role is tool +} type ToolCall struct { Type string `json:"type"` // function @@ -39,24 +47,94 @@ type ToolCallFunction struct { } // Data represents the raw binary data of an image file. -type Data []byte +type ImageData []byte -// ToolResult -type ToolResult struct { - Name string `json:"name,omitempty"` // function name - when role is tool +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS - MESSAGE FACTORY + +func (messagefactory) SystemPrompt(prompt string) llm.Completion { + return &Message{ + RoleContent: RoleContent{ + Role: "system", + Content: prompt, + }, + } +} + +func (messagefactory) UserPrompt(prompt string, opts ...llm.Opt) (llm.Completion, error) { + // Get attachments + opt, err := llm.ApplyPromptOpts(opts...) + if err != nil { + return nil, err + } + + // Append image attachments + attachments := opt.Attachments() + images := make([]ImageData, 0, len(attachments)) + for _, attachment := range attachments { + images = append(images, attachment.Data()) + } + + // Return success + return &Message{ + RoleContent: RoleContent{ + Role: "user", + Content: prompt, + }, + Images: images, + }, nil +} + +func (messagefactory) ToolResults(results ...llm.ToolResult) ([]llm.Completion, error) { + // Check for no results + if len(results) == 0 { + return nil, llm.ErrBadParameter.Withf("No tool results") + } + + // Create results + messages := make([]llm.Completion, 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", + Content: string(value), + }, + ToolResults: &ToolResults{ + Name: result.Call().Name(), + }, + }) + } + + // Return success + return messages, nil } /////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS - MESSAGE +// Return the number of completions func (m Message) Num() int { return 1 } +// Return the current session role func (m Message) Role() string { return m.RoleContent.Role } +// Return the completion +func (message *Message) Choice(index int) llm.Completion { + if index != 0 { + return nil + } + return message +} + +// Return the text func (m Message) Text(index int) string { if index != 0 { return "" @@ -64,14 +142,21 @@ func (m Message) Text(index int) string { return m.Content } +// Return the audio - not supported on ollama +func (message *Message) Audio(index int) *llm.Attachment { + return nil +} + +// Return the current session tool calls given the completion index. +// Will return nil if no tool calls were returned. 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 := make([]llm.ToolCall, 0, len(m.Calls)) + for _, call := range m.Calls { calls = append(calls, tool.NewCall(fmt.Sprint(call.Function.Index), call.Function.Name, call.Function.Arguments)) } diff --git a/pkg/ollama/model.go b/pkg/ollama/model.go index 94fb7d4..29b7c97 100644 --- a/pkg/ollama/model.go +++ b/pkg/ollama/model.go @@ -9,6 +9,7 @@ import ( // Packages client "github.com/mutablelogic/go-client" llm "github.com/mutablelogic/go-llm" + "github.com/mutablelogic/go-llm/pkg/session" ) /////////////////////////////////////////////////////////////////////////////// @@ -81,29 +82,41 @@ func (m PullStatus) String() string { } /////////////////////////////////////////////////////////////////////////////// -// INTERFACE IMPLEMENTATION +// PUBLIC METHODS - llm.Model implementation func (m model) Name() string { return m.ModelMeta.Name } -/////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - // Agent interface func (ollama *Client) Models(ctx context.Context) ([]llm.Model, error) { + // We don't explicitly cache models return ollama.ListModels(ctx) } -// Agent interface +// Return the a model by name func (ollama *Client) Model(ctx context.Context, name string) llm.Model { model, err := ollama.GetModel(ctx, name) if err != nil { panic(err) } + + // In the ollama version, we attempt to load the model into + // memory here, so that we can use it immediately + ollama.LoadModel(ctx, name) + + // Return the model return model } +// Return a new empty session +func (model *model) Context(opts ...llm.Opt) llm.Context { + return session.NewSession(model, &messagefactory{}, opts...) +} + +/////////////////////////////////////////////////////////////////////////////// +// API CALLS + // List models func (ollama *Client) ListModels(ctx context.Context) ([]llm.Model, error) { type respListModel struct { diff --git a/pkg/ollama/opt.go b/pkg/ollama/opt.go index 2b1afd4..b1b81f9 100644 --- a/pkg/ollama/opt.go +++ b/pkg/ollama/opt.go @@ -1,6 +1,7 @@ package ollama import ( + "strings" "time" // Packages @@ -88,7 +89,11 @@ func optTools(agent *Client, opts *llm.Opts) []llm.Tool { } func optFormat(opts *llm.Opts) string { - return opts.GetString("format") + format := strings.ToLower(opts.GetString("format")) + if format == "json_format" { + return "json" + } + return format } func optStopSequence(opts *llm.Opts) []string { @@ -135,15 +140,24 @@ func optOptions(opts *llm.Opts) map[string]any { return result } -func optStream(agent *Client, opts *llm.Opts) bool { +func optStream(agent *Client, opts *llm.Opts) *bool { + var stream bool + + // Based on stream function + if opts.StreamFn() != nil { + stream = true + } + // Streaming only if there is a stream function and no tools toolkit := opts.ToolKit() if toolkit != nil { if tools := toolkit.Tools(agent); len(tools) > 0 { - return false + stream = false } } - return opts.StreamFn() != nil + + // Return the value + return &stream } func optKeepAlive(opts *llm.Opts) *time.Duration { diff --git a/pkg/ollama/session.go b/pkg/ollama/session.go deleted file mode 100644 index 50c702c..0000000 --- a/pkg/ollama/session.go +++ /dev/null @@ -1,221 +0,0 @@ -package ollama - -import ( - "context" - "encoding/json" - - // Packages - llm "github.com/mutablelogic/go-llm" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -// Implementation of a message session, which is a sequence of messages -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 -} - -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, - seq: make([]*Message, 0, 10), - } -} - -// 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...) - - // 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 -} - -/////////////////////////////////////////////////////////////////////////////// -// 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 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 (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' method - r, err := session.model.Chat(ctx, session, chatopts...) - if err != nil { - return err - } - - // 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 (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.Chat(ctx, session, session.opts...) - if err != nil { - return err - } - - // Append the first message from the set of completions - session.seq = append(session.seq, &r.Message) - - // Return success - return nil -} - -/////////////////////////////////////////////////////////////////////////////// -// PRIVATE METHODS - -func systemPrompt(prompt string) *Message { - return &Message{ - RoleContent: RoleContent{ - Role: "system", - Content: prompt, - }, - } -} - -func userPrompt(prompt string, opts ...llm.Opt) (*Message, error) { - // Get attachments - opt, err := llm.ApplyPromptOpts(opts...) - if err != nil { - return nil, err - } - - // 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 &Message{ - RoleContent: RoleContent{ - Role: "user", - Content: prompt, - Images: data, - }, - }, 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", - ToolResult: ToolResult{ - Name: result.Call().Name(), - }, - Content: string(value), - }, - }) - } - - // Return success - 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 diff --git a/pkg/openai/client.go b/pkg/openai/client.go index 1494902..fd31659 100644 --- a/pkg/openai/client.go +++ b/pkg/openai/client.go @@ -5,7 +5,6 @@ https://platform.openai.com/docs/api-reference package openai import ( - // Packages client "github.com/mutablelogic/go-client" llm "github.com/mutablelogic/go-llm" diff --git a/pkg/openai/model.go b/pkg/openai/model.go index df75b5f..b7a943f 100644 --- a/pkg/openai/model.go +++ b/pkg/openai/model.go @@ -7,7 +7,7 @@ import ( // Packages client "github.com/mutablelogic/go-client" llm "github.com/mutablelogic/go-llm" - "github.com/mutablelogic/go-llm/pkg/session" + session "github.com/mutablelogic/go-llm/pkg/session" ) /////////////////////////////////////////////////////////////////////////////// From 4b771a6f018220700dc7949e90dcfd045e488f55 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Thu, 6 Feb 2025 22:31:47 +0100 Subject: [PATCH 13/25] Updated --- go.mod | 1 + go.sum | 2 + pkg/ollama/completion.go | 121 +++++++++++++-------------------------- pkg/telegram/telegram.go | 61 ++++++++++++++++++++ 4 files changed, 105 insertions(+), 80 deletions(-) create mode 100644 pkg/telegram/telegram.go diff --git a/go.mod b/go.mod index 199ec9e..ec1292d 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 // indirect github.com/mattn/go-colorable v0.1.4 // indirect github.com/mattn/go-isatty v0.0.11 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect diff --git a/go.sum b/go.sum index e9cc101..4127777 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,8 @@ github.com/djthorpe/go-errors v1.0.3 h1:GZeMPkC1mx2vteXLI/gvxZS0Ee9zxzwD1mcYyKU5 github.com/djthorpe/go-errors v1.0.3/go.mod h1:HtfrZnMd6HsX75Mtbv9Qcnn0BqOrrFArvCaj3RMnZhY= github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= +github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= diff --git a/pkg/ollama/completion.go b/pkg/ollama/completion.go index 12c18aa..6d13a9e 100644 --- a/pkg/ollama/completion.go +++ b/pkg/ollama/completion.go @@ -20,7 +20,7 @@ type Response struct { CreatedAt time.Time `json:"created_at"` Done bool `json:"done"` Reason string `json:"done_reason,omitempty"` - Response *string `json:"response"` // For completion + Response *string `json:"response,omitempty"` // For completion Message `json:"message"` // For chat Metrics } @@ -90,6 +90,7 @@ type reqCompletion struct { Options map[string]any `json:"options,omitempty"` } +// Create a completion from a prompt func (model *model) Completion(ctx context.Context, prompt string, opts ...llm.Opt) (llm.Completion, error) { // Apply options - including prompt options opt, err := llm.ApplyPromptOpts(opts...) @@ -125,104 +126,62 @@ func (model *model) Completion(ctx context.Context, prompt string, opts ...llm.O return model.request(ctx, req, opt.StreamFn(), client.OptPath("generate")) } -func (model *model) request(ctx context.Context, req client.Payload, streamfn func(llm.Completion), opts ...client.RequestOpt) (*Response, error) { - var delta, response Response - if streamfn != nil { - opts = append(opts, client.OptJsonStreamCallback(func(v any) error { - if v, ok := v.(*Response); !ok || v == nil { - return llm.ErrConflict.Withf("Invalid stream response: %v", delta) - } else if err := streamEvent(&response, v); err != nil { - return err - } - if fn := streamfn; fn != nil { - fn(&response) - } - return nil - })) - } - - // Response - if err := model.DoWithContext(ctx, req, &delta, opts...); err != nil { - return nil, err - } - - // Return success - if streamfn != nil { - return &response, nil - } else if delta.Response != nil { - delta.Message = Message{ - RoleContent: RoleContent{ - Role: "user", - Content: *delta.Response, - }, - } - return &delta, nil - } else { - return nil, llm.ErrInternalServerError.Withf("No response") - } -} - -// Create a completion from a chat session -func (model *model) Chat(context.Context, []llm.Completion, ...llm.Opt) (llm.Completion, error) { - return nil, llm.ErrNotImplemented -} - -/* type reqChat struct { - Model string `json:"model"` - 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"` + Model string `json:"model"` + Messages []llm.Completion `json:"messages"` + Tools []llm.Tool `json:"tools,omitempty"` + Format string `json:"format,omitempty"` + Options map[string]any `json:"options,omitempty"` + Stream *bool `json:"stream"` + KeepAlive *time.Duration `json:"keep_alive,omitempty"` } -func (ollama *Client) Chat(ctx context.Context, context llm.Context, opts ...llm.Opt) (*Response, error) { +// Create a completion from a chat session +func (model *model) Chat(ctx context.Context, completions []llm.Completion, opts ...llm.Opt) (llm.Completion, error) { // Apply options opt, err := llm.ApplyOpts(opts...) if err != nil { return nil, err } - // Append the system prompt at the beginning - messages := make([]*Message, 0, len(context.(*session).seq)+1) + // Create the completions including the system prompt + messages := make([]llm.Completion, 0, len(completions)+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) + messages = append(messages, messagefactory{}.SystemPrompt(system)) } + messages = append(messages, completions...) // Request req, err := client.NewJSONRequest(reqChat{ - Model: context.(*session).model.Name(), + Model: model.Name(), Messages: messages, - Tools: optTools(ollama, opt), + Tools: optTools(model.Client, opt), Format: optFormat(opt), Options: optOptions(opt), - Stream: optStream(ollama, opt), + Stream: optStream(model.Client, opt), KeepAlive: optKeepAlive(opt), }) if err != nil { return nil, err } - // Response - var response, delta Response - reqopts := []client.RequestOpt{ - client.OptPath("chat"), - } - if optStream(ollama, opt) { - reqopts = append(reqopts, client.OptJsonStreamCallback(func(v any) error { + // Make the request + return model.request(ctx, req, opt.StreamFn(), client.OptPath("chat")) +} + +/////////////////////////////////////////////////////////////////////////////// +// PRIVATE METHODS + +func (model *model) request(ctx context.Context, req client.Payload, streamfn func(llm.Completion), opts ...client.RequestOpt) (*Response, error) { + var delta, response Response + if streamfn != nil { + opts = append(opts, client.OptJsonStreamCallback(func(v any) error { if v, ok := v.(*Response); !ok || v == nil { - return llm.ErrConflict.Withf("Invalid stream response: %v", v) + return llm.ErrConflict.Withf("Invalid stream response: %v", delta) } else if err := streamEvent(&response, v); err != nil { return err } - if fn := opt.StreamFn(); fn != nil { + if fn := streamfn; fn != nil { fn(&response) } return nil @@ -230,21 +189,23 @@ func (ollama *Client) Chat(ctx context.Context, context llm.Context, opts ...llm } // Response - if err := ollama.DoWithContext(ctx, req, &delta, reqopts...); err != nil { + if err := model.DoWithContext(ctx, req, &delta, opts...); err != nil { return nil, err } // Return success - if optStream(ollama, opt) { + if streamfn != nil { return &response, nil - } else { - return &delta, nil + } else if delta.Response != nil { + delta.Message = Message{ + RoleContent: RoleContent{ + Role: "user", + Content: *delta.Response, + }, + } } + return &delta, nil } -*/ - -/////////////////////////////////////////////////////////////////////////////// -// PRIVATE METHODS func streamEvent(response, delta *Response) error { // Completion instead of chat diff --git a/pkg/telegram/telegram.go b/pkg/telegram/telegram.go new file mode 100644 index 0000000..052161a --- /dev/null +++ b/pkg/telegram/telegram.go @@ -0,0 +1,61 @@ +package telegram + +import ( + "context" + "fmt" + + // Packages + telegram "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + +///////////////////////////////////////////////////////////////////// +// TYPES + +type t struct { + *telegram.BotAPI +} + +///////////////////////////////////////////////////////////////////// +// LIFECYCLE + +func NewTelegram(token string) (*t, error) { + bot, err := telegram.NewBotAPI(token) + if err != nil { + return nil, err + } + + // Create a new telegram instance + telegram := &t{bot} + + // Return the instance + return telegram, nil +} + +///////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +func (t *t) Run(ctx context.Context) error { + updates := t.GetUpdatesChan(telegram.NewUpdate(0)) +FOR_LOOP: + for { + select { + case <-ctx.Done(): + break FOR_LOOP + case evt := <-updates: + if evt.Message != nil && !evt.Message.IsCommand() { + t.handleMessage(evt.Message) + } + } + } + + // Return success + return nil +} + +///////////////////////////////////////////////////////////////////// +// PRIVATE METHODS + +func (t *t) handleMessage(update *telegram.Message) { + fmt.Println("Received message from", update.From.UserName) + fmt.Println(" => ", update.Text) +} From ed66517250e30a8bab51c0217a7cb00f625d6444 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Thu, 6 Feb 2025 23:03:53 +0100 Subject: [PATCH 14/25] Added Gemini --- cmd/llm/main.go | 8 +++ go.mod | 2 +- pkg/agent/opt.go | 14 ++++- pkg/gemini/client.go | 60 ++++++++++++++++++ pkg/gemini/client_test.go | 58 +++++++++++++++++ pkg/gemini/model.go | 129 ++++++++++++++++++++++++++++++++++++++ pkg/gemini/model_test.go | 22 +++++++ 7 files changed, 291 insertions(+), 2 deletions(-) create mode 100644 pkg/gemini/client.go create mode 100644 pkg/gemini/client_test.go create mode 100644 pkg/gemini/model.go create mode 100644 pkg/gemini/model_test.go diff --git a/cmd/llm/main.go b/cmd/llm/main.go index e87039f..37af988 100644 --- a/cmd/llm/main.go +++ b/cmd/llm/main.go @@ -31,6 +31,7 @@ type Globals struct { Anthropic `embed:"" help:"Anthropic configuration"` Mistral `embed:"" help:"Mistral configuration"` OpenAI `embed:"" help:"OpenAI configuration"` + Gemini `embed:"" help:"Gemini configuration"` // Tools NewsAPI `embed:"" help:"NewsAPI configuration"` @@ -58,6 +59,10 @@ type OpenAI struct { OpenAIKey string `env:"OPENAI_API_KEY" help:"OpenAI API Key"` } +type Gemini struct { + GeminiKey string `env:"GEMINI_API_KEY" help:"Gemini API Key"` +} + type NewsAPI struct { NewsKey string `env:"NEWSAPI_KEY" help:"News API Key"` } @@ -129,6 +134,9 @@ func main() { if cli.OpenAIKey != "" { opts = append(opts, agent.WithOpenAI(cli.OpenAIKey, clientopts...)) } + if cli.GeminiKey != "" { + opts = append(opts, agent.WithGemini(cli.GeminiKey, clientopts...)) + } // Make a toolkit toolkit := tool.NewToolKit() diff --git a/go.mod b/go.mod index ec1292d..fb5121f 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/alecthomas/kong v1.7.0 github.com/djthorpe/go-errors v1.0.3 github.com/fatih/color v1.9.0 + github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 github.com/mutablelogic/go-client v1.0.10 github.com/stretchr/testify v1.10.0 golang.org/x/term v0.28.0 @@ -14,7 +15,6 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 // indirect github.com/mattn/go-colorable v0.1.4 // indirect github.com/mattn/go-isatty v0.0.11 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect diff --git a/pkg/agent/opt.go b/pkg/agent/opt.go index 36f637b..f410bb3 100644 --- a/pkg/agent/opt.go +++ b/pkg/agent/opt.go @@ -4,7 +4,8 @@ import ( // Packages client "github.com/mutablelogic/go-client" llm "github.com/mutablelogic/go-llm" - "github.com/mutablelogic/go-llm/pkg/ollama" + gemini "github.com/mutablelogic/go-llm/pkg/gemini" + ollama "github.com/mutablelogic/go-llm/pkg/ollama" openai "github.com/mutablelogic/go-llm/pkg/openai" ) @@ -56,3 +57,14 @@ func WithOpenAI(key string, opts ...client.ClientOpt) llm.Opt { } } } + +func WithGemini(key string, opts ...client.ClientOpt) llm.Opt { + return func(o *llm.Opts) error { + client, err := gemini.New(key, opts...) + if err != nil { + return err + } else { + return llm.WithAgent(client)(o) + } + } +} diff --git a/pkg/gemini/client.go b/pkg/gemini/client.go new file mode 100644 index 0000000..508ebef --- /dev/null +++ b/pkg/gemini/client.go @@ -0,0 +1,60 @@ +/* +gemini implements an API client for Google's Gemini LLM (https://ai.google.dev/gemini-api/docs) +*/ +package gemini + +import ( + + // Packages + client "github.com/mutablelogic/go-client" + llm "github.com/mutablelogic/go-llm" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +type Client struct { + *client.Client + cache map[string]llm.Model +} + +var _ llm.Agent = (*Client)(nil) + +/////////////////////////////////////////////////////////////////////////////// +// GLOBALS + +const ( + endPoint = "https://generativelanguage.googleapis.com/v1beta" + defaultName = "gemini" +) + +/////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +// Create a new client +func New(ApiKey string, opts ...client.ClientOpt) (*Client, error) { + // Create client + opts = append(opts, client.OptEndpoint(endPointWithKey(endPoint, ApiKey))) + client, err := client.New(opts...) + if err != nil { + return nil, err + } + + // Return the client + return &Client{client, nil}, nil +} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Return the name of the agent +func (Client) Name() string { + return defaultName +} + +/////////////////////////////////////////////////////////////////////////////// +// PRIVATE METHODS + +func endPointWithKey(endpoint, key string) string { + return endpoint + "?key=" + key +} diff --git a/pkg/gemini/client_test.go b/pkg/gemini/client_test.go new file mode 100644 index 0000000..f55f396 --- /dev/null +++ b/pkg/gemini/client_test.go @@ -0,0 +1,58 @@ +package gemini_test + +import ( + "flag" + "log" + "os" + "strconv" + "testing" + + // Packages + opts "github.com/mutablelogic/go-client" + gemini "github.com/mutablelogic/go-llm/pkg/gemini" + assert "github.com/stretchr/testify/assert" +) + +/////////////////////////////////////////////////////////////////////////////// +// TEST SET-UP + +var ( + client *gemini.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("GEMINI_API_KEY") + if api_key == "" { + log.Print("GEMINI_API_KEY not set") + os.Exit(0) + } + + // Create client + var err error + client, err = gemini.New(api_key, opts.OptTrace(os.Stderr, verbose)) + if err != nil { + log.Println(err) + os.Exit(-1) + } + os.Exit(m.Run()) +} + +/////////////////////////////////////////////////////////////////////////////// +// TESTS + +func Test_client_001(t *testing.T) { + assert := assert.New(t) + assert.NotNil(client) + t.Log(client) +} diff --git a/pkg/gemini/model.go b/pkg/gemini/model.go new file mode 100644 index 0000000..eaa83e9 --- /dev/null +++ b/pkg/gemini/model.go @@ -0,0 +1,129 @@ +package gemini + +import ( + "context" + "encoding/json" + + "github.com/mutablelogic/go-client" + "github.com/mutablelogic/go-llm" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +type model struct { + *Client `json:"-"` + meta Model +} + +var _ llm.Model = (*model)(nil) + +type Model struct { + Name string `json:"name"` + Version string `json:"version"` + DisplayName string `json:"displayName"` + Description string `json:"description"` + InputTokenLimit uint64 `json:"inputTokenLimit"` + OutputTokenLimit uint64 `json:"outputTokenLimit"` + SupportedGenerationMethods []string `json:"supportedGenerationMethods"` + Temperature float64 `json:"temperature"` + TopP float64 `json:"topP"` + TopK uint64 `json:"topK"` +} + +/////////////////////////////////////////////////////////////////////////////// +// 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 - llm.Model implementation + +// Return model name +func (m model) Name() string { + return m.meta.Name +} + +// Return the models +func (gemini *Client) Models(ctx context.Context) ([]llm.Model, error) { + // Cache models + if gemini.cache == nil { + models, err := gemini.ListModels(ctx) + if err != nil { + return nil, err + } + gemini.cache = make(map[string]llm.Model, len(models)) + for _, m := range models { + gemini.cache[m.Name] = &model{gemini, m} + } + } + + // Return models + result := make([]llm.Model, 0, len(gemini.cache)) + for _, model := range gemini.cache { + result = append(result, model) + } + return result, nil +} + +// Return a model by name, or nil if not found. +// Panics on error. +func (openai *Client) Model(ctx context.Context, name string) llm.Model { + if openai.cache == nil { + if _, err := openai.Models(ctx); err != nil { + panic(err) + } + } + return openai.cache[name] +} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS - API + +// ListModels returns all the models +func (c *Client) ListModels(ctx context.Context) ([]Model, error) { + // Response + var response struct { + Data []Model `json:"models"` + } + if err := c.DoWithContext(ctx, nil, &response, client.OptPath("models")); err != nil { + return nil, err + } + + // Return success + return response.Data, nil +} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS - MODEL + +// Return am empty session context object for the model, +// setting session options +func (m model) Context(...llm.Opt) llm.Context { + return nil +} + +// Create a completion from a text prompt +func (m model) Completion(context.Context, string, ...llm.Opt) (llm.Completion, error) { + return nil, llm.ErrNotImplemented +} + +// Create a completion from a chat session +func (m model) Chat(context.Context, []llm.Completion, ...llm.Opt) (llm.Completion, error) { + return nil, llm.ErrNotImplemented +} + +// Embedding vector generation +func (m model) Embedding(context.Context, string, ...llm.Opt) ([]float64, error) { + return nil, llm.ErrNotImplemented +} diff --git a/pkg/gemini/model_test.go b/pkg/gemini/model_test.go new file mode 100644 index 0000000..3d18877 --- /dev/null +++ b/pkg/gemini/model_test.go @@ -0,0 +1,22 @@ +package gemini_test + +import ( + "context" + "encoding/json" + "testing" + + // Packages + assert "github.com/stretchr/testify/assert" +) + +func Test_models_001(t *testing.T) { + assert := assert.New(t) + + 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 5b510e14bf617a604e4cbe77d032d3ba0a98fca1 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 7 Feb 2025 08:09:37 +0100 Subject: [PATCH 15/25] Updated ollama --- pkg/gemini/model.go | 9 +- pkg/ollama/client_test.go | 3 +- pkg/ollama/completion_test.go | 187 +++++++++++++++++++++++++++++ pkg/ollama/completion_test.go_old | 189 ------------------------------ pkg/ollama/opt.go | 8 +- pkg/ollama/session_test.go_old | 58 --------- pkg/openai/completion_test.go | 5 +- pkg/{ => ui}/telegram/telegram.go | 0 pkg/ui/term/term.go | 89 ++++++++++++++ pkg/ui/ui.go | 14 +++ 10 files changed, 306 insertions(+), 256 deletions(-) create mode 100644 pkg/ollama/completion_test.go delete mode 100644 pkg/ollama/completion_test.go_old delete mode 100644 pkg/ollama/session_test.go_old rename pkg/{ => ui}/telegram/telegram.go (100%) create mode 100644 pkg/ui/term/term.go create mode 100644 pkg/ui/ui.go diff --git a/pkg/gemini/model.go b/pkg/gemini/model.go index eaa83e9..ed682d4 100644 --- a/pkg/gemini/model.go +++ b/pkg/gemini/model.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" + // Packages "github.com/mutablelogic/go-client" "github.com/mutablelogic/go-llm" ) @@ -78,13 +79,13 @@ func (gemini *Client) Models(ctx context.Context) ([]llm.Model, error) { // Return a model by name, or nil if not found. // Panics on error. -func (openai *Client) Model(ctx context.Context, name string) llm.Model { - if openai.cache == nil { - if _, err := openai.Models(ctx); err != nil { +func (gemini *Client) Model(ctx context.Context, name string) llm.Model { + if gemini.cache == nil { + if _, err := gemini.Models(ctx); err != nil { panic(err) } } - return openai.cache[name] + return gemini.cache[name] } /////////////////////////////////////////////////////////////////////////////// diff --git a/pkg/ollama/client_test.go b/pkg/ollama/client_test.go index e1987c2..a7f3452 100644 --- a/pkg/ollama/client_test.go +++ b/pkg/ollama/client_test.go @@ -6,6 +6,7 @@ import ( "os" "strconv" "testing" + "time" // Packages opts "github.com/mutablelogic/go-client" @@ -40,7 +41,7 @@ func TestMain(m *testing.M) { // Create client var err error - client, err = ollama.New(endpoint_url, opts.OptTrace(os.Stderr, verbose)) + client, err = ollama.New(endpoint_url, opts.OptTrace(os.Stderr, verbose), opts.OptTimeout(5*time.Minute)) if err != nil { log.Println(err) os.Exit(-1) diff --git a/pkg/ollama/completion_test.go b/pkg/ollama/completion_test.go new file mode 100644 index 0000000..0ae884d --- /dev/null +++ b/pkg/ollama/completion_test.go @@ -0,0 +1,187 @@ +package ollama_test + +import ( + "context" + "fmt" + "os" + "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" +) + +func Test_completion_001(t *testing.T) { + assert := assert.New(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() + } + + // Get a completion + response, err := model.Completion(context.TODO(), "Hello, how are you?") + if assert.NoError(err) { + assert.NotEmpty(response) + } +} + +func Test_completion_002(t *testing.T) { + assert := assert.New(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) { + response, err := model.Completion(context.TODO(), "Tell me in less than five words why the sky is blue?", llm.WithTemperature(0.5)) + if assert.NoError(err) { + t.Log(response) + } + }) + + t.Run("TopP", func(t *testing.T) { + response, err := model.Completion(context.TODO(), "Tell me in less than five words why the sky is blue?", llm.WithTopP(0.5)) + if assert.NoError(err) { + t.Log(response) + } + }) + + t.Run("TopK", func(t *testing.T) { + response, err := model.Completion(context.TODO(), "Tell me in less than five words why the sky is blue?", llm.WithTopK(50)) + if assert.NoError(err) { + t.Log(response) + } + }) + + t.Run("Stop", func(t *testing.T) { + response, err := model.Completion(context.TODO(), "Tell me in less than five words why the sky is blue?", llm.WithStopSequence("sky")) + if assert.NoError(err) { + t.Log(response) + } + }) + + t.Run("System", func(t *testing.T) { + response, err := model.Completion(context.TODO(), "Tell me in less than five words why the sky is blue?", llm.WithSystemPrompt("reply as if you are shakespeare")) + if assert.NoError(err) { + t.Log(response) + } + }) + + t.Run("Seed", func(t *testing.T) { + response, err := model.Completion(context.TODO(), "Tell me in less than five words why the sky is blue?", llm.WithSeed(123)) + if assert.NoError(err) { + t.Log(response) + } + }) + + t.Run("Format", func(t *testing.T) { + response, err := model.Completion(context.TODO(), "Why the sky is blue? Reply in JSON format", llm.WithFormat("json")) + if assert.NoError(err) { + t.Log(response) + } + }) + + t.Run("FrequencyPenalty", func(t *testing.T) { + response, err := model.Completion(context.TODO(), "Why the sky is blue?", llm.WithFrequencyPenalty(1.0)) + if assert.NoError(err) { + t.Log(response) + } + }) +} + +func Test_completion_003(t *testing.T) { + assert := assert.New(t) + + // Pull the model + model, err := client.PullModel(context.TODO(), "llama3.2-vision", ollama.WithPullStatus(func(status *ollama.PullStatus) { + t.Log(status) + })) + if err != nil { + t.FailNow() + } + + t.Run("Vision", func(t *testing.T) { + f, err := os.Open("testdata/guggenheim.jpg") + if !assert.NoError(err) { + t.FailNow() + } + defer f.Close() + response, err := model.Completion(context.TODO(), "Describe this image", llm.WithAttachment(f)) + if assert.NoError(err) { + t.Log(response) + } + }) +} + +func Test_completion_004(t *testing.T) { + assert := assert.New(t) + + // Pull the model + model, err := client.PullModel(context.TODO(), "mistral", ollama.WithPullStatus(func(status *ollama.PullStatus) { + t.Log(status) + })) + if err != nil { + t.FailNow() + } + + // Test tool support + t.Run("Toolkit", func(t *testing.T) { + toolkit := tool.NewToolKit() + toolkit.Register(&weather{}) + + session := model.Context(llm.WithToolKit(toolkit)) + err := session.FromUser(context.TODO(), + "What is the weather in the capital city of Germany?", + ) + if !assert.NoError(err) { + t.FailNow() + } + + assert.Equal("assistant", session.Role()) + assert.Greater(session.Num(), 0) + assert.NotEmpty(session.ToolCalls(0)) + + toolcalls := session.ToolCalls(0) + assert.NotEmpty(toolcalls) + assert.Equal("weather_in_city", toolcalls[0].Name()) + + results, err := toolkit.Run(context.TODO(), toolcalls...) + if !assert.NoError(err) { + t.FailNow() + } + + assert.Len(results, len(toolcalls)) + + err = session.FromTool(context.TODO(), results...) + if !assert.NoError(err) { + t.FailNow() + } + }) +} + +type weather struct { + City string `json:"city" help:"The city to get the weather for" required:"true"` +} + +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) { + return fmt.Sprintf("The weather in %q is sunny and warm", w.City), nil +} diff --git a/pkg/ollama/completion_test.go_old b/pkg/ollama/completion_test.go_old deleted file mode 100644 index b0ea189..0000000 --- a/pkg/ollama/completion_test.go_old +++ /dev/null @@ -1,189 +0,0 @@ -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" -) - -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) - }) - - 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/opt.go b/pkg/ollama/opt.go index b1b81f9..8785f3f 100644 --- a/pkg/ollama/opt.go +++ b/pkg/ollama/opt.go @@ -1,6 +1,7 @@ package ollama import ( + "strconv" "strings" "time" @@ -90,10 +91,13 @@ func optTools(agent *Client, opts *llm.Opts) []llm.Tool { func optFormat(opts *llm.Opts) string { format := strings.ToLower(opts.GetString("format")) + if format == "" { + return "" + } if format == "json_format" { - return "json" + return strconv.Quote("json") } - return format + return strconv.Quote(format) } func optStopSequence(opts *llm.Opts) []string { diff --git a/pkg/ollama/session_test.go_old b/pkg/ollama/session_test.go_old deleted file mode 100644 index e343eff..0000000 --- a/pkg/ollama/session_test.go_old +++ /dev/null @@ -1,58 +0,0 @@ -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/openai/completion_test.go b/pkg/openai/completion_test.go index c4bbaa8..fc5a3c6 100644 --- a/pkg/openai/completion_test.go +++ b/pkg/openai/completion_test.go @@ -359,11 +359,12 @@ func Test_completion_004(t *testing.T) { if !assert.NotNil(model) { t.FailNow() } - toolkit := tool.NewToolKit() - toolkit.Register(weather{}) // Test tool support t.Run("Toolkit", func(t *testing.T) { + toolkit := tool.NewToolKit() + toolkit.Register(weather{}) + r, err := model.Completion( context.TODO(), "What is the weather in the capital city of Germany?", diff --git a/pkg/telegram/telegram.go b/pkg/ui/telegram/telegram.go similarity index 100% rename from pkg/telegram/telegram.go rename to pkg/ui/telegram/telegram.go diff --git a/pkg/ui/term/term.go b/pkg/ui/term/term.go new file mode 100644 index 0000000..f7bb5da --- /dev/null +++ b/pkg/ui/term/term.go @@ -0,0 +1,89 @@ +package term + +import ( + "fmt" + "io" + "os" + + // Packages + format "github.com/MichaelMure/go-term-text" + color "github.com/fatih/color" + term "golang.org/x/term" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +type Term struct { + r io.Reader + fd int + *term.Terminal +} + +/////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +func NewTerm(r io.Reader) (*Term, error) { + t := new(Term) + t.r = r + + // Set file descriptor + if osf, ok := r.(*os.File); ok { + t.fd = int(osf.Fd()) + if term.IsTerminal(t.fd) { + t.Terminal = term.NewTerminal(osf, "") + } + } + + // Return success + return t, nil +} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Returns the width and height of the terminal, or (0,0) if we aren't in +// a terminal +func (t *Term) Size() (int, int) { + if t.Terminal != nil { + if w, h, err := term.GetSize(t.fd); err == nil { + return w, h + } + } + // Unable to get the size + return 0, 0 +} + +func (t *Term) Println(v ...any) { + text := fmt.Sprint(v...) + w, _ := t.Size() + if w > 0 { + text, _ = format.Wrap(text, w) + } + fmt.Fprintln(os.Stdout, text) +} + +func (t *Term) ReadLine(prompt string) (string, error) { + // Set terminal raw mode + if t.Terminal != nil { + state, err := term.MakeRaw(t.fd) + if err != nil { + return "", err + } + defer term.Restore(t.fd, state) + } + + // Set the prompt with color + if t.Terminal != nil { + prompt = color.New(color.Bold).Sprint(prompt) + t.Terminal.SetPrompt(prompt) + } + + // Read the line + if t.Terminal != nil { + return t.Terminal.ReadLine() + } else { + // Don't support non-terminal input yet + return "", io.EOF + } +} diff --git a/pkg/ui/ui.go b/pkg/ui/ui.go new file mode 100644 index 0000000..25ef44e --- /dev/null +++ b/pkg/ui/ui.go @@ -0,0 +1,14 @@ +package ui + +import "context" + +////////////////////////////////////////////////////////////////////////////// +// TYPES + +type UI interface { + // Run the runloop for the UI + Run(ctx context.Context) error + + // Send a system message + SysPrint(format string, args ...interface{}) error +} From b56655feeaf476d98f06814b6f5b5c9e6795b047 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 7 Feb 2025 08:34:33 +0100 Subject: [PATCH 16/25] Updated --- README.md | 108 +++++++++++++++++++++++++++++++++++++++-- model.go | 9 ++-- pkg/ollama/opt.go | 4 +- pkg/session/session.go | 13 ++++- 4 files changed, 120 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 1f3e7fc..a482350 100644 --- a/README.md +++ b/README.md @@ -374,7 +374,110 @@ The transation of field types is as follows: * `uint`, `int` - Translates to JSON `integer` * `float32`, `float64` - Translates to JSON `number` -## Options +## Complete and Chat Options + +These are the options you can use with the `Completion` and `Chat` methods. + + + + + + + + + + + + + + + + + + + +
OllamaAnthropicMistralOpenAIGemini
+ llm.WithTemperature(float64) + 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. +
YesYesYesYesYes
+ +## Embedding Options + +These are the options you can include for the `Embedding`method. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OllamaAnthropicMistralOpenAIGemini
+ ollama.WithKeepAlive(time.Duration) + Controls how long the model will stay loaded into memory following the request +
YesNoNoNoNo
+ ollama.WithTruncate() + Does not truncate the end of each input to fit within context length. Returns error if context length is exceeded. +
YesNoNoNoNo
+ ollama.WithOption(string, any) + Set model-specific option value. +
YesNoNoNoNo
+ openai.WithDimensions(uint64) + The number of dimensions the resulting output embeddings + should have. Only supported in text-embedding-3 and later models. +
NoNoNoYesNo
+ llm.WithFormat(string) + The format to return the embeddings in. Can be either . +
NoNo'float''float' or 'base64'No
+ +## Older Content You can add options to sessions, or to prompts. Different providers and models support different options. @@ -389,9 +492,6 @@ type Model interface { // Create a completion from a text prompt Completion(context.Context, string, ...Opt) (Completion, error) - // Create a completion from a chat session - Chat(context.Context, []Completion, ...Opt) (Completion, error) - // Embedding vector generation Embedding(context.Context, string, ...Opt) ([]float64, error) } diff --git a/model.go b/model.go index 456ecf2..0da4f4b 100644 --- a/model.go +++ b/model.go @@ -5,22 +5,19 @@ import ( ) // An Model can be used to generate a response to a user prompt, -// which is passed to an agent. The interaction occurs through +// which is passed to an agent. A back-and-forth interaction occurs through // a session context object. type Model interface { // Return the name of the model Name() string - // Return am empty session context object for the model, - // setting session options + // Return am empty session context object for the model, setting + // session options Context(...Opt) Context // Create a completion from a text prompt Completion(context.Context, string, ...Opt) (Completion, error) - // Create a completion from a chat session - Chat(context.Context, []Completion, ...Opt) (Completion, error) - // Embedding vector generation Embedding(context.Context, string, ...Opt) ([]float64, error) } diff --git a/pkg/ollama/opt.go b/pkg/ollama/opt.go index 8785f3f..2127d49 100644 --- a/pkg/ollama/opt.go +++ b/pkg/ollama/opt.go @@ -21,9 +21,9 @@ func WithInsecure() llm.Opt { } // Embeddings: Does not truncate the end of each input to fit within context length. Returns error if context length is exceeded. -func WithTruncate(v bool) llm.Opt { +func WithTruncate() llm.Opt { return func(o *llm.Opts) error { - o.Set("truncate", v) + o.Set("truncate", true) return nil } } diff --git a/pkg/session/session.go b/pkg/session/session.go index f557190..e9e2994 100644 --- a/pkg/session/session.go +++ b/pkg/session/session.go @@ -23,12 +23,17 @@ type MessageFactory interface { ToolResults(...llm.ToolResult) ([]llm.Completion, error) } +type Model interface { + // Additional method for a context object + Chat(ctx context.Context, completions []llm.Completion, opts ...llm.Opt) (llm.Completion, error) +} + /////////////////////////////////////////////////////////////////////////////// // TYPES // A chat session with history type session struct { - model llm.Model // The model used for the session + model Model // The model used for the session opts []llm.Opt // Options to apply to the session seq []llm.Completion // Sequence of messages factory MessageFactory // Factory for generating messages @@ -41,8 +46,12 @@ var _ llm.Context = (*session)(nil) // Create a new empty session to store a context window func NewSession(model llm.Model, factory MessageFactory, opts ...llm.Opt) *session { + chatmodel, ok := model.(Model) + if !ok || model == nil { + panic("Model does not implement the session.Model interface") + } return &session{ - model: model, + model: chatmodel, opts: opts, seq: make([]llm.Completion, 0, 10), factory: factory, From 9247d077b656878459e54f762f308112742b5f9a Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 7 Feb 2025 09:57:29 +0100 Subject: [PATCH 17/25] Updated --- pkg/internal/impl/cache.go | 66 ++++++++ pkg/{session => internal/impl}/session.go | 2 +- pkg/mistral/client.go | 43 +---- pkg/mistral/completion.go | 54 +++++++ pkg/mistral/content.go | 47 ++++++ pkg/mistral/message.go | 152 +++--------------- pkg/mistral/messagefactory.go | 79 +++++++++ pkg/mistral/model.go | 88 ++++++++-- pkg/mistral/opt.go | 2 +- pkg/mistral/{session.go => session.go_old} | 0 .../{session_test.go => session_test.go_old} | 0 pkg/mistral/tool.go | 29 ++++ pkg/openai/message.go | 74 +-------- pkg/openai/messagefactory.go | 79 +++++++++ pkg/openai/model.go | 19 ++- 15 files changed, 472 insertions(+), 262 deletions(-) create mode 100644 pkg/internal/impl/cache.go rename pkg/{session => internal/impl}/session.go (99%) create mode 100644 pkg/mistral/content.go create mode 100644 pkg/mistral/messagefactory.go rename pkg/mistral/{session.go => session.go_old} (100%) rename pkg/mistral/{session_test.go => session_test.go_old} (100%) create mode 100644 pkg/openai/messagefactory.go diff --git a/pkg/internal/impl/cache.go b/pkg/internal/impl/cache.go new file mode 100644 index 0000000..1c91376 --- /dev/null +++ b/pkg/internal/impl/cache.go @@ -0,0 +1,66 @@ +package impl + +import ( + // Packages + "sync" + + llm "github.com/mutablelogic/go-llm" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +type ModelCache struct { + sync.RWMutex + cache map[string]llm.Model +} + +type ModelLoadFunc func() ([]llm.Model, error) + +/////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +func NewModelCache() *ModelCache { + cache := new(ModelCache) + cache.cache = make(map[string]llm.Model, 20) + return cache +} + +/////////////////////////////////////////////////////////////////////////////// +// METHODS + +// Load models and return them +func (c *ModelCache) Load(fn ModelLoadFunc) ([]llm.Model, error) { + c.Lock() + defer c.Unlock() + + // Load models + if len(c.cache) == 0 { + if models, err := fn(); err != nil { + return nil, err + } else { + for _, m := range models { + c.cache[m.Name()] = m + } + } + } + + // 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 +func (c *ModelCache) Get(fn ModelLoadFunc, name string) (llm.Model, error) { + if len(c.cache) == 0 { + if _, err := c.Load(fn); err != nil { + return nil, err + } + } + c.RLock() + defer c.RUnlock() + return c.cache[name], nil +} diff --git a/pkg/session/session.go b/pkg/internal/impl/session.go similarity index 99% rename from pkg/session/session.go rename to pkg/internal/impl/session.go index e9e2994..d56846e 100644 --- a/pkg/session/session.go +++ b/pkg/internal/impl/session.go @@ -1,4 +1,4 @@ -package session +package impl import ( "context" diff --git a/pkg/mistral/client.go b/pkg/mistral/client.go index f70f8bf..ac7a523 100644 --- a/pkg/mistral/client.go +++ b/pkg/mistral/client.go @@ -1,14 +1,14 @@ /* -mistral implements an API client for mistral (https://docs.mistral.ai/api/) +mistral implements an API client for mistral +https://docs.mistral.ai/api/ */ package mistral import ( - "context" - // Packages client "github.com/mutablelogic/go-client" llm "github.com/mutablelogic/go-llm" + impl "github.com/mutablelogic/go-llm/pkg/internal/impl" ) /////////////////////////////////////////////////////////////////////////////// @@ -16,7 +16,7 @@ import ( type Client struct { *client.Client - cache map[string]llm.Model + *impl.ModelCache } var _ llm.Agent = (*Client)(nil) @@ -46,7 +46,7 @@ func New(ApiKey string, opts ...client.ClientOpt) (*Client, error) { } // Return the client - return &Client{client, nil}, nil + return &Client{client, impl.NewModelCache()}, nil } /////////////////////////////////////////////////////////////////////////////// @@ -56,36 +56,3 @@ func New(ApiKey string, opts ...client.ClientOpt) (*Client, error) { func (Client) Name() string { return defaultName } - -// Return the models -func (c *Client) Models(ctx context.Context) ([]llm.Model, error) { - // 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 { - if c.cache == nil { - if _, err := c.Models(ctx); err != nil { - panic(err) - } - } - return c.cache[name] -} diff --git a/pkg/mistral/completion.go b/pkg/mistral/completion.go index f008138..9b55a44 100644 --- a/pkg/mistral/completion.go +++ b/pkg/mistral/completion.go @@ -5,6 +5,7 @@ import ( "encoding/json" "strings" + // Packages "github.com/mutablelogic/go-client" "github.com/mutablelogic/go-llm" ) @@ -22,6 +23,17 @@ type Response struct { Metrics `json:"usage,omitempty"` } +// Possible completions +type Completions []Completion + +// Completion Variation +type Completion struct { + Index uint64 `json:"index"` + Message *Message `json:"message"` + Delta *Message `json:"delta,omitempty"` // For streaming + Reason string `json:"finish_reason,omitempty"` +} + // Metrics type Metrics struct { InputTokens uint64 `json:"prompt_tokens,omitempty"` @@ -203,3 +215,45 @@ func appendCompletion(response *Response, c *Completion) { } } } + +/////////////////////////////////////////////////////////////////////////////// +// 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) { + return nil + } + 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/content.go b/pkg/mistral/content.go new file mode 100644 index 0000000..994ddd0 --- /dev/null +++ b/pkg/mistral/content.go @@ -0,0 +1,47 @@ +package mistral + +import ( + "net/url" + + "github.com/mutablelogic/go-llm" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +type Content struct { + Type string `json:"type"` // text or content + *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 + +/////////////////////////////////////////////////////////////////////////////// +// LICECYCLE + +func NewPrediction(content Prediction) *Content { + return &Content{Type: "content", Prediction: &content} +} + +func NewTextContext(text Text) *Content { + return &Content{Type: "text", Text: &text} +} + +func NewImageData(image *llm.Attachment) *Content { + url := Image(image.Url()) + return &Content{Type: "image_url", Image: &url} +} + +func NewImageUrl(u *url.URL) *Content { + url := Image(u.String()) + return &Content{Type: "image_url", Image: &url} +} diff --git a/pkg/mistral/message.go b/pkg/mistral/message.go index 6300b9e..70cb108 100644 --- a/pkg/mistral/message.go +++ b/pkg/mistral/message.go @@ -1,25 +1,18 @@ package mistral import ( - "encoding/json" - // Packages "github.com/mutablelogic/go-llm" - "github.com/mutablelogic/go-llm/pkg/tool" ) /////////////////////////////////////////////////////////////////////////////// // TYPES -// Possible completions -type Completions []Completion - -var _ llm.Completion = Completions{} - // Message with text or object content type Message struct { RoleContent - ToolCallArray `json:"tool_calls,omitempty"` + Calls ToolCalls `json:"tool_calls,omitempty"` + *ToolResults } type RoleContent struct { @@ -29,147 +22,52 @@ type RoleContent struct { Name string `json:"name,omitempty"` // function name - when role is tool } -// Completion Variation -type Completion struct { - Index uint64 `json:"index"` - Message *Message `json:"message"` - Delta *Message `json:"delta,omitempty"` // For streaming - 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 - *Prediction `json:"content,omitempty"` // prediction - *Image `json:"image_url,omitempty"` // image_url -} - -// A set of tool calls -type ToolCallArray []ToolCall - -// 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 (either in "text" or "prediction" field) -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, "") -} - -// 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 - MESSAGE -func (m Message) Num() int { +func (Message) Num() int { return 1 } -func (m Message) Role() string { - return m.RoleContent.Role +func (message *Message) Role() string { + return message.RoleContent.Role +} + +// Return the completion +func (message *Message) Choice(index int) llm.Completion { + if index != 0 { + return nil + } + return message } -func (m Message) Text(index int) string { +func (message *Message) Text(index int) string { if index != 0 { return "" } // If content is text, return it - if text, ok := m.Content.(string); ok { + if text, ok := message.Content.(string); ok { return text } // For other kinds, return empty string for the moment return "" } -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 { - 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) +// Unsupported +func (message *Message) Audio(index int) *llm.Attachment { + return nil } -// Return message for a specific completion -func (c Completions) Message(index int) *Message { - if index < 0 || index >= len(c) { +// Return all the tool calls +func (message *Message) ToolCalls(index int) []llm.ToolCall { + if index != 0 { return nil } - 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 + calls := make([]llm.ToolCall, 0, len(message.Calls)) + for _, call := range message.Calls { + calls = append(calls, call) } - return c[index].Message.ToolCalls(0) + return calls } diff --git a/pkg/mistral/messagefactory.go b/pkg/mistral/messagefactory.go new file mode 100644 index 0000000..9ff2d77 --- /dev/null +++ b/pkg/mistral/messagefactory.go @@ -0,0 +1,79 @@ +package mistral + +import ( + "encoding/json" + + // Packages + llm "github.com/mutablelogic/go-llm" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +type messagefactory struct{} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS - MESSAGE FACTORY + +func (messagefactory) SystemPrompt(prompt string) llm.Completion { + return &Message{ + RoleContent: RoleContent{ + Role: "system", + Content: prompt, + }, + } +} + +func (messagefactory) UserPrompt(prompt string, opts ...llm.Opt) (llm.Completion, error) { + // Get attachments + opt, err := llm.ApplyPromptOpts(opts...) + if err != nil { + return nil, err + } + + // Get attachments, allocate content + attachments := opt.Attachments() + content := make([]*Content, 1, len(attachments)+1) + + // Append the text and the attachments + content[0] = NewTextContext(Text(prompt)) + for _, attachment := range attachments { + content = append(content, NewImageData(attachment)) + } + + // Return success + return &Message{ + RoleContent: RoleContent{ + Role: "user", + Content: content, + }, + }, nil +} + +func (messagefactory) ToolResults(results ...llm.ToolResult) ([]llm.Completion, error) { + // Check for no results + if len(results) == 0 { + return nil, llm.ErrBadParameter.Withf("No tool results") + } + + // Create results + messages := make([]llm.Completion, 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", + Content: string(value), + }, + ToolResults: &ToolResults{ + Id: result.Call().Id(), + }, + }) + } + + // Return success + return messages, nil +} diff --git a/pkg/mistral/model.go b/pkg/mistral/model.go index ab7374b..ef4f393 100644 --- a/pkg/mistral/model.go +++ b/pkg/mistral/model.go @@ -4,8 +4,10 @@ import ( "context" "encoding/json" - "github.com/mutablelogic/go-client" - "github.com/mutablelogic/go-llm" + // Packages + client "github.com/mutablelogic/go-client" + llm "github.com/mutablelogic/go-llm" + impl "github.com/mutablelogic/go-llm/pkg/internal/impl" ) /////////////////////////////////////////////////////////////////////////////// @@ -52,33 +54,89 @@ func (m model) String() string { return string(data) } +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS - llm.Agent + +// Return the models +func (mistral *Client) Models(ctx context.Context) ([]llm.Model, error) { + return mistral.ModelCache.Load(func() ([]llm.Model, error) { + return mistral.loadmodels(ctx) + }) +} + +// Return a model by name, or nil if not found. +// Panics on error. +func (mistral *Client) Model(ctx context.Context, name string) llm.Model { + model, err := mistral.ModelCache.Get(func() ([]llm.Model, error) { + return mistral.loadmodels(ctx) + }, name) + if err != nil { + panic(err) + } + return model +} + +// Function called to load models +func (mistral *Client) loadmodels(ctx context.Context) ([]llm.Model, error) { + if models, err := mistral.ListModels(ctx); err != nil { + return nil, err + } else { + result := make([]llm.Model, len(models)) + for i, meta := range models { + result[i] = &model{mistral, meta} + } + return result, nil + } +} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS - llm.Model + +// Return model name +func (model model) Name() string { + return model.meta.Name +} + +// Return a new empty session +func (model *model) Context(opts ...llm.Opt) llm.Context { + return impl.NewSession(model, &messagefactory{}, opts...) +} + /////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS - API // ListModels returns all the models -func (c *Client) ListModels(ctx context.Context) ([]llm.Model, error) { +func (mistral *Client) ListModels(ctx context.Context) ([]Model, error) { // Response var response struct { Data []Model `json:"data"` } - if err := c.DoWithContext(ctx, nil, &response, client.OptPath("models")); err != nil { + if err := mistral.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{c, meta}) + // Return success + return response.Data, nil +} + +// GetModel returns one model +func (mistral *Client) GetModel(ctx context.Context, model string) (*Model, error) { + // Return the response + var response Model + if err := mistral.DoWithContext(ctx, nil, &response, client.OptPath("models", model)); err != nil { + return nil, err } - // Return models - return result, nil + // Return success + return &response, nil } -/////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - MODEL +// Delete a fine-tuned model +func (c *Client) DeleteModel(ctx context.Context, model string) error { + if err := c.DoWithContext(ctx, client.MethodDelete, nil, client.OptPath("models", model)); err != nil { + return err + } -// Return the name of the model -func (m model) Name() string { - return m.meta.Name + // Return success + return nil } diff --git a/pkg/mistral/opt.go b/pkg/mistral/opt.go index e76bc5d..ceb4e74 100644 --- a/pkg/mistral/opt.go +++ b/pkg/mistral/opt.go @@ -103,7 +103,7 @@ func optPrediction(opts *llm.Opts) *Content { if prediction == "" { return nil } - return NewContent("content", "", prediction) + return NewPrediction(Prediction(prediction)) } func optSafePrompt(opts *llm.Opts) bool { diff --git a/pkg/mistral/session.go b/pkg/mistral/session.go_old similarity index 100% rename from pkg/mistral/session.go rename to pkg/mistral/session.go_old diff --git a/pkg/mistral/session_test.go b/pkg/mistral/session_test.go_old similarity index 100% rename from pkg/mistral/session_test.go rename to pkg/mistral/session_test.go_old diff --git a/pkg/mistral/tool.go b/pkg/mistral/tool.go index 255146e..15607be 100644 --- a/pkg/mistral/tool.go +++ b/pkg/mistral/tool.go @@ -7,8 +7,11 @@ import ( /////////////////////////////////////////////////////////////////////////////// // TYPES +type ToolCalls []toolcall + type ToolCall struct { Id string `json:"id,omitempty"` // tool id + Type string `json:"type,omitempty"` // tool type (function) Index uint64 `json:"index,omitempty"` // tool index Function struct { Name string `json:"name,omitempty"` // tool name @@ -20,9 +23,17 @@ type toolcall struct { meta ToolCall } +type ToolResults struct { + Id string `json:"tool_call_id,omitempty"` +} + /////////////////////////////////////////////////////////////////////////////// // STRINGIFY +func (t *toolcall) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &t.meta) +} + func (t toolcall) MarshalJSON() ([]byte, error) { return json.Marshal(t.meta) } @@ -34,3 +45,21 @@ func (t toolcall) String() string { } return string(data) } + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// The tool name +func (t toolcall) Name() string { + return t.meta.Function.Name +} + +// The tool identifier +func (t toolcall) Id() string { + return t.meta.Id +} + +// Decode the calling parameters +func (t toolcall) Decode(v any) error { + return json.Unmarshal([]byte(t.meta.Function.Arguments), v) +} diff --git a/pkg/openai/message.go b/pkg/openai/message.go index e0c4cee..93522b9 100644 --- a/pkg/openai/message.go +++ b/pkg/openai/message.go @@ -1,8 +1,6 @@ package openai import ( - "encoding/json" - // Packages llm "github.com/mutablelogic/go-llm" ) @@ -10,8 +8,6 @@ import ( /////////////////////////////////////////////////////////////////////////////// // TYPES -type messagefactory struct{} - // Message with text or object content type Message struct { RoleContent @@ -34,76 +30,10 @@ type ToolResults struct { } /////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - MESSAGE FACTORY - -func (messagefactory) SystemPrompt(prompt string) llm.Completion { - return &Message{ - RoleContent: RoleContent{ - Role: "system", - Content: prompt, - }, - } -} - -func (messagefactory) UserPrompt(prompt string, opts ...llm.Opt) (llm.Completion, error) { - // Get attachments - opt, err := llm.ApplyPromptOpts(opts...) - if err != nil { - return nil, err - } - - // Get attachments, allocate content - attachments := opt.Attachments() - content := make([]*Content, 1, len(attachments)+1) - - // Append the text and the attachments - content[0] = NewTextContext(prompt) - for _, attachment := range attachments { - content = append(content, NewImageData(attachment)) - } - - // Return success - return &Message{ - RoleContent: RoleContent{ - Role: "user", - Content: content, - }, - }, nil -} - -func (messagefactory) ToolResults(results ...llm.ToolResult) ([]llm.Completion, error) { - // Check for no results - if len(results) == 0 { - return nil, llm.ErrBadParameter.Withf("No tool results") - } - - // Create results - messages := make([]llm.Completion, 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", - Content: string(value), - }, - ToolResults: &ToolResults{ - Id: result.Call().Id(), - }, - }) - } - - // Return success - return messages, nil -} - -/////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - COMPLETION +// PUBLIC METHODS // Return the number of completions -func (message *Message) Num() int { +func (Message) Num() int { return 1 } diff --git a/pkg/openai/messagefactory.go b/pkg/openai/messagefactory.go new file mode 100644 index 0000000..9d05210 --- /dev/null +++ b/pkg/openai/messagefactory.go @@ -0,0 +1,79 @@ +package openai + +import ( + "encoding/json" + + // Packages + llm "github.com/mutablelogic/go-llm" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +type messagefactory struct{} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS - MESSAGE FACTORY + +func (messagefactory) SystemPrompt(prompt string) llm.Completion { + return &Message{ + RoleContent: RoleContent{ + Role: "system", + Content: prompt, + }, + } +} + +func (messagefactory) UserPrompt(prompt string, opts ...llm.Opt) (llm.Completion, error) { + // Get attachments + opt, err := llm.ApplyPromptOpts(opts...) + if err != nil { + return nil, err + } + + // Get attachments, allocate content + attachments := opt.Attachments() + content := make([]*Content, 1, len(attachments)+1) + + // Append the text and the attachments + content[0] = NewTextContext(prompt) + for _, attachment := range attachments { + content = append(content, NewImageData(attachment)) + } + + // Return success + return &Message{ + RoleContent: RoleContent{ + Role: "user", + Content: content, + }, + }, nil +} + +func (messagefactory) ToolResults(results ...llm.ToolResult) ([]llm.Completion, error) { + // Check for no results + if len(results) == 0 { + return nil, llm.ErrBadParameter.Withf("No tool results") + } + + // Create results + messages := make([]llm.Completion, 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", + Content: string(value), + }, + ToolResults: &ToolResults{ + Id: result.Call().Id(), + }, + }) + } + + // Return success + return messages, nil +} diff --git a/pkg/openai/model.go b/pkg/openai/model.go index b7a943f..85f88d8 100644 --- a/pkg/openai/model.go +++ b/pkg/openai/model.go @@ -7,7 +7,7 @@ import ( // Packages client "github.com/mutablelogic/go-client" llm "github.com/mutablelogic/go-llm" - session "github.com/mutablelogic/go-llm/pkg/session" + impl "github.com/mutablelogic/go-llm/pkg/internal/impl" ) /////////////////////////////////////////////////////////////////////////////// @@ -43,12 +43,7 @@ func (m model) String() string { } /////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - llm.Model implementation - -// Return model name -func (m model) Name() string { - return m.meta.Name -} +// PUBLIC METHODS - llm.Agent // Return the models func (openai *Client) Models(ctx context.Context) ([]llm.Model, error) { @@ -83,9 +78,17 @@ func (openai *Client) Model(ctx context.Context, name string) llm.Model { return openai.cache[name] } +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS - llm.Model + +// Return model name +func (model model) Name() string { + return model.meta.Name +} + // Return a new empty session func (model *model) Context(opts ...llm.Opt) llm.Context { - return session.NewSession(model, &messagefactory{}, opts...) + return impl.NewSession(model, &messagefactory{}, opts...) } /////////////////////////////////////////////////////////////////////////////// From 26ea0f0ba01f59402bb99ea17e1d96ae505335b5 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 7 Feb 2025 10:43:38 +0100 Subject: [PATCH 18/25] Updated mistral --- cmd/llm/main.go | 6 +- {pkg/mistral => etc}/testdata/LICENSE | 0 {pkg/mistral => etc}/testdata/guggenheim.jpg | Bin pkg/agent/opt.go | 3 +- pkg/internal/impl/{cache.go => modelcache.go} | 0 pkg/mistral/completion.go | 179 +++++++++----- pkg/mistral/completion_test.go | 93 +++++++- pkg/mistral/message.go | 3 +- pkg/mistral/messagefactory.go | 5 +- pkg/mistral/session.go_old | 219 ------------------ pkg/mistral/session_test.go_old | 56 ----- pkg/mistral/tool.go | 4 - pkg/ollama/model.go | 4 +- 13 files changed, 221 insertions(+), 351 deletions(-) rename {pkg/mistral => etc}/testdata/LICENSE (100%) rename {pkg/mistral => etc}/testdata/guggenheim.jpg (100%) rename pkg/internal/impl/{cache.go => modelcache.go} (100%) delete mode 100644 pkg/mistral/session.go_old delete mode 100644 pkg/mistral/session_test.go_old diff --git a/cmd/llm/main.go b/cmd/llm/main.go index 37af988..a3e568f 100644 --- a/cmd/llm/main.go +++ b/cmd/llm/main.go @@ -127,10 +127,10 @@ func main() { if cli.AnthropicKey != "" { opts = append(opts, agent.WithAnthropic(cli.AnthropicKey, clientopts...)) } - if cli.MistralKey != "" { - opts = append(opts, agent.WithMistral(cli.MistralKey, clientopts...)) - } */ + if cli.MistralKey != "" { + opts = append(opts, agent.WithMistral(cli.MistralKey, clientopts...)) + } if cli.OpenAIKey != "" { opts = append(opts, agent.WithOpenAI(cli.OpenAIKey, clientopts...)) } diff --git a/pkg/mistral/testdata/LICENSE b/etc/testdata/LICENSE similarity index 100% rename from pkg/mistral/testdata/LICENSE rename to etc/testdata/LICENSE diff --git a/pkg/mistral/testdata/guggenheim.jpg b/etc/testdata/guggenheim.jpg similarity index 100% rename from pkg/mistral/testdata/guggenheim.jpg rename to etc/testdata/guggenheim.jpg diff --git a/pkg/agent/opt.go b/pkg/agent/opt.go index f410bb3..c69e765 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" gemini "github.com/mutablelogic/go-llm/pkg/gemini" + "github.com/mutablelogic/go-llm/pkg/mistral" ollama "github.com/mutablelogic/go-llm/pkg/ollama" openai "github.com/mutablelogic/go-llm/pkg/openai" ) @@ -34,6 +35,7 @@ func WithAnthropic(key string, opts ...client.ClientOpt) llm.Opt { } } } +*/ func WithMistral(key string, opts ...client.ClientOpt) llm.Opt { return func(o *llm.Opts) error { @@ -45,7 +47,6 @@ func WithMistral(key string, opts ...client.ClientOpt) llm.Opt { } } } -*/ func WithOpenAI(key string, opts ...client.ClientOpt) llm.Opt { return func(o *llm.Opts) error { diff --git a/pkg/internal/impl/cache.go b/pkg/internal/impl/modelcache.go similarity index 100% rename from pkg/internal/impl/cache.go rename to pkg/internal/impl/modelcache.go diff --git a/pkg/mistral/completion.go b/pkg/mistral/completion.go index 9b55a44..75aab09 100644 --- a/pkg/mistral/completion.go +++ b/pkg/mistral/completion.go @@ -20,7 +20,7 @@ type Response struct { Created uint64 `json:"created"` Model string `json:"model"` Completions `json:"choices"` - Metrics `json:"usage,omitempty"` + *Metrics `json:"usage,omitempty"` } // Possible completions @@ -54,78 +54,98 @@ func (r Response) String() string { return string(data) } +func (c Completion) String() string { + data, err := json.MarshalIndent(c, "", " ") + if err != nil { + return err.Error() + } + return string(data) +} + +func (m Metrics) String() string { + data, err := json.MarshalIndent(m, "", " ") + 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 []*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"` + 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"` + 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"` + NumCompletions uint64 `json:"n,omitempty"` + Prediction *Content `json:"prediction,omitempty"` + SafePrompt bool `json:"safe_prompt,omitempty"` + Messages []llm.Completion `json:"messages"` } +// Send a completion request with a single prompt, and return the next completion func (model *model) Completion(ctx context.Context, prompt string, opts ...llm.Opt) (llm.Completion, error) { - // TODO - return nil, llm.ErrNotImplemented + message, err := messagefactory{}.UserPrompt(prompt, opts...) + if err != nil { + return nil, err + } + return model.Chat(ctx, []llm.Completion{message}, opts...) } -func (mistral *Client) ChatCompletion(ctx context.Context, context llm.Context, opts ...llm.Opt) (*Response, error) { +// Send a completion request with multiple completions, and return the next completion +func (model *model) Chat(ctx context.Context, completions []llm.Completion, opts ...llm.Opt) (llm.Completion, error) { // Apply options opt, err := llm.ApplyOpts(opts...) if err != nil { return nil, err } - // Append the system prompt at the beginning - messages := make([]*Message, 0, len(context.(*session).seq)+1) + // Create the completions including the system prompt + messages := make([]llm.Completion, 0, len(completions)+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) + messages = append(messages, messagefactory{}.SystemPrompt(system)) } + messages = append(messages, completions...) // Request req, err := client.NewJSONRequest(reqChatCompletion{ - Model: context.(*session).model.Name(), + Model: model.Name(), Temperature: optTemperature(opt), TopP: optTopP(opt), MaxTokens: optMaxTokens(opt), Stream: optStream(opt), StopSequences: optStopSequences(opt), Seed: optSeed(opt), - Messages: messages, Format: optFormat(opt), - Tools: optTools(mistral, opt), + Tools: optTools(model.Client, opt), ToolChoice: optToolChoice(opt), PresencePenalty: optPresencePenalty(opt), FrequencyPenalty: optFrequencyPenalty(opt), - NumChoices: optNumCompletions(opt), + NumCompletions: optNumCompletions(opt), Prediction: optPrediction(opt), SafePrompt: optSafePrompt(opt), + Messages: messages, }) if err != nil { return nil, err } + // Response options var response Response reqopts := []client.RequestOpt{ client.OptPath("chat", "completions"), } + + // Streaming if optStream(opt) { reqopts = append(reqopts, client.OptTextStreamCallback(func(evt client.TextStreamEvent) error { if err := streamEvent(&response, evt); err != nil { @@ -139,7 +159,7 @@ func (mistral *Client) ChatCompletion(ctx context.Context, context llm.Context, } // Response - if err := mistral.DoWithContext(ctx, req, &response, reqopts...); err != nil { + if err := model.DoWithContext(ctx, req, &response, reqopts...); err != nil { return nil, err } @@ -148,7 +168,7 @@ func (mistral *Client) ChatCompletion(ctx context.Context, context llm.Context, } /////////////////////////////////////////////////////////////////////////////// -// PRIVATE METHODS +// PRIVATE METHODS - STREAMING func streamEvent(response *Response, evt client.TextStreamEvent) error { var delta Response @@ -164,28 +184,32 @@ func streamEvent(response *Response, evt client.TextStreamEvent) error { if delta.Id != "" { response.Id = delta.Id } + if delta.Type != "" { + response.Type = delta.Type + } if delta.Created != 0 { response.Created = delta.Created } if delta.Model != "" { response.Model = delta.Model } + + // Append the delta to the response 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 err := appendCompletion(response, &completion); err != nil { + return err + } } - if delta.Metrics.TotalTokens > 0 { - response.Metrics.TotalTokens += delta.Metrics.TotalTokens + + // Apend the metrics to the response + if delta.Metrics != nil { + response.Metrics = delta.Metrics } return nil } -func appendCompletion(response *Response, c *Completion) { +func appendCompletion(response *Response, c *Completion) error { + // Append a new completion for { if c.Index < uint64(len(response.Completions)) { break @@ -200,24 +224,67 @@ func appendCompletion(response *Response, c *Completion) { }, }) } - // Add the completion delta + + // Add the reason if c.Reason != "" { response.Completions[c.Index].Reason = c.Reason } + + // Get the completion + message := response.Completions[c.Index].Message + if message == nil { + return llm.ErrBadParameter + } + + // Add the role if role := c.Delta.Role(); role != "" { - response.Completions[c.Index].Message.RoleContent.Role = role + message.RoleContent.Role = 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 + // We only allow deltas which are strings at the moment + if c.Delta.Content != nil { + if str, ok := c.Delta.Content.(string); ok { + if text, ok := message.Content.(string); ok { + message.Content = text + str + } else { + message.Content = str + } + } else { + return llm.ErrNotImplemented.Withf("appendCompletion not implemented: %T", c.Delta.Content) } } + + // Append tool calls + for i := range c.Delta.Calls { + if i >= len(message.Calls) { + message.Calls = append(message.Calls, toolcall{}) + } + } + + for i, call := range c.Delta.Calls { + if call.meta.Id != "" { + message.Calls[i].meta.Id = call.meta.Id + } + if call.meta.Index != 0 { + message.Calls[i].meta.Index = call.meta.Index + } + if call.meta.Type != "" { + message.Calls[i].meta.Type = call.meta.Type + } + if call.meta.Function.Name != "" { + message.Calls[i].meta.Function.Name = call.meta.Function.Name + } + if call.meta.Function.Arguments != "" { + message.Calls[i].meta.Function.Arguments += call.meta.Function.Arguments + } + } + + // Return success + return nil } /////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - COMPLETIONS +// COMPLETIONS // Return the number of completions func (c Completions) Num() int { @@ -225,7 +292,7 @@ func (c Completions) Num() int { } // Return message for a specific completion -func (c Completions) Message(index int) *Message { +func (c Completions) Choice(index int) llm.Completion { if index < 0 || index >= len(c) { return nil } @@ -249,6 +316,14 @@ func (c Completions) Text(index int) string { return c[index].Message.Text(0) } +// Return audio content for a specific completion +func (c Completions) Audio(index int) *llm.Attachment { + if index < 0 || index >= len(c) { + return nil + } + return c[index].Message.Audio(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 { diff --git a/pkg/mistral/completion_test.go b/pkg/mistral/completion_test.go index 899d364..39f36ee 100644 --- a/pkg/mistral/completion_test.go +++ b/pkg/mistral/completion_test.go @@ -4,37 +4,111 @@ import ( "context" "fmt" "os" - "strings" "testing" // Packages + llm "github.com/mutablelogic/go-llm" - mistral "github.com/mutablelogic/go-llm/pkg/mistral" - tool "github.com/mutablelogic/go-llm/pkg/tool" + "github.com/mutablelogic/go-llm/pkg/tool" assert "github.com/stretchr/testify/assert" ) -func Test_chat_001(t *testing.T) { +func Test_completion_001(t *testing.T) { assert := assert.New(t) model := client.Model(context.TODO(), "mistral-small-latest") + if !assert.NotNil(model) { + t.FailNow() + } - if assert.NotNil(model) { - response, err := client.ChatCompletion(context.TODO(), model.UserPrompt("Hello, how are you?")) - assert.NoError(err) + response, err := model.Completion(context.TODO(), "Hello, how are you?") + if assert.NoError(err) { assert.NotEmpty(response) t.Log(response) } } +func Test_completion_004(t *testing.T) { + assert := assert.New(t) + + model := client.Model(context.TODO(), "mistral-small-latest") + if !assert.NotNil(model) { + t.FailNow() + } + + // Test tool support + t.Run("Toolkit", func(t *testing.T) { + toolkit := tool.NewToolKit() + toolkit.Register(&weather{}) + session := model.Context(llm.WithToolKit(toolkit)) + + assert.NoError(session.FromUser(context.TODO(), "What is the weather in the capital city of Germany?")) + + assert.Equal("assistant", session.Role()) + assert.Equal(1, session.Num()) + assert.NotEmpty(session.ToolCalls(0)) + + results, err := toolkit.Run(context.TODO(), session.ToolCalls(0)...) + assert.NoError(err) + assert.NotEmpty(results) + + assert.NoError(session.FromTool(context.TODO(), results...)) + + }) +} + +type weather struct { + City string `json:"city" help:"The city to get the weather for" required:"true"` +} + +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) { + return fmt.Sprintf("The weather in %q is sunny and warm", w.City), nil +} + +func Test_completion_005(t *testing.T) { + assert := assert.New(t) + model := client.Model(context.TODO(), "pixtral-12b-2409") + if !assert.NotNil(model) { + t.FailNow() + } + + // Test image captioning + t.Run("ImageCaption", func(t *testing.T) { + f, err := os.Open("../../etc/testdata/guggenheim.jpg") + if !assert.NoError(err) { + t.FailNow() + } + defer f.Close() + + r, err := model.Completion( + context.TODO(), + "Describe this picture", + llm.WithAttachment(f), + ) + if assert.NoError(err) { + assert.Equal("assistant", r.Role()) + assert.Equal(1, r.Num()) + } + }) +} + +/* func Test_chat_002(t *testing.T) { assert := assert.New(t) - model := client.Model(context.TODO(), "mistral-large-latest") + model := client.Model(context.TODO(), "mistral-small-latest") if !assert.NotNil(model) { t.FailNow() } t.Run("Temperature", func(t *testing.T) { - r, err := client.ChatCompletion(context.TODO(), model.UserPrompt("What is the temperature in London?"), llm.WithTemperature(0.5)) + r, err := client.Completion(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()) @@ -236,3 +310,4 @@ func (weather) Description() string { 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 70cb108..8af898a 100644 --- a/pkg/mistral/message.go +++ b/pkg/mistral/message.go @@ -12,14 +12,13 @@ import ( type Message struct { RoleContent Calls ToolCalls `json:"tool_calls,omitempty"` - *ToolResults } 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 + Id string `json:"tool_call_id,omitempty"` // tool call - when role is tool } var _ llm.Completion = (*Message)(nil) diff --git a/pkg/mistral/messagefactory.go b/pkg/mistral/messagefactory.go index 9ff2d77..07b8f10 100644 --- a/pkg/mistral/messagefactory.go +++ b/pkg/mistral/messagefactory.go @@ -66,10 +66,9 @@ func (messagefactory) ToolResults(results ...llm.ToolResult) ([]llm.Completion, messages = append(messages, &Message{ RoleContent: RoleContent{ Role: "tool", + Name: result.Call().Name(), Content: string(value), - }, - ToolResults: &ToolResults{ - Id: result.Call().Id(), + Id: result.Call().Id(), }, }) } diff --git a/pkg/mistral/session.go_old b/pkg/mistral/session.go_old deleted file mode 100644 index 3b50539..0000000 --- a/pkg/mistral/session.go_old +++ /dev/null @@ -1,219 +0,0 @@ -package mistral - -import ( - "context" - "encoding/json" - - // Packages - 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 []*Message // 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, - seq: make([]*Message, 0, 10), - } -} - -// 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...) - - // 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 -} - -/////////////////////////////////////////////////////////////////////////////// -// 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 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) -} - -// Generate a response from a user prompt (with attachments and -// other options) -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(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 -} - -/////////////////////////////////////////////////////////////////////////////// -// PRIVATE METHODS - -func systemPrompt(prompt string) *Message { - return &Message{ - RoleContent: RoleContent{ - Role: "system", - Content: prompt, - }, - } -} - -func userPrompt(prompt string, opts ...llm.Opt) (*Message, error) { - // Get attachments - opt, err := llm.ApplyPromptOpts(opts...) - if err != nil { - return nil, err - } - - // Get attachments, allocate content - attachments := opt.Attachments() - content := make([]*Content, 1, len(attachments)+1) - - // Append the text and the attachments - content[0] = NewTextContent(prompt) - for _, attachment := range attachments { - content = append(content, NewImageAttachment(attachment)) - } - - // Return success - return &Message{ - 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_old b/pkg/mistral/session_test.go_old deleted file mode 100644 index 7fbcaa3..0000000 --- a/pkg/mistral/session_test.go_old +++ /dev/null @@ -1,56 +0,0 @@ -package mistral_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 := 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) - 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) - 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/mistral/tool.go b/pkg/mistral/tool.go index 15607be..0eecbf9 100644 --- a/pkg/mistral/tool.go +++ b/pkg/mistral/tool.go @@ -23,10 +23,6 @@ type toolcall struct { meta ToolCall } -type ToolResults struct { - Id string `json:"tool_call_id,omitempty"` -} - /////////////////////////////////////////////////////////////////////////////// // STRINGIFY diff --git a/pkg/ollama/model.go b/pkg/ollama/model.go index 29b7c97..50d7211 100644 --- a/pkg/ollama/model.go +++ b/pkg/ollama/model.go @@ -9,7 +9,7 @@ import ( // Packages client "github.com/mutablelogic/go-client" llm "github.com/mutablelogic/go-llm" - "github.com/mutablelogic/go-llm/pkg/session" + impl "github.com/mutablelogic/go-llm/pkg/internal/impl" ) /////////////////////////////////////////////////////////////////////////////// @@ -111,7 +111,7 @@ func (ollama *Client) Model(ctx context.Context, name string) llm.Model { // Return a new empty session func (model *model) Context(opts ...llm.Opt) llm.Context { - return session.NewSession(model, &messagefactory{}, opts...) + return impl.NewSession(model, &messagefactory{}, opts...) } /////////////////////////////////////////////////////////////////////////////// From 0c84bb686ad7ac1da9a4d1b6a313bbfe186d5e8c Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 7 Feb 2025 10:54:08 +0100 Subject: [PATCH 19/25] Updated --- cmd/llm/complete.go | 18 +++++++++++++----- cmd/llm/main.go | 4 ++-- opt.go | 7 +++++++ 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/cmd/llm/complete.go b/cmd/llm/complete.go index d950933..8984936 100644 --- a/cmd/llm/complete.go +++ b/cmd/llm/complete.go @@ -16,11 +16,13 @@ import ( // TYPES type CompleteCmd struct { - Model string `arg:"" help:"Model name"` - Prompt string `arg:"" optional:"" help:"Prompt"` - File []string `type:"file" help:"Files to attach"` - System string `flag:"system" help:"Set the system prompt"` - NoStream bool `flag:"no-stream" help:"Do not stream output"` + Model string `arg:"" help:"Model name"` + Prompt string `arg:"" optional:"" help:"Prompt"` + File []string `type:"file" short:"f" help:"Files to attach"` + System string `flag:"system" help:"Set the system prompt"` + NoStream bool `flag:"no-stream" help:"Do not stream output"` + Format string `flag:"format" enum:"text,json" default:"text" help:"Output format. You may also need to specify the output in the system or user prompt."` + Temperature *float64 `flag:"temperature" short:"t" help:"Temperature for sampling"` } //////////////////////////////////////////////////////////////////////////////// @@ -98,5 +100,11 @@ func (cmd *CompleteCmd) opts() []llm.Opt { if cmd.System != "" { opts = append(opts, llm.WithSystemPrompt(cmd.System)) } + if cmd.Format == "json" { + opts = append(opts, llm.WithFormat("json")) + } + if cmd.Temperature != nil { + opts = append(opts, llm.WithTemperature(*cmd.Temperature)) + } return opts } diff --git a/cmd/llm/main.go b/cmd/llm/main.go index a3e568f..4544b06 100644 --- a/cmd/llm/main.go +++ b/cmd/llm/main.go @@ -23,8 +23,8 @@ import ( type Globals struct { // Debugging Debug bool `name:"debug" help:"Enable debug output"` - Verbose bool `name:"verbose" help:"Enable verbose output"` - Timeout time.Duration `name:"timeout" help:"Timeout for the command"` + Verbose bool `name:"verbose" short:"v" help:"Enable verbose output"` + Timeout time.Duration `name:"timeout" help:"Agent connection timeout"` // Agents Ollama `embed:"" help:"Ollama configuration"` diff --git a/opt.go b/opt.go index eb1a356..112a6c8 100644 --- a/opt.go +++ b/opt.go @@ -3,6 +3,7 @@ package llm import ( "encoding/json" "io" + "strings" "time" ) @@ -330,6 +331,12 @@ func WithSeed(v uint64) Opt { // Set format func WithFormat(v any) Opt { return func(o *Opts) error { + if v_, ok := v.(string); ok { + v_ = strings.TrimSpace(strings.ToLower(v_)) + if v_ == "json" { + v = "json_object" + } + } o.Set("format", v) return nil } From e896bc7aae93a49540840344343fac0499ab1623 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 7 Feb 2025 12:32:05 +0100 Subject: [PATCH 20/25] Updated --- cmd/llm/complete.go | 20 +++++++++-- cmd/llm/models.go | 63 +++++++++++++++++++++++++++------- go.mod | 1 + go.sum | 2 ++ model.go | 6 ++++ pkg/agent/agent.go | 24 ++++++++++--- pkg/gemini/model.go | 10 ++++++ pkg/mistral/completion_test.go | 3 +- pkg/mistral/model.go | 10 ++++++ pkg/ollama/model.go | 11 ++++++ pkg/openai/model.go | 10 ++++++ 11 files changed, 139 insertions(+), 21 deletions(-) diff --git a/cmd/llm/complete.go b/cmd/llm/complete.go index 8984936..e325e12 100644 --- a/cmd/llm/complete.go +++ b/cmd/llm/complete.go @@ -21,7 +21,7 @@ type CompleteCmd struct { File []string `type:"file" short:"f" help:"Files to attach"` System string `flag:"system" help:"Set the system prompt"` NoStream bool `flag:"no-stream" help:"Do not stream output"` - Format string `flag:"format" enum:"text,json" default:"text" help:"Output format. You may also need to specify the output in the system or user prompt."` + Format string `flag:"format" enum:"text,markdown,json" default:"text" help:"Output format"` Temperature *float64 `flag:"temperature" short:"t" help:"Temperature for sampling"` } @@ -97,14 +97,30 @@ func (cmd *CompleteCmd) Run(globals *Globals) error { func (cmd *CompleteCmd) opts() []llm.Opt { opts := []llm.Opt{} + + // Set system prompt + var system []string + if cmd.Format == "markdown" { + system = append(system, "Return the completion in markdown format.") + } else if cmd.Format == "json" { + system = append(system, "Return the completion in JSON format.") + } if cmd.System != "" { - opts = append(opts, llm.WithSystemPrompt(cmd.System)) + system = append(system, cmd.System) + } + if len(system) > 0 { + opts = append(opts, llm.WithSystemPrompt(strings.Join(system, "\n"))) } + + // Set format if cmd.Format == "json" { opts = append(opts, llm.WithFormat("json")) } + + // Set temperature if cmd.Temperature != nil { opts = append(opts, llm.WithTemperature(*cmd.Temperature)) } + return opts } diff --git a/cmd/llm/models.go b/cmd/llm/models.go index 2bf0072..42df39a 100644 --- a/cmd/llm/models.go +++ b/cmd/llm/models.go @@ -4,10 +4,15 @@ import ( "context" "encoding/json" "fmt" + "os" + "sort" + "strings" // Packages + tablewriter "github.com/djthorpe/go-tablewriter" llm "github.com/mutablelogic/go-llm" agent "github.com/mutablelogic/go-llm/pkg/agent" + "github.com/mutablelogic/go-llm/pkg/ollama" ) //////////////////////////////////////////////////////////////////////////////// @@ -39,16 +44,28 @@ func (cmd *ListToolsCmd) Run(globals *Globals) error { func (cmd *ListModelsCmd) Run(globals *Globals) error { return runagent(globals, func(ctx context.Context, client llm.Agent) error { - agent, ok := client.(*agent.Agent) + agent_, ok := client.(*agent.Agent) if !ok { return fmt.Errorf("No agents found") } - models, err := agent.ListModels(ctx, cmd.Agent...) + models, err := agent_.ListModels(ctx, cmd.Agent...) if err != nil { return err } - fmt.Println(models) - return nil + result := make(ModelList, 0, len(models)) + for _, model := range models { + result = append(result, Model{ + Agent: model.(*agent.Model).Agent, + Model: model.Name(), + Description: model.Description(), + Aliases: strings.Join(model.Aliases(), ", "), + }) + } + // Sort models by name + sort.Sort(result) + + // Write out the models + return tablewriter.New(os.Stdout).Write(result, tablewriter.OptOutputText(), tablewriter.OptHeader()) }) } @@ -82,14 +99,12 @@ func (cmd *DownloadModelCmd) Run(globals *Globals) error { } // Download the model switch agent.Name() { - /* - case "ollama": - model, err := agent.(*ollama.Client).PullModel(ctx, cmd.Model) - if err != nil { - return err - } - fmt.Println(model) - */ + case "ollama": + model, err := agent.(*ollama.Client).PullModel(ctx, cmd.Model) + if err != nil { + return err + } + fmt.Println(model) default: return fmt.Errorf("Agent %q does not support model download", agent.Name()) } @@ -116,3 +131,27 @@ func getagent(client llm.Agent, name string) llm.Agent { } return nil } + +// ////////////////////////////////////////////////////////////////////////////// +// MODEL LIST + +type Model struct { + Agent string `json:"agent" writer:"Agent,width:10"` + Model string `json:"model" writer:"Model,wrap,width:40"` + Description string `json:"description" writer:"Description,wrap,width:60"` + Aliases string `json:"aliases" writer:"Aliases,wrap,width:30"` +} + +type ModelList []Model + +func (models ModelList) Len() int { + return len(models) +} + +func (models ModelList) Less(a, b int) bool { + return strings.Compare(models[a].Model, models[b].Model) < 0 +} + +func (models ModelList) Swap(a, b int) { + models[a], models[b] = models[b], models[a] +} diff --git a/go.mod b/go.mod index fb5121f..8446bb4 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/MichaelMure/go-term-text v0.3.1 github.com/alecthomas/kong v1.7.0 github.com/djthorpe/go-errors v1.0.3 + github.com/djthorpe/go-tablewriter v0.0.7 github.com/fatih/color v1.9.0 github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 github.com/mutablelogic/go-client v1.0.10 diff --git a/go.sum b/go.sum index 4127777..48692ca 100644 --- a/go.sum +++ b/go.sum @@ -11,6 +11,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/djthorpe/go-errors v1.0.3 h1:GZeMPkC1mx2vteXLI/gvxZS0Ee9zxzwD1mcYyKU5jD0= github.com/djthorpe/go-errors v1.0.3/go.mod h1:HtfrZnMd6HsX75Mtbv9Qcnn0BqOrrFArvCaj3RMnZhY= +github.com/djthorpe/go-tablewriter v0.0.7 h1:jnNsJDjjLLCt0OAqB5DzGZN7V3beT1IpNMQ8GcOwZDU= +github.com/djthorpe/go-tablewriter v0.0.7/go.mod h1:NVBvytpL+6fHfCKn0+3lSi15/G3A1HWf2cLNeHg6YBg= github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= diff --git a/model.go b/model.go index 0da4f4b..f388fad 100644 --- a/model.go +++ b/model.go @@ -11,6 +11,12 @@ type Model interface { // Return the name of the model Name() string + // Return the description of the model + Description() string + + // Return any model aliases + Aliases() []string + // Return am empty session context object for the model, setting // session options Context(...Opt) Context diff --git a/pkg/agent/agent.go b/pkg/agent/agent.go index cd50c87..3766917 100644 --- a/pkg/agent/agent.go +++ b/pkg/agent/agent.go @@ -18,7 +18,7 @@ type Agent struct { *llm.Opts } -type model struct { +type Model struct { Agent string `json:"agent"` llm.Model `json:"model"` } @@ -44,7 +44,7 @@ func New(opts ...llm.Opt) (*Agent, error) { /////////////////////////////////////////////////////////////////////////////// // STRINGIFY -func (m model) String() string { +func (m Model) String() string { data, err := json.MarshalIndent(m, "", " ") if err != nil { return err.Error() @@ -168,13 +168,27 @@ func modelsForAgent(ctx context.Context, agent llm.Agent, names ...string) ([]ll return nil, err } + match_model := func(model llm.Model, names ...string) bool { + if len(names) == 0 { + return true + } + if slices.Contains(names, model.Name()) { + return true + } + for _, alias := range model.Aliases() { + if slices.Contains(names, alias) { + return true + } + } + return false + } + // Filter models result := make([]llm.Model, 0, len(models)) for _, agentmodel := range models { - if len(names) > 0 && !slices.Contains(names, agentmodel.Name()) { - continue + if match_model(agentmodel, names...) { + result = append(result, &Model{Agent: agent.Name(), Model: agentmodel}) } - result = append(result, &model{Agent: agent.Name(), Model: agentmodel}) } // Return success diff --git a/pkg/gemini/model.go b/pkg/gemini/model.go index ed682d4..435436e 100644 --- a/pkg/gemini/model.go +++ b/pkg/gemini/model.go @@ -55,6 +55,16 @@ func (m model) Name() string { return m.meta.Name } +// Return model aliases +func (model model) Aliases() []string { + return nil +} + +// Return model description +func (model model) Description() string { + return model.meta.Description +} + // Return the models func (gemini *Client) Models(ctx context.Context) ([]llm.Model, error) { // Cache models diff --git a/pkg/mistral/completion_test.go b/pkg/mistral/completion_test.go index 39f36ee..d5fd83c 100644 --- a/pkg/mistral/completion_test.go +++ b/pkg/mistral/completion_test.go @@ -7,9 +7,8 @@ import ( "testing" // Packages - llm "github.com/mutablelogic/go-llm" - "github.com/mutablelogic/go-llm/pkg/tool" + tool "github.com/mutablelogic/go-llm/pkg/tool" assert "github.com/stretchr/testify/assert" ) diff --git a/pkg/mistral/model.go b/pkg/mistral/model.go index ef4f393..06047c0 100644 --- a/pkg/mistral/model.go +++ b/pkg/mistral/model.go @@ -97,6 +97,16 @@ func (model model) Name() string { return model.meta.Name } +// Return model aliases +func (model model) Aliases() []string { + return model.meta.Aliases +} + +// Return model description +func (model model) Description() string { + return model.meta.Description +} + // Return a new empty session func (model *model) Context(opts ...llm.Opt) llm.Context { return impl.NewSession(model, &messagefactory{}, opts...) diff --git a/pkg/ollama/model.go b/pkg/ollama/model.go index 50d7211..9e62452 100644 --- a/pkg/ollama/model.go +++ b/pkg/ollama/model.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "net/http" + "strings" "time" // Packages @@ -88,6 +89,16 @@ func (m model) Name() string { return m.ModelMeta.Name } +// Return model name +func (model) Aliases() []string { + return nil +} + +// Return model description +func (model model) Description() string { + return strings.Join(model.ModelMeta.Details.Families, ", ") +} + // Agent interface func (ollama *Client) Models(ctx context.Context) ([]llm.Model, error) { // We don't explicitly cache models diff --git a/pkg/openai/model.go b/pkg/openai/model.go index 85f88d8..7a14566 100644 --- a/pkg/openai/model.go +++ b/pkg/openai/model.go @@ -86,6 +86,16 @@ func (model model) Name() string { return model.meta.Name } +// Return model description +func (model model) Description() string { + return model.meta.OwnedBy +} + +// Return model aliases +func (model) Aliases() []string { + return nil +} + // Return a new empty session func (model *model) Context(opts ...llm.Opt) llm.Context { return impl.NewSession(model, &messagefactory{}, opts...) From eb5a34edbf0dcbba9159a2c967736e347305b08b Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 7 Feb 2025 12:53:44 +0100 Subject: [PATCH 21/25] Added download code --- cmd/llm/chat.go | 14 +---- cmd/llm/complete.go | 9 +-- cmd/llm/embedding.go | 27 +++++++++ cmd/llm/main.go | 22 +++++-- cmd/llm/models.go | 133 ++++++++++++++++--------------------------- 5 files changed, 97 insertions(+), 108 deletions(-) create mode 100644 cmd/llm/embedding.go diff --git a/cmd/llm/chat.go b/cmd/llm/chat.go index 4368318..d072514 100644 --- a/cmd/llm/chat.go +++ b/cmd/llm/chat.go @@ -9,7 +9,6 @@ import ( // Packages llm "github.com/mutablelogic/go-llm" - agent "github.com/mutablelogic/go-llm/pkg/agent" ) //////////////////////////////////////////////////////////////////////////////// @@ -27,17 +26,7 @@ type ChatCmd struct { // PUBLIC METHODS func (cmd *ChatCmd) Run(globals *Globals) error { - return runagent(globals, func(ctx context.Context, client llm.Agent) error { - // Get the model - a, ok := client.(*agent.Agent) - if !ok { - return fmt.Errorf("No agents found") - } - model, err := a.GetModel(ctx, cmd.Model) - if err != nil { - return err - } - + return run(globals, cmd.Model, func(ctx context.Context, model llm.Model) error { // Current buffer var buf string @@ -67,6 +56,7 @@ func (cmd *ChatCmd) Run(globals *Globals) error { input = cmd.Prompt cmd.Prompt = "" } else { + var err error input, err = globals.term.ReadLine(model.Name() + "> ") if errors.Is(err, io.EOF) { return nil diff --git a/cmd/llm/complete.go b/cmd/llm/complete.go index e325e12..69bfd4e 100644 --- a/cmd/llm/complete.go +++ b/cmd/llm/complete.go @@ -9,7 +9,6 @@ import ( // Packages llm "github.com/mutablelogic/go-llm" - agent "github.com/mutablelogic/go-llm/pkg/agent" ) //////////////////////////////////////////////////////////////////////////////// @@ -29,15 +28,9 @@ type CompleteCmd struct { // PUBLIC METHODS func (cmd *CompleteCmd) Run(globals *Globals) error { - return runagent(globals, func(ctx context.Context, client llm.Agent) error { + return run(globals, cmd.Model, func(ctx context.Context, model llm.Model) error { var prompt []byte - // Load the model - model, err := client.(*agent.Agent).GetModel(ctx, cmd.Model) - if err != nil { - return err - } - // If we are pipeline content in via stdin fileInfo, err := os.Stdin.Stat() if err != nil { diff --git a/cmd/llm/embedding.go b/cmd/llm/embedding.go new file mode 100644 index 0000000..39e1496 --- /dev/null +++ b/cmd/llm/embedding.go @@ -0,0 +1,27 @@ +package main + +import ( + "context" + "fmt" + + // Packages + llm "github.com/mutablelogic/go-llm" +) + +//////////////////////////////////////////////////////////////////////////////// +// TYPES + +type EmbeddingCmd struct { + Model string `arg:"" help:"Model name"` + Prompt string `arg:"" help:"Prompt"` +} + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +func (cmd *EmbeddingCmd) Run(globals *Globals) error { + return run(globals, cmd.Model, func(ctx context.Context, model llm.Model) error { + fmt.Println(model) + return nil + }) +} diff --git a/cmd/llm/main.go b/cmd/llm/main.go index 4544b06..c8084ff 100644 --- a/cmd/llm/main.go +++ b/cmd/llm/main.go @@ -38,7 +38,7 @@ type Globals struct { // Context ctx context.Context - agent llm.Agent + agent *agent.Agent toolkit *tool.ToolKit term *Term } @@ -76,9 +76,10 @@ type CLI struct { Tools ListToolsCmd `cmd:"" help:"Return a list of tools"` // Commands - Download DownloadModelCmd `cmd:"" help:"Download a model"` - Chat ChatCmd `cmd:"" help:"Start a chat session"` - Complete CompleteCmd `cmd:"" help:"Complete a prompt"` + Download DownloadModelCmd `cmd:"" help:"Download a model"` + Chat ChatCmd `cmd:"" help:"Start a chat session"` + Complete CompleteCmd `cmd:"" help:"Complete a prompt"` + Embedding EmbeddingCmd `cmd:"" help:"Generate an embedding"` } //////////////////////////////////////////////////////////////////////////////// @@ -186,3 +187,16 @@ func clientOpts(cli *CLI) []client.ClientOpt { } return result } + +//////////////////////////////////////////////////////////////////////////////// +// PRIVATE METHODS + +func run(globals *Globals, name string, fn func(ctx context.Context, model llm.Model) error) error { + model, err := globals.agent.GetModel(globals.ctx, name) + if err != nil { + return err + } + + // Get the model + return fn(globals.ctx, model) +} diff --git a/cmd/llm/models.go b/cmd/llm/models.go index 42df39a..5cec47b 100644 --- a/cmd/llm/models.go +++ b/cmd/llm/models.go @@ -1,7 +1,6 @@ package main import ( - "context" "encoding/json" "fmt" "os" @@ -10,7 +9,6 @@ import ( // Packages tablewriter "github.com/djthorpe/go-tablewriter" - llm "github.com/mutablelogic/go-llm" agent "github.com/mutablelogic/go-llm/pkg/agent" "github.com/mutablelogic/go-llm/pkg/ollama" ) @@ -35,104 +33,71 @@ type DownloadModelCmd struct { // PUBLIC METHODS func (cmd *ListToolsCmd) Run(globals *Globals) error { - return runagent(globals, func(ctx context.Context, client llm.Agent) error { - tools := globals.toolkit.Tools(client) - fmt.Println(tools) - return nil - }) + tools := globals.toolkit.Tools(globals.agent) + fmt.Println(tools) + return nil } func (cmd *ListModelsCmd) Run(globals *Globals) error { - return runagent(globals, func(ctx context.Context, client llm.Agent) error { - agent_, ok := client.(*agent.Agent) - if !ok { - return fmt.Errorf("No agents found") - } - models, err := agent_.ListModels(ctx, cmd.Agent...) - if err != nil { - return err - } - result := make(ModelList, 0, len(models)) - for _, model := range models { - result = append(result, Model{ - Agent: model.(*agent.Model).Agent, - Model: model.Name(), - Description: model.Description(), - Aliases: strings.Join(model.Aliases(), ", "), - }) - } - // Sort models by name - sort.Sort(result) + models, err := globals.agent.ListModels(globals.ctx, cmd.Agent...) + if err != nil { + return err + } + result := make(ModelList, 0, len(models)) + for _, model := range models { + result = append(result, Model{ + Agent: model.(*agent.Model).Agent, + Model: model.Name(), + Description: model.Description(), + Aliases: strings.Join(model.Aliases(), ", "), + }) + } + + // Sort models by name + sort.Sort(result) - // Write out the models - return tablewriter.New(os.Stdout).Write(result, tablewriter.OptOutputText(), tablewriter.OptHeader()) - }) + // Write out the models + return tablewriter.New(os.Stdout).Write(result, tablewriter.OptOutputText(), tablewriter.OptHeader()) } func (*ListAgentsCmd) Run(globals *Globals) error { - return runagent(globals, func(ctx context.Context, client llm.Agent) error { - agent, ok := client.(*agent.Agent) - if !ok { - return fmt.Errorf("No agents found") - } - - agents := make([]string, 0, len(agent.Agents())) - for _, agent := range agent.Agents() { - agents = append(agents, agent.Name()) - } - - data, err := json.MarshalIndent(agents, "", " ") - if err != nil { - return err - } - fmt.Println(string(data)) - - return nil - }) + agents := globals.agent.AgentNames() + data, err := json.MarshalIndent(agents, "", " ") + if err != nil { + return err + } + fmt.Println(string(data)) + return nil } func (cmd *DownloadModelCmd) Run(globals *Globals) error { - return runagent(globals, func(ctx context.Context, client llm.Agent) error { - agent := getagent(client, cmd.Agent) - if agent == nil { - return fmt.Errorf("No agents found with name %q", cmd.Agent) - } - // Download the model - switch agent.Name() { - case "ollama": - model, err := agent.(*ollama.Client).PullModel(ctx, cmd.Model) - if err != nil { - return err - } - fmt.Println(model) - default: - return fmt.Errorf("Agent %q does not support model download", agent.Name()) - } - return nil - }) -} - -//////////////////////////////////////////////////////////////////////////////// -// PRIVATE METHODS - -func runagent(globals *Globals, fn func(ctx context.Context, agent llm.Agent) error) error { - return fn(globals.ctx, globals.agent) -} - -func getagent(client llm.Agent, name string) llm.Agent { - agent, ok := client.(*agent.Agent) - if !ok { - return nil + agents := globals.agent.AgentsWithName(cmd.Agent) + if len(agents) == 0 { + return fmt.Errorf("No agents found with name %q", cmd.Agent) } - for _, agent := range agent.Agents() { - if agent.Name() == name { - return agent + switch agents[0].Name() { + case "ollama": + model, err := agents[0].(*ollama.Client).PullModel(globals.ctx, cmd.Model, ollama.WithPullStatus(func(status *ollama.PullStatus) { + var pct int64 + if status.TotalBytes > 0 { + pct = status.CompletedBytes * 100 / status.TotalBytes + } + fmt.Print("\r", status.Status, " ", pct, "%") + if status.Status == "success" { + fmt.Println("") + } + })) + if err != nil { + return err } + fmt.Println(model) + default: + return fmt.Errorf("Agent %q does not support model download", agents[0].Name()) } return nil } -// ////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// // MODEL LIST type Model struct { From f751b7b3b167fbb7e6255c27022057bed22d22b4 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 7 Feb 2025 13:11:51 +0100 Subject: [PATCH 22/25] Updated embedding --- cmd/llm/embedding.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/cmd/llm/embedding.go b/cmd/llm/embedding.go index 39e1496..8f8b469 100644 --- a/cmd/llm/embedding.go +++ b/cmd/llm/embedding.go @@ -2,6 +2,7 @@ package main import ( "context" + "encoding/json" "fmt" // Packages @@ -21,7 +22,15 @@ type EmbeddingCmd struct { func (cmd *EmbeddingCmd) Run(globals *Globals) error { return run(globals, cmd.Model, func(ctx context.Context, model llm.Model) error { - fmt.Println(model) + vector, err := model.Embedding(ctx, cmd.Prompt) + if err != nil { + return err + } + data, err := json.Marshal(vector) + if err != nil { + return err + } + fmt.Println(string(data)) return nil }) } From ce37f7eb191a67c95c1870c18224cc78523b7c7e Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 7 Feb 2025 13:45:38 +0100 Subject: [PATCH 23/25] Updated Anthropic --- pkg/anthropic/client.go | 46 +--- pkg/anthropic/content.go | 160 ++++++++++++++ pkg/anthropic/message.go | 183 +++------------- pkg/anthropic/messagefactory.go | 74 +++++++ pkg/anthropic/model.go | 80 +++++-- pkg/anthropic/{session.go => session.go_old} | 0 .../{session_test.go => session_test.go_old} | 0 pkg/anthropic/testdata/LICENSE | 201 ------------------ pkg/anthropic/testdata/guggenheim.jpg | Bin 139053 -> 0 bytes pkg/mistral/model.go | 9 +- pkg/ollama/client.go | 4 + pkg/ollama/doc.go | 5 - pkg/ollama/embedding.go | 11 +- pkg/openai/client.go | 11 +- pkg/openai/message.go | 6 - pkg/openai/model.go | 63 +++--- pkg/openai/tool.go | 6 + 17 files changed, 390 insertions(+), 469 deletions(-) create mode 100644 pkg/anthropic/content.go create mode 100644 pkg/anthropic/messagefactory.go rename pkg/anthropic/{session.go => session.go_old} (100%) rename pkg/anthropic/{session_test.go => session_test.go_old} (100%) delete mode 100644 pkg/anthropic/testdata/LICENSE delete mode 100644 pkg/anthropic/testdata/guggenheim.jpg delete mode 100644 pkg/ollama/doc.go diff --git a/pkg/anthropic/client.go b/pkg/anthropic/client.go index f446edc..a5ab813 100644 --- a/pkg/anthropic/client.go +++ b/pkg/anthropic/client.go @@ -1,14 +1,14 @@ /* -anthropic implements an API client for anthropic (https://docs.anthropic.com/en/api/getting-started) +anthropic implements an API client for anthropic +https://docs.anthropic.com/en/api/getting-started */ package anthropic import ( // Packages - "context" - client "github.com/mutablelogic/go-client" llm "github.com/mutablelogic/go-llm" + impl "github.com/mutablelogic/go-llm/pkg/internal/impl" ) /////////////////////////////////////////////////////////////////////////////// @@ -16,7 +16,7 @@ import ( type Client struct { *client.Client - cache map[string]llm.Model + *impl.ModelCache } var _ llm.Agent = (*Client)(nil) @@ -37,14 +37,15 @@ const ( func New(ApiKey string, opts ...client.ClientOpt) (*Client, error) { // Create client opts = append(opts, client.OptEndpoint(endPoint)) - opts = append(opts, client.OptHeader("x-api-key", ApiKey), client.OptHeader("anthropic-version", defaultVersion)) + opts = append(opts, client.OptHeader("x-api-key", ApiKey)) + opts = append(opts, client.OptHeader("anthropic-version", defaultVersion)) client, err := client.New(opts...) if err != nil { return nil, err } // Return the client - return &Client{client, nil}, nil + return &Client{client, impl.NewModelCache()}, nil } /////////////////////////////////////////////////////////////////////////////// @@ -54,36 +55,3 @@ func New(ApiKey string, opts ...client.ClientOpt) (*Client, error) { func (*Client) Name() string { return defaultName } - -// Return the models -func (anthropic *Client) Models(ctx context.Context) ([]llm.Model, error) { - // Cache models - if anthropic.cache == nil { - models, err := anthropic.ListModels(ctx) - if err != nil { - return nil, err - } - anthropic.cache = make(map[string]llm.Model, len(models)) - for _, model := range models { - anthropic.cache[model.Name()] = model - } - } - - // Return models - result := make([]llm.Model, 0, len(anthropic.cache)) - for _, model := range anthropic.cache { - result = append(result, model) - } - return result, nil -} - -// Return a model by name, or nil if not found. -// Panics on error. -func (anthropic *Client) Model(ctx context.Context, name string) llm.Model { - if anthropic.cache == nil { - if _, err := anthropic.Models(ctx); err != nil { - panic(err) - } - } - return anthropic.cache[name] -} diff --git a/pkg/anthropic/content.go b/pkg/anthropic/content.go new file mode 100644 index 0000000..4f79eb4 --- /dev/null +++ b/pkg/anthropic/content.go @@ -0,0 +1,160 @@ +package anthropic + +import ( + "encoding/json" + "strings" + + llm "github.com/mutablelogic/go-llm" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +type Content struct { + Type string `json:"type"` // image, document, text, tool_use + ContentText + ContentAttachment + *ContentTool + ContentToolResult + CacheControl *cachecontrol `json:"cache_control,omitempty"` // ephemeral +} + +type ContentText struct { + Text string `json:"text,omitempty"` // text content +} + +type ContentTool struct { + Id string `json:"id,omitempty"` // tool id + Name string `json:"name,omitempty"` // tool name + Input map[string]any `json:"input"` // tool input + InputJson string `json:"partial_json,omitempty"` // partial json input (for streaming) +} + +type ContentAttachment struct { + Title string `json:"title,omitempty"` // title of the document + Context string `json:"context,omitempty"` // context of the document + Citations *contentcitation `json:"citations,omitempty"` // citations of the document + Source *contentsource `json:"source,omitempty"` // image or document content +} + +type ContentToolResult struct { + Id string `json:"tool_use_id,omitempty"` // tool id + Content any `json:"content,omitempty"` +} + +type contentsource struct { + Type string `json:"type"` // base64 or text + MediaType string `json:"media_type"` // image/jpeg, image/png, image/gif, image/webp, application/pdf, text/plain + Data any `json:"data"` // ...base64 or text encoded data +} + +type cachecontrol struct { + Type string `json:"type"` // ephemeral +} + +type contentcitation struct { + Enabled bool `json:"enabled"` // true +} + +/////////////////////////////////////////////////////////////////////////////// +// GLOBALS + +var ( + supportedAttachments = map[string]string{ + "image/jpeg": "image", + "image/png": "image", + "image/gif": "image", + "image/webp": "image", + "application/pdf": "document", + "text/plain": "text", + } +) + +/////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +// Return a content object with text content +func NewTextContent(v string) *Content { + return &Content{ + Type: "text", + ContentText: ContentText{ + Text: v, + }, + } +} + +// Return a content object with tool result +func NewToolResultContent(v llm.ToolResult) *Content { + content := new(Content) + content.Type = "tool_result" + content.ContentToolResult.Id = v.Call().Id() + // content.ContentToolResult.Name = v.Call().Name() + + // We only support JSON encoding for the moment + data, err := json.Marshal(v.Value()) + if err != nil { + content.ContentToolResult.Content = err.Error() + } else { + content.ContentToolResult.Content = string(data) + } + + return content +} + +// Make attachment content +func NewAttachment(attachment *llm.Attachment, ephemeral, citations bool) (*Content, error) { + // Detect mimetype + mimetype := attachment.Type() + if strings.HasPrefix(mimetype, "text/") { + // Switch to text/plain - TODO: charsets? + mimetype = "text/plain" + } + + // Check supported mimetype + typ, exists := supportedAttachments[mimetype] + if !exists { + return nil, llm.ErrBadParameter.Withf("unsupported or undetected mimetype %q", mimetype) + } + + // Create attachment + content := new(Content) + content.Type = typ + if ephemeral { + content.CacheControl = &cachecontrol{Type: "ephemeral"} + } + + // Handle by type + switch typ { + case "text": + content.Type = "document" + content.Title = attachment.Filename() + content.Source = &contentsource{ + Type: "text", + MediaType: mimetype, + Data: string(attachment.Data()), + } + if citations { + content.Citations = &contentcitation{Enabled: true} + } + case "document": + content.Source = &contentsource{ + Type: "base64", + MediaType: mimetype, + Data: attachment.Data(), + } + if citations { + content.Citations = &contentcitation{Enabled: true} + } + case "image": + content.Source = &contentsource{ + Type: "base64", + MediaType: mimetype, + Data: attachment.Data(), + } + default: + return nil, llm.ErrBadParameter.Withf("unsupported attachment type %q", typ) + } + + // Return success + return content, nil +} diff --git a/pkg/anthropic/message.go b/pkg/anthropic/message.go index dae3c48..c5b8b59 100644 --- a/pkg/anthropic/message.go +++ b/pkg/anthropic/message.go @@ -17,162 +17,13 @@ type Message struct { RoleContent } +var _ llm.Completion = (*Message)(nil) + type RoleContent struct { Role string `json:"role"` Content []*Content `json:"content,omitempty"` } -var _ llm.Completion = (*Message)(nil) - -type Content struct { - Type string `json:"type"` // image, document, text, tool_use - ContentText - ContentAttachment - *ContentTool - ContentToolResult - CacheControl *cachecontrol `json:"cache_control,omitempty"` // ephemeral -} - -type ContentText struct { - Text string `json:"text,omitempty"` // text content -} - -type ContentTool struct { - Id string `json:"id,omitempty"` // tool id - Name string `json:"name,omitempty"` // tool name - Input map[string]any `json:"input"` // tool input - InputJson string `json:"partial_json,omitempty"` // partial json input (for streaming) -} - -type ContentAttachment struct { - Title string `json:"title,omitempty"` // title of the document - Context string `json:"context,omitempty"` // context of the document - Citations *contentcitation `json:"citations,omitempty"` // citations of the document - Source *contentsource `json:"source,omitempty"` // image or document content -} - -type ContentToolResult struct { - Id string `json:"tool_use_id,omitempty"` // tool id - Content any `json:"content,omitempty"` -} - -type contentsource struct { - Type string `json:"type"` // base64 or text - MediaType string `json:"media_type"` // image/jpeg, image/png, image/gif, image/webp, application/pdf, text/plain - Data any `json:"data"` // ...base64 or text encoded data -} - -type cachecontrol struct { - Type string `json:"type"` // ephemeral -} - -type contentcitation struct { - Enabled bool `json:"enabled"` // true -} - -/////////////////////////////////////////////////////////////////////////////// -// GLOBALS - -var ( - supportedAttachments = map[string]string{ - "image/jpeg": "image", - "image/png": "image", - "image/gif": "image", - "image/webp": "image", - "application/pdf": "document", - "text/plain": "text", - } -) - -/////////////////////////////////////////////////////////////////////////////// -// LIFECYCLE - -// Return a content object with text content -func NewTextContent(v string) *Content { - return &Content{ - Type: "text", - ContentText: ContentText{ - Text: v, - }, - } -} - -// Return a content object with tool result -func NewToolResultContent(v llm.ToolResult) *Content { - content := new(Content) - content.Type = "tool_result" - content.ContentToolResult.Id = v.Call().Id() - // content.ContentToolResult.Name = v.Call().Name() - - // We only support JSON encoding for the moment - data, err := json.Marshal(v.Value()) - if err != nil { - content.ContentToolResult.Content = err.Error() - } else { - content.ContentToolResult.Content = string(data) - } - - return content -} - -// Make attachment content -func NewAttachment(attachment *llm.Attachment, ephemeral, citations bool) (*Content, error) { - // Detect mimetype - mimetype := attachment.Type() - if strings.HasPrefix(mimetype, "text/") { - // Switch to text/plain - TODO: charsets? - mimetype = "text/plain" - } - - // Check supported mimetype - typ, exists := supportedAttachments[mimetype] - if !exists { - return nil, llm.ErrBadParameter.Withf("unsupported or undetected mimetype %q", mimetype) - } - - // Create attachment - content := new(Content) - content.Type = typ - if ephemeral { - content.CacheControl = &cachecontrol{Type: "ephemeral"} - } - - // Handle by type - switch typ { - case "text": - content.Type = "document" - content.Title = attachment.Filename() - content.Source = &contentsource{ - Type: "text", - MediaType: mimetype, - Data: string(attachment.Data()), - } - if citations { - content.Citations = &contentcitation{Enabled: true} - } - case "document": - content.Source = &contentsource{ - Type: "base64", - MediaType: mimetype, - Data: attachment.Data(), - } - if citations { - content.Citations = &contentcitation{Enabled: true} - } - case "image": - content.Source = &contentsource{ - Type: "base64", - MediaType: mimetype, - Data: attachment.Data(), - } - default: - return nil, llm.ErrBadParameter.Withf("unsupported attachment type %q", typ) - } - - // Return success - return content, nil -} - /////////////////////////////////////////////////////////////////////////////// // STRINGIFY @@ -187,20 +38,31 @@ func (m Message) String() string { /////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS - MESSAGE -func (m Message) Num() int { +// Return the number of completions +func (Message) Num() int { return 1 } -func (m Message) Role() string { - return m.RoleContent.Role +// Return the current session role +func (message Message) Role() string { + return message.RoleContent.Role } -func (m Message) Text(index int) string { +// Return the completion +func (message Message) Choice(index int) llm.Completion { + if index != 0 { + return nil + } + return message +} + +// Return the text for the last completion +func (message Message) Text(index int) string { if index != 0 { return "" } var text []string - for _, content := range m.RoleContent.Content { + for _, content := range message.RoleContent.Content { if content.Type == "text" { text = append(text, content.ContentText.Text) } @@ -208,14 +70,19 @@ func (m Message) Text(index int) string { return strings.Join(text, "\n") } -func (m Message) ToolCalls(index int) []llm.ToolCall { +// Return the audio - unsupported +func (Message) Audio(index int) *llm.Attachment { + return nil +} + +func (message Message) ToolCalls(index int) []llm.ToolCall { if index != 0 { return nil } // Gather tool calls var result []llm.ToolCall - for _, content := range m.Content { + for _, content := range message.Content { if content.Type == "tool_use" { result = append(result, tool.NewCall(content.ContentTool.Id, content.ContentTool.Name, content.ContentTool.Input)) } diff --git a/pkg/anthropic/messagefactory.go b/pkg/anthropic/messagefactory.go new file mode 100644 index 0000000..5d19e22 --- /dev/null +++ b/pkg/anthropic/messagefactory.go @@ -0,0 +1,74 @@ +package anthropic + +import ( + // Packages + llm "github.com/mutablelogic/go-llm" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +type messagefactory struct{} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS - MESSAGE FACTORY + +func (messagefactory) SystemPrompt(prompt string) llm.Completion { + return &Message{ + RoleContent: RoleContent{ + Role: "system", + Content: []*Content{NewTextContent(prompt)}, + }, + } +} + +func (messagefactory) UserPrompt(prompt string, opts ...llm.Opt) (llm.Completion, error) { + // Get attachments + opt, err := llm.ApplyPromptOpts(opts...) + if err != nil { + return nil, err + } + + // Get attachments, allocate content + attachments := opt.Attachments() + contents := make([]*Content, 1, len(attachments)+1) + + // Append the text and the attachments + contents[0] = NewTextContent(prompt) + for _, attachment := range attachments { + if content, err := NewAttachment(attachment, optEphemeral(opt), optCitations(opt)); err != nil { + return nil, err + } else { + contents = append(contents, content) + } + } + + // Return success + return &Message{ + RoleContent: RoleContent{ + Role: "user", + Content: contents, + }, + }, nil +} + +func (messagefactory) ToolResults(results ...llm.ToolResult) ([]llm.Completion, error) { + // Check for no results + if len(results) == 0 { + return nil, llm.ErrBadParameter.Withf("No tool results") + } + + // Create user message + message := Message{ + RoleContent{ + Role: "user", + Content: make([]*Content, 0, len(results)), + }, + } + for _, result := range results { + message.RoleContent.Content = append(message.RoleContent.Content, NewToolResultContent(result)) + } + + // Return success + return []llm.Completion{message}, nil +} diff --git a/pkg/anthropic/model.go b/pkg/anthropic/model.go index 2b50ee6..6f86d8f 100644 --- a/pkg/anthropic/model.go +++ b/pkg/anthropic/model.go @@ -9,6 +9,7 @@ import ( // Packages client "github.com/mutablelogic/go-client" llm "github.com/mutablelogic/go-llm" + impl "github.com/mutablelogic/go-llm/pkg/internal/impl" ) /////////////////////////////////////////////////////////////////////////////// @@ -44,21 +45,68 @@ func (m model) String() string { } /////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - API +// PUBLIC METHODS - llm.Agent -// Get a model by name -func (anthropic *Client) GetModel(ctx context.Context, name string) (llm.Model, error) { - var response Model - if err := anthropic.DoWithContext(ctx, nil, &response, client.OptPath("models", name)); err != nil { +// Return the models +func (anthropic *Client) Models(ctx context.Context) ([]llm.Model, error) { + return anthropic.ModelCache.Load(func() ([]llm.Model, error) { + return anthropic.loadmodels(ctx) + }) +} + +// Return a model by name, or nil if not found. +// Panics on error. +func (anthropic *Client) Model(ctx context.Context, name string) llm.Model { + model, err := anthropic.ModelCache.Get(func() ([]llm.Model, error) { + return anthropic.loadmodels(ctx) + }, name) + if err != nil { + panic(err) + } + return model +} + +// Function called to load models +func (anthropic *Client) loadmodels(ctx context.Context) ([]llm.Model, error) { + if models, err := anthropic.ListModels(ctx); err != nil { return nil, err + } else { + result := make([]llm.Model, len(models)) + for i, meta := range models { + result[i] = &model{anthropic, meta} + } + return result, nil } +} - // Return success - return &model{anthropic, response}, nil +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS - llm.Model + +// Return the name of a model +func (model *model) Name() string { + return model.meta.Name } +// Return model description +func (model model) Description() string { + return model.meta.Description +} + +// Return model aliases +func (model) Aliases() []string { + return nil +} + +// Return a new empty session +func (model *model) Context(opts ...llm.Opt) llm.Context { + return impl.NewSession(model, &messagefactory{}, opts...) +} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS - API + // List models -func (anthropic *Client) ListModels(ctx context.Context) ([]llm.Model, error) { +func (anthropic *Client) ListModels(ctx context.Context) ([]Model, error) { var response struct { Body []Model `json:"data"` HasMore bool `json:"has_more"` @@ -68,7 +116,7 @@ func (anthropic *Client) ListModels(ctx context.Context) ([]llm.Model, error) { // Request request := url.Values{} - result := make([]llm.Model, 0, 100) + result := make([]Model, 0, 100) for { if err := anthropic.DoWithContext(ctx, nil, &response, client.OptPath("models"), client.OptQuery(request)); err != nil { return nil, err @@ -76,7 +124,7 @@ func (anthropic *Client) ListModels(ctx context.Context) ([]llm.Model, error) { // Convert to llm.Model for _, meta := range response.Body { - result = append(result, &model{anthropic, meta}) + result = append(result, meta) } // If there are no more models, return @@ -91,9 +139,15 @@ func (anthropic *Client) ListModels(ctx context.Context) ([]llm.Model, error) { return result, nil } -// Return the name of a model -func (model *model) Name() string { - return model.meta.Name +// Get a model by name +func (anthropic *Client) GetModel(ctx context.Context, name string) (*Model, error) { + var response Model + if err := anthropic.DoWithContext(ctx, nil, &response, client.OptPath("models", name)); err != nil { + return nil, err + } + + // Return success + return &response, nil } // Embedding vector generation - not supported on Anthropic diff --git a/pkg/anthropic/session.go b/pkg/anthropic/session.go_old similarity index 100% rename from pkg/anthropic/session.go rename to pkg/anthropic/session.go_old diff --git a/pkg/anthropic/session_test.go b/pkg/anthropic/session_test.go_old similarity index 100% rename from pkg/anthropic/session_test.go rename to pkg/anthropic/session_test.go_old diff --git a/pkg/anthropic/testdata/LICENSE b/pkg/anthropic/testdata/LICENSE deleted file mode 100644 index 261eeb9..0000000 --- a/pkg/anthropic/testdata/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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/anthropic/testdata/guggenheim.jpg b/pkg/anthropic/testdata/guggenheim.jpg deleted file mode 100644 index 7e1651729c5008c35446d52049de346f86962ef3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 139053 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 0 { + return embedding.Embeddings[0], nil + } + return nil, llm.ErrNotFound.With("no embeddings returned") } diff --git a/pkg/openai/client.go b/pkg/openai/client.go index fd31659..23e9086 100644 --- a/pkg/openai/client.go +++ b/pkg/openai/client.go @@ -8,6 +8,7 @@ import ( // Packages client "github.com/mutablelogic/go-client" llm "github.com/mutablelogic/go-llm" + impl "github.com/mutablelogic/go-llm/pkg/internal/impl" ) /////////////////////////////////////////////////////////////////////////////// @@ -15,7 +16,7 @@ import ( type Client struct { *client.Client - cache map[string]llm.Model + *impl.ModelCache } var _ llm.Agent = (*Client)(nil) @@ -34,16 +35,18 @@ const ( // Create a new client func New(ApiKey string, opts ...client.ClientOpt) (*Client, error) { // Create client - client, err := client.New(append(opts, client.OptEndpoint(endPoint), client.OptReqToken(client.Token{ + 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}, nil + return &Client{client, impl.NewModelCache()}, nil } /////////////////////////////////////////////////////////////////////////////// diff --git a/pkg/openai/message.go b/pkg/openai/message.go index 93522b9..37128f2 100644 --- a/pkg/openai/message.go +++ b/pkg/openai/message.go @@ -23,12 +23,6 @@ type RoleContent struct { Content any `json:"content,omitempty"` // string or array of text, reference, image_url } -type ToolCalls []toolcall - -type ToolResults struct { - Id string `json:"tool_call_id,omitempty"` -} - /////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS diff --git a/pkg/openai/model.go b/pkg/openai/model.go index 7a14566..ae8ba89 100644 --- a/pkg/openai/model.go +++ b/pkg/openai/model.go @@ -3,6 +3,7 @@ package openai import ( "context" "encoding/json" + "fmt" // Packages client "github.com/mutablelogic/go-client" @@ -47,35 +48,34 @@ func (m model) String() string { // Return the models func (openai *Client) Models(ctx context.Context) ([]llm.Model, error) { - // Cache models - if openai.cache == nil { - models, err := openai.ListModels(ctx) - if err != nil { - return nil, err - } - openai.cache = make(map[string]llm.Model, len(models)) - for _, m := range models { - openai.cache[m.Name] = &model{openai, m} - } - } - - // Return models - result := make([]llm.Model, 0, len(openai.cache)) - for _, model := range openai.cache { - result = append(result, model) - } - return result, nil + return openai.ModelCache.Load(func() ([]llm.Model, error) { + return openai.loadmodels(ctx) + }) } // Return a model by name, or nil if not found. // Panics on error. func (openai *Client) Model(ctx context.Context, name string) llm.Model { - if openai.cache == nil { - if _, err := openai.Models(ctx); err != nil { - panic(err) + model, err := openai.ModelCache.Get(func() ([]llm.Model, error) { + return openai.loadmodels(ctx) + }, name) + if err != nil { + panic(err) + } + return model +} + +// Function called to load models +func (openai *Client) loadmodels(ctx context.Context) ([]llm.Model, error) { + if models, err := openai.ListModels(ctx); err != nil { + return nil, err + } else { + result := make([]llm.Model, len(models)) + for i, meta := range models { + result[i] = &model{openai, meta} } + return result, nil } - return openai.cache[name] } /////////////////////////////////////////////////////////////////////////////// @@ -88,7 +88,7 @@ func (model model) Name() string { // Return model description func (model model) Description() string { - return model.meta.OwnedBy + return fmt.Sprintf("Owner: %q", model.meta.OwnedBy) } // Return model aliases @@ -105,12 +105,12 @@ func (model *model) Context(opts ...llm.Opt) llm.Context { // API CALLS // ListModels returns all the models -func (c *Client) ListModels(ctx context.Context) ([]Model, error) { +func (openai *Client) ListModels(ctx context.Context) ([]Model, error) { // Return the response var response struct { Data []Model `json:"data"` } - if err := c.DoWithContext(ctx, nil, &response, client.OptPath("models")); err != nil { + if err := openai.DoWithContext(ctx, nil, &response, client.OptPath("models")); err != nil { return nil, err } @@ -119,10 +119,10 @@ func (c *Client) ListModels(ctx context.Context) ([]Model, error) { } // GetModel returns one model -func (c *Client) GetModel(ctx context.Context, model string) (*Model, error) { +func (openai *Client) GetModel(ctx context.Context, model string) (*Model, error) { // Return the response var response Model - if err := c.DoWithContext(ctx, nil, &response, client.OptPath("models", model)); err != nil { + if err := openai.DoWithContext(ctx, nil, &response, client.OptPath("models", model)); err != nil { return nil, err } @@ -132,11 +132,6 @@ func (c *Client) GetModel(ctx context.Context, model string) (*Model, error) { // Delete a fine-tuned model. You must have the Owner role in your organization // to delete a model. -func (c *Client) DeleteModel(ctx context.Context, model string) error { - if err := c.DoWithContext(ctx, client.MethodDelete, nil, client.OptPath("models", model)); err != nil { - return err - } - - // Return success - return nil +func (openai *Client) DeleteModel(ctx context.Context, model string) error { + return openai.DoWithContext(ctx, client.MethodDelete, nil, client.OptPath("models", model)) } diff --git a/pkg/openai/tool.go b/pkg/openai/tool.go index aaca622..68865a3 100644 --- a/pkg/openai/tool.go +++ b/pkg/openai/tool.go @@ -21,6 +21,12 @@ type toolcall struct { meta ToolCall } +type ToolCalls []toolcall + +type ToolResults struct { + Id string `json:"tool_call_id,omitempty"` +} + /////////////////////////////////////////////////////////////////////////////// // STRINGIFY From e6d2565076a74a71d8155f0bd2b8e2559a1bfe2d Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 7 Feb 2025 14:19:43 +0100 Subject: [PATCH 24/25] Updated --- cmd/llm/complete.go | 4 +- cmd/llm/main.go | 8 +-- pkg/agent/opt.go | 5 +- pkg/anthropic/completion.go | 59 ++++++++++++------- ...pletion_test.go => completion_test.go_old} | 0 pkg/anthropic/messagefactory.go | 1 - pkg/ollama/opt.go | 5 +- 7 files changed, 46 insertions(+), 36 deletions(-) rename pkg/anthropic/{completion_test.go => completion_test.go_old} (100%) diff --git a/cmd/llm/complete.go b/cmd/llm/complete.go index 69bfd4e..f6de7fd 100644 --- a/cmd/llm/complete.go +++ b/cmd/llm/complete.go @@ -94,9 +94,9 @@ func (cmd *CompleteCmd) opts() []llm.Opt { // Set system prompt var system []string if cmd.Format == "markdown" { - system = append(system, "Return the completion in markdown format.") + system = append(system, "Structure your output in markdown format.") } else if cmd.Format == "json" { - system = append(system, "Return the completion in JSON format.") + system = append(system, "Structure your output in JSON format.") } if cmd.System != "" { system = append(system, cmd.System) diff --git a/cmd/llm/main.go b/cmd/llm/main.go index c8084ff..bc8db89 100644 --- a/cmd/llm/main.go +++ b/cmd/llm/main.go @@ -124,11 +124,9 @@ 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 c69e765..ddc3350 100644 --- a/pkg/agent/opt.go +++ b/pkg/agent/opt.go @@ -4,8 +4,9 @@ import ( // Packages client "github.com/mutablelogic/go-client" llm "github.com/mutablelogic/go-llm" + anthropic "github.com/mutablelogic/go-llm/pkg/anthropic" gemini "github.com/mutablelogic/go-llm/pkg/gemini" - "github.com/mutablelogic/go-llm/pkg/mistral" + mistral "github.com/mutablelogic/go-llm/pkg/mistral" ollama "github.com/mutablelogic/go-llm/pkg/ollama" openai "github.com/mutablelogic/go-llm/pkg/openai" ) @@ -24,7 +25,6 @@ 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...) @@ -35,7 +35,6 @@ func WithAnthropic(key string, opts ...client.ClientOpt) llm.Opt { } } } -*/ func WithMistral(key string, opts ...client.ClientOpt) llm.Opt { return func(o *llm.Opts) error { diff --git a/pkg/anthropic/completion.go b/pkg/anthropic/completion.go index 109a063..f9e490c 100644 --- a/pkg/anthropic/completion.go +++ b/pkg/anthropic/completion.go @@ -21,7 +21,7 @@ type Response struct { Reason string `json:"stop_reason,omitempty"` StopSequence *string `json:"stop_sequence,omitempty"` Message - Metrics `json:"usage,omitempty"` + *Metrics `json:"usage,omitempty"` } // Metrics @@ -43,30 +43,43 @@ func (r Response) String() string { return string(data) } +func (m Metrics) String() string { + data, err := json.MarshalIndent(m, "", " ") + if err != nil { + return err.Error() + } + return string(data) +} + /////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS type reqMessages struct { - Model string `json:"model"` - MaxTokens uint64 `json:"max_tokens,omitempty"` - Metadata *optmetadata `json:"metadata,omitempty"` - StopSequences []string `json:"stop_sequences,omitempty"` - Stream bool `json:"stream,omitempty"` - System string `json:"system,omitempty"` - Temperature float64 `json:"temperature,omitempty"` - TopK uint64 `json:"top_k,omitempty"` - TopP float64 `json:"top_p,omitempty"` - Messages []*Message `json:"messages"` - Tools []llm.Tool `json:"tools,omitempty"` - ToolChoice any `json:"tool_choice,omitempty"` + Model string `json:"model"` + MaxTokens uint64 `json:"max_tokens,omitempty"` + Metadata *optmetadata `json:"metadata,omitempty"` + StopSequences []string `json:"stop_sequences,omitempty"` + Stream bool `json:"stream,omitempty"` + System string `json:"system,omitempty"` + Temperature float64 `json:"temperature,omitempty"` + TopK uint64 `json:"top_k,omitempty"` + TopP float64 `json:"top_p,omitempty"` + Tools []llm.Tool `json:"tools,omitempty"` + ToolChoice any `json:"tool_choice,omitempty"` + Messages []llm.Completion `json:"messages"` } +// Send a completion request with a single prompt, and return the next completion func (model *model) Completion(ctx context.Context, prompt string, opts ...llm.Opt) (llm.Completion, error) { - // TODO - return nil, llm.ErrNotImplemented + message, err := messagefactory{}.UserPrompt(prompt, opts...) + if err != nil { + return nil, err + } + return model.Chat(ctx, []llm.Completion{message}, opts...) } -func (anthropic *Client) Messages(ctx context.Context, context llm.Context, opts ...llm.Opt) (*Response, error) { +// Send a completion request with multiple completions, and return the next completion +func (model *model) Chat(ctx context.Context, completions []llm.Completion, opts ...llm.Opt) (llm.Completion, error) { // Apply options opt, err := llm.ApplyOpts(opts...) if err != nil { @@ -75,8 +88,8 @@ func (anthropic *Client) Messages(ctx context.Context, context llm.Context, opts // Request req, err := client.NewJSONRequest(reqMessages{ - Model: context.(*session).model.Name(), - MaxTokens: optMaxTokens(context.(*session).model, opt), + Model: model.Name(), + MaxTokens: optMaxTokens(model, opt), Metadata: optMetadata(opt), StopSequences: optStopSequences(opt), Stream: optStream(opt), @@ -84,19 +97,21 @@ func (anthropic *Client) Messages(ctx context.Context, context llm.Context, opts Temperature: optTemperature(opt), TopK: optTopK(opt), TopP: optTopP(opt), - Messages: context.(*session).seq, - Tools: optTools(anthropic, opt), + Tools: optTools(model.Client, opt), ToolChoice: optToolChoice(opt), + Messages: completions, }) if err != nil { return nil, err } - // Stream + // Response options var response Response reqopts := []client.RequestOpt{ client.OptPath("messages"), } + + // Streaming if optStream(opt) { reqopts = append(reqopts, client.OptTextStreamCallback(func(evt client.TextStreamEvent) error { if err := streamEvent(&response, evt); err != nil { @@ -110,7 +125,7 @@ func (anthropic *Client) Messages(ctx context.Context, context llm.Context, opts } // Response - if err := anthropic.DoWithContext(ctx, req, &response, reqopts...); err != nil { + if err := model.DoWithContext(ctx, req, &response, reqopts...); err != nil { return nil, err } diff --git a/pkg/anthropic/completion_test.go b/pkg/anthropic/completion_test.go_old similarity index 100% rename from pkg/anthropic/completion_test.go rename to pkg/anthropic/completion_test.go_old diff --git a/pkg/anthropic/messagefactory.go b/pkg/anthropic/messagefactory.go index 5d19e22..1280ca5 100644 --- a/pkg/anthropic/messagefactory.go +++ b/pkg/anthropic/messagefactory.go @@ -1,7 +1,6 @@ package anthropic import ( - // Packages llm "github.com/mutablelogic/go-llm" ) diff --git a/pkg/ollama/opt.go b/pkg/ollama/opt.go index 2127d49..769da41 100644 --- a/pkg/ollama/opt.go +++ b/pkg/ollama/opt.go @@ -1,7 +1,6 @@ package ollama import ( - "strconv" "strings" "time" @@ -95,9 +94,9 @@ func optFormat(opts *llm.Opts) string { return "" } if format == "json_format" { - return strconv.Quote("json") + return "json" } - return strconv.Quote(format) + return format } func optStopSequence(opts *llm.Opts) []string { From 932b720734189f079df1d5cef6b6dcd73285cf1b Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 7 Feb 2025 14:33:32 +0100 Subject: [PATCH 25/25] Added version command --- cmd/llm/main.go | 1 + cmd/llm/version.go | 34 ++++++++++++++++++++++++++++++++++ pkg/version/version.go | 12 ++++++++++++ 3 files changed, 47 insertions(+) create mode 100644 cmd/llm/version.go create mode 100644 pkg/version/version.go diff --git a/cmd/llm/main.go b/cmd/llm/main.go index bc8db89..ce6b532 100644 --- a/cmd/llm/main.go +++ b/cmd/llm/main.go @@ -80,6 +80,7 @@ type CLI struct { Chat ChatCmd `cmd:"" help:"Start a chat session"` Complete CompleteCmd `cmd:"" help:"Complete a prompt"` Embedding EmbeddingCmd `cmd:"" help:"Generate an embedding"` + Version VersionCmd `cmd:"" help:"Print the version of this tool"` } //////////////////////////////////////////////////////////////////////////////// diff --git a/cmd/llm/version.go b/cmd/llm/version.go new file mode 100644 index 0000000..35515a0 --- /dev/null +++ b/cmd/llm/version.go @@ -0,0 +1,34 @@ +package main + +import ( + "fmt" + "os" + "runtime" + + // Packages + "github.com/mutablelogic/go-llm/pkg/version" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +type VersionCmd struct{} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Run the version command +func (cmd *VersionCmd) Run() error { + w := os.Stdout + if version.GitSource != "" { + fmt.Fprintf(w, "Source: https://%v\n", version.GitSource) + } + if version.GitTag != "" || version.GitBranch != "" { + fmt.Fprintf(w, "Version: %v (branch: %q hash:%q)\n", version.GitTag, version.GitBranch, version.GitHash) + } + if version.GoBuildTime != "" { + fmt.Fprintf(w, "Build: %v\n", version.GoBuildTime) + } + fmt.Fprintf(w, "Go: %v (%v/%v)\n", runtime.Version(), runtime.GOOS, runtime.GOARCH) + return nil +} diff --git a/pkg/version/version.go b/pkg/version/version.go new file mode 100644 index 0000000..da44dc0 --- /dev/null +++ b/pkg/version/version.go @@ -0,0 +1,12 @@ +package version + +/////////////////////////////////////////////////////////////////////////////// +// GLOBALS + +var ( + GitSource string + GitTag string + GitBranch string + GitHash string + GoBuildTime string +)