Första intrycket av Genkit Go

Genkit Go 1.0 with viking Gopher

Genkit är ett öppet ramverk för att bygga fullstack-applikationer som drivs av AI, utvecklat och använt i produktion av Google.

Bakgrund

I början av september presenterade Google den stabila och produktionsklara Genkit Go 1.0.

Genkit har sedan tidigare funnits för JavaScript/TypeScript, men finns nu även i en stabil version för Go.

(En variant för Python är under utveckling och finns i dagsläget som en förhandsversion)

Vad du behöver installera

Du behöver ha installerat Go 1.24+ på din dator samt en valfri textredigerare.

Om du även vill köra en helt lokal modell behöver du även Ollama

Om du använder Homebrew så bör du kunna göra:

$ brew install go neovim ollama

Komma igång

Dokumentationen för att komma igång med Genkit är föredömligt bra och tydlig, i de fall där den är uppdaterad för Go.

Notera: Vissa sektioner har dock ännu inte skrivits om (tutorials, authorization, monitorering, osv) men det kommer förhoppningsvis att förbättras med tiden då mer funktionalitet portas över.

Installation av genkit

Nu installerar vi CLI-verktyget genkit (detta ger framförallt tillgång till genkit ui:start) via;

$ curl -sL cli.genkit.dev | bash

Notera: Om du inte vill delta i Googles insamling av analysvärden så kan du:

$ genkit config set analyticsOptOut true

Installation av Genkit Go ✨

Först behöver vi initialisera vår Go-modul i en ny mapp;

$ mkdir genkit-with-ollama && cd genkit-with-ollama
$ go mod init genkit-with-ollama

Genkit Go hämtas sedan ner med hjälp av go get;

$ go get github.com/google/genkit/go@latest

Detta bör nu resultera i en go.mod likt detta;

module genkit-with-ollama

go 1.25.1

require github.com/firebase/genkit/go v1.0.2

Uppdatera beroenden i go.mod och checksummor i go.sum via go mod tidy.

Val av lokal AI-modell

Då vi ämnar använda oss av Ollama för att köra en lokal AI-modell så behöver vi ladda ner en lämplig sådan.

Man bör antagligen välja en Ollama-modell taggad som tools.

Sådana modeller hittar du under https://ollama.com/search?c=tools

Notera: Du kommer med största säkerhet vilja jämföra ett antal olika modeller innan du hittar den som lämpar sig bäst för dina syften, och hårdvara.

Efter att ha testat lite olika modeller så landade jag i att använda llama3-groq-tool-use:8b i det här exemplet.

Med hjälp av ollama pull hämtar du ner denna modell;

$ ollama pull llama3-groq-tool-use:8b

Starta utvecklingsgränssnittet för Genkit

I genkit-with-ollama/ kör du följande;

$ genkit ui:start
Starting...

  Genkit Developer UI started at: http://localhost:4000
  To stop the UI, run `genkit ui:stop`.

Set env variable `GENKIT_ENV` to `dev` and start 
your app code to interact with it in the UI.

Nu bör du kunna gå till http://localhost:4000 och se webbgränssnittet för Genkit, med ett meddelande som säger Waiting to connect to Genkit runtime...

Ett första Genkit-flöde för att testa att webbgränssnittet fungerar

För att verifiera att webbgränssnittet hittar vår applikation, så kan vi skriva ett litet program likt detta;

Notera: Här använder vi oss inte av någon AI-modell, utan funktionen konstruerar bara en sträng.

minimal.go

package main

import (
	"context"
	"fmt"
	"log"
	"net/http"

	"github.com/firebase/genkit/go/genkit"
	"github.com/firebase/genkit/go/plugins/server"
)

func main() {
	log.Fatal(run(context.Background(), "127.0.0.1:3400"))
}

func run(ctx context.Context, addr string) error {
	g := genkit.Init(ctx)

	genkit.DefineFlow(g, "greetingFlow",
		func(ctx context.Context, name string) (string, error) {
			return fmt.Sprintf("Hello, %s!", name), nil
		},
	)

	mux := http.NewServeMux()

	for _, a := range genkit.ListFlows(g) {
		mux.HandleFunc("POST /"+a.Name(), genkit.Handler(a))
	}

	return server.Start(ctx, addr, mux)
}
$ GENKIT_ENV=dev go run minimal.go

Nu bör vi se följande i webbläsaren:

Your Genkit app - Flows - greetingFlow

Och om du skickar "Peter" som input JSON till flödet bör du få ett resultat (och trace) likt detta;

greetingFlow

Du kan naturligtvis även använda dig av cURL och jq

