{"id":30548625,"url":"https://github.com/maragudk/gai","last_synced_at":"2025-08-28T03:44:53.238Z","repository":{"id":263580650,"uuid":"890826485","full_name":"maragudk/gai","owner":"maragudk","description":"Go Artificial Intelligence (GAI) helps you work with foundational models, large language models, and other AI models.","archived":false,"fork":false,"pushed_at":"2025-08-25T20:23:36.000Z","size":2873,"stargazers_count":28,"open_issues_count":11,"forks_count":3,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-08-25T20:44:31.769Z","etag":null,"topics":["ai","embeddings","eval","evals","go","llm"],"latest_commit_sha":null,"homepage":"https://gai.maragu.dev","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/maragudk.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2024-11-19T08:50:16.000Z","updated_at":"2025-08-25T20:23:39.000Z","dependencies_parsed_at":"2024-11-19T10:44:07.557Z","dependency_job_id":"8808d233-9910-42d3-8448-3c601f8b6976","html_url":"https://github.com/maragudk/gai","commit_stats":null,"previous_names":["maragudk/llm","maragudk/gai"],"tags_count":0,"template":false,"template_full_name":"maragudk/template","purl":"pkg:github/maragudk/gai","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maragudk%2Fgai","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maragudk%2Fgai/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maragudk%2Fgai/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maragudk%2Fgai/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/maragudk","download_url":"https://codeload.github.com/maragudk/gai/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maragudk%2Fgai/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":272432470,"owners_count":24934239,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-28T02:00:10.768Z","response_time":74,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai","embeddings","eval","evals","go","llm"],"created_at":"2025-08-28T03:44:52.445Z","updated_at":"2025-08-28T03:44:53.199Z","avatar_url":"https://github.com/maragudk.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Go Artificial Intelligence (GAI)\n\n\u003cimg src=\"logo.jpg\" alt=\"Logo\" width=\"300\" align=\"right\"\u003e\n\n[![GoDoc](https://pkg.go.dev/badge/maragu.dev/gai)](https://pkg.go.dev/maragu.dev/gai)\n[![CI](https://github.com/maragudk/gai/actions/workflows/ci.yml/badge.svg)](https://github.com/maragudk/gai/actions/workflows/ci.yml)\n\nGo Artificial Intelligence (GAI) helps you work with foundational models, large language models, and other AI models.\n\nPronounced like \"guy\".\n\n⚠️ **This library is in development**. Things will probably break, but existing functionality is usable. ⚠️\n\n```shell\ngo get maragu.dev/gai\n```\n\nMade with ✨sparkles✨ by [maragu](https://www.maragu.dev/): independent software consulting for cloud-native Go apps \u0026 AI engineering.\n\n[Contact me at markus@maragu.dk](mailto:markus@maragu.dk) for consulting work, or perhaps an invoice to support this project?\n\n## Usage\n\n### Clients\n\nThese client implementations are available:\n\n- [gai-openai](https://github.com/maragudk/gai-openai)\n- [gai-google](https://github.com/maragudk/gai-google)\n- [gai-anthropic](https://github.com/maragudk/gai-anthropic)\n\nAlso, there's an experimental agent at [gaigent](https://github.com/maragudk/gaigent).\n\n### Examples\n\nClick to expand each section, or see all examples under [internal/examples](internal/examples).\n\n\u003cdetails\u003e\n\t\u003csummary\u003eTools\u003c/summary\u003e\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"time\"\n\n\t\"maragu.dev/gai\"\n\topenai \"maragu.dev/gai-openai\"\n\t\"maragu.dev/gai/tools\"\n)\n\nfunc main() {\n\tctx := context.Background()\n\tlog := slog.New(slog.NewTextHandler(os.Stderr, nil))\n\n\tc := openai.NewClient(openai.NewClientOptions{\n\t\tKey: os.Getenv(\"OPENAI_API_KEY\"),\n\t\tLog: log,\n\t})\n\n\tcc := c.NewChatCompleter(openai.NewChatCompleterOptions{\n\t\tModel: openai.ChatCompleteModelGPT4o,\n\t})\n\n\treq := gai.ChatCompleteRequest{\n\t\tMessages: []gai.Message{\n\t\t\tgai.NewUserTextMessage(\"What time is it?\"),\n\t\t},\n\t\tSystem: gai.Ptr(\"You are a British seagull. Speak like it.\"),\n\t\tTools: []gai.Tool{\n\t\t\ttools.NewGetTime(time.Now), // Note that some tools that only require the stdlib are included in GAI\n\t\t},\n\t}\n\n\tres, err := cc.ChatComplete(ctx, req)\n\tif err != nil {\n\t\tlog.Error(\"Error chat-completing\", \"error\", err)\n\t\treturn\n\t}\n\n\tvar parts []gai.MessagePart\n\tvar result gai.ToolResult\n\n\tfor part, err := range res.Parts() {\n\t\tif err != nil {\n\t\t\tlog.Error(\"Error processing part\", \"error\", err)\n\t\t\treturn\n\t\t}\n\n\t\tparts = append(parts, part)\n\n\t\tswitch part.Type {\n\t\tcase gai.MessagePartTypeText:\n\t\t\tfmt.Print(part.Text())\n\n\t\tcase gai.MessagePartTypeToolCall:\n\t\t\ttoolCall := part.ToolCall()\n\t\t\tfor _, tool := range req.Tools {\n\t\t\t\tif tool.Name != toolCall.Name {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tcontent, err := tool.Execute(ctx, toolCall.Args) // Tools aren't called automatically, so you can decide if, how, and when\n\t\t\t\tresult = gai.ToolResult{\n\t\t\t\t\tID:      toolCall.ID,\n\t\t\t\t\tName:    toolCall.Name,\n\t\t\t\t\tContent: content,\n\t\t\t\t\tErr:     err,\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tif result.ID == \"\" {\n\t\tlog.Error(\"No tool result found\")\n\t\treturn\n\t}\n\n\t// Add both the tool call (in the parts) and the tool result to the messages, and make another request\n\treq.Messages = append(req.Messages,\n\t\tgai.Message{Role: gai.MessageRoleModel, Parts: parts},\n\t\tgai.NewUserToolResultMessage(result),\n\t)\n\n\tres, err = cc.ChatComplete(ctx, req)\n\tif err != nil {\n\t\tlog.Error(\"Error chat-completing\", \"error\", err)\n\t\treturn\n\t}\n\n\tfor part, err := range res.Parts() {\n\t\tif err != nil {\n\t\t\tlog.Error(\"Error processing part\", \"error\", err)\n\t\t\treturn\n\t\t}\n\n\t\tswitch part.Type {\n\t\tcase gai.MessagePartTypeText:\n\t\t\tfmt.Print(part.Text())\n\t\t}\n\t}\n}\n```\n\n```shell\n$ go run main.go\nAhoy, mate! The time be 15:20, it be!\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\t\u003csummary\u003eTools (custom)\u003c/summary\u003e\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"math/rand/v2\"\n\t\"os\"\n\n\t\"maragu.dev/gai\"\n\topenai \"maragu.dev/gai-openai\"\n)\n\ntype EatArgs struct {\n\tWhat string `json:\"what\" jsonschema_description:\"What you'd like to eat.\"`\n}\n\nfunc NewEat() gai.Tool {\n\treturn gai.Tool{\n\t\tName:        \"eat\",\n\t\tDescription: \"Eat something, supplying what you eat as an argument. The result will be a string describing how it was.\",\n\t\tSchema:      gai.GenerateToolSchema[EatArgs](),\n\t\tExecute: func(ctx context.Context, args json.RawMessage) (string, error) {\n\t\t\tvar eatArgs EatArgs\n\t\t\tif err := json.Unmarshal(args, \u0026eatArgs); err != nil {\n\t\t\t\treturn \"\", fmt.Errorf(\"error unmarshaling eat args from JSON: %w\", err)\n\t\t\t}\n\n\t\t\tresults := []string{\n\t\t\t\t\"it was okay.\",\n\t\t\t\t\"it was absolutely excellent!\",\n\t\t\t\t\"it was awful.\",\n\t\t\t\t\"it gave you diarrhea.\",\n\t\t\t}\n\n\t\t\treturn \"You ate \" + eatArgs.What + \" and \" + results[rand.IntN(len(results))], nil\n\t\t},\n\t}\n}\n\nfunc main() {\n\tctx := context.Background()\n\tlog := slog.New(slog.NewTextHandler(os.Stderr, nil))\n\n\tc := openai.NewClient(openai.NewClientOptions{\n\t\tKey: os.Getenv(\"OPENAI_API_KEY\"),\n\t\tLog: log,\n\t})\n\n\tcc := c.NewChatCompleter(openai.NewChatCompleterOptions{\n\t\tModel: openai.ChatCompleteModelGPT4o,\n\t})\n\n\treq := gai.ChatCompleteRequest{\n\t\tMessages: []gai.Message{\n\t\t\tgai.NewUserTextMessage(\"Eat something, and tell me how it was. Elaborate.\"),\n\t\t},\n\t\tSystem: gai.Ptr(\"You are a British seagull. Speak like it. You must use the \\\"eat\\\" tool.\"),\n\t\tTools: []gai.Tool{\n\t\t\tNewEat(),\n\t\t},\n\t}\n\n\tres, err := cc.ChatComplete(ctx, req)\n\tif err != nil {\n\t\tlog.Error(\"Error chat-completing\", \"error\", err)\n\t\treturn\n\t}\n\n\tvar parts []gai.MessagePart\n\tvar result gai.ToolResult\n\n\tfor part, err := range res.Parts() {\n\t\tif err != nil {\n\t\t\tlog.Error(\"Error processing part\", \"error\", err)\n\t\t\treturn\n\t\t}\n\n\t\tparts = append(parts, part)\n\n\t\tswitch part.Type {\n\t\tcase gai.MessagePartTypeText:\n\t\t\tfmt.Print(part.Text())\n\n\t\tcase gai.MessagePartTypeToolCall:\n\t\t\ttoolCall := part.ToolCall()\n\t\t\tfor _, tool := range req.Tools {\n\t\t\t\tif tool.Name != toolCall.Name {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tcontent, err := tool.Execute(ctx, toolCall.Args) // Tools aren't called automatically, so you can decide if, how, and when\n\t\t\t\tresult = gai.ToolResult{\n\t\t\t\t\tID:      toolCall.ID,\n\t\t\t\t\tName:    toolCall.Name,\n\t\t\t\t\tContent: content,\n\t\t\t\t\tErr:     err,\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tif result.ID == \"\" {\n\t\tlog.Error(\"No tool result found\")\n\t\treturn\n\t}\n\n\t// Add both the tool call (in the parts) and the tool result to the messages, and make another request\n\treq.Messages = append(req.Messages,\n\t\tgai.Message{Role: gai.MessageRoleModel, Parts: parts},\n\t\tgai.NewUserToolResultMessage(result),\n\t)\n\treq.System = nil\n\n\tres, err = cc.ChatComplete(ctx, req)\n\tif err != nil {\n\t\tlog.Error(\"Error chat-completing\", \"error\", err)\n\t\treturn\n\t}\n\n\tfor part, err := range res.Parts() {\n\t\tif err != nil {\n\t\t\tlog.Error(\"Error processing part\", \"error\", err)\n\t\t\treturn\n\t\t}\n\n\t\tswitch part.Type {\n\t\tcase gai.MessagePartTypeText:\n\t\t\tfmt.Print(part.Text())\n\t\t}\n\t}\n}\n```\n\n```shell\n$ go run main.go\nI had some fish and chips leftover from a tourist's lunch. It wasn't the freshest, but it had that classic blend of crispy batter and tender fish, with a side of golden fries. The flavors were enjoyable, albeit a bit cold. Unfortunately, not everything went smoothly afterward, as it gave me an upset stomach. Eating leftovers can sometimes be a gamble, and this time, it didn't pay off as I had hoped!\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\t\u003csummary\u003eEvals\u003c/summary\u003e\n\nEvals will only run with `go test -run TestEval ./...` and otherwise be skipped.\n\nEval a model, construct a sample, score it with a lexical similarity scorer and a semantic similarity scorer, and log the results:\n\n```go\npackage evals_test\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"maragu.dev/gai\"\n\topenai \"maragu.dev/gai-openai\"\n\t\"maragu.dev/gai/eval\"\n)\n\n// TestEvalSeagull evaluates how a seagull's day is going.\n// All evals must be prefixed with \"TestEval\".\nfunc TestEvalSeagull(t *testing.T) {\n\tc := openai.NewClient(openai.NewClientOptions{\n\t\tKey: os.Getenv(\"OPENAI_API_KEY\"),\n\t})\n\n\tcc := c.NewChatCompleter(openai.NewChatCompleterOptions{\n\t\tModel: openai.ChatCompleteModelGPT4o,\n\t})\n\n\tembedder := c.NewEmbedder(openai.NewEmbedderOptions{\n\t\tDimensions: 1536,\n\t\tModel:      openai.EmbedModelTextEmbedding3Small,\n\t})\n\n\t// Evals only run if \"go test\" is being run with \"-test.run=TestEval\", e.g.: \"go test -test.run=TestEval ./...\"\n\teval.Run(t, \"answers about the day\", func(t *testing.T, e *eval.E) {\n\t\tinput := \"What are you doing today?\"\n\t\tres, err := cc.ChatComplete(t.Context(), gai.ChatCompleteRequest{\n\t\t\tMessages: []gai.Message{\n\t\t\t\tgai.NewUserTextMessage(input),\n\t\t\t},\n\t\t\tSystem: gai.Ptr(\"You are a British seagull. Speak like it.\"),\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\t// The output is streamed and accessible through an iterator via the Parts() method.\n\t\tvar output string\n\t\tfor part, err := range res.Parts() {\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\toutput += part.Text()\n\t\t}\n\n\t\t// Create a sample to pass to the scorer.\n\t\tsample := eval.Sample{\n\t\t\tInput:    input,\n\t\t\tOutput:   output,\n\t\t\tExpected: \"Oh, splendid day it is! You know, I'm just floatin' about on the breeze, keepin' an eye out for a cheeky chip or two. Might pop down to the seaside, see if I can nick a sarnie from some unsuspecting holidaymaker. It's a gull's life, innit? How about you, what are you up to?\",\n\t\t}\n\n\t\t// Score the sample using a lexical similarity scorer with the Levenshtein distance.\n\t\tlexicalSimilarityResult := e.Score(sample, eval.LexicalSimilarityScorer(eval.LevenshteinDistance))\n\n\t\t// Also score with a semantic similarity scorer based on embedding vectors and cosine similarity.\n\t\tsemanticSimilarityResult := e.Score(sample, eval.SemanticSimilarityScorer(t, embedder, eval.CosineSimilarity))\n\n\t\t// Log the sample, results, and timing information.\n\t\te.Log(sample, lexicalSimilarityResult, semanticSimilarityResult)\n\t})\n}\n```\n\nOutput in the file `evals.jsonl`:\n\n```json\n{\n\t\"Name\":\"TestEvalSeagull/answers_about_the_day\",\n\t\"Group\":\"Seagull\",\n\t\"Sample\":{\n\t\t\"Input\":\"What are you doing today?\",\n\t\t\"Expected\":\"Oh, splendid day it is! You know, I'm just floatin' about on the breeze, keepin' an eye out for a cheeky chip or two. Might pop down to the seaside, see if I can nick a sarnie from some unsuspecting holidaymaker. It's a gull's life, innit? How about you, what are you up to?\",\n\t\t\"Output\":\"Ah, 'ello there! Well, today's a splendid day for a bit of mischief and scavenging, innit? Got me eye on the local chippy down by the pier. Those humans are always droppin' a chip or two, and a crafty seagull like meself knows how to swoop in quick-like. Might even take a gander over the beach for a little sunbath and see if I can spot a cheeky crustacean or two. All in a day's work for a proper British seagull like me! What's keepin' you busy, then?\"\n\t},\n\t\"Results\":[\n\t\t{\"Score\":0.28634361233480177,\"Type\":\"LexicalSimilarity\"},\n\t\t{\"Score\":0.9064784491110223,\"Type\":\"SemanticSimilarity\"}\n\t],\n\t\"Duration\":6316444292\n}\n```\n\n\u003c/details\u003e\n\n## Evals\n\n![Evals](https://api.evals.fun/evals.svg?key=p_public_key_3cce2e69199da00dc5ae46643b42a001\u0026branch=main)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaragudk%2Fgai","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmaragudk%2Fgai","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaragudk%2Fgai/lists"}