Consolidated a bunch of reused code to agents

This commit is contained in:
Steve Dudenhoeffer 2025-03-26 00:21:19 -04:00
parent 5407c1a7cc
commit 5d2c350acf
33 changed files with 2866 additions and 803 deletions

View File

@ -7,14 +7,8 @@ import (
"os"
"strings"
knowledge2 "gitea.stevedudenhoeffer.com/steve/answer/pkg/agents"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents/shared"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents/searcher"
"github.com/joho/godotenv"
"github.com/urfave/cli"
)
@ -102,18 +96,9 @@ func main() {
}
question := strings.Join(c.Args(), " ")
search := searcher.Agent{
Model: m,
agent := agents.NewAgent(m, nil).WithMaxCalls(200)
OnDone: func(ctx context.Context, knowledge shared.Knowledge) error {
slog.Info("done", "knowledge", knowledge)
return nil
},
MaxReads: 20,
}
processor := knowledge2.KnowledgeProcessor{Model: m}
knowledge, err := search.Search(ctx, question, question)
knowledge, err := agent.SearchAndRead(ctx, question, []string{question}, true, 10)
if err != nil {
panic(err)
@ -121,9 +106,12 @@ func main() {
slog.Info("knowledge", "knowledge", knowledge)
sum, err := processor.Process(ctx, knowledge)
res, err := agent.AnswerQuestionWithKnowledge(ctx, knowledge)
if err != nil {
panic(err)
}
fmt.Println(sum)
fmt.Println(res)
return nil
},
}

151
cmd/console/cmd.go Normal file
View File

@ -0,0 +1,151 @@
package main
import (
"context"
"fmt"
"log/slog"
"os"
"strings"
"time"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents/console"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
"github.com/joho/godotenv"
"github.com/urfave/cli"
)
func getKey(key string, env string) string {
if key != "" {
return key
}
return os.Getenv(env)
}
func main() {
// Usage: go run cmd/answer.go question...
// - flags:
// --model=[model string such as openai/gpt-4o, anthropic/claude..., google/gemini-1.5. Default: openai/gpt-4o]
// --search-provider=[search provider string such as google, duckduckgo. Default: google]
// --cache-provider=[cache provider string such as memory, redis, file. Default: memory]
var app = cli.App{
Name: "console",
Usage: "has an llm control a console for you",
Version: "0.1",
Description: "",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "env-file",
Value: ".env",
Usage: "file to read environment variables from",
},
&cli.StringFlag{
Name: "model",
Value: "openai/gpt-4o-mini",
Usage: "model to use for answering the question, syntax: provider/model such as openai/gpt-4o",
},
&cli.StringFlag{
Name: "llm-key",
Value: "",
Usage: "key for the llm model (if empty, will use env var of PROVIDER_API_KEY, such as OPENAI_API_KEY)",
},
},
Action: func(ctx *cli.Context) error {
// if there is no question to answer, print usage
if ctx.NArg() == 0 {
return cli.ShowAppHelp(ctx)
}
if ctx.String("env-file") != "" {
_ = godotenv.Load(ctx.String("env-file"))
}
var llm gollm.LLM
model := ctx.String("model")
a := strings.Split(model, "/")
if len(a) != 2 {
panic("invalid model, expected: provider/model (such as openai/gpt-4o)")
}
switch a[0] {
case "openai":
llm = gollm.OpenAI(getKey(ctx.String("llm-key"), "OPENAI_API_KEY"))
case "anthropic":
llm = gollm.Anthropic(getKey(ctx.String("llm-key"), "ANTHROPI_API_KEY"))
case "google":
llm = gollm.Google(getKey(ctx.String("llm-key"), "GOOGLE_API_KEY"))
default:
panic("unknown model provider")
}
m, err := llm.ModelVersion(a[1])
if err != nil {
panic(err)
}
question := strings.Join(ctx.Args(), " ")
c := console.Agent{
Model: m,
OnDone: func(ctx context.Context, knowledge agents.Knowledge) error { return nil },
OnCommandStart: func(ctx context.Context, command string) error {
slog.Info("command", "command", command)
return nil
},
OnCommandDone: func(ctx context.Context, command string, output string, err error) error {
slog.Info("command done", "command", command, "output", output, "err", err)
return nil
},
ContextualInformation: []string{
fmt.Sprintf("The current time is %s", time.Now().Format(time.RFC3339)),
},
MaxCommands: 0,
}
res, err := c.Answer(context.Background(), []string{question})
if res.Directory != "" {
defer func() {
_ = os.RemoveAll(res.Directory)
}()
}
if err != nil {
panic(err)
}
fmt.Println("Results: ", fmt.Sprintf("%+v", res))
answer, err := agents.AnswerQuestionWithKnowledge(context.Background(), res.Knowledge, m, []string{
fmt.Sprintf("The current time is %s", time.Now().Format(time.RFC3339)),
})
if err != nil {
panic(err)
}
fmt.Println("Answer: ", answer)
return nil
},
}
err := app.Run(os.Args)
if err != nil {
slog.Error("Error: ", err)
}
}

118
cmd/steps/cmd.go Normal file
View File

@ -0,0 +1,118 @@
package main
import (
"context"
"fmt"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents"
"log/slog"
"os"
"strings"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
"github.com/joho/godotenv"
"github.com/urfave/cli"
)
func getKey(key string, env string) string {
if key != "" {
return key
}
return os.Getenv(env)
}
func main() {
// Usage: go run cmd/answer.go question...
// - flags:
// --model=[model string such as openai/gpt-4o, anthropic/claude..., google/gemini-1.5. Default: openai/gpt-4o]
// --search-provider=[search provider string such as google, duckduckgo. Default: google]
// --cache-provider=[cache provider string such as memory, redis, file. Default: memory]
var app = cli.App{
Name: "console",
Usage: "has an llm control a console for you",
Version: "0.1",
Description: "",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "env-file",
Value: ".env",
Usage: "file to read environment variables from",
},
&cli.StringFlag{
Name: "model",
Value: "openai/gpt-4o-mini",
Usage: "model to use for answering the question, syntax: provider/model such as openai/gpt-4o",
},
&cli.StringFlag{
Name: "llm-key",
Value: "",
Usage: "key for the llm model (if empty, will use env var of PROVIDER_API_KEY, such as OPENAI_API_KEY)",
},
},
Action: func(ctx *cli.Context) error {
// if there is no question to answer, print usage
if ctx.NArg() == 0 {
return cli.ShowAppHelp(ctx)
}
if ctx.String("env-file") != "" {
_ = godotenv.Load(ctx.String("env-file"))
}
var llm gollm.LLM
model := ctx.String("model")
a := strings.Split(model, "/")
if len(a) != 2 {
panic("invalid model, expected: provider/model (such as openai/gpt-4o)")
}
switch a[0] {
case "openai":
llm = gollm.OpenAI(getKey(ctx.String("llm-key"), "OPENAI_API_KEY"))
case "anthropic":
llm = gollm.Anthropic(getKey(ctx.String("llm-key"), "ANTHROPI_API_KEY"))
case "google":
llm = gollm.Google(getKey(ctx.String("llm-key"), "GOOGLE_API_KEY"))
default:
panic("unknown model provider")
}
m, err := llm.ModelVersion(a[1])
if err != nil {
panic(err)
}
question := strings.Join(ctx.Args(), " ")
ag := agents.NewAgent(m, nil)
steps, err := ag.SplitQuestion(context.Background(), question)
fmt.Println("Input question: ", question)
fmt.Println("Steps: ")
for i, s := range steps {
fmt.Println(" - Step ", i+1, ": ", s)
}
fmt.Println("Error: ", err)
return nil
},
}
err := app.Run(os.Args)
if err != nil {
slog.Error("Error: ", err)
}
}

28
go.mod
View File