$ curl -s -d $(jq -n -c '{data: "Peter"}') http://127.0.0.1:3400/greetingFlow | jq
{
  "result": "Hello, Peter!"
}

Om allt fungerade som det skulle så är vi nu redo att skriva kod där Genkit kommunicerar med Ollama!

Första AI-flödet med Genkit och Ollama

Exemplet i dokumentationen är en receptgenerator men jag tänkte att vi kunde göra något lite roligare;
En karaktärsgenerator för rollspel.

Ett flöde som genererar rollspelskaraktärer

main.go

package main

import (
	"cmp"
	"context"
	"fmt"
	"log"
	"math/rand"
	"net/http"
	"os"
	"strings"

	"github.com/firebase/genkit/go/ai"
	"github.com/firebase/genkit/go/genkit"
	"github.com/firebase/genkit/go/plugins/ollama"
	"github.com/firebase/genkit/go/plugins/server"
)

const modelName = "llama3-groq-tool-use:8b"

func main() {
	addr := "127.0.0.1:3400"

	log.Fatal(run(context.Background(), addr))
}

func run(ctx context.Context, addr string) error {
	o := newOllamaPlugin()

	g := genkit.Init(ctx,
		genkit.WithPlugins(o),
		genkit.WithDefaultModel("ollama/"+modelName),
	)

	o.DefineModel(g, ollama.ModelDefinition{
		Name: modelName,
		Type: "llm",
	}, nil)

	genkit.DefineFlow(g, "rpgCharacterGeneratorFlow",
		func(ctx context.Context, input *RPGCharacterInput) (*RPGCharacter, error) {
			c := &RPGCharacter{
				Race:  randomRace(input.Race),
				Class: randomClass(input.Class),
			}

			{ // Generate the attributes of the character
				out, _, err := genkit.GenerateData[RPGCharacterAttributes](ctx, g,
					ai.WithPrompt(fmt.Sprintf(
						`Create attribute scores in the inclusive range 1-20 
						for a Role Playing Game character, for inspiration: 
						Class: %s Race: %s`,
						c.Class, c.Race,
					)),
				)
				if err != nil {
					return nil, fmt.Errorf("failed to generate attributes: %w", err)
				}

				c.RPGCharacterAttributes = *out
			}

			{ // Generate the name of the character
				out, _, err := genkit.GenerateData[RPGCharacterDetails](ctx, g,
					ai.WithPrompt(fmt.Sprintf(
						`The name and age of a Role Playing Game character with:
							Race:  %q 
							Class: %q
							Attributes: %+v
						`,
						c.Race, c.Class, c.RPGCharacterAttributes,
					)),
				)

				if err != nil {
					return nil, fmt.Errorf("failed to generate details: %w", err)
				}

				c.RPGCharacterDetails = *out
			}

			{ // Generate some extra information about the character
				out, _, err := genkit.GenerateData[RPGCharacterExtra](ctx, g,
					ai.WithPrompt(fmt.Sprintf(
						`Extra information about a Role Playing Game character, 
							for inspiration: %s %s named %s and its age is %d 
							with the attributes %+v
						`,
						c.Class, c.Race, c.Name, c.Age, c.RPGCharacterAttributes,
					)),
				)
				if err != nil {
					return nil, fmt.Errorf("failed to generate extra: %w", err)
				}

				c.RPGCharacterExtra = *out
			}

			return c, nil
		},
	)

	mux := setupMux(g)

	log.Println("Starting server on http://" + addr)
	log.Println("Available workflows:")

	for _, a := range genkit.ListFlows(g) {
		log.Println(" POST http://" + addr + "/" + a.Name())
	}

	return server.Start(ctx, addr, mux)
}

func setupMux(g *genkit.Genkit) *http.ServeMux {
	mux := http.NewServeMux()

	for _, a := range genkit.ListFlows(g) {
		mux.HandleFunc("POST /"+a.Name(), genkit.Handler(a))
	}

	return mux
}

func newOllamaPlugin() *ollama.Ollama {
	return &ollama.Ollama{ServerAddress: ollamaServerAddress()}
}

func ollamaServerAddress() string {
	ollamaAddr := cmp.Or(os.Getenv("OLLAMA_HOST"), "127.0.0.1:11434")

	if !strings.Contains(ollamaAddr, ":") {
		ollamaAddr += ":11434"
	}

	return fmt.Sprintf("http://%s", ollamaAddr)
}

func randomRace(races ...string) string {
	if len(races) == 0 ||
		len(races) == 1 && races[0] == "" {
		races = []string{
			"Human",
			"Elf",
			"Dwarf",
			"Halfling",
			"Gnome",
			"Half-Elf",
			"Half-Orc",
			"Tiefling",
		}
	}

	return races[rand.Intn(len(races))]
}

