// Package ollama provides Lua functions for communicating with a local Ollama server package ollama import ( "fmt" "strings" "sync" "github.com/dustin/go-humanize" "github.com/sirupsen/logrus" "github.com/xyproto/algernon/lua/convert" lua "github.com/xyproto/gopher-lua" "github.com/xyproto/ollamaclient/v2" ) const ( // Class is an identifier for the OllamaClient class in Lua Class = "OllamaClient" // Only used when constructing new ollama clients with no explicit model given. // qwen2.5-coder:1.5b is fast and small, but not great. // Good alternatives right now: mixtral, llama3-gradient defaultModel = "qwen2.5-coder:1.5b" imageDescriptionModel = "llava-llama3" imageDescriptionPrompt = "Describe this image." defaultPrompt = "Generate a haiku about the poet Algernon" // For measuring the distance between embeddings defaultMetric = "cosine" ) var mut sync.RWMutex // Get the first argument, "self", and cast it from userdata to a library (which is really a hash map). func checkOllamaClient(L *lua.LState) *ollamaclient.Config { ud := L.CheckUserData(1) if ollama, ok := ud.Value.(*ollamaclient.Config); ok { return ollama } L.ArgError(1, "ollamaclient.Config expected") return nil } // ollamaPullIfNeeded will download the active model, if it's missing // it takes an optional "verbose" argument that is bool. func ollamaPullIfNeeded(L *lua.LState) int { oc := checkOllamaClient(L) // arg 1 // Pull the model, in a verbose way err := oc.PullIfNeeded(true) if err != nil { logrus.Error(err) L.Push(lua.LString(err.Error())) return 1 // number of results } return 0 // number of results } // ollamaHas will check if the given model has been downloaded func ollamaHas(L *lua.LState) int { oc := checkOllamaClient(L) // arg 1 // Check if the given model name has already been downloaded modelName := L.ToString(2) // arg 2 found, err := oc.Has(modelName) if err != nil { logrus.Error(err) L.Push(lua.LString(err.Error())) return 1 // number of results } L.Push(lua.LBool(found)) return 1 // number of results } // ollamaList will list all downloaded models, if possible func ollamaList(L *lua.LState) int { oc := checkOllamaClient(L) // arg 1 downloadedModels, _, _, err := oc.List() if err != nil { logrus.Error(err) L.Push(convert.Strings2table(L, []string{})) return 1 // number of results } L.Push(convert.Strings2table(L, downloadedModels)) return 1 // number of results } // ollamaSizeInBytes will check the size on disk for the given model, if possible func ollamaSizeInBytes(L *lua.LState) int { oc := checkOllamaClient(L) // arg 1 top := L.GetTop() mut.RLock() modelName := oc.ModelName mut.RUnlock() if top > 1 { modelName = L.ToString(2) } size, err := oc.SizeOf(modelName) // get the size of the given model name if err != nil { logrus.Error(err) L.Push(lua.LString(err.Error())) return 1 // number of results } L.Push(lua.LNumber(size)) return 1 // number of results } // ollamaSize checks the size on disk for the given model, if possible, // and returns the size as a human-friendly string using the humanize package. func ollamaSize(L *lua.LState) int { oc := checkOllamaClient(L) // Assume this is a function that checks for an Ollama client instance top := L.GetTop() mut.RLock() modelName := oc.ModelName mut.RUnlock() if top > 1 { modelName = L.ToString(2) } size, err := oc.SizeOf(modelName) // Assume this gets the size of the given model name in bytes if err != nil { logrus.Println(err) L.Push(lua.LString(err.Error())) return 1 // number of results } // Use humanize package to format size sizeStr := humanize.Bytes(uint64(size)) L.Push(lua.LString(sizeStr)) return 1 // number of results } // ollamaModel sets or gets the given model name, but does not pull anything func ollamaModel(L *lua.LState) int { oc := checkOllamaClient(L) // Assume this is a function that checks for an Ollama client instance top := L.GetTop() if top < 2 { // Return the current model name if no model is passed in mut.RLock() model := oc.ModelName mut.RUnlock() L.Push(lua.LString(model)) return 1 // number of results } modelName := strings.TrimSpace(L.ToString(2)) L.Push(lua.LString(modelName)) mut.Lock() oc.ModelName = modelName mut.Unlock() return 1 // number of results } func ollamaGenerateOutput(L *lua.LState) int { mut.Lock() defer mut.Unlock() oc := checkOllamaClient(L) // arg 1 prompt := defaultPrompt top := L.GetTop() if top > 2 { prompt = L.ToString(2) // arg 2 oc.ModelName = L.ToString(3) // arg 3 err := oc.PullIfNeeded(true) if err != nil { logrus.Error(err) L.Push(lua.LString(err.Error())) return 1 // number of results } } else if top > 1 { prompt = L.ToString(2) // arg 2 } oc.SetReproducible() output, err := oc.GetOutput(prompt) if err != nil { logrus.Error(err) L.Push(lua.LString(err.Error())) return 1 // number of results } L.Push(lua.LString(strings.TrimPrefix(output, " "))) return 1 // number of results } // Use ollama to get a []float64 representation of a given prompt. // This can be used to ie. find the distance between two strings. func ollamaEmbeddings(L *lua.LState) int { mut.Lock() defer mut.Unlock() oc := checkOllamaClient(L) // arg 1 prompt := defaultPrompt top := L.GetTop() if top > 2 { prompt = L.ToString(2) // arg 2 oc.ModelName = L.ToString(3) // arg 3 err := oc.PullIfNeeded(true) if err != nil { logrus.Error(err) L.Push(lua.LString(err.Error())) return 1 // number of results } } else if top > 1 { prompt = L.ToString(2) // arg 2 } oc.SetReproducible() var err error var floats []float64 floats, err = oc.Embeddings(prompt) if err != nil { logrus.Error(err) L.Push(lua.LString(err.Error())) return 1 // number of results } // Push the floats as Lua numbers tbl := L.NewTable() for i, f := range floats { index := fmt.Sprintf("%d", i+1) // Convert index to string for Lua table L.SetField(tbl, index, lua.LNumber(f)) } L.Push(tbl) return 1 // number of results } // Use ollama to get a []float64 representation of two given prompts, // and then find the distance between them and return the distance. // Also takes an optional model name and an optional metric: // cosine, euclidean, manhattan, chebyshev or hamming func ollamaDistance(L *lua.LState) int { mut.Lock() defer mut.Unlock() oc := checkOllamaClient(L) // arg 1 prompt1 := L.ToString(2) // arg 2 prompt2 := L.ToString(3) // arg 3 metric := L.OptString(4, defaultMetric) // arg 4 oc.ModelName = L.OptString(5, oc.ModelName) // arg 5 err := oc.PullIfNeeded(true) if err != nil { logrus.Error(err) L.Push(lua.LString(err.Error())) return 1 // number of results } oc.SetReproducible() var slice1 []float64 slice1, err = oc.Embeddings(prompt1) if err != nil { logrus.Error(err) L.Push(lua.LString(err.Error())) return 1 // number of results } var slice2 []float64 slice2, err = oc.Embeddings(prompt2) if err != nil { logrus.Error(err) L.Push(lua.LString(err.Error())) return 1 // number of results } // Measure the distance between slice1 and slice2 and return the distance var distance float64 metricLetters := defaultMetric[0:2] if len(metric) > 1 { metricLetters = metric[0:2] } switch metricLetters { case "co": // cosine distance, err = cosineDistance(slice1, slice2) case "eu": // euclidean distance, err = euclideanDistance(slice1, slice2) case "ch": //chebyshev distance, err = chebyshevDistance(slice1, slice2) case "ha": // hamming distance, err = hammingDistance(slice1, slice2) case "ma": // manhattan distance, err = manhattanDistance(slice1, slice2) default: // cosine distance, err = cosineDistance(slice1, slice2) } if err != nil { L.Push(lua.LString(err.Error())) return 1 // number of results (error message) } L.Push(lua.LNumber(distance)) return 1 // number of results (distance) } // Return the given file as a base64-encoded string func base64EncodeFile(L *lua.LState) int { filename := L.ToString(1) // try to get the first argument base64encodedImage, err := ollamaclient.Base64EncodeFile(filename) if err != nil { logrus.Error(err) L.Push(lua.LString(err.Error())) return 1 // number of results } // return the encoded image L.Push(lua.LString(base64encodedImage)) return 1 // number of results } // Use a new ollama client to describe the given base64 encoded image // Also takes an optional model name, such as "llava" or "llava-llama3" func describeImage(L *lua.LState) int { base64encodedImage := L.ToString(1) // arg 1 modelName := imageDescriptionModel if L.GetTop() > 1 { if givenModelName := L.ToString(2); givenModelName != "" { // arg 2 modelName = givenModelName } } oc := ollamaclient.New() oc.ModelName = modelName err := oc.PullIfNeeded(true) if err != nil { logrus.Error(err) L.Push(lua.LString(err.Error())) return 1 // number of results } oc.SetReproducible() // Note that more images can be added as arguments, GetOutput accepts varargs description, err := oc.GetOutput(imageDescriptionPrompt, base64encodedImage) if err != nil { logrus.Error(err) L.Push(lua.LString(err.Error())) return 1 // number of results } L.Push(lua.LString(description)) return 1 // number of results } func ollamaGenerateOutputCreative(L *lua.LState) int { mut.Lock() defer mut.Unlock() oc := checkOllamaClient(L) // arg 1 prompt := defaultPrompt top := L.GetTop() if top > 2 { prompt = L.ToString(2) // arg 2 oc.ModelName = L.ToString(3) // arg 3 err := oc.PullIfNeeded(true) if err != nil { logrus.Error(err) L.Push(lua.LString(err.Error())) return 1 // number of results } } else if top > 1 { prompt = L.ToString(2) // arg 2 } oc.SetRandom() output, err := oc.GetOutput(prompt) if err != nil { logrus.Error(err) L.Push(lua.LString(err.Error())) return 1 // number of results } L.Push(lua.LString(output)) return 1 // number of results } func constructOllamaClient(L *lua.LState) (*lua.LUserData, error) { oc := ollamaclient.New() top := L.GetTop() if top > 1 { // given two strings, the model and host address oc.ModelName = L.ToString(1) oc.ServerAddr = L.ToString(2) } else if top > 0 { // given one string, the model oc.ModelName = L.ToString(1) } else { oc.ModelName = defaultModel } // Create a new userdata struct ud := L.NewUserData() ud.Value = oc L.SetMetatable(ud, L.GetTypeMetatable(Class)) return ud, nil } // The hash map methods that are to be registered var ollamaMethods = map[string]lua.LGFunction{ "ask": ollamaGenerateOutput, "bytesize": ollamaSizeInBytes, "creative": ollamaGenerateOutputCreative, "has": ollamaHas, "list": ollamaList, "model": ollamaModel, // set or get the current model, but don't pull anything "pull": ollamaPullIfNeeded, "size": ollamaSize, "embeddings": ollamaEmbeddings, // get a []float64 representation of a given prompt "distance": ollamaDistance, // get the distance between two prompts (via embeddings) } func askOllama(L *lua.LState) int { prompt := defaultPrompt model := defaultModel top := L.GetTop() if top > 1 { prompt = L.ToString(1) model = L.ToString(2) } else if top > 0 { prompt = L.ToString(1) } oc := ollamaclient.New() oc.ModelName = model // Pull the model, in a verbose way err := oc.PullIfNeeded(true) if err != nil { logrus.Error(err) L.Push(lua.LString(err.Error())) return 1 // number of results } output, err := oc.GetOutput(prompt) if err != nil { logrus.Error(err) L.Push(lua.LString(err.Error())) return 1 // number of results } L.Push(lua.LString(output)) return 1 // number of results } // embeddedDistance calculates the distance between two embeddings (Lua tables of floats). func embeddedDistance(L *lua.LState) int { // Check and get the first table argument tbl1 := L.CheckTable(1) // Check and get the second table argument tbl2 := L.CheckTable(2) // Get the distance metric (optional) metric := L.OptString(3, defaultMetric) // Convert Lua tables to slices of float64 slice1, err := tableToFloatSlice(tbl1) if err != nil { L.Push(lua.LString(err.Error())) return 1 // number of results (error message) } slice2, err := tableToFloatSlice(tbl2) if err != nil { L.Push(lua.LString(err.Error())) return 1 // number of results (error message) } // Check if the slices have different length if len(slice1) != len(slice2) { L.Push(lua.LString("error: embeddings must be of the same length")) return 1 // number of results (error message) } // Choose the distance metric var distance float64 metricLetters := defaultMetric[0:2] if len(metric) > 1 { metricLetters = metric[0:2] } switch metricLetters { case "co": // cosine distance, err = cosineDistance(slice1, slice2) case "eu": // euclidean distance, err = euclideanDistance(slice1, slice2) case "ch": //chebyshev distance, err = chebyshevDistance(slice1, slice2) case "ha": // hamming distance, err = hammingDistance(slice1, slice2) case "ma": // manhattan distance, err = manhattanDistance(slice1, slice2) default: // cosine distance, err = cosineDistance(slice1, slice2) } if err != nil { L.Push(lua.LString(err.Error())) return 1 // number of results (error message) } L.Push(lua.LNumber(distance)) return 1 // number of results (distance) } // Load makes functions related to Ollama clients available to the given Lua state func Load(L *lua.LState) { // Register the OllamaClient class and the methods that belong with it. mt := L.NewTypeMetatable(Class) mt.RawSetH(lua.LString("__index"), mt) L.SetFuncs(mt, ollamaMethods) // The constructor for new Libraries takes only an optional id L.SetGlobal("OllamaClient", L.NewFunction(func(L *lua.LState) int { // Construct a new OllamaClient userdata, err := constructOllamaClient(L) if err != nil { logrus.Error(err) L.Push(lua.LString(err.Error())) return 1 // Number of returned values } // Return the Lua OllamaClient object L.Push(userdata) return 1 // number of results })) // Create an ollama client on the fly and submit a prompt L.SetGlobal("ollama", L.NewFunction(askOllama)) // Find the distance between two embeddings L.SetGlobal("embeddedDistance", L.NewFunction(embeddedDistance)) // Base64-encode the given file L.SetGlobal("base64EncodeFile", L.NewFunction(base64EncodeFile)) // Use Ollama and an optional model name to describe the given base64-encoded image L.SetGlobal("describeImage", L.NewFunction(describeImage)) }