@ -1,6 +1,6 @@
module gitea.stevedudenhoeffer.com/steve/answer
go 1.23.2
go 1.24.1
replace github.com/rocketlaunchr/google-search => github.com/chrisjoyce911/google-search v0.0.0-20230910003754-e501aedf805a
@ -8,15 +8,17 @@ replace github.com/rocketlaunchr/google-search => github.com/chrisjoyce911/googl
require (
gitea.stevedudenhoeffer.com/steve/go-extractor v0.0.0-20250318064250-39453288ce2a
gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250321150932-5ba42056adfc
gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250326035309-82feb7d8b415
github.com/Edw590/go-wolfram v0.0.0-20241010091529-fb9031908c5d
github.com/advancedlogic/GoOse v0.0.0-20231203033844-ae6b36caf275
github.com/davecgh/go-spew v1.1.1
github.com/docker/docker v28.0.2+incompatible
github.com/joho/godotenv v1.5.1
github.com/opencontainers/image-spec v1.1.1
github.com/playwright-community/playwright-go v0.5001.0
github.com/rocketlaunchr/google-search v1.1.6
github.com/urfave/cli v1.22.16
go.starlark.net v0.0.0-20250225190231-0d3f41d403af
go.starlark.net v0.0.0-20250318223901-d9371fef63fe
)
require (
@ -26,14 +28,19 @@ require (
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.6.0 // indirect
cloud.google.com/go/longrunning v0.6.6 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/PuerkitoBio/goquery v1.10.2 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/antchfx/htmlquery v1.3.4 // indirect
github.com/antchfx/xmlquery v1.4.4 // indirect
github.com/antchfx/xpath v1.3.3 // indirect
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/deckarep/golang-set/v2 v2.8.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/fatih/set v0.2.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gigawattio/window v0.0.0-20180317192513-0f5467e35573 // indirect
@ -46,6 +53,7 @@ require (
github.com/go-stack/stack v1.8.1 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gocolly/colly/v2 v2.1.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/protobuf v1.5.4 // indirect
@ -58,7 +66,11 @@ require (
github.com/kennygrant/sanitize v1.2.4 // indirect
github.com/liushuangls/go-anthropic/v2 v2.14.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
@ -70,6 +82,7 @@ require (
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
golang.org/x/crypto v0.36.0 // indirect
@ -80,10 +93,11 @@ require (
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/time v0.11.0 // indirect
google.golang.org/api v0.227.0 // indirect
google.golang.org/api v0.228.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
google.golang.org/grpc v1.71.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gotest.tools/v3 v3.5.2 // indirect
)

447
go.sum Normal file
View File

@ -0,0 +1,447 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA=
cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q=
cloud.google.com/go/ai v0.10.1 h1:EU93KqYmMeOKgaBXAz2DshH2C/BzAT1P+iJORksLIic=
cloud.google.com/go/ai v0.10.1/go.mod h1:sWWHZvmJ83BjuxAQtYEiA0SFTpijtbH+SXWFO14ri5A=
cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps=
cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
cloud.google.com/go/longrunning v0.6.6 h1:XJNDo5MUfMM05xK3ewpbSdmt7R2Zw+aQEMbdQR65Rbw=
cloud.google.com/go/longrunning v0.6.6/go.mod h1:hyeGJUrPHcx0u2Uu1UFSoYZLn4lkMrccJig0t4FI7yw=
gitea.stevedudenhoeffer.com/steve/go-extractor v0.0.0-20250318064250-39453288ce2a h1:LZriHuPVjdus7Haz+qEFYgr+g/eOdmeAvlbgk67DDHA=
gitea.stevedudenhoeffer.com/steve/go-extractor v0.0.0-20250318064250-39453288ce2a/go.mod h1:fzvvUfN8ej2u1ruCsABG+D+2dAPfOklInS4b1pvog1M=
gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250321150932-5ba42056adfc h1:t37fsWEfZu5DjCrJgzRT3i8iglB1a/nRrWFj6e/KzoU=
gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250321150932-5ba42056adfc/go.mod h1:LitFWQ+Q5db6zo6K2mzqfFvz/8EM/vUNAaG+TaSGZf0=
gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250326035309-82feb7d8b415 h1:LT6sPXU/mZaTmFHMOS90UtfmGaaSD8CaOt40q0komlE=
gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250326035309-82feb7d8b415/go.mod h1:LitFWQ+Q5db6zo6K2mzqfFvz/8EM/vUNAaG+TaSGZf0=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/Edw590/go-wolfram v0.0.0-20241010091529-fb9031908c5d h1:dxGZ0drmrUfNOQ93n9kAWkxOXK4bQHRUaFhRzGySTU4=
github.com/Edw590/go-wolfram v0.0.0-20241010091529-fb9031908c5d/go.mod h1:ubjYqrt3dF4G+YVEDQr+qa2aveeMzt27o/GOH2hswPo=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/PuerkitoBio/goquery v1.4.1/go.mod h1:T9ezsOHcCrDCgA8aF1Cqr3sSYbO/xgdy8/R/XiIMAhA=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/PuerkitoBio/goquery v1.10.2 h1:7fh2BdHcG6VFZsK7toXBT/Bh1z5Wmy8Q9MV9HqT2AM8=
github.com/PuerkitoBio/goquery v1.10.2/go.mod h1:0guWGjcLu9AYC7C1GHnpysHy056u9aEkUHwhdnePMCU=
github.com/advancedlogic/GoOse v0.0.0-20231203033844-ae6b36caf275 h1:Kuhf+w+ilOGoXaR4O4nZ6Dp+ZS83LdANUjwyMXsPGX4=
github.com/advancedlogic/GoOse v0.0.0-20231203033844-ae6b36caf275/go.mod h1:98NztIIMIntZGtQVIs8H85Q5b88fTbwWFbLz/lM9/xU=
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/antchfx/htmlquery v1.2.3/go.mod h1:B0ABL+F5irhhMWg54ymEZinzMSi0Kt3I2if0BLYa3V0=
github.com/antchfx/htmlquery v1.3.0/go.mod h1:zKPDVTMhfOmcwxheXUsx4rKJy8KEY/PU6eXr/2SebQ8=
github.com/antchfx/htmlquery v1.3.4 h1:Isd0srPkni2iNTWCwVj/72t7uCphFeor5Q8nCzj1jdQ=
github.com/antchfx/htmlquery v1.3.4/go.mod h1:K9os0BwIEmLAvTqaNSua8tXLWRWZpocZIH73OzWQbwM=
github.com/antchfx/xmlquery v1.2.4/go.mod h1:KQQuESaxSlqugE2ZBcM/qn+ebIpt+d+4Xx7YcSGAIrM=
github.com/antchfx/xmlquery v1.3.15/go.mod h1:zMDv5tIGjOxY/JCNNinnle7V/EwthZ5IT8eeCGJKRWA=
github.com/antchfx/xmlquery v1.4.4 h1:mxMEkdYP3pjKSftxss4nUHfjBhnMk4imGoR96FRY2dg=
github.com/antchfx/xmlquery v1.4.4/go.mod h1:AEPEEPYE9GnA2mj5Ur2L5Q5/2PycJ0N9Fusrx9b12fc=
github.com/antchfx/xpath v1.1.6/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
github.com/antchfx/xpath v1.1.8/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
github.com/antchfx/xpath v1.2.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/antchfx/xpath v1.2.4/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/antchfx/xpath v1.3.3 h1:tmuPQa1Uye0Ym1Zn65vxPgfltWb/Lxu2jeqIGteJSRs=
github.com/antchfx/xpath v1.3.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/araddon/dateparse v0.0.0-20180729174819-cfd92a431d0e/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI=
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chrisjoyce911/google-search v0.0.0-20230910003754-e501aedf805a h1:OZQiBySVd55npXVsIKnJT6q+9A1tPiXhGnFlc+q0YqQ=
github.com/chrisjoyce911/google-search v0.0.0-20230910003754-e501aedf805a/go.mod h1:fk5J/qPpaRDjLWdFxT+dmuiqG7kxXArC7K8A+gj88Nk=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ=
github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v28.0.2+incompatible h1:9BILleFwug5FSSqWBgVevgL3ewDJfWWWyZVqlDMttE8=
github.com/docker/docker v28.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA=
github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gigawattio/window v0.0.0-20180317192513-0f5467e35573 h1:u8AQ9bPa9oC+8/A/jlWouakhIvkFfuxgIIRjiy8av7I=
github.com/gigawattio/window v0.0.0-20180317192513-0f5467e35573/go.mod h1:eBvb3i++NHDH4Ugo9qCvMw8t0mTSctaEa5blJbWcNxs=
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-resty/resty/v2 v2.0.0/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8=
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
github.com/go-shiori/dom v0.0.0-20230515143342-73569d674e1c h1:wpkoddUomPfHiOziHZixGO5ZBS73cKqVzZipfrLmO1w=
github.com/go-shiori/dom v0.0.0-20230515143342-73569d674e1c/go.mod h1:oVDCh3qjJMLVUSILBRwrm+Bc6RNXGZYtoh9xdvf1ffM=
github.com/go-shiori/go-readability v0.0.0-20250217085726-9f5bf5ca7612 h1:BYLNYdZaepitbZreRIa9xeCQZocWmy/wj4cGIH0qyw0=
github.com/go-shiori/go-readability v0.0.0-20250217085726-9f5bf5ca7612/go.mod h1:wgqthQa8SAYs0yyljVeCOQlZ027VW5CmLsbi9jWC08c=
github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA=
github.com/gocolly/colly/v2 v2.1.0 h1:k0DuZkDoCsx51bKpRJNEmcxcp+W5N8ziuwGaSDuFoGs=
github.com/gocolly/colly/v2 v2.1.0/go.mod h1:I2MuhsLjQ+Ex+IzK3afNS8/1qP3AedHOusRPcRdC5o0=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs=
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/generative-ai-go v0.19.0 h1:R71szggh8wHMCUlEMsW2A/3T+5LdEIkiaHSYgSpUgdg=
github.com/google/generative-ai-go v0.19.0/go.mod h1:JYolL13VG7j79kM5BtHz4qwONHkeJQzOCkKXnpqtS/E=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M=
github.com/jawher/mow.cli v1.1.0/go.mod h1:aNaQlc7ozF3vw6IJ2dHjp2ZFiA4ozMIYY6PyuRJwlUg=
github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA=
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o=
github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/liushuangls/go-anthropic/v2 v2.14.1 h1:t07ckMN7qLkI4yIPJMPNjkwyLV6SEou6UHT/a4rpIHY=
github.com/liushuangls/go-anthropic/v2 v2.14.1/go.mod h1:HQ3//ql9jcgP6zpL5R11OkHijWuYVH1iwJSSF0x+Jlk=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/playwright-community/playwright-go v0.5001.0 h1:EY3oB+rU9cUp6CLHguWE8VMZTwAg+83Yyb7dQqEmGLg=
github.com/playwright-community/playwright-go v0.5001.0/go.mod h1:kBNWs/w2aJ2ZUp1wEOOFLXgOqvppFngM5OS+qyhl+ZM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww=
github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/sashabaranov/go-openai v1.38.1 h1:TtZabbFQZa1nEni/IhVtDF/WQjVqDgd+cWR5OeddzF8=
github.com/sashabaranov/go-openai v1.38.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/simplereach/timeutils v1.2.0/go.mod h1:VVbQDfN/FHRZa1LSqcwo4kNZ62OOyqLLGQKYB3pB0Q8=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/temoto/robotstxt v1.1.1/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo=
github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fxg=
github.com/temoto/robotstxt v1.1.2/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo=
github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ=
github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.starlark.net v0.0.0-20250318223901-d9371fef63fe h1:Wf00k2WTLCW/L1/+gA1gxfTcU4yI+nK4YRTjumYezD8=
go.starlark.net v0.0.0-20250318223901-d9371fef63fe/go.mod h1:YKMCv9b1WrfWmeqdV5MAuEHWsu5iC+fe6kYl2sQjdI8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.228.0 h1:X2DJ/uoWGnY5obVjewbp8icSL5U4FzuCfy9OjbLSnLs=
google.golang.org/api v0.228.0/go.mod h1:wNvRS1Pbe8r4+IfBIniV8fwCpGwTrYa+kMUDiC5z5a4=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM=
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

238
pkg/agents/agent.go Normal file
View File

@ -0,0 +1,238 @@
package agents
import (
"context"
"fmt"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
"sync"
"sync/atomic"
)
// Agent is essentially the bones of a chat agent. It has a model and a toolbox, and can be used to call the model
// with messages and execute the resulting calls.
// The agent will keep track of how many calls it has made to the model, and any agents which inherit from this
// one (e.g.: all made from sharing pointers or from the With... helpers) will share the same call count.
// This package contains a number of helper functions to make it easier to create and use agents.
type Agent struct {
model gollm.ChatCompletion
toolbox *gollm.ToolBox
systemPrompt string
contextualInformation []string
systemPromptSuffix string
maxCalls *int32
calls *atomic.Int32
}
// NewAgent creates a new agent struct with the given model and toolbox.
// Any inherited agents (e.g.: all made from sharing pointers or from the With... helpers) from this one will
// share the same call count.
func NewAgent(model gollm.ChatCompletion, toolbox *gollm.ToolBox) Agent {
return Agent{
model: model,
toolbox: toolbox,
calls: &atomic.Int32{},
}
}
func (a Agent) Calls() int32 {
return a.calls.Load()
}
func (a Agent) WithModel(model gollm.ChatCompletion) Agent {
a.model = model
return a
}
func (a Agent) WithToolbox(toolbox *gollm.ToolBox) Agent {
a.toolbox = toolbox
return a
}
func (a Agent) WithSystemPrompt(systemPrompt string) Agent {
a.systemPrompt = systemPrompt
return a
}
func (a Agent) WithContextualInformation(contextualInformation []string) Agent {
a.contextualInformation = append(a.contextualInformation, contextualInformation...)
return a
}
func (a Agent) WithSystemPromptSuffix(systemPromptSuffix string) Agent {
a.systemPromptSuffix = systemPromptSuffix
return a
}
func (a Agent) WithMaxCalls(maxCalls int32) Agent {
a.maxCalls = &maxCalls
return a
}
func (a Agent) _readAnyMessages(messages ...any) ([]gollm.Message, error) {
var res []gollm.Message
for _, msg := range messages {
switch v := msg.(type) {
case gollm.Message:
res = append(res, v)
case string:
res = append(res, gollm.Message{
Role: gollm.RoleUser,
Text: v,
})
default:
return res, fmt.Errorf("unknown type %T used as message", msg)
}
}
return res, nil
}
// ToRequest will convert the current agent configuration into a gollm.Request. Any messages passed in will be added
// to the request at the end. messages can be either a gollm.Message or a string. All string entries will be added as
// simple user messages.
func (a Agent) ToRequest(messages ...any) (gollm.Request, error) {
sysPrompt := a.systemPrompt
if len(a.contextualInformation) > 0 {
if len(sysPrompt) > 0 {
sysPrompt += "\n\n"
}
sysPrompt += fmt.Sprintf(" Contextual information you should be aware of: %v", a.contextualInformation)
}
if len(a.systemPromptSuffix) > 0 {
if len(sysPrompt) > 0 {
sysPrompt += "\n\n"
}
sysPrompt += a.systemPromptSuffix
}
req := gollm.Request{
Toolbox: a.toolbox,
}
if len(sysPrompt) > 0 {
req.Messages = append(req.Messages, gollm.Message{
Role: gollm.RoleSystem,
Text: sysPrompt,
})
}
msgs, err := a._readAnyMessages(messages...)
if err != nil {
return req, fmt.Errorf("failed to read messages: %w", err)
}
req.Messages = append(req.Messages, msgs...)
return req, nil
}
// CallModel calls the model with the given messages and returns the raw response.
// note that the msgs can be either a gollm.Message or a string. All string entries will be added as simple
// user messages.
func (a Agent) CallModel(ctx context.Context, msgs ...any) (gollm.Response, error) {
calls := a.calls.Add(1)
if a.maxCalls != nil && calls > *a.maxCalls {
return gollm.Response{}, fmt.Errorf("max model calls exceeded")
}
req, err := a.ToRequest(msgs...)
if err != nil {
return gollm.Response{}, fmt.Errorf("failed to create request: %w", err)
}
return a.model.ChatComplete(ctx, req)
}
type CallResults struct {
ID string
Function string
Arguments string
Result any
Error error
}
type CallAndExecuteResults struct {
Text string
CallResults []CallResults
}
// CallAndExecute calls the model with the given messages and executes the resulting calls in serial order. The results
// are returned in the same order as the calls.
func (a Agent) CallAndExecute(ctx context.Context, msgs ...any) (CallAndExecuteResults, error) {
return a._callAndExecuteParallel(ctx, false, msgs...)
}
// CallAndExecuteParallel will call the model with the given messages and all the tool calls in the response will be
// executed in parallel. The results will be returned in the same order as the calls.
func (a Agent) CallAndExecuteParallel(ctx context.Context, msgs ...any) (CallAndExecuteResults, error) {
return a._callAndExecuteParallel(ctx, true, msgs...)
}
func (a Agent) _callAndExecuteParallel(ctx context.Context, parallel bool, msgs ...any) (CallAndExecuteResults, error) {
calls := a.calls.Add(1)
if a.maxCalls != nil && calls > *a.maxCalls {
return CallAndExecuteResults{}, fmt.Errorf("max model calls exceeded")
}
req, err := a.ToRequest(msgs...)
if err != nil {
return CallAndExecuteResults{}, fmt.Errorf("failed to create request: %w", err)
}
response, err := a.model.ChatComplete(ctx, req)
if err != nil {
return CallAndExecuteResults{}, fmt.Errorf("error calling model: %w", err)
}
if len(response.Choices) == 0 {
return CallAndExecuteResults{}, fmt.Errorf("no choices found")
}
choice := response.Choices[0]
var res = CallAndExecuteResults{
Text: choice.Content,
CallResults: make([]CallResults, len(choice.Calls)),
}
if parallel {
var wg sync.WaitGroup
for i, call := range choice.Calls {
wg.Add(1)
go func() {
var callRes = CallResults{
ID: call.ID,
Function: call.FunctionCall.Name,
Arguments: call.FunctionCall.Arguments,
}
callRes.Result, callRes.Error = req.Toolbox.Execute(gollm.NewContext(ctx, req, &choice, &call), call)
res.CallResults[i] = callRes
wg.Done()
}()
}
wg.Wait()
} else {
for i, call := range choice.Calls {
var callRes = CallResults{
ID: call.ID,
Function: call.FunctionCall.Name,
Arguments: call.FunctionCall.Arguments,
}
callRes.Result, callRes.Error = req.Toolbox.Execute(gollm.NewContext(ctx, req, &choice, &call), call)
res.CallResults[i] = callRes
}
}
return res, nil
}

3
pkg/agents/consensus.go Normal file
View File

@ -0,0 +1,3 @@
package agents
// TODO: Consensus system?

View File

@ -0,0 +1,302 @@
package console_new
import (
"context"
"fmt"
"log/slog"
"os"
"path/filepath"
"strings"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/client"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents/shared"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
)
type Agent struct {
// Model is the chat completion model to use
Model gollm.ChatCompletion
OnLoopComplete func(ctx context.Context, knowledge agents.Knowledge) error
OnCommandStart func(ctx context.Context, command string) error
OnCommandDone func(ctx context.Context, command string, output string, err error) error
OnDone func(ctx context.Context, knowledge agents.Knowledge) error
ContextualInformation []string
MaxCommands int
}
type Response struct {
Knowledge agents.Knowledge
Directory string
}
// Answer will give the model access to an ubuntu console with python and pip installed, and then ask the model to
// do what is necessary to answer the question.
func (a Agent) Answer(ctx context.Context, questions []string) (Response, error) {
var res Response
if a.MaxCommands <= 0 {
a.MaxCommands = 10000
}
res.Knowledge = agents.Knowledge{
OriginalQuestions: questions,
RemainingQuestions: questions,
}
// create a temporary scratch directory
dir, err := os.MkdirTemp("", "console-")
if err != nil {
return res, err
}
res.Directory = dir
cl, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
return res, err
}
defer cl.Close()
mounts := []mount.Mount{
{
Type: mount.TypeBind,
Source: dir,
Target: "/home/user",
},
}
c, err := CreateContainer(ctx, cl, ContainerConfig{
Config: &container.Config{
Image: "ubuntu:latest",
Cmd: []string{"tail", "-f", "/dev/null"},
Tty: true,
WorkingDir: "/home/user",
},
HostConfig: &container.HostConfig{
AutoRemove: true,
Mounts: mounts,
},
Name: filepath.Base(dir),
})
if err != nil {
return res, fmt.Errorf("failed to create container: %w", err)
}
defer func() {
_ = c.Close(ctx)
}()
slog.Info("starting container", "dir", dir, "container", fmt.Sprintf("%+v", c))
err = c.Start(ctx)
if err != nil {
return res, fmt.Errorf("failed to start container: %w", err)
}
// Run the model
var history executions
var keepGoing = true
opwd, epwd := c.Execute(ctx, "ls -al /home")
fmt.Println(opwd)
slog.Info("pwd", "pwd", opwd, "epwd", epwd)
tools := map[string]*gollm.Function{
"exit": gollm.NewFunction(
"exit",
"exit the container",
func(ctx *gollm.Context, args struct {
RemainingQuestions []string `description:"any remaining questions that remain unanswered"`
}) (any, error) {
keepGoing = false
return "exiting", nil
}),
"write": gollm.NewFunction(
"write",
"write a file in the /root directory",
func(ctx *gollm.Context, args struct {
Filename string `description:"The name of the file to write"`
Content string `description:"The content of the file to write"`
}) (any, error) {
target, err := SafeJoinPath(dir, args.Filename)
if err != nil {
return "", err
}
f, err := os.Create(target)
if err != nil {
return "", err
}
defer f.Close()
_, err = f.WriteString(args.Content)
if err != nil {
return "", err
}
return "wrote file", nil
}),
"read": gollm.NewFunction(
"read",
"read a file in the /root directory",
func(ctx *gollm.Context, args struct {
Filename string `description:"The name of the file to read"`
}) (any, error) {
target, err := SafeJoinPath(dir, args.Filename)
if err != nil {
return "", err
}
b, err := os.ReadFile(target)
if err != nil {
return "", err
}
return string(b), nil
}),
"execute": gollm.NewFunction(
"execute",
"execute a command in the container",
func(ctx *gollm.Context, args struct {
Command string `description:"The command to execute"`
}) (any, error) {
if len(history) >= a.MaxCommands {
return "too many commands", nil
}
if a.OnCommandStart != nil {
err := a.OnCommandStart(ctx, args.Command)
if err != nil {
return "", err
}
}
var res string
// if the command starts with sudo then we need to use the sudo function
if strings.HasPrefix(args.Command, "sudo ") {
res, err = c.Sudo(ctx, args.Command[5:])
} else {
res, err = c.Execute(ctx, args.Command)
}
if a.OnCommandDone != nil {
err = a.OnCommandDone(ctx, args.Command, res, nil)
if err != nil {
return "", err
}
}
history = append(history, execution{
Command: args.Command,
Output: res,
})
return res, nil
}),
"sudo": gollm.NewFunction(
"sudo",
"execute a command in the container",
func(ctx *gollm.Context, args struct {
Command string `description:"The command to execute"`
}) (any, error) {
if len(history) >= a.MaxCommands {
return "too many commands", nil
}
if a.OnCommandStart != nil {
err := a.OnCommandStart(ctx, args.Command)
if err != nil {
return "", err
}
}
res, err := c.Sudo(ctx, args.Command)
if a.OnCommandDone != nil {
err = a.OnCommandDone(ctx, args.Command, res, nil)
if err != nil {
return "", err
}
}
if err != nil {
res = "error executing: " + err.Error()
}
history = append(history, execution{
Command: "sudo " + args.Command,
Output: res,
})
return res, nil
}),
}
for i := 0; i < a.MaxCommands && len(history) < a.MaxCommands && keepGoing; i++ {
systemPrompt := `You are now in a shell in a container of the ubuntu:latest image to answer a question asked by the user, it is very basic install of ubuntu, simple things (like python) are not preinstalled but can be installed via apt. You will be run multiple times and gain knowledge throughout the process.`
if len(history) < a.MaxCommands {
systemPrompt += `You can run any command you like to get to the needed results.`
}
systemPrompt += `Alternatively, you can use the tool "write" to write a file in the home directory, and also the tool "read" to read a file in the home directory.
When you are done, please use "exit" to exit the container.
Respond with any number of commands to answer the question, they will be executed in order.`
var toolbox []*gollm.Function
// add unrestricted tools
toolbox = append(toolbox, tools["exit"], tools["write"], tools["read"])
if len(history) < a.MaxCommands {
toolbox = append(toolbox, tools["execute"], tools["sudo"])
}
kw := shared.KnowledgeWorker{
Model: a.Model,
ToolBox: gollm.NewToolBox(toolbox...),
ContextualInformation: a.ContextualInformation,
OnNewFunction: func(ctx context.Context, funcName string, args string) (any, error) {
slog.Info("new function called", "function name", funcName, "args", args)
return nil, nil
},
}
slog.Info("answering question", "question", questions[0])
r, err := kw.Answer(ctx, &res.Knowledge, systemPrompt, "", "", history.ToGeneralButLastMessageHistory(), func(res gollm.ToolCallResponse) {
})
if err != nil {
return res, fmt.Errorf("error answering question: %w", err)
}
if len(r.Knowledge) > 0 {
slog.Info("answered question and learned", "knowledge", r.Knowledge)
} else {
slog.Info("answered question and learned nothing")
}
res.Knowledge, err = agents.KnowledgeIntegrate(ctx, a.Model, res.Knowledge, r)
if err != nil {
return res, fmt.Errorf("error integrating knowledge: %w", err)
}
slog.Info("knowledge integrated", "question", questions[0], "knowledge", res.Knowledge)
}
return res, nil
}

View File

@ -0,0 +1,115 @@
package console_new
import (
"context"
"fmt"
"io"
"log/slog"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
type Container struct {
client *client.Client
id string
}
func (c Container) Start(ctx context.Context) error {
return c.client.ContainerStart(ctx, c.id, container.StartOptions{})
}
func (c Container) Close(ctx context.Context) error {
timeout := 10
if err := c.client.ContainerStop(ctx, c.id, container.StopOptions{
Timeout: &timeout,
}); err != nil {
// If stop fails, force kill
if err := c.client.ContainerKill(ctx, c.id, "SIGKILL"); err != nil {
return err
}
}
// Remove container and volumes
removeOptions := container.RemoveOptions{
RemoveVolumes: true,
Force: true,
}
return c.client.ContainerRemove(ctx, c.id, removeOptions)
}
func (c Container) execUserCmd(ctx context.Context, user string, cmd string) (string, error) {
slog.Info("executing command", "user", user, "cmd", cmd)
var cmdStr = []string{"/bin/bash", "-c"}
cmdStr = append(cmdStr, cmd)
slog.Info("executing command", "user", user, "cmd", fmt.Sprintf("%#v", cmdStr))
exec, err := c.client.ContainerExecCreate(ctx, c.id, container.ExecOptions{
Cmd: cmdStr,
User: user,
AttachStdout: true,
AttachStderr: true,
})
if err != nil {
return "", err
}
resp, err := c.client.ContainerExecAttach(ctx, exec.ID, container.ExecAttachOptions{})
if err != nil {
return "", err
}
defer resp.Close()
output, err := io.ReadAll(resp.Reader)
if err != nil {
return "", err
}
// Wait for command to finish and get exit code
inspectResp, err := c.client.ContainerExecInspect(ctx, exec.ID)
if err != nil {
return "", err
}
slog.Info("command finished", "output", string(output), "err", err, "resp", inspectResp)
if inspectResp.ExitCode != 0 {
return string(output), fmt.Errorf("command exited with code %d", inspectResp.ExitCode)
}
return string(output), nil
}
func (c Container) Execute(ctx context.Context, cmd string) (string, error) {
return c.execUserCmd(ctx, "", cmd)
}
func (c Container) ExecuteAs(ctx context.Context, user string, cmd string) (string, error) {
return c.execUserCmd(ctx, user, cmd)
}
func (c Container) Sudo(ctx context.Context, cmd string) (string, error) {
return c.execUserCmd(ctx, "root", cmd)
}
type ContainerConfig struct {
Config *container.Config
HostConfig *container.HostConfig
NetConfig *network.NetworkingConfig
Platform *v1.Platform
Name string
}
func CreateContainer(ctx context.Context, cl *client.Client, cfg ContainerConfig) (*Container, error) {
resp, err := cl.ContainerCreate(ctx, cfg.Config, cfg.HostConfig, cfg.NetConfig, cfg.Platform, cfg.Name)
slog.Info("creating container", "resp", resp, "err", err)
if err != nil {
return nil, err
}
return &Container{client: cl, id: resp.ID}, nil
}

View File

@ -0,0 +1,97 @@
package console_new
import (
"strings"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
)
type execution struct {
Command string
Output string
WhatILearned []string
WhatIStillNeedToLearn []string
}
const kMaxLenCommandSummary = 200
const kMaxLenCommandOutputSummary = 200
func (e execution) ToGeneralMessageHistory() gollm.Message {
if len(e.Command) > kMaxLenCommandSummary {
e.Command = e.Command[:kMaxLenCommandSummary] + "... (truncated)"
}
if len(e.Output) > kMaxLenCommandOutputSummary {
e.Output = e.Output[:kMaxLenCommandOutputSummary] + "... (truncated)"
}
text := "# " + e.Command + "\n" + e.Output
return gollm.Message{
Role: gollm.RoleUser,
Text: text,
}
}
func (e execution) ToDetailedMessageHistory() gollm.Message {
prompt := "$ "
if strings.HasPrefix(e.Command, "sudo ") {
prompt = "# "
e.Command = e.Command[5:]
}
text := prompt + strings.TrimSpace(e.Command) + "\n" + e.Output
if len(e.WhatILearned) > 0 {
text += "\n\nWhat I learned:\n" + strings.Join(e.WhatILearned, "\n")
} else {
text += "\n\nI didn't learn anything new."
}
if len(e.WhatIStillNeedToLearn) > 0 {
text += "\n\nWhat I still need to learn:\n" + strings.Join(e.WhatIStillNeedToLearn, "\n")
} else {
text += "\n\nI don't need to learn anything else."
}
return gollm.Message{
Role: gollm.RoleUser,
Text: text,
}
}
type executions []execution
func (e executions) ToGeneralMessageHistory() []gollm.Message {
var messages []gollm.Message
for _, v := range e {
messages = append(messages, v.ToGeneralMessageHistory())
}
return messages
}
func (e executions) ToGeneralButLastMessageHistory() []gollm.Message {
var messages []gollm.Message
for i, v := range e {
if i == len(e)-1 {
messages = append(messages, v.ToDetailedMessageHistory())
break
}
messages = append(messages, v.ToGeneralMessageHistory())
}
return messages
}
func (e executions) ToDetailedMessageHistory() []gollm.Message {
var messages []gollm.Message
for _, v := range e {
messages = append(messages, v.ToDetailedMessageHistory())
}
return messages
}

View File

@ -0,0 +1,23 @@
package console_new
import (
"fmt"
"path/filepath"
"strings"
)
func SafeJoinPath(tempDir, fileName string) (string, error) {
// Clean both paths
tempDir = filepath.Clean(tempDir)
fileName = filepath.Clean(fileName)
// Join paths and clean result
fullPath := filepath.Clean(filepath.Join(tempDir, fileName))
// Verify the path is still within tempDir
if !strings.HasPrefix(fullPath, tempDir+string(filepath.Separator)) {
return "", fmt.Errorf("invalid path")
}
return fullPath, nil
}

302
pkg/agents/console/agent.go Normal file
View File

@ -0,0 +1,302 @@
package console
import (
"context"
"fmt"
"log/slog"
"os"
"path/filepath"
"strings"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/client"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents/shared"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
)
type Agent struct {
// Model is the chat completion model to use
Model gollm.ChatCompletion
OnLoopComplete func(ctx context.Context, knowledge agents.Knowledge) error
OnCommandStart func(ctx context.Context, command string) error
OnCommandDone func(ctx context.Context, command string, output string, err error) error
OnDone func(ctx context.Context, knowledge agents.Knowledge) error
ContextualInformation []string
MaxCommands int
}
type Response struct {
Knowledge agents.Knowledge
Directory string
}
// Answer will give the model access to an ubuntu console with python and pip installed, and then ask the model to
// do what is necessary to answer the question.
func (a Agent) Answer(ctx context.Context, questions []string) (Response, error) {
var res Response
if a.MaxCommands <= 0 {
a.MaxCommands = 10000
}
res.Knowledge = agents.Knowledge{
OriginalQuestions: questions,
RemainingQuestions: questions,
}
// create a temporary scratch directory
dir, err := os.MkdirTemp("", "console-")
if err != nil {
return res, err
}
res.Directory = dir
cl, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
return res, err
}
defer cl.Close()
mounts := []mount.Mount{
{
Type: mount.TypeBind,
Source: dir,
Target: "/home/user",
},
}
c, err := CreateContainer(ctx, cl, ContainerConfig{
Config: &container.Config{
Image: "ubuntu:latest",
Cmd: []string{"tail", "-f", "/dev/null"},
Tty: true,
WorkingDir: "/home/user",
},
HostConfig: &container.HostConfig{
AutoRemove: true,
Mounts: mounts,
},
Name: filepath.Base(dir),
})
if err != nil {
return res, fmt.Errorf("failed to create container: %w", err)
}
defer func() {
_ = c.Close(ctx)
}()
slog.Info("starting container", "dir", dir, "container", fmt.Sprintf("%+v", c))
err = c.Start(ctx)
if err != nil {
return res, fmt.Errorf("failed to start container: %w", err)
}
// Run the model
var history executions
var keepGoing = true
opwd, epwd := c.Execute(ctx, "ls -al /home")
fmt.Println(opwd)
slog.Info("pwd", "pwd", opwd, "epwd", epwd)
tools := map[string]*gollm.Function{
"exit": gollm.NewFunction(
"exit",
"exit the container",
func(ctx *gollm.Context, args struct {
RemainingQuestions []string `description:"any remaining questions that remain unanswered"`
}) (any, error) {
keepGoing = false
return "exiting", nil
}),
"write": gollm.NewFunction(
"write",
"write a file in the /root directory",
func(ctx *gollm.Context, args struct {
Filename string `description:"The name of the file to write"`
Content string `description:"The content of the file to write"`
}) (any, error) {
target, err := SafeJoinPath(dir, args.Filename)
if err != nil {
return "", err
}
f, err := os.Create(target)
if err != nil {
return "", err
}
defer f.Close()
_, err = f.WriteString(args.Content)
if err != nil {
return "", err
}
return "wrote file", nil
}),
"read": gollm.NewFunction(
"read",
"read a file in the /root directory",
func(ctx *gollm.Context, args struct {
Filename string `description:"The name of the file to read"`
}) (any, error) {
target, err := SafeJoinPath(dir, args.Filename)
if err != nil {
return "", err
}
b, err := os.ReadFile(target)
if err != nil {
return "", err
}
return string(b), nil
}),
"execute": gollm.NewFunction(
"execute",
"execute a command in the container",
func(ctx *gollm.Context, args struct {
Command string `description:"The command to execute"`
}) (any, error) {
if len(history) >= a.MaxCommands {
return "too many commands", nil
}
if a.OnCommandStart != nil {
err := a.OnCommandStart(ctx, args.Command)
if err != nil {
return "", err
}
}
var res string
// if the command starts with sudo then we need to use the sudo function
if strings.HasPrefix(args.Command, "sudo ") {
res, err = c.Sudo(ctx, args.Command[5:])
} else {
res, err = c.Execute(ctx, args.Command)
}
if a.OnCommandDone != nil {
err = a.OnCommandDone(ctx, args.Command, res, nil)
if err != nil {
return "", err
}
}
history = append(history, execution{
Command: args.Command,
Output: res,
})
return res, nil
}),
"sudo": gollm.NewFunction(
"sudo",
"execute a command in the container",
func(ctx *gollm.Context, args struct {
Command string `description:"The command to execute"`
}) (any, error) {
if len(history) >= a.MaxCommands {
return "too many commands", nil
}
if a.OnCommandStart != nil {
err := a.OnCommandStart(ctx, args.Command)
if err != nil {
return "", err
}
}
res, err := c.Sudo(ctx, args.Command)
if a.OnCommandDone != nil {
err = a.OnCommandDone(ctx, args.Command, res, nil)
if err != nil {
return "", err
}
}
if err != nil {
res = "error executing: " + err.Error()
}
history = append(history, execution{
Command: "sudo " + args.Command,
Output: res,
})
return res, nil
}),
}
for i := 0; i < a.MaxCommands && len(history) < a.MaxCommands && keepGoing; i++ {
systemPrompt := `You are now in a shell in a container of the ubuntu:latest image to answer a question asked by the user, it is very basic install of ubuntu, simple things (like python) are not preinstalled but can be installed via apt. You will be run multiple times and gain knowledge throughout the process.`
if len(history) < a.MaxCommands {
systemPrompt += `You can run any command you like to get to the needed results.`
}
systemPrompt += `Alternatively, you can use the tool "write" to write a file in the home directory, and also the tool "read" to read a file in the home directory.
When you are done, please use "exit" to exit the container.
Respond with any number of commands to answer the question, they will be executed in order.`
var toolbox []*gollm.Function
// add unrestricted tools
toolbox = append(toolbox, tools["exit"], tools["write"], tools["read"])
if len(history) < a.MaxCommands {
toolbox = append(toolbox, tools["execute"], tools["sudo"])
}
kw := shared.KnowledgeWorker{
Model: a.Model,
ToolBox: gollm.NewToolBox(toolbox...),
ContextualInformation: a.ContextualInformation,
OnNewFunction: func(ctx context.Context, funcName string, args string) (any, error) {
slog.Info("new function called", "function name", funcName, "args", args)
return nil, nil
},
}
slog.Info("answering question", "question", questions[0])
r, err := kw.Answer(ctx, &res.Knowledge, systemPrompt, "", "", history.ToGeneralButLastMessageHistory(), func(res gollm.ToolCallResponse) {
})
if err != nil {
return res, fmt.Errorf("error answering question: %w", err)
}
if len(r.Knowledge) > 0 {
slog.Info("answered question and learned", "knowledge", r.Knowledge)
} else {
slog.Info("answered question and learned nothing")
}
res.Knowledge, err = agents.KnowledgeIntegrate(ctx, a.Model, res.Knowledge, r)
if err != nil {
return res, fmt.Errorf("error integrating knowledge: %w", err)
}
slog.Info("knowledge integrated", "question", questions[0], "knowledge", res.Knowledge)
}
return res, nil
}

View File

@ -0,0 +1,115 @@
package console
import (
"context"
"fmt"
"io"
"log/slog"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
type Container struct {
client *client.Client
id string
}
func (c Container) Start(ctx context.Context) error {
return c.client.ContainerStart(ctx, c.id, container.StartOptions{})
}
func (c Container) Close(ctx context.Context) error {
timeout := 10
if err := c.client.ContainerStop(ctx, c.id, container.StopOptions{
Timeout: &timeout,
}); err != nil {
// If stop fails, force kill
if err := c.client.ContainerKill(ctx, c.id, "SIGKILL"); err != nil {
return err
}
}
// Remove container and volumes
removeOptions := container.RemoveOptions{
RemoveVolumes: true,
Force: true,
}
return c.client.ContainerRemove(ctx, c.id, removeOptions)
}
func (c Container) execUserCmd(ctx context.Context, user string, cmd string) (string, error) {
slog.Info("executing command", "user", user, "cmd", cmd)
var cmdStr = []string{"/bin/bash", "-c"}
cmdStr = append(cmdStr, cmd)
slog.Info("executing command", "user", user, "cmd", fmt.Sprintf("%#v", cmdStr))
exec, err := c.client.ContainerExecCreate(ctx, c.id, container.ExecOptions{
Cmd: cmdStr,
User: user,
AttachStdout: true,
AttachStderr: true,
})
if err != nil {
return "", err
}
resp, err := c.client.ContainerExecAttach(ctx, exec.ID, container.ExecAttachOptions{})
if err != nil {
return "", err
}
defer resp.Close()
output, err := io.ReadAll(resp.Reader)
if err != nil {
return "", err
}
// Wait for command to finish and get exit code
inspectResp, err := c.client.ContainerExecInspect(ctx, exec.ID)
if err != nil {
return "", err
}
slog.Info("command finished", "output", string(output), "err", err, "resp", inspectResp)
if inspectResp.ExitCode != 0 {
return string(output), fmt.Errorf("command exited with code %d", inspectResp.ExitCode)
}
return string(output), nil
}
func (c Container) Execute(ctx context.Context, cmd string) (string, error) {
return c.execUserCmd(ctx, "", cmd)
}
func (c Container) ExecuteAs(ctx context.Context, user string, cmd string) (string, error) {
return c.execUserCmd(ctx, user, cmd)
}
func (c Container) Sudo(ctx context.Context, cmd string) (string, error) {
return c.execUserCmd(ctx, "root", cmd)
}
type ContainerConfig struct {
Config *container.Config
HostConfig *container.HostConfig
NetConfig *network.NetworkingConfig
Platform *v1.Platform
Name string
}
func CreateContainer(ctx context.Context, cl *client.Client, cfg ContainerConfig) (*Container, error) {
resp, err := cl.ContainerCreate(ctx, cfg.Config, cfg.HostConfig, cfg.NetConfig, cfg.Platform, cfg.Name)
slog.Info("creating container", "resp", resp, "err", err)
if err != nil {
return nil, err
}
return &Container{client: cl, id: resp.ID}, nil
}

View File

@ -0,0 +1,97 @@
package console
import (
"strings"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
)
type execution struct {
Command string
Output string
WhatILearned []string
WhatIStillNeedToLearn []string
}
const kMaxLenCommandSummary = 200
const kMaxLenCommandOutputSummary = 200
func (e execution) ToGeneralMessageHistory() gollm.Message {
if len(e.Command) > kMaxLenCommandSummary {
e.Command = e.Command[:kMaxLenCommandSummary] + "... (truncated)"
}
if len(e.Output) > kMaxLenCommandOutputSummary {
e.Output = e.Output[:kMaxLenCommandOutputSummary] + "... (truncated)"
}
text := "# " + e.Command + "\n" + e.Output
return gollm.Message{
Role: gollm.RoleUser,
Text: text,
}
}
func (e execution) ToDetailedMessageHistory() gollm.Message {
prompt := "$ "
if strings.HasPrefix(e.Command, "sudo ") {
prompt = "# "
e.Command = e.Command[5:]
}
text := prompt + strings.TrimSpace(e.Command) + "\n" + e.Output
if len(e.WhatILearned) > 0 {
text += "\n\nWhat I learned:\n" + strings.Join(e.WhatILearned, "\n")
} else {
text += "\n\nI didn't learn anything new."
}
if len(e.WhatIStillNeedToLearn) > 0 {
text += "\n\nWhat I still need to learn:\n" + strings.Join(e.WhatIStillNeedToLearn, "\n")
} else {
text += "\n\nI don't need to learn anything else."
}
return gollm.Message{
Role: gollm.RoleUser,
Text: text,
}
}
type executions []execution
func (e executions) ToGeneralMessageHistory() []gollm.Message {
var messages []gollm.Message
for _, v := range e {
messages = append(messages, v.ToGeneralMessageHistory())
}
return messages
}
func (e executions) ToGeneralButLastMessageHistory() []gollm.Message {
var messages []gollm.Message
for i, v := range e {
if i == len(e)-1 {
messages = append(messages, v.ToDetailedMessageHistory())
break
}
messages = append(messages, v.ToGeneralMessageHistory())
}
return messages
}
func (e executions) ToDetailedMessageHistory() []gollm.Message {
var messages []gollm.Message
for _, v := range e {
messages = append(messages, v.ToDetailedMessageHistory())
}
return messages
}

View File

@ -0,0 +1,23 @@
package console
import (
"fmt"
"path/filepath"
"strings"
)
func SafeJoinPath(tempDir, fileName string) (string, error) {
// Clean both paths
tempDir = filepath.Clean(tempDir)
fileName = filepath.Clean(fileName)
// Join paths and clean result
fullPath := filepath.Clean(filepath.Join(tempDir, fileName))
// Verify the path is still within tempDir
if !strings.HasPrefix(fullPath, tempDir+string(filepath.Separator)) {
return "", fmt.Errorf("invalid path")
}
return fullPath, nil
}

View File

@ -0,0 +1,59 @@
package agents
import (
"context"
"strings"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
)
// ExtractKnowledge will take a knowledge object and use the gained knowledge to extract the knowledge relevant to the
// questions provided.
// sourceData is the raw text to analyze for the knowledge.
// source is the source of the information, such as a URL.
// questions are the questions that the knowledge is trying to answer.
// model is the chat completion model to use.
// contextualInformation is any contextual information that should be provided to the model.
// It will return the knowledge extracted from the sourceData along with any remaining questions.
// This agent call will not use the Agent's system prompts, but will instead form its own. The contextual information will be used.
func (a Agent) ExtractKnowledge(ctx context.Context, sourceData string, source string, questions []string) (Knowledge, error) {
var knowledge Knowledge
fnAnswer := gollm.NewFunction(
"learn",
`Use learn to pass some relevant information to the model. The model will use this information to answer the question. Use it to learn relevant information from the text. Keep these concise and relevant to the question.`,
func(ctx *gollm.Context, args struct {
Info string `description:"The information to learn from the text."`
}) (any, error) {
knowledge.Knowledge = append(knowledge.Knowledge, TidBit{Info: args.Info, Source: source})
return "", nil
})
fnNoAnswer := gollm.NewFunction(
"finished",
"Indicate that the text does not answer the question.",
func(ctx *gollm.Context, args struct {
Remaining string `description:"After all the knowledge has been learned, this is the parts of the question that are not answered, if any. Leave this blank if the text fully answers the question."`
}) (any, error) {
knowledge.RemainingQuestions = []string{args.Remaining}
return "", nil
})
// Overwrite this agent's system prompts with the ones needed for this function.
var questionPrompt = "The questions you are trying to answer using the text are:\n" + strings.Join(questions, "\n")
if len(questions) == 1 {
questionPrompt = "The question you are trying to answer using the text is: " + questions[0]
}
_, err := a.
WithSystemPrompt(`Evaluate the given text to see if you can answer any information from it relevant to the question that the user asks.
Use the "learn" function to pass relevant information to the model. You can use the "learn" function multiple times to pass multiple pieces of relevant information to the model.
If the text does not answer the question or you are done using "learn" to pass on knowledge then use the "finished" function and indicate the parts of the question that are not answered by anything learned.
You can call "learn" multiple times before calling "finished".`).
WithSystemPromptSuffix(``).
WithToolbox(gollm.NewToolBox(fnAnswer, fnNoAnswer)).
CallAndExecute(ctx, "The text for you to evaluate is: "+sourceData, questionPrompt)
return knowledge, err
}

108
pkg/agents/knowledge.go Normal file
View File

@ -0,0 +1,108 @@
package agents
import (
"strings"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
)
// TidBit is a small piece of information that the AI has learned.
type TidBit struct {
Info string `json:"info"`
Source string `json:"source"`
}
type Knowledge struct {
// OriginalQuestions are the questions that was asked first to the AI before any processing was done.
OriginalQuestions []string `json:"originalQuestions"`
// RemainingQuestions is the questions that are left to find answers for.
RemainingQuestions []string `json:"remainingQuestions"`
// NotesToSelf are notes that the AI has made for itself.
NotesToSelf []string `json:"notesToSelf"`
// CurrentObjectives are the objectives that the AI is currently working on.
CurrentObjectives []string `json:"currentObjectives"`
// Knowledge are the tidbits of information that the AI has learned.
Knowledge []TidBit `json:"knowledge"`
}
func (k Knowledge) ToSystemMessage() gollm.Message {
var sources = map[string][]string{}
for _, t := range k.Knowledge {
sources[t.Source] = append(sources[t.Source], t.Info)
}
var msg string
if len(k.OriginalQuestions) > 0 {
msg += "Original questions asked:\n - " + strings.Join(k.OriginalQuestions, "\n - ") + "\n"
}
if len(k.NotesToSelf) > 0 {
msg += "Notes to self:\n - " + strings.Join(k.NotesToSelf, "\n - ") + "\n"
}
if len(sources) > 0 {
msg += "Learned information:\n"
for source, info := range sources {
if source == "" {
source = "(unsourced)"
}
msg += " - From " + source + ":\n - " + strings.Join(info, "\n - ") + "\n"
}
}
if len(k.CurrentObjectives) > 0 {
msg += "Current objectives:\n - " + strings.Join(k.CurrentObjectives, "\n - ") + "\n"
}
if len(k.RemainingQuestions) > 0 {
msg += "Remaining questions:\n - " + strings.Join(k.RemainingQuestions, "\n - ") + "\n"
}
return gollm.Message{
Role: gollm.RoleSystem,
Text: msg,
}
}
// ToMessage converts the knowledge to a message that can be sent to the LLM.
func (k Knowledge) ToMessage() string {
var learned []string
for _, t := range k.Knowledge {
learned = append(learned, t.Info)
}
return "Original questions asked:\n" + strings.Join(k.OriginalQuestions, "\n") + "\n" +
"Learned information:\n" + strings.Join(learned, "\n") + "\n" +
"Remaining questions:\n" + strings.Join(k.RemainingQuestions, "\n")
}
// Absorb adds the knowledge from the other knowledge objects to this one.
// The OriginalQuestions returned will be from value of k's OriginalQuestions.
// If any absorbed knowledge has any "CurrentObjectives" set they will explicitly overwrite the current objectives in
// aggregate, all CurrentObjectives from all absorbed knowledge will be used.
// Any new Knowledge or NotesToSelf will be appended to the current Knowledge and NotesToSelf.
func (k Knowledge) Absorb(o ...Knowledge) Knowledge {
res := k
var newObjectives []string
for _, ok := range o {
if len(ok.CurrentObjectives) > 0 {
newObjectives = append(newObjectives, ok.CurrentObjectives...)
}
res.NotesToSelf = append(res.NotesToSelf, ok.NotesToSelf...)
res.Knowledge = append(res.Knowledge, ok.Knowledge...)
}
if len(newObjectives) > 0 {
res.CurrentObjectives = newObjectives
}
return res
}

View File

@ -0,0 +1,86 @@
package agents
import (
"context"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
)
// KnowledgeIntegrate will ask the LLM to combine the gained knowledge with the current knowledge, and return the new representation of overall.
// If source is not empty, then any new Knowledge will an empty source will be given the source.
// This will override objectives, notes, and remaining questions.
func (a Agent) KnowledgeIntegrate(ctx context.Context, base Knowledge, in ...Knowledge) (Knowledge, error) {
// if there are no changes we can just return the knowledge
if len(in) == 0 {
return base, nil
} else {
// if there are no entries in Knowledge.Knowledge, Knowledge.CurrentObjectives, or Knowledge.NotesToSelf then
// we can just exit out
needToRun := false
for _, k := range in {
if len(k.Knowledge) > 0 || len(k.CurrentObjectives) > 0 || len(k.NotesToSelf) > 0 {
needToRun = true
break
}
}
if !needToRun {
return base, nil
}
}
var incoming Knowledge
for _, k := range in {
incoming.NotesToSelf = append(incoming.NotesToSelf, k.NotesToSelf...)
incoming.CurrentObjectives = append(incoming.CurrentObjectives, k.CurrentObjectives...)
incoming.Knowledge = append(incoming.Knowledge, k.Knowledge...)
}
baseMsg := base.ToSystemMessage()
incomingMsg := incoming.ToSystemMessage()
baseMsg.Text = "The original knowledge is as follows: " + baseMsg.Text
incomingMsg.Text = "The new knowledge is as follows: " + incomingMsg.Text
var result = Knowledge{
OriginalQuestions: base.OriginalQuestions,
Knowledge: append(base.Knowledge, incoming.Knowledge...),
}
tools := gollm.NewToolBox(
gollm.NewFunction(
"remaining_questions",
"Use the remaining_questions function to indicate what questions remain unanswered. Call this exactly one time. If your current questions are repetitive or not useful, then you can call this function to edit or remove them. If you have new questions, then you can call this function to add them. What is set here will be the final set of questions.",
func(ctx *gollm.Context, args struct {
RemainingQuestions []string `description:"The questions that remain unanswered."`
}) (any, error) {
result.RemainingQuestions = append(result.RemainingQuestions, args.RemainingQuestions...)
return "ok", nil
}),
gollm.NewFunction(
"notes_to_self",
"Use the notes_to_self function to leave or edit notes for yourself. Call this exactly one time. If your current notes are repetitive or not useful, then you can call this function to edit or remove them. What is set here will be the final set",
func(ctx *gollm.Context, args struct {
Notes []string `description:"The notes to leave for yourself."`
}) (any, error) {
result.NotesToSelf = append(result.NotesToSelf, args.Notes...)
return "ok", nil
}),
gollm.NewFunction(
"new_objectives",
"Use the new_objectives function to set new objectives for the LLM to work on. Call this exactly one time. If your current objectives are repetitive or not useful, then you can call this function to edit or remove them. What is set here will be the final set of objectives.",
func(ctx *gollm.Context, args struct {
Objectives []string `description:"The objectives to set for the LLM to work on."`
}) (any, error) {
result.CurrentObjectives = append(result.CurrentObjectives, args.Objectives...)
return "ok", nil
}),
)
_, err := a.WithSystemPrompt(`You are combining knowledge that has been observed in two states, the original knowledge is what was known before an action was taken, and the new knowledge is what was learned after an action was taken. If the new knowledge answers or opens any new questions, notes, or objectives you should add, edit, or remove them now. The original knowledge is as follows: ` + baseMsg.Text + ` The new knowledge is as follows: ` + incomingMsg.Text).
WithSystemPromptSuffix(``).
WithToolbox(tools).
CallAndExecute(ctx)
return result, err
}

View File

@ -7,18 +7,11 @@ import (
"strconv"
"strings"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents/shared"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
)
type KnowledgeProcessor struct {
Model gollm.ChatCompletion
ContextualInformation []string
}
// Process takes a knowledge object and processes it into a response string.
func (a KnowledgeProcessor) Process(ctx context.Context, knowledge shared.Knowledge) (string, error) {
// AnswerQuestionWithKnowledge will take a knowledge object and use the gained knowledge to answer a question.
func (a Agent) AnswerQuestionWithKnowledge(ctx context.Context, knowledge Knowledge) (string, error) {
originalQuestions := strings.Join(knowledge.OriginalQuestions, "\n")
infoGained := ""
@ -60,25 +53,12 @@ Here is the knowledge I have gathered from ` + fmt.Sprint(len(sources)) + ` sour
systemPrompt += "\n\nUsing the sources, write an answer to the original question. Note any information that wasn't able to be answered."
req := gollm.Request{
Messages: []gollm.Message{
{
Role: gollm.RoleSystem,
Text: systemPrompt,
},
},
}
if len(a.ContextualInformation) > 0 {
req.Messages = append(req.Messages, gollm.Message{
Role: gollm.RoleSystem,
Text: "Some contextual information you should be aware of: " + strings.Join(a.ContextualInformation, "\n"),
})
}
resp, err := a.Model.ChatComplete(ctx, req)
res, err := a.WithSystemPrompt(systemPrompt).
WithSystemPromptSuffix(``).
WithToolbox(nil).
CallAndExecute(ctx)
if err != nil {
return "", fmt.Errorf("failed to chat complete: %w", err)
return "", err
}
systemPrompt = `I am trying to source an analysis of information I have gathered.
@ -118,27 +98,15 @@ The moon is 4.5 billion years old [1,2], 238,855 miles away from the Earth [3],
}
summarizedData := `Here is the I need you to source with citations:
` + resp.Choices[0].Content
req = gollm.Request{
Messages: []gollm.Message{
{
Role: gollm.RoleSystem,
Text: systemPrompt,
},
{
Role: gollm.RoleSystem,
Text: providedIntel,
},
{
Role: gollm.RoleUser,
Text: summarizedData,
},
},
}
` + res.Text
res, err = a.WithSystemPrompt(systemPrompt).
WithSystemPromptSuffix(``).
WithToolbox(nil).
CallAndExecute(ctx, gollm.Message{Role: gollm.RoleSystem, Text: providedIntel}, summarizedData)
resp, err = a.Model.ChatComplete(ctx, req)
if err != nil {
return "", fmt.Errorf("failed to chat complete: %w", err)
return "", err
}
// now go through the response and find all citations
@ -148,7 +116,7 @@ The moon is 4.5 billion years old [1,2], 238,855 miles away from the Earth [3],
re := regexp.MustCompile(`\[([\d,\s]+)]`)
// find all the citations
citations := re.FindAllString(resp.Choices[0].Content, -1)
citations := re.FindAllString(res.Text, -1)
// now we need to find the sources
lookup := map[int][]string{}
@ -168,19 +136,19 @@ The moon is 4.5 billion years old [1,2], 238,855 miles away from the Earth [3],
}
}
res := resp.Choices[0].Content
text := res.Text
if len(lookup) > 0 {
res += "\n\nHere are the sources for the information provided:\n"
text += "\n\nHere are the sources for the information provided:\n"
for i := 1; i <= len(sources); i++ {
if _, ok := lookup[i]; !ok {
continue
}
res += "[" + fmt.Sprint(i) + "] <" + lookup[i][0] + ">\n"
text += "[" + fmt.Sprint(i) + "] <" + lookup[i][0] + ">\n"
}
}
return res, nil
return text, nil
}

View File

@ -2,65 +2,31 @@ package agents
import (
"context"
"fmt"
"strings"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
)
type QuestionSplitter struct {
Model gollm.ChatCompletion
ContextualInfo []string
}
func (q QuestionSplitter) SplitQuestion(ctx context.Context, question string) ([]string, error) {
// SplitQuestion is a utility function that will ask the LLM to split a question into sub-questions.
// It is a utility function and as such it does not use the agent's set system prompts, it will however use any
// contextual information that it has.
func (a Agent) SplitQuestion(ctx context.Context, question string) ([]string, error) {
var res []string
req := gollm.Request{
Toolbox: gollm.NewToolBox(
gollm.NewFunction(
fnQuestions := gollm.NewFunction(
"questions",
"split the provided question by the user into sub-questions",
func(ctx *gollm.Context, args struct {
Questions []string `description:"The questions to evaluate"`
}) (string, error) {
}) (any, error) {
res = args.Questions
return "", nil
}),
),
Messages: []gollm.Message{
{
Role: gollm.RoleSystem,
Text: `The user is going to ask you a question, if the question would be better answered split into multiple questions, please do so.
})
_, err := a.WithSystemPrompt(`The user is going to ask you a question, if the question would be better answered split into multiple questions, please do so.
Respond using the "questions" function.
If the question is fine as is, respond with the original question passed to the "questions" function.`,
},
},
}
if len(q.ContextualInfo) > 0 {
req.Messages = append(req.Messages, gollm.Message{
Role: gollm.RoleSystem,
Text: "Some contextual information you should be aware of: " + strings.Join(q.ContextualInfo, "\n"),
})
}
req.Messages = append(req.Messages, gollm.Message{
Role: gollm.RoleUser,
Text: question,
})
resp, err := q.Model.ChatComplete(ctx, req)
if err != nil {
return nil, err
}
if len(resp.Choices) == 0 {
return nil, fmt.Errorf("no choices found")
}
choice := resp.Choices[0]
_, _ = req.Toolbox.ExecuteCallbacks(gollm.NewContext(ctx, req, &choice, nil), choice.Calls, nil, nil)
return res, nil
If the question is fine as is, respond with the original question passed to the "questions" function.`).
WithSystemPromptSuffix(``).
WithToolbox(gollm.NewToolBox(fnQuestions)).
CallAndExecute(ctx, question)
return res, err
}

72
pkg/agents/read_page.go Normal file
View File

@ -0,0 +1,72 @@
package agents
import (
"context"
"fmt"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/cache"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/extractor"
"net/url"
)
func (a Agent) ReadPage(ctx context.Context, u *url.URL, questions []string) (Knowledge, error) {
ar, err := extractArticle(ctx, u)
if err != nil {
return Knowledge{}, err
}
if ar.Body == "" {
return Knowledge{}, fmt.Errorf("could not extract body from page")
}
return a.ExtractKnowledge(ctx, ar.Body, u.String(), questions)
}
type article struct {
URL string
Title string
Body string
}
func extractArticle(ctx context.Context, u *url.URL) (res article, err error) {
defer func() {
e := recover()
if e != nil {
if e, ok := e.(error); ok {
err = fmt.Errorf("panic: %w", e)
} else {
err = fmt.Errorf("panic: %v", e)
}
}
}()
extractors := extractor.MultiExtractor(
extractor.CacheExtractor{
Cache: cache.Nop{},
Tag: "goose",
Extractor: extractor.GooseExtractor{},
},
extractor.CacheExtractor{
Cache: cache.Nop{},
Tag: "playwright",
Extractor: extractor.PlaywrightExtractor{},
},
)
a, err := extractors.Extract(ctx, u.String())
if err != nil {
return article{
URL: "",
Title: "",
Body: "",
}, err
}
return article{
URL: a.URL,
Title: a.Title,
Body: a.Body,
}, nil
}

View File

@ -1,46 +0,0 @@
package reader
import (
"context"
"fmt"
"net/url"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents/shared"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/cache"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
)
type Agent struct {
// Model is the chat completion model to use
Model gollm.ChatCompletion
// OnNewFunction is called when a new function is created
OnNewFunction func(ctx context.Context, funcName string, question string, parameter string) (any, error)
// OnFunctionFinished is called when a function is finished
OnFunctionFinished func(ctx context.Context, funcName string, question string, parameter string, result string, err error, newFunctionResult any) error
Cache cache.Cache
ContextualInformation []string
}
// Read will try to read the source and return the answer if possible.
func (a Agent) Read(ctx context.Context, question string, source *url.URL) (shared.Knowledge, error) {
if a.Cache == nil {
a.Cache = cache.Nop{}
}
ar, err := extractArticle(ctx, a.Cache, source)
if err != nil {
return shared.Knowledge{}, err
}
if ar.Body == "" {
return shared.Knowledge{}, fmt.Errorf("could not extract body from page")
}
return doesTextAnswerQuestion(ctx, question, ar.Body, source.String(), a)
}

View File

@ -1,142 +0,0 @@
package reader
import (
"context"
"fmt"
"net/url"
"strings"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents/shared"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/cache"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/extractor"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
)
type article struct {
URL string
Title string
Body string
}
func extractArticle(ctx context.Context, c cache.Cache, u *url.URL) (res article, err error) {
defer func() {
e := recover()
if e != nil {
if e, ok := e.(error); ok {
err = fmt.Errorf("panic: %w", e)
} else {
err = fmt.Errorf("panic: %v", e)
}
}
}()
extractors := extractor.MultiExtractor(
extractor.CacheExtractor{
Cache: c,
Tag: "goose",
Extractor: extractor.GooseExtractor{},
},
extractor.CacheExtractor{
Cache: c,
Tag: "playwright",
Extractor: extractor.PlaywrightExtractor{},
},
)
a, err := extractors.Extract(ctx, u.String())
if err != nil {
return article{
URL: "",
Title: "",
Body: "",
}, err
}
return article{
URL: a.URL,
Title: a.Title,
Body: a.Body,
}, nil
}
type Response struct {
Knowledge []string
Remaining string
}
type Learn struct {
Info string `description:"The information to learn from the text."`
}
func doesTextAnswerQuestion(ctx context.Context, question string, text string, source string, a Agent) (shared.Knowledge, error) {
var knowledge shared.Knowledge
fnAnswer := gollm.NewFunction(
"learn",
`Use learn to pass some relevant information to the model. The model will use this information to answer the question. Use it to learn relevant information from the text. Keep these concise and relevant to the question.`,
func(ctx *gollm.Context, args Learn) (string, error) {
knowledge.Knowledge = append(knowledge.Knowledge, shared.TidBit{Info: args.Info, Source: source})
return "", nil
})
fnNoAnswer := gollm.NewFunction(
"finished",
"Indicate that the text does not answer the question.",
func(ctx *gollm.Context, args struct {
Remaining string `description:"After all the knowledge has been learned, this is the parts of the question that are not answered, if any. Leave this blank if the text fully answers the question."`
}) (string, error) {
knowledge.RemainingQuestions = []string{args.Remaining}
return "", nil
})
req := gollm.Request{
Messages: []gollm.Message{
{
Role: gollm.RoleSystem,
Text: `Evaluate the given text to see if you can answer any information from it relevant to the question that the user asks.
Use the "learn" function to pass relevant information to the model. You can use the "learn" function multiple times to pass multiple pieces of relevant information to the model.
If the text does not answer the question or you are done using "learn" to pass on knowledge then use the "finished" function and indicate the parts of the question that are not answered by anything learned.
You can call "learn" multiple times before calling "finished".`,
},
{
Role: gollm.RoleSystem,
Text: "The text to evaluate: " + text,
},
},
Toolbox: gollm.NewToolBox(fnAnswer, fnNoAnswer),
}
if len(a.ContextualInformation) > 0 {
req.Messages = append(req.Messages, gollm.Message{
Role: gollm.RoleSystem,
Text: "Some contextual information you should be aware of: " + strings.Join(a.ContextualInformation, "\n"),
})
}
req.Messages = append(req.Messages, gollm.Message{
Role: gollm.RoleUser,
Text: "My question to learn from the text is: " + question,
})
resp, err := a.Model.ChatComplete(ctx, req)
if err != nil {
return knowledge, err
}
if len(resp.Choices) == 0 {
return knowledge, nil
}
choice := resp.Choices[0]
if len(choice.Calls) == 0 {
return knowledge, nil
}
_, err = req.Toolbox.ExecuteCallbacks(gollm.NewContext(ctx, req, &choice, nil), choice.Calls, nil, nil)
return knowledge, err
}

View File

@ -1,101 +0,0 @@
package agents
import (
"context"
"fmt"
"strings"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents/shared"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
)
type RemainingQuestions struct {
Model gollm.ChatCompletion
ContextualInformation []string
}
// Process takes a knowledge object and processes it into a response string.
func (a RemainingQuestions) Process(ctx context.Context, knowledge shared.Knowledge) ([]string, error) {
originalQuestions := strings.Join(knowledge.OriginalQuestions, "\n")
infoGained := ""
// group all the gained knowledge by source
var m = map[string][]string{}
for _, k := range knowledge.Knowledge {
m[k.Source] = append(m[k.Source], k.Info)
}
// now order them in a list so they can be referenced by index
type source struct {
source string
info []string
}
var sources []source
for k, v := range m {
sources = append(sources, source{
source: k,
info: v,
})
if len(infoGained) > 0 {
infoGained += "\n"
}
infoGained += strings.Join(v, "\n")
}
systemPrompt := `I am trying to answer a question, and I gathered some knowledge in an attempt to do so. Here is what I am trying to answer:
` + originalQuestions + `
Here is the knowledge I have gathered from ` + fmt.Sprint(len(sources)) + ` sources:
` + infoGained
systemPrompt += "\n\nUsing the information gathered, have all of the questions been answered? If not, what questions remain? Use the function 'remaining_questions' to answer this question with 0 or more remaining questions."
var res []string
req := gollm.Request{
Messages: []gollm.Message{
{
Role: gollm.RoleSystem,
Text: systemPrompt,
},
},
Toolbox: gollm.NewToolBox(
gollm.NewFunction(
"remaining_questions",
"Given the information learned above, the following questions remain unanswered",
func(ctx *gollm.Context, args struct {
RemainingQuestions []string `description:"The questions that remain unanswered, if any"`
}) (string, error) {
res = append(res, args.RemainingQuestions...)
return "ok", nil
})),
}
if len(a.ContextualInformation) > 0 {
req.Messages = append(req.Messages, gollm.Message{
Role: gollm.RoleSystem,
Text: "Some contextual information you should be aware of: " + strings.Join(a.ContextualInformation, "\n"),
})
}
resp, err := a.Model.ChatComplete(ctx, req)
if err != nil {
return nil, fmt.Errorf("failed to chat complete: %w", err)
}
if len(resp.Choices) == 0 {
return nil, fmt.Errorf("no choices returned")
}
choice := resp.Choices[0]
if len(choice.Calls) == 0 {
return nil, fmt.Errorf("no calls returned")
}
_, err = req.Toolbox.ExecuteCallbacks(gollm.NewContext(ctx, req, &choice, nil), choice.Calls, nil, nil)
return res, err
}

216
pkg/agents/search.go Normal file
View File

@ -0,0 +1,216 @@
package agents
import (
"context"
"fmt"
"io"
"log/slog"
"net/url"
"slices"
"strings"
"sync"
"time"
"gitea.stevedudenhoeffer.com/steve/go-extractor"
"gitea.stevedudenhoeffer.com/steve/go-extractor/sites/duckduckgo"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
)
func deferClose(c io.Closer) {
if c != nil {
_ = c.Close()
}
}
type SearchTool struct {
Name string
Description string
Function func(ctx context.Context, src *url.URL, questions []string) (Knowledge, error)
}
// SearchAndUseTools will search duckduckgo for the given question, and then ask the LLM to select a search result to
// analyze. The LLM will be given a list of tools to use to analyze the search result, and then the LLM will be asked to
// determine if the search results answers the question.
// If the context contains a "browser" key that is an extractor.Browser, it will use that browser to search, otherwise a
// new one will be created and used for the life of this search and then closed.
// searchQuery is the question to search for.
// questions is the list of questions that the LLM is trying to answer with the search results.
// loops is the number of times to ask the LLM to analyze results if there are remaining questions before giving up.
// readers is a list of functions that will be used to read the search results. Any knowledge gained from these readers
// will be combined and returned.
// messages will be appended to all search results. The types of messages that can be appended are both string and
// gollm.Message.
func (a Agent) SearchAndUseTools(ctx context.Context, searchQuery string, questions []string, loops int, allowConcurrent bool, maxReads int, tools []SearchTool, messages ...any) (Knowledge, error) {
var knowledge = Knowledge{
OriginalQuestions: questions,
RemainingQuestions: questions,
}
browser, ok := ctx.Value("browser").(extractor.Browser)
if !ok {
b, err := extractor.NewPlayWrightBrowser(extractor.PlayWrightBrowserOptions{})
if err != nil {
return knowledge, err
}
defer deferClose(browser)
ctx = context.WithValue(ctx, "browser", b)
browser = b
}
cfg := duckduckgo.Config{
SafeSearch: duckduckgo.SafeSearchOff,
Region: "us-en",
}
page, err := cfg.OpenSearch(ctx, browser, searchQuery)
defer deferClose(page)
if err != nil {
return knowledge, err
}
var searchResults []duckduckgo.Result
// filterResults will remove any search results that are in oldSearchResults, or are empty
filterResults := func(in []duckduckgo.Result) []duckduckgo.Result {
var res []duckduckgo.Result
for _, r := range in {
if r.URL == "" {
continue
}
res = append(res, r)
}
return res
}
if maxReads == 0 {
maxReads = 100
}
var lock sync.Mutex
var analyzed []int
var converted []*gollm.Function
for _, t := range tools {
fn := gollm.NewFunction(t.Name, t.Description,
func(c *gollm.Context, arg struct {
Num int `description:"The # of search result to analyze."`
}) (any, error) {
i := arg.Num - 1
defer func() {
lock.Lock()
defer lock.Unlock()
analyzed = append(analyzed, i)
}()
if i < 0 || i >= len(searchResults) {
return nil, fmt.Errorf("index out of range: expect 1-%d", len(searchResults))
}
u, err := url.Parse(searchResults[i].URL)
if err != nil {
return nil, fmt.Errorf("error parsing url: %w", err)
}
return t.Function(c.Context, u, questions)
})
converted = append(converted, fn)
}
for i := 0; i < loops; i++ {
// if any search results have already been analyzed, remove them
// but to make this easier, sort the list of analyzed results descending so they can be removed in order
// without changing the indexes of the remaining results
// but first remove any duplicates
var unique = map[int]struct{}{}
for _, v := range analyzed {
unique[v] = struct{}{}
}
analyzed = analyzed[:0]
for k, _ := range unique {
analyzed = append(analyzed, k)
}
slices.Sort(analyzed)
for j := len(analyzed) - 1; j >= 0; j-- {
searchResults = append(searchResults[:analyzed[j]], searchResults[analyzed[j]+1:]...)
}
// remove any search results that have already been analyzed
analyzed = analyzed[:0]
_ = page.LoadMore()
time.Sleep(2 * time.Second)
searchResults = filterResults(page.GetResults())
a = a.WithSystemPrompt(`You are searching DuckDuckGo for the answer to the question that will be posed by the user. The search results will be provided in system messages in the format of: #. "https://url.here" - "Title of Page" - "Description here". For instance:
1. "https://example.com" - "Example Title" - "This is an example description."
2. "https://example2.com" - "Example Title 2" - "This is an example description 2."
Use appropriate tools to analyze the search results and determine if they answer the question.`).
WithSystemPromptSuffix(``).
WithToolbox(gollm.NewToolBox(converted...))
var searches = make([]string, len(searchResults))
for i, r := range searchResults {
searches[i] = fmt.Sprintf("%d. %q - %q - %q", i+1, r.URL, r.Title, r.Description)
}
if len(searches) > 0 {
messages = append(messages, "The search results are:\n"+strings.Join(searches, "\n"))
}
var results CallAndExecuteResults
if allowConcurrent {
results, err = a.CallAndExecuteParallel(ctx, messages...)
} else {
results, err = a.CallAndExecute(ctx, messages...)
}
if err != nil {
return knowledge, fmt.Errorf("error executing search function: %w", err)
}
var learned []Knowledge
for _, r := range results.CallResults {
if r.Error != nil {
slog.Error("error executing search function", "error", err)
continue
}
if k, ok := r.Result.(Knowledge); ok {
learned = append(learned, k)
} else {
slog.Error("result is not knowledge", "result", r.Result)
}
}
knowledge, err = a.KnowledgeIntegrate(ctx, knowledge, learned...)
if err != nil {
return knowledge, fmt.Errorf("error integrating knowledge: %w", err)
}
if len(knowledge.RemainingQuestions) == 0 {
return knowledge, nil
}
}
return knowledge, nil
}
func (a Agent) SearchAndRead(ctx context.Context, searchQuery string, questions []string, allowConcurrent bool, maxReads int) (Knowledge, error) {
return a.SearchAndUseTools(ctx, searchQuery, questions, 2, allowConcurrent, maxReads, []SearchTool{
{
Name: "ReadPage",
Description: "Read the search result and see if it answers the question. Try to avoid using this on low quality or spammy sites. You can use this function" + fmt.Sprint(maxReads) + " times, but do not call it multiple times on the same result.",
Function: a.ReadPage,
},
})
}

View File

@ -2,64 +2,53 @@ package agents
import (
"context"
"fmt"
"errors"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
"strings"
)
type SearchTerms struct {
Model gollm.ChatCompletion
Context []string
}
var ErrNoSearchTerms = errors.New("no search terms")
// SearchTerms will create search terms for the given question.
// GenerateSearchTerms will create search terms for the given question.
// alreadySearched is a list of search terms that have already been used, and should not be used again.
func (q SearchTerms) SearchTerms(ctx context.Context, question string, alreadySearched []string) (string, error) {
func (a Agent) GenerateSearchTerms(ctx context.Context, question string, alreadySearched []string) (string, error) {
var res string
req := gollm.Request{
Toolbox: gollm.NewToolBox(
gollm.NewFunction(
var cantFind bool
fnSearch := gollm.NewFunction(
"search_terms",
"search DuckDuckGo with these search terms for the given question",
func(ctx *gollm.Context, args struct {
SearchTerms string `description:"The search terms to use for the search"`
}) (string, error) {
}) (any, error) {
res = args.SearchTerms
return "", nil
}),
),
Messages: []gollm.Message{
{
Role: gollm.RoleSystem,
Text: `You are to generate search terms for a question using DuckDuckGo. The question will be provided by the user.`,
},
},
}
})
fnCantThinkOfAny := gollm.NewFunction(
"cant_think_of_any",
"tell the user that you cannot think of any search terms for the given question",
func(ctx *gollm.Context, args struct{}) (any, error) {
cantFind = true
return "", nil
})
var suffix string
if len(alreadySearched) > 0 {
req.Messages = append(req.Messages, gollm.Message{
Role: gollm.RoleSystem,
Text: fmt.Sprintf("The following search terms have already been used: %v", alreadySearched),
})
suffix = "The following search terms have already been used, please avoid them: " + strings.Join(alreadySearched, ", ") + "\n"
}
req.Messages = append(req.Messages, gollm.Message{
Role: gollm.RoleUser,
Text: fmt.Sprintf("The question is: %s", question),
})
_, err := a.WithSystemPrompt(`You are to generate search terms for a question using DuckDuckGo. The question will be provided by the user.`).
WithSystemPromptSuffix(suffix).
WithToolbox(gollm.NewToolBox(fnSearch, fnCantThinkOfAny)).
CallAndExecute(ctx, question)
resp, err := q.Model.ChatComplete(ctx, req)
if err != nil {
return "", err
}
if len(resp.Choices) == 0 {
return "", fmt.Errorf("no choices found")
if cantFind {
return "cannot think of any search terms", ErrNoSearchTerms
}
choice := resp.Choices[0]
_, _ = req.Toolbox.ExecuteCallbacks(gollm.NewContext(ctx, req, &choice, nil), choice.Calls, nil, nil)
return res, nil
}

View File

@ -1,258 +0,0 @@
package searcher
import (
"context"
"fmt"
"log/slog"
"net/url"
"strings"
"sync"
"time"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents/reader"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents/shared"
"gitea.stevedudenhoeffer.com/steve/go-extractor"
"gitea.stevedudenhoeffer.com/steve/go-extractor/sites/duckduckgo"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
)
type Result struct {
// Answer is the answer to the question that was asked.
Answer string
// Sources is a list of sources that were used to find the answer.
Sources []string
// Remaining is the remaining part(s) of the question that was not answered.
Remaining string
}
type Agent struct {
// Model is the chat completion model to use
Model gollm.ChatCompletion
OnDone func(ctx context.Context, knowledge shared.Knowledge) error
// MaxReads is the maximum number of pages that can be read by the agent. Unlimited if <= 0.
MaxReads int
ContextualInformation []string
AllowConcurrent bool
}
// Search will search duckduckgo for the given question, and then read the results to figure out the answer.
// searchQuery is the query that you want to search for, e.g. "what is the capital of France site:reddit.com"
// question is the question that you are trying to answer when reading the search results.
// If the context contains a "browser" key that is an extractor.Browser, it will use that browser to search, otherwise a
// new one will be created and used for the life of this search and then closed.
func (a Agent) Search(ctx context.Context, searchQuery string, question string) (shared.Knowledge, error) {
var knowledge = shared.Knowledge{
OriginalQuestions: []string{question},
RemainingQuestions: []string{question},
}
browser, ok := ctx.Value("browser").(extractor.Browser)
if !ok {
b, err := extractor.NewPlayWrightBrowser(extractor.PlayWrightBrowserOptions{})
if err != nil {
return knowledge, err
}
defer deferClose(browser)
ctx = context.WithValue(ctx, "browser", b)
browser = b
}
cfg := duckduckgo.Config{
SafeSearch: duckduckgo.SafeSearchOff,
Region: "us-en",
}
page, err := cfg.OpenSearch(ctx, browser, searchQuery)
defer deferClose(page)
if err != nil {
return knowledge, err
}
var searchResults []duckduckgo.Result
// filterResults will remove any search results that are in oldSearchResults, or are empty
filterResults := func(in []duckduckgo.Result) []duckduckgo.Result {
var res []duckduckgo.Result
for _, r := range in {
if r.URL == "" {
continue
}
res = append(res, r)
}
return res
}
_ = page.LoadMore()
time.Sleep(2 * time.Second)
searchResults = filterResults(page.GetResults())
var toRead = make(chan int, a.MaxReads)
fnReadSearchResult := gollm.NewFunction("read",
"read the search result and see if it answers the question",
func(c *gollm.Context, arg struct {
Num int `description:"The # of the search result to read."`
}) (string, error) {
toRead <- arg.Num - 1
return "ok", nil
})
readSource := func(ctx context.Context, src duckduckgo.Result) (shared.Knowledge, error) {
r := reader.Agent{
Model: a.Model,
ContextualInformation: a.ContextualInformation,
}
u, err := url.Parse(src.URL)
if err != nil {
return shared.Knowledge{}, err
}
slog.Info("reading search result", "url", u)
response, err := r.Read(ctx, question, u)
if err != nil {
return shared.Knowledge{}, err
}
return response, nil
}
tools := gollm.NewToolBox(fnReadSearchResult)
var req = gollm.Request{
Toolbox: tools,
}
req.Messages = append(req.Messages, gollm.Message{
Role: gollm.RoleSystem,
Text: `You are searching DuckDuckGo for the answer to the question that will be posed by the user. The search results will be provided in system messages in the format of: #. "https://url.here" - "Title of Page" - "Description here". For instance:
1. "https://example.com" - "Example Title" - "This is an example description."
2. "https://example2.com" - "Example Title 2" - "This is an example description 2."`,
})
if a.MaxReads == 0 {
a.MaxReads = 100
}
req.Messages = append(req.Messages, gollm.Message{
Role: gollm.RoleSystem,
Text: fmt.Sprintf(`You can read a search result by using the function "read_search_result" with the # of the page to read, it will attempt to read the page, and then an LLM will read the page and see if it answers the question.
can call read_search_result multiple times, up to %d times. All sources you read will be evaulated to see if they answer the question in full or at least in part.`, a.MaxReads),
})
if len(a.ContextualInformation) > 0 {
req.Messages = append(req.Messages, gollm.Message{
Role: gollm.RoleSystem,
Text: "Some contextual information you should be aware of: " + strings.Join(a.ContextualInformation, "\n"),
})
}
searches := ""
for i, r := range searchResults {
if i > 0 {
searches += "\n"
}
searches += fmt.Sprintf("%d. %q - %q - %q", i+1, r.URL, r.Title, r.Description)
}
req.Messages = append(req.Messages, gollm.Message{
Role: gollm.RoleSystem,
Text: "Search results are:\n" + searches,
})
results, err := a.Model.ChatComplete(ctx, req)
if err != nil {
return knowledge, err
}
if len(results.Choices) == 0 {
return knowledge, fmt.Errorf("no choices were returned")
}
choice := results.Choices[0]
// enforce the maximum number of reads
calls := choice.Calls
if len(calls) > a.MaxReads {
slog.Warn("too many calls, trimming to max", "len", len(calls), "max", a.MaxReads)
calls = calls[:a.MaxReads]
}
_, err = tools.ExecuteCallbacks(gollm.NewContext(ctx, req, &choice, nil), choice.Calls, nil, nil)
if err != nil {
return knowledge, err
}
close(toRead)
// make sure there are no duplicates
var uniques = map[int]struct{}{}
for i := range toRead {
uniques[i] = struct{}{}
}
var sources []duckduckgo.Result
for k := range uniques {
if k < 0 || k >= len(searchResults) {
slog.Warn("search result index out of range", "index", k, "len", len(searchResults))
continue
}
sources = append(sources, searchResults[k])
}
type result struct {
Knowledge shared.Knowledge
Err error
}
var gainedKnowledge = make(chan result, len(sources))
wg := sync.WaitGroup{}
for _, v := range sources {
wg.Add(1)
go func() {
res, err := readSource(ctx, v)
slog.Info("read search result", "url", v.URL, "err", err)
gainedKnowledge <- result{Knowledge: res, Err: err}
wg.Done()
}()
}
slog.Info("reading search results", "len", len(sources))
wg.Wait()
close(gainedKnowledge)
slog.Info("done reading search results", "len", len(gainedKnowledge))
for r := range gainedKnowledge {
if r.Err != nil {
slog.Info("error reading search result", "err", r.Err)
continue
}
knowledge.Knowledge = append(knowledge.Knowledge, r.Knowledge.Knowledge...)
knowledge.RemainingQuestions = append(knowledge.RemainingQuestions, r.Knowledge.RemainingQuestions...)
}
if a.OnDone != nil {
err := a.OnDone(ctx, knowledge)
if err != nil {
return knowledge, err
}
}
return knowledge, nil
}

View File

@ -1,45 +0,0 @@
package searcher
import (
"fmt"
"io"
"gitea.stevedudenhoeffer.com/steve/go-extractor"
"gitea.stevedudenhoeffer.com/steve/go-extractor/sites/duckduckgo"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
)
func deferClose(closer io.Closer) {
if closer != nil {
_ = closer.Close()
}
}
type searchResult struct {
Answer string `json:"answer"`
Sources []string `json:"sources"`
}
func fnSearch(ctx *gollm.Context, args struct {
Query string `description:"The search query to perform on duckduckgo"`
Question string `description:"The question(s) you are trying to answer when you read the search results. e.g: "`
}) (string, error) {
browser, ok := ctx.Value("browser").(extractor.Browser)
if !ok {
return "", fmt.Errorf("browser not found")
}
cfg := duckduckgo.Config{
SafeSearch: duckduckgo.SafeSearchOff,
Region: "us-en",
}
page, err := cfg.OpenSearch(ctx, browser, args.Query)
defer deferClose(page)
if err != nil {
return "", fmt.Errorf("failed to search: %w", err)
}
return "", nil
}

View File

@ -1,33 +0,0 @@
package shared
import (
"strings"
)
// TidBit is a small piece of information that the AI has learned.
type TidBit struct {
Info string
Source string
}
type Knowledge struct {
// OriginalQuestions are the questions that was asked first to the AI before any processing was done.
OriginalQuestions []string
// RemainingQuestions is the questions that are left to find answers for.
RemainingQuestions []string
// Knowledge are the tidbits of information that the AI has learned.
Knowledge []TidBit
}
// ToMessage converts the knowledge to a message that can be sent to the LLM.
func (k Knowledge) ToMessage() string {
var learned []string
for _, t := range k.Knowledge {
learned = append(learned, t.Info)
}
return "Original questions asked:\n" + strings.Join(k.OriginalQuestions, "\n") + "\n" +
"Learned information:\n" + strings.Join(learned, "\n") + "\n" +
"Remaining questions:\n" + strings.Join(k.RemainingQuestions, "\n")
}

View File

@ -0,0 +1,162 @@
package shared
import (
"context"
"fmt"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents"
"strings"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
)
type KnowledgeWorker struct {
Model gollm.ChatCompletion
ToolBox *gollm.ToolBox
ContextualInformation []string
OnNewFunction func(ctx context.Context, funcName string, args string) (any, error)
OnFunctionFinished func(ctx context.Context, funcName string, args string, result any, err error, newFunctionResult any) error
}
const DefaultPrompt = `Use the provided tools to answer the questions in your current knowledge.`
// Answer will try to answer the remaining questions in the knowledge object, while providing the LLM with a couple
// extra state update objects to manage Knowledge.CurrentObjectives and Knowledge.NotesToSelf.
// systemPrompt is the main prompt to tell the LLM what to do.
// userInput is the input that the LLM is trying to learn answers from.
// source is the source of the knowledge, for example a URL.
// Any tool call that returns a Knowledge object will be handled by this function in crafting the final Knowledge object.
// Any other return type will be passed to the resultWorker function, if provided.
func (w KnowledgeWorker) Answer(context context.Context, knowledge *agents.Knowledge, systemPrompt string, userInput string, source string, history []gollm.Message, resultWorker func(res gollm.ToolCallResponse)) (agents.Knowledge, error) {
var req gollm.Request
if systemPrompt != "" {
req.Messages = append(req.Messages, gollm.Message{
Role: gollm.RoleSystem,
Text: systemPrompt,
})
} else {
req.Messages = append(req.Messages, gollm.Message{
Role: gollm.RoleSystem,
Text: DefaultPrompt,
})
}
k := knowledge.ToSystemMessage()
if k.Text != "" {
req.Messages = append(req.Messages, k)
}
if len(w.ContextualInformation) > 0 {
req.Messages = append(req.Messages, gollm.Message{
Role: gollm.RoleSystem,
Text: "Contextual Information: " + strings.Join(w.ContextualInformation, ", "),
})
}
if len(history) > 0 {
req.Messages = append(req.Messages, history...)
}
req.Messages = append(req.Messages, gollm.Message{
Role: gollm.RoleSystem,
Text: `Feel free to call "learn", "notes_to_self", or "new_objectives" to leave notes for yourself or set new objectives for the LLM to work on, all can be called multiple times.`,
})
lastMsg := "Please try to batch all of your calls, such as if there are things you are learning and notes you are setting, try to do a call for each in one response."
if userInput != "" {
lastMsg += "\nNext input you are trying to use function calls to answer your objectives from is: " + userInput
}
req.Messages = append(req.Messages, gollm.Message{
Role: gollm.RoleUser,
Text: lastMsg,
})
req.Toolbox = w.ToolBox.
WithFunction(*gollm.NewFunction(
"notes_to_self",
"leave future executions of the LLM a note or two, can be called many times",
func(ctx *gollm.Context, args struct {
NotesToSelf []string `description:"Notes to leave for yourself for later."`
}) (any, error) {
return agents.Knowledge{
NotesToSelf: args.NotesToSelf,
}, nil
})).
WithFunction(*gollm.NewFunction(
"new_objectives",
"Set new objectives for the LLM to work on, can be called many times. If no new objectives are set, the LLM will continue to work on the current objectives.",
func(ctx *gollm.Context, args struct {
Objectives []string `description:"The objectives to set for executions going forward."`
}) (any, error) {
return agents.Knowledge{
CurrentObjectives: args.Objectives,
}, nil
})).
WithFunction(*gollm.NewFunction(
"learn",
`Use learn to pass some relevant information to the model. The model will use this information to answer the question. Use it to learn relevant information from the text. Keep these concise and relevant to the question. Can be called many times.`,
func(ctx *gollm.Context, args struct {
Info []string `description:"The information to learn from the input."`
}) (any, error) {
var k []agents.TidBit
for _, i := range args.Info {
k = append(k, agents.TidBit{Info: i, Source: source})
}
return agents.Knowledge{
Knowledge: k,
}, nil
})).
WithRequireTool(true)
for _, m := range req.Messages {
fmt.Println("Role: ", m.Role, "Text: ", m.Text)
}
fmt.Println("Calling...")
resp, err := w.Model.ChatComplete(context, req)
if err != nil {
return agents.Knowledge{}, fmt.Errorf("error calling model: %w", err)
}
if len(resp.Choices) == 0 {
return agents.Knowledge{}, fmt.Errorf("no choices found")
}
choice := resp.Choices[0]
if len(choice.Calls) == 0 {
return agents.Knowledge{}, fmt.Errorf("no calls found")
}
var callNames []string
for _, c := range choice.Calls {
callNames = append(callNames, c.FunctionCall.Name)
}
results, err := w.ToolBox.ExecuteCallbacks(gollm.NewContext(context, req, &choice, nil), choice.Calls, w.OnNewFunction, w.OnFunctionFinished)
if err != nil {
return agents.Knowledge{}, fmt.Errorf("error executing callbacks: %w", err)
}
var res = agents.Knowledge{}
for _, r := range results {
switch v := r.Result.(type) {
case agents.Knowledge:
res = res.Absorb(v)
default:
if resultWorker != nil {
resultWorker(r)
}
}
}
return res, nil
}

29
pkg/agents/steps.go Normal file
View File

@ -0,0 +1,29 @@
package agents
import (
"context"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
)
// Steps is a utility function that will ask the LLM to split a goal into smaller steps.
// It is a utility function and as such it does not use the agent's set system prompts, it will however use any
// contextual information that it has.
func (a Agent) Steps(ctx context.Context, goal string) ([]string, error) {
var res []string
fnSteps := gollm.NewFunction(
"steps",
"split the provided goal by the user into sub-steps",
func(ctx *gollm.Context, args struct {
Steps []string `description:"The steps that should be taken to achieve the goal"`
}) (any, error) {
res = append(res, args.Steps...)
return "", nil
})
_, err := a.WithSystemPrompt(`The user is going to mention a goal to you, and you are to respond using only one call of the "steps" function. You should provide the "steps" function with steps that should be taken to achieve the goal the user requests.`).
WithSystemPromptSuffix(``).
WithToolbox(gollm.NewToolBox(fnSteps)).
CallAndExecute(ctx, goal)
return res, err
}

View File

@ -13,7 +13,7 @@ var Calculator = gollm.NewFunction(
"A starlark calculator",
func(ctx *gollm.Context, args struct {
Expression string `description:"The expression to evaluate using starlark"`
}) (string, error) {
}) (any, error) {
val, err := starlark.EvalOptions(&syntax.FileOptions{},
&starlark.Thread{Name: "main"},
"input",

View File

@ -19,7 +19,7 @@ func CreateWolframFunctions(appId string) WolframFunctions {
"Query the Wolfram Alpha API",
func(ctx *gollm.Context, args struct {
Question string `description:"The question to ask Wolfram|Alpha"`
}) (string, error) {
}) (any, error) {
return client.GetShortAnswerQuery(args.Question, wolfram.Imperial, 10)
}),
Metric: gollm.NewFunction(
@ -27,7 +27,7 @@ func CreateWolframFunctions(appId string) WolframFunctions {
"Query the Wolfram Alpha API",
func(ctx *gollm.Context, args struct {
Question string `description:"The question to ask Wolfram|Alpha"`
}) (string, error) {
}) (any, error) {
return client.GetShortAnswerQuery(args.Question, wolfram.Metric, 10)
}),
}