func randomClass(classes ...string) string {
	if len(classes) == 0 ||
		len(classes) == 1 && classes[0] == "" {
		classes = []string{
			"Fighter",
			"Rogue",
			"Wizard",
			"Cleric",
			"Ranger",
			"Bard",
			"Paladin",
			"Sorcerer",
		}
	}

	return classes[rand.Intn(len(classes))]
}

type RPGCharacterInput struct {
	Race  string `json:"race,omitempty" jsonschema:"description=Race of the RPG character"`
	Class string `json:"class,omitempty" jsonschema:"description=Class of the RPG character"`
}

type RPGCharacter struct {
	Race                   string `json:"race"`
	Class                  string `json:"class"`
	RPGCharacterAttributes `json:"attributes"`
	RPGCharacterDetails    `json:"details"`
	RPGCharacterExtra      `json:"extra"`
}

type RPGCharacterAttributes struct {
	STR int `json:"STR" jsonschema:"description=Physical power; affects melee attack rolls, carrying capacity, and physical feats."`
	DEX int `json:"DEX" jsonschema:"description=Agility, reflexes, and balance; affects ranged attacks, Armor Class (AC), and skills like Stealth."`
	CON int `json:"CON" jsonschema:"description=Endurance and health; affects hit points (HP) and resistance to fatigue or poison."`
	INT int `json:"INT" jsonschema:"description=Reasoning, memory, and knowledge; affects spellcasting for Wizards and skills like Arcana or Investigation."`
	WIS int `json:"WIS" jsonschema:"description=Perception, insight, and willpower; affects spellcasting for Clerics and Druids, and skills like Perception or Survival."`
	CHA int `json:"CHA" jsonschema:"description=Force of personality; affects social interactions and spellcasting for Bards, Sorcerers, and Warlocks."`
}

type RPGCharacterDetails struct {
	Name string `json:"name" jsonschema:"description=Name of the RPG character"`
	Age  int    `json:"age" jsonschema:"description=Age of the RPG character (max age per race: Human=90 Elf=750 Dwarf=350 Halfling=250 Gnome=400 Half-Elf=180 Half-Orc=75 Tiefling=100)"`
}

type RPGCharacterExtra struct {
	Background  string `json:"background" jsonschema:"description=Background of the character"`
	Personality string `json:"personality" jsonschema:"description=Personality of the character"`
	Appearance  string `json:"appearance" jsonschema:"description=Appearance of the character"`
}
$ GENKIT_ENV=dev go run main.go

Nu bör du kunna generera en karaktär;

$ curl -sd '{"data":{"race":"Elf"}}' http://127.0.0.1:3400/rpgCharacterGeneratorFlow | jq
{
  "result": {
    "race": "Elf",
    "class": "Cleric",
    "attributes": {
      "STR": 14,
      "DEX": 16,
      "CON": 10,
      "INT": 12,
      "WIS": 18,
      "CHA": 15
    },
    "details": {
      "name": "Eira Moonwhisper",
      "age": 550
    },
    "extra": {
      "background": "Born in the mystical realm of the Elven kingdom, Eira was raised in harmony with nature and the whispers of ancient magic.",
      "personality": "Eira is compassionate and empathetic, often seen healing the wounded or offering guidance. Her wisdom is unmatched, sought out by many for her insight into the workings of the universe.",
      "appearance": "Eira stands tall at 6 feet, with long silver hair that falls like a river down her back. Her eyes shimmer like moonlight on a quiet lake, and her skin has an ethereal glow."
    }
  }
}

Notera: Tyvärr är det inte helt problemfritt att använda en relativt liten lokal modell på detta sätt.

Vanliga fel är att modellen inte lyckas svara med ett välformaterat svar, eller att svaret inte ens är JSON.
Att använda en större modell “i molnet” fungerar förhoppningsvis något bättre.

Vad Genkit Go anbelangar behöver man då bara byta ut vilket plugin man använder (via genkit.WithPlugins)
Om du exempelvis vill använda dig av Google: &googlegenai.GoogleAI{APIKey: "YOUR_API_KEY"}

action-execution rpgCharacterGeneratorFlow

Som synes är det är inte heller jättesnabbt att kalla på en sådan modell flera gånger.
(Jag använder mig av en Apple M1 samt ett Nvidia GeForce RTX 4070 när jag kör lokala modeller)

/ Peter

När denna artikel skrevs var den aktuella versionen av Go 1.25.1 och Genkit Go 1.0.2