Ctrl K
ComunidadGuías…

lua/ollama/ollama.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// 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))
}