Compare commits

..

2 Commits

Author SHA1 Message Date
ff1c369772 restructured answers a bit 2025-03-01 01:25:34 -05:00
090b28d956 Update LLM integration and add new agent tools and utilities
Refactored LLM handling to use updated langchaingo models and tools, replacing gollm dependencies. Introduced agent-related utilities, tools, and counters for better modular functionality. Added a parser for LLM model configuration and revamped the answering mechanism with enhanced support for tool-based interaction.
2025-02-25 22:56:32 -05:00
45 changed files with 1297 additions and 4624 deletions

View File

@ -1,125 +0,0 @@
package main
import (
"context"
"fmt"
"log/slog"
"os"
"strings"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
"github.com/joho/godotenv"
"github.com/urfave/cli"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents"
)
func getKey(key string, env string) string {
if key != "" {
return key
}
return os.Getenv(env)
}
func main() {
ctx := context.Background()
// 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: "answer",
Usage: "has an llm search the web for you to answer a question",
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(c *cli.Context) error {
// if there is no question to answer, print usage
if c.NArg() == 0 {
return cli.ShowAppHelp(c)
}
if c.String("env-file") != "" {
_ = godotenv.Load(c.String("env-file"))
}
var llm gollm.LLM
model := c.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(c.String("llm-key"), "OPENAI_API_KEY"))
case "anthropic":
llm = gollm.Anthropic(getKey(c.String("llm-key"), "ANTHROPIC_API_KEY"))
case "google":
llm = gollm.Google(getKey(c.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(c.Args(), " ")
agent := agents.NewAgent(m, gollm.ToolBox{}).WithMaxCalls(200)
knowledge, err := agent.SearchAndRead(ctx, question, []string{question}, true, 10)
if err != nil {
panic(err)
}
slog.Info("knowledge", "knowledge", knowledge)
res, err := agent.AnswerQuestionWithKnowledge(ctx, knowledge)
if err != nil {
panic(err)
}
fmt.Println(res)
return nil
},
}
err := app.Run(os.Args)
if err != nil {
slog.Error("Error: ", err)
}
}

View File

@ -2,7 +2,6 @@ package main
import (
"context"
"fmt"
"log/slog"
"os"
"strings"
@ -163,7 +162,9 @@ func main() {
panic(err)
}
fmt.Println(fmt.Sprintf("Question: %s\nAnswer: %q", question.Question, answers))
for i, a := range answers {
slog.Info("answer", "index", i, "answer", a)
}
return nil
},

View File

@ -1,151 +0,0 @@
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)
}
}

View File

@ -1,118 +0,0 @@
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)
}
}

115
go.mod
View File

@ -1,56 +1,48 @@
module gitea.stevedudenhoeffer.com/steve/answer
go 1.24.1
go 1.23.2
replace github.com/rocketlaunchr/google-search => github.com/chrisjoyce911/google-search v0.0.0-20230910003754-e501aedf805a
//replace gitea.stevedudenhoeffer.com/steve/go-llm => ../go-llm
replace gitea.stevedudenhoeffer.com/steve/go-llm => ../go-llm
require (
gitea.stevedudenhoeffer.com/steve/go-extractor v0.0.0-20250318064250-39453288ce2a
gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250412190744-39ffb8223775
gitea.stevedudenhoeffer.com/steve/go-extractor v0.0.0-20250123020607-964a98a5a884
gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250123045620-0d909edd44d9
github.com/Edw590/go-wolfram v0.0.0-20241010091529-fb9031908c5d
github.com/advancedlogic/GoOse v0.0.0-20231203033844-ae6b36caf275
github.com/asticode/go-astisub v0.34.0
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/lrstanley/go-ytdlp v0.0.0-20250401014907-da1707e4fb85
github.com/opencontainers/image-spec v1.1.1
github.com/playwright-community/playwright-go v0.5101.0
github.com/playwright-community/playwright-go v0.5001.0
github.com/rocketlaunchr/google-search v1.1.6
github.com/tmc/langchaingo v0.1.13
github.com/urfave/cli v1.22.16
go.starlark.net v0.0.0-20250318223901-d9371fef63fe
golang.org/x/sync v0.11.0
)
require (
cloud.google.com/go v0.120.0 // indirect
cloud.google.com/go/ai v0.10.1 // indirect
cloud.google.com/go v0.118.3 // indirect
cloud.google.com/go/ai v0.10.0 // indirect
cloud.google.com/go/auth v0.15.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.7 // 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/ProtonMail/go-crypto v1.2.0 // indirect
github.com/PuerkitoBio/goquery v1.10.3 // indirect
cloud.google.com/go/longrunning v0.6.4 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.0 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // 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/asticode/go-astikit v0.54.0 // indirect
github.com/asticode/go-astits v1.13.0 // indirect
github.com/cloudflare/circl v1.6.1 // 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/deckarep/golang-set/v2 v2.7.0 // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect
github.com/dustin/go-humanize v1.0.1 // 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
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-resty/resty/v2 v2.16.5 // indirect
@ -59,55 +51,60 @@ 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
github.com/google/generative-ai-go v0.19.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
github.com/goph/emperror v0.17.2 // indirect
github.com/huandu/xstrings v1.3.3 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kennygrant/sanitize v1.2.4 // indirect
github.com/liushuangls/go-anthropic/v2 v2.15.0 // indirect
github.com/liushuangls/go-anthropic/v2 v2.13.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/mitchellh/copystructure v1.0.0 // indirect
github.com/mitchellh/reflectwalk v1.0.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/nikolalohinski/gonja v1.5.3 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/openai/openai-go v0.1.0-beta.9 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkoukk/tiktoken-go v0.1.6 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
github.com/sashabaranov/go-openai v1.38.1 // indirect
github.com/sashabaranov/go-openai v1.37.0 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/temoto/robotstxt v1.1.2 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/yargevad/filepathx v1.0.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
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.37.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/oauth2 v0.29.0 // indirect
golang.org/x/sync v0.13.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
golang.org/x/time v0.11.0 // indirect
google.golang.org/api v0.228.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect
go.opentelemetry.io/otel v1.34.0 // indirect
go.opentelemetry.io/otel/metric v1.34.0 // indirect
go.opentelemetry.io/otel/trace v1.34.0 // indirect
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 // indirect
golang.org/x/crypto v0.34.0 // indirect
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/oauth2 v0.26.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
golang.org/x/time v0.10.0 // indirect
google.golang.org/api v0.222.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250409194420-de1ac958c67a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250409194420-de1ac958c67a // indirect
google.golang.org/grpc v1.71.1 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gotest.tools/v3 v3.5.2 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250219182151-9fdb1cabc7b2 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 // indirect
google.golang.org/grpc v1.70.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

502
go.sum
View File

@ -1,502 +0,0 @@
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-20250407055702-5ba0d5df7e96 h1:kT0pwH+q9i4TcFSRems8UFgaKCO94bCzLCf0IgAj6qw=
gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250407055702-5ba0d5df7e96/go.mod h1:Puz2eDyIwyQLKFt20BU9eRrfkUpBFo+ZX+PtTI64XSo=
gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250408003321-2ae583e9f360 h1:eZ8CZ1o4ZaciaDL0B/6tYwIERFZ94tNQtG7NKdb7cEQ=
gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250408003321-2ae583e9f360/go.mod h1:Puz2eDyIwyQLKFt20BU9eRrfkUpBFo+ZX+PtTI64XSo=
gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250412062040-3093b988f80a h1:SH2fWzDtv2KeWiwW+wtPr4NLI3CwdCSoYKn3FCRDdZc=
gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250412062040-3093b988f80a/go.mod h1:RPbuI2VSwQJArwr4tdqmu+fEKlhpro5Cqtq6aC4Cp1w=
gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250412074148-916f07be1840 h1:Yf33CXaCYwBG6AQxQAiK5MrdCQrRrf+Y0tzSfuXPb30=
gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250412074148-916f07be1840/go.mod h1:RPbuI2VSwQJArwr4tdqmu+fEKlhpro5Cqtq6aC4Cp1w=
gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250412190744-39ffb8223775 h1:KF6HdT7A5fqDnEWRjoYCm2mm5booxd+YD6j0wJkh+GU=
gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250412190744-39ffb8223775/go.mod h1:RPbuI2VSwQJArwr4tdqmu+fEKlhpro5Cqtq6aC4Cp1w=
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/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/ProtonMail/go-crypto v1.2.0 h1:+PhXXn4SPGd+qk76TlEePBfOfivE0zkWFenhGhFLzWs=
github.com/ProtonMail/go-crypto v1.2.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
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/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
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/asticode/go-astikit v0.20.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
github.com/asticode/go-astikit v0.54.0 h1:uq9eurgisdkYwJU9vSWIQaPH4MH0cac82sQH00kmSNQ=
github.com/asticode/go-astikit v0.54.0/go.mod h1:fV43j20UZYfXzP9oBn33udkvCvDvCDhzjVqoLFuuYZE=
github.com/asticode/go-astisub v0.34.0 h1:owKNj0A9pc7YVW/rNy2MJZ1mf0L8DTdklZVfyZDhTWI=
github.com/asticode/go-astisub v0.34.0/go.mod h1:WTkuSzFB+Bp7wezuSf2Oxulj5A8zu2zLRVFf6bIFQK8=
github.com/asticode/go-astits v1.8.0/go.mod h1:DkOWmBNQpnr9mv24KfZjq4JawCFX1FCqjLVGvO0DygQ=
github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c=
github.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
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/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
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.15.0 h1:zpplg7BRV/9FlMmeMPI0eDwhViB0l9SkNrF8ErYlRoQ=
github.com/liushuangls/go-anthropic/v2 v2.15.0/go.mod h1:kq2yW3JVy1/rph8u5KzX7F3q95CEpCT2RXp/2nfCmb4=
github.com/lrstanley/go-ytdlp v0.0.0-20250401014907-da1707e4fb85 h1:fgU9HcQ95uG9vqkYP/YW/H6DhwsmkXHriuMM27bwpYU=
github.com/lrstanley/go-ytdlp v0.0.0-20250401014907-da1707e4fb85/go.mod h1:HpxGaeaOpXVUPxUUmj8Izr3helrDGN90haPtmpY5xzA=
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/openai/openai-go v0.1.0-beta.6 h1:JquYDpprfrGnlKvQQg+apy9dQ8R9mIrm+wNvAPp6jCQ=
github.com/openai/openai-go v0.1.0-beta.6/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y=
github.com/openai/openai-go v0.1.0-beta.7 h1:ykC09BCIgdXL69wE/8NUjL2rCdAbo9kL3AjnGR6H91o=
github.com/openai/openai-go v0.1.0-beta.7/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y=
github.com/openai/openai-go v0.1.0-beta.9 h1:ABpubc5yU/3ejee2GgRrbFta81SG/d7bQbB8mIdP0Xo=
github.com/openai/openai-go v0.1.0-beta.9/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y=
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/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
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/playwright-community/playwright-go v0.5101.0 h1:gVCMZThDO76LJ/aCI27lpB8hEAWhZszeS0YB+oTxJp0=
github.com/playwright-community/playwright-go v0.5101.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/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
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.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
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-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
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.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
golang.org/x/oauth2 v0.29.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.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.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.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.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.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
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-20250404141209-ee84b53bf3d0 h1:Qbb5RVn5xzI4naMJSpJ7lhvmos6UwZkbekd5Uz7rt9E=
google.golang.org/genproto/googleapis/api v0.0.0-20250404141209-ee84b53bf3d0/go.mod h1:6T35kB3IPpdw7Wul09by0G/JuOuIFkXV6OOvt8IZeT8=
google.golang.org/genproto/googleapis/api v0.0.0-20250407143221-ac9807e6c755 h1:AMLTAunltONNuzWgVPZXrjLWtXpsG6A3yLLPEoJ/IjU=
google.golang.org/genproto/googleapis/api v0.0.0-20250407143221-ac9807e6c755/go.mod h1:2R6XrVC8Oc08GlNh8ujEpc7HkLiEZ16QeY7FxIs20ac=
google.golang.org/genproto/googleapis/api v0.0.0-20250409194420-de1ac958c67a h1:OQ7sHVzkx6L57dQpzUS4ckfWJ51KDH74XHTDe23xWAs=
google.golang.org/genproto/googleapis/api v0.0.0-20250409194420-de1ac958c67a/go.mod h1:2R6XrVC8Oc08GlNh8ujEpc7HkLiEZ16QeY7FxIs20ac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250404141209-ee84b53bf3d0 h1:0K7wTWyzxZ7J+L47+LbFogJW1nn/gnnMCN0vGXNYtTI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250404141209-ee84b53bf3d0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250407143221-ac9807e6c755 h1:TwXJCGVREgQ/cl18iY0Z4wJCTL/GmW+Um2oSwZiZPnc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250407143221-ac9807e6c755/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250409194420-de1ac958c67a h1:GIqLhp/cYUkuGuiT+vJk8vhOP86L4+SP5j8yXgeVpvI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250409194420-de1ac958c67a/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.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
google.golang.org/grpc v1.71.1/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=

View File

@ -2,152 +2,116 @@ package agent
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"github.com/davecgh/go-spew/spew"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/toolbox"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
"github.com/tmc/langchaingo/llms"
cache2 "gitea.stevedudenhoeffer.com/steve/answer/pkg/cache"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/extractor"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/search"
)
type Options struct {
MaxSearches int
MaxQuestions int
}
type Agent struct {
// ToolBox is the toolbox to use for the agent.
ToolBox *gollm.ToolBox
// Model is the model to use for the agent.
Model gollm.ChatCompletion
// OnNewFunction is a callback that, if non-nil, will be called when a new function is called by the LLM.
// The "answer" and "no_answer" functions are not included in this callback.
// Return an error to stop the function from being called.
OnNewFunction func(ctx context.Context, funcName string, question string, parameter string) (any, error)
// OnFunctionFinished is a callback that, if non-nil, will be called when a function has finished executing. The
// function name is passed in, as well as the question, the parameter, all similar to OnNewFunction. The result of
// the function is also passed in, as well as any error that occurred. Finally, the result passed from the
// OnNewFunction that preceded this function is passed in as well.
OnFunctionFinished func(ctx context.Context, funcName string, question string, parameter string, result string, err error, newFunctionResult any) error
req gollm.Request
RemainingSearches *Counter
RemainingQuestions *Counter
Search search.Search
Cache cache2.Cache
Extractor extractor.Extractor
Model llms.Model
}
func NewAgent(req gollm.Request) *Agent {
return &Agent{req: req}
type Answers struct {
Response llms.MessageContent
Answers []Answer
}
type Response struct {
Text string
Sources []string
type Answer struct {
Answer string
Source string
ToolCallResponse llms.ToolCallResponse `json:"-"`
}
func deferClose(cl io.Closer) {
if cl != nil {
_ = cl.Close()
type Question struct {
Question string
}
func New(o Options) *Agent {
searches := &Counter{}
searches.Add(int32(o.MaxSearches))
questions := &Counter{}
questions.Add(int32(o.MaxQuestions))
return &Agent{
RemainingSearches: searches,
RemainingQuestions: questions,
}
}
func (a *Agent) AddConversation(in gollm.Input) {
a.req.Conversation = append(a.req.Conversation, in)
var (
ErrOutOfSearches = errors.New("out of searches")
ErrOutOfQuestions = errors.New("out of questions")
)
func ask(ctx *Context, q Question) (Answers, error) {
var tb = toolbox.ToolBox{}
if ctx.Agent.RemainingSearches.Load() > 0 {
tb.Register(SearchTool)
}
tb.Register(WolframTool)
tb.Register(AnswerTool)
return tb.Run(ctx, q)
}
func (a *Agent) AddMessage(msg gollm.Message) {
slog.Info("adding message", "message", msg)
a.req.Messages = append(a.req.Messages, msg)
}
var SummarizeAnswers = toolbox.FromFunction(
func(ctx *Context, args struct {
Summary string `description:"the summary of the answers"`
}) (toolbox.FuncResponse, error) {
return toolbox.FuncResponse{Result: args.Summary}, nil
}).
WithName("summarize_answers").
WithDescription(`You are given previously figured out answers and they are in the format of: [ { "answer": "the answer", "source": "the source of the answer" }, { "answer": "answer 2", "source": "the source for answer2" } ]. You need to summarize the answers into a single string. Be sure to make the summary clear and concise, but include the sources at some point.`)
// Execute will execute the current request with the given messages. The messages will be appended to the current
// request, but they will _not_ be saved into the embedded request. However, the embedded request will be
// generated with the on the results from the ChatComplete call.
func (a *Agent) Execute(ctx context.Context, msgs ...gollm.Message) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
req := a.req
slog.Info("executing", "request", req, "messages", msgs)
for _, c := range req.Conversation {
slog.Info("conversation", "message", c)
// Ask is an incoming call to the agent, it will create an internal Context from an incoming context.Context
func (a *Agent) Ask(ctx context.Context, q Question) (string, error) {
c := From(ctx, a)
if !a.RemainingQuestions.Take() {
return "", ErrOutOfQuestions
}
req.Messages = append(req.Messages, msgs...)
for _, m := range req.Messages {
slog.Info("messages", "message", m)
}
req.Toolbox = a.ToolBox
fmt.Println("req:")
spew.Dump(req)
res, err := a.Model.ChatComplete(ctx, req)
fmt.Println("res:")
spew.Dump(res)
answers, err := ask(c, q)
if err != nil {
return err
return "", err
}
if len(res.Choices) == 0 {
return nil
tb := toolbox.ToolBox{}
tb.Register(SummarizeAnswers)
b, err := json.Marshal(answers.Answers)
if err != nil {
return "", fmt.Errorf("failed to marshal answers: %w", err)
}
choice := res.Choices[0]
var callsOutput = make(chan gollm.ToolCallResponse, len(choice.Calls))
fnCall := func(call gollm.ToolCall) gollm.ToolCallResponse {
str, err := a.ToolBox.Execute(gollm.NewContext(ctx, a.req, &choice, &call), call)
answers, err = tb.Run(c, Question{Question: string(b)})
if err != nil {
return gollm.ToolCallResponse{
ID: call.ID,
Error: err,
}
}
return gollm.ToolCallResponse{
ID: call.ID,
Result: str,
}
if err != nil {
return "", fmt.Errorf("failed to summarize answers: %w", err)
}
for _, call := range choice.Calls {
go func(call gollm.ToolCall) {
var arg any
var err error
if a.OnNewFunction != nil {
arg, err = a.OnNewFunction(ctx, call.FunctionCall.Name, choice.Content, call.FunctionCall.Arguments)
if err != nil {
callsOutput <- gollm.ToolCallResponse{
ID: call.ID,
Error: err,
}
return
}
}
callRes := fnCall(call)
if a.OnFunctionFinished != nil {
err = a.OnFunctionFinished(ctx, call.FunctionCall.Name, choice.Content, call.FunctionCall.Arguments, callRes.Result, callRes.Error, arg)
if err != nil {
callsOutput <- gollm.ToolCallResponse{
ID: call.ID,
Error: err,
}
return
}
}
callsOutput <- callRes
}(call)
if len(answers.Answers) == 0 {
return "", errors.New("no response from model")
}
return answers.Answers[0].Answer, nil
var answers []gollm.ToolCallResponse
for i := 0; i < len(choice.Calls); i++ {
result := <-callsOutput
answers = append(answers, result)
}
close(callsOutput)
slog.Info("generating new request", "answers", answers, "choice", choice)
a.req = gollm.NewContext(ctx, a.req, &choice, nil).ToNewRequest(answers...)
return nil
}

12
pkg/agent/answertool.go Normal file
View File

@ -0,0 +1,12 @@
package agent
import "gitea.stevedudenhoeffer.com/steve/answer/pkg/toolbox"
var AnswerTool = toolbox.FromFunction(
func(ctx *Context, args struct {
Answer string `description:"the answer to the question"`
}) (toolbox.FuncResponse, error) {
return toolbox.FuncResponse{Result: args.Answer}, nil
}).
WithName("answer").
WithDescription("Answer the question")

48
pkg/agent/ask.go Normal file
View File

@ -0,0 +1,48 @@
package agent
import (
"encoding/json"
"fmt"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/toolbox"
)
var AskTool = toolbox.FromFunction(
func(ctx *Context, args struct {
Question string `description:"the question to answer"`
}) (toolbox.FuncResponse, error) {
var q Question
q.Question = args.Question
ctx = ctx.WithQuestion(q)
answers, err := ask(ctx, q)
if err != nil {
return toolbox.FuncResponse{}, err
}
tb := toolbox.ToolBox{}
tb.Register(SummarizeAnswers)
b, err := json.Marshal(answers.Answers)
if err != nil {
return toolbox.FuncResponse{}, fmt.Errorf("failed to marshal answers: %w", err)
}
q = Question{Question: string(b)}
ctx = ctx.WithQuestion(q)
answers, err = tb.Run(ctx, q)
if err != nil {
return toolbox.FuncResponse{}, fmt.Errorf("failed to summarize answers: %w", err)
}
if len(answers.Answers) == 0 {
return toolbox.FuncResponse{}, fmt.Errorf("no response from model")
}
return toolbox.FuncResponse{Result: answers.Answers[0].Answer}, nil
}).
WithName("ask").
WithDescription("Ask the agent a question, this is useful for splitting a question into multiple parts")

112
pkg/agent/context.go Normal file
View File

@ -0,0 +1,112 @@
package agent
import (
"context"
"time"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/toolbox"
"github.com/tmc/langchaingo/llms"
)
type Context struct {
context.Context
Messages []llms.MessageContent
Agent *Agent
}
func From(ctx context.Context, a *Agent) *Context {
if ctx == nil {
ctx = context.Background()
}
return &Context{
Context: ctx,
Agent: a,
}
}
func (c *Context) WithMessage(m llms.MessageContent) *Context {
c.Messages = append(c.Messages, m)
return c
}
func (c *Context) WithMessages(m ...llms.MessageContent) *Context {
c.Messages = append(c.Messages, m...)
return c
}
func (c *Context) WithToolResults(r ...toolbox.ToolResult) *Context {
msg := llms.MessageContent{
Role: llms.ChatMessageTypeTool,
}
for _, v := range r {
res := v.Result
if v.Error != nil {
res = "error executing: " + v.Error.Error()
}
msg.Parts = append(msg.Parts, llms.ToolCallResponse{
ToolCallID: v.ID,
Name: v.Name,
Content: res,
})
}
return c.WithMessage(msg)
}
func (c *Context) WithAgent(a *Agent) *Context {
return &Context{
Context: c.Context,
Agent: a,
}
}
func (c *Context) WithCancel() (*Context, func()) {
ctx, cancel := context.WithCancel(c.Context)
return &Context{
Context: ctx,
Agent: c.Agent,
}, cancel
}
func (c *Context) WithDeadline(deadline time.Time) (*Context, func()) {
ctx, cancel := context.WithDeadline(c.Context, deadline)
return &Context{
Context: ctx,
Agent: c.Agent,
}, cancel
}
func (c *Context) WithTimeout(timeout time.Duration) (*Context, func()) {
ctx, cancel := context.WithTimeout(c.Context, timeout)
return &Context{
Context: ctx,
Agent: c.Agent,
}, cancel
}
func (c *Context) WithValue(key, value interface{}) *Context {
return &Context{
Context: context.WithValue(c.Context, key, value),
Agent: c.Agent,
}
}
func (c *Context) Done() <-chan struct{} {
return c.Context.Done()
}
func (c *Context) Err() error {
return c.Context.Err()
}
func (c *Context) Value(key interface{}) interface{} {
return c.Context.Value(key)
}
func (c *Context) Deadline() (time.Time, bool) {
return c.Context.Deadline()
}

25
pkg/agent/counter.go Normal file
View File

@ -0,0 +1,25 @@
package agent
import "sync/atomic"
type Counter struct {
atomic.Int32
}
// Take will attempt to take an item from the counter. If the counter is zero, it will return false.
func (c *Counter) Take() bool {
for {
current := c.Load()
if current <= 0 {
return false
}
if c.CompareAndSwap(current, current-1) {
return true
}
}
}
// Return will return an item to the counter.
func (c *Counter) Return() {
c.Add(1)
}

13
pkg/agent/search.go Normal file
View File

@ -0,0 +1,13 @@
package agent
import "gitea.stevedudenhoeffer.com/steve/answer/pkg/toolbox"
var SearchTool = toolbox.FromFunction(
func(ctx *Context, args struct {
SearchFor string `description:"what to search for"`
Question string `description:"the question to answer with the search results"`
}) (toolbox.FuncResponse, error) {
return toolbox.FuncResponse{}, nil
}).
WithName("search").
WithDescription("Search the web and read a few articles to find the answer to the question")

34
pkg/agent/test/main.go Normal file
View File

@ -0,0 +1,34 @@
package main
import (
"reflect"
"github.com/tmc/langchaingo/llms"
)
func testFunction(args struct{ a, b int }) {
// This is a test function
}
func main() {
v := reflect.New(reflect.TypeOf(testFunction))
t := reflect.TypeOf(testFunction)
for i := 0; i < t.NumIn(); i++ {
param := t.In(i)
llms.MessageContent{
Role: llms.ChatMessageTypeTool,
Parts: []llms.ContentPart{
llms.ToolCallResponse{
Name: "testFunction",
},
},
}
if param.Type().Kind() == reflect.Struct {
}
println(param.Name(), param.Kind().String())
}
}

30
pkg/agent/wolfram.go Normal file
View File

@ -0,0 +1,30 @@
package agent
import (
"fmt"
"os"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/toolbox"
"github.com/Edw590/go-wolfram"
)
var WolframTool = toolbox.FromFunction(
func(ctx *Context, args struct {
Query string `description:"what to ask wolfram alpha"`
}) (toolbox.FuncResponse, error) {
var cl = wolfram.Client{
AppID: os.Getenv("WOLFRAM_APPID"),
}
unit := wolfram.Imperial
a, err := cl.GetShortAnswerQuery(args.Query, unit, 10)
if err != nil {
return toolbox.FuncResponse{}, fmt.Errorf("failed to get short answer from wolfram: %w", err)
}
return toolbox.FuncResponse{Result: a, Source: "Wolfram|Alpha"}, nil
}).
WithName("wolfram").
WithDescription("ask wolfram alpha for the answer")

View File

@ -1,242 +0,0 @@
package agents
import (
"context"
"fmt"
"sync"
"sync/atomic"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
)
// 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
insertReason bool
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.WithSyntheticFieldsAddedToAllFunctions(map[string]string{
"reason": "The reason you are calling this function. This will be remembered and presenting to the LLM when it continues after the function call.",
})
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
}

View File

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

View File

@ -1,302 +0,0 @@
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

@ -1,115 +0,0 @@
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

@ -1,97 +0,0 @@
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

@ -1,23 +0,0 @@
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
}

View File

@ -1,302 +0,0 @@
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

@ -1,115 +0,0 @@
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

@ -1,97 +0,0 @@
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

@ -1,23 +0,0 @@
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

@ -1,59 +0,0 @@
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
}

View File

@ -1,108 +0,0 @@
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

@ -1,87 +0,0 @@
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.`).
WithSystemPromptSuffix(``).
WithToolbox(tools).
CallAndExecute(ctx, `The original knowledge is as follows: `+baseMsg.Text+` The new knowledge is as follows: `+incomingMsg.Text)
return result, err
}

View File

@ -1,150 +0,0 @@
package agents
import (
"context"
"fmt"
"regexp"
"strconv"
"strings"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
)
// 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 := ""
// 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
if len(knowledge.RemainingQuestions) > 0 {
systemPrompt += "\n\nI still have some questions that I could not find an answer to:\n" + strings.Join(knowledge.RemainingQuestions, "\n")
}
res, err := a.WithSystemPrompt(systemPrompt).
WithSystemPromptSuffix(``).
CallAndExecute(ctx, "Using the sources, write an answer to the original question. Note any information that wasn't able to be answered.")
if err != nil {
return "", err
}
systemPrompt = `I am trying to source an analysis of information I have gathered.
To do this I will provide you with all of the sourced information I have gathered in the format of:
[Source]
- Information
- Information
- Information
Where Source will be a number from 1 to ` + fmt.Sprint(len(sources)) + ` and Information will be the information gathered from that source.
You should then read the information provided by the user and tag the information with citations from the sources provided. If a fact is provided by multiple sources, you should tag it with all of the sources that provide that information.
For instance, if the sourced data were:
[1]
- The diameter of the moon is 3,474.8 km
- The moon's age is 4.53 billion years
[2]
- The moon's age is 4.53 billion years
[3]
- The moon is on average 238,855 miles away from the Earth
And the user provided the following information:
The moon is 4.5 billion years old, 238,855 miles away from the Earth, and has a diameter of 3,474.8 km.
You would then tag the information with the sources like so:
The moon is 4.5 billion years old [1,2], 238,855 miles away from the Earth [3], and has a diameter of 3,474.8 km [1].`
providedIntel := `Here is the information I have gathered:
`
for i, s := range sources {
providedIntel += "[" + fmt.Sprint(i+1) + "]\n"
for _, info := range s.info {
providedIntel += " - " + info + "\n"
}
}
summarizedData := `Here is the I need you to source with citations:
` + res.Text
res, err = a.WithSystemPrompt(systemPrompt).
WithSystemPromptSuffix(``).
CallAndExecute(ctx, gollm.Message{Role: gollm.RoleSystem, Text: providedIntel}, summarizedData)
if err != nil {
return "", err
}
// now go through the response and find all citations
// use this by looking for \[[\d+,]+\]
// then use the number to find the source
re := regexp.MustCompile(`\[([\d,\s]+)]`)
// find all the citations
citations := re.FindAllString(res.Text, -1)
// now we need to find the sources
lookup := map[int][]string{}
for _, c := range citations {
c = strings.Trim(c, "[]")
a := strings.Split(c, ",")
for _, v := range a {
v = strings.TrimSpace(v)
i, _ := strconv.Atoi(v)
if i < 1 || i > len(sources) {
continue
}
lookup[i] = append(lookup[i], sources[i-1].source)
}
}
text := res.Text
if len(lookup) > 0 {
text += "\n\nHere are the sources for the information provided:\n"
for i := 1; i <= len(sources); i++ {
if _, ok := lookup[i]; !ok {
continue
}
text += "[" + fmt.Sprint(i) + "] <" + lookup[i][0] + ">\n"
}
}
return text, nil
}

View File

@ -1,32 +0,0 @@
package agents
import (
"context"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
)
// 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
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"`
}) (any, error) {
res = args.Questions
return "", nil
})
_, 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.`).
WithSystemPromptSuffix(``).
WithToolbox(gollm.NewToolBox(fnQuestions)).
CallAndExecute(ctx, question)
return res, err
}

View File

@ -1,72 +0,0 @@
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,227 +0,0 @@
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
}
ctx = context.WithValue(ctx, "browser", b)
browser = b
defer deferClose(browser)
}
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-- {
v := analyzed[j]
if v < 0 || v >= len(searchResults) {
continue
}
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...).WithRequireTool(true))
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)
}
slog.Info("search results called and executed", "error", err, "results text", results.Text, "results", results.CallResults)
var learned []Knowledge
for _, r := range results.CallResults {
if r.Error != nil {
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,
},
{
Name: "youtube",
Description: "Read the transcript to a youtube video and see if it answers the question. Try to avoid using this on low quality or spammy links. You can use this function" + fmt.Sprint(maxReads) + " times, but do not call it multiple times on the same result.",
Function: a.ReadYouTubeTranscript,
},
},
gollm.Message{Role: gollm.RoleSystem, Text: "For youtube links, only use the youtube tool. For other links, only use the readpage tool."})
}

View File

@ -1,54 +0,0 @@
package agents
import (
"context"
"errors"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
"strings"
)
var ErrNoSearchTerms = errors.New("no search terms")
// 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 (a Agent) GenerateSearchTerms(ctx context.Context, question string, alreadySearched []string) (string, error) {
var res string
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"`
}) (any, error) {
res = args.SearchTerms
return "", nil
})
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 {
suffix = "The following search terms have already been used, please avoid them: " + strings.Join(alreadySearched, ", ") + "\n"
}
_, 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)
if err != nil {
return "", err
}
if cantFind {
return "cannot think of any search terms", ErrNoSearchTerms
}
return res, nil
}

View File

@ -1,163 +0,0 @@
package shared
import (
"context"
"fmt"
"strings"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents"
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.WithFunctions(
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
}),
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
}),
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
}

View File

@ -1,42 +0,0 @@
package shared
import (
"context"
"errors"
"sync/atomic"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
)
type ModelTracker struct {
parent gollm.ChatCompletion
maximum int64
calls int64
}
var _ gollm.ChatCompletion = &ModelTracker{}
// NewModelTracker creates a new model tracker that will limit the number of calls to the parent.
// Set to 0 to disable the limit.
func NewModelTracker(parent gollm.ChatCompletion, maximum int64) *ModelTracker {
return &ModelTracker{parent: parent, maximum: maximum}
}
var ErrModelCapacity = errors.New("maximum model capacity reached")
func (m *ModelTracker) ChatComplete(ctx context.Context, req gollm.Request) (gollm.Response, error) {
if m.maximum > 0 && atomic.AddInt64(&m.calls, 1) >= m.maximum {
return gollm.Response{}, ErrModelCapacity
}
return m.parent.ChatComplete(ctx, req)
}
// ResetCalls resets the number of calls made to the parent.
func (m *ModelTracker) ResetCalls() {
atomic.StoreInt64(&m.calls, 0)
}
func (m *ModelTracker) GetCalls() int64 {
return atomic.LoadInt64(&m.calls)
}

View File

@ -1,29 +0,0 @@
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

@ -1,27 +0,0 @@
package tools
import (
"go.starlark.net/lib/math"
"go.starlark.net/starlark"
"go.starlark.net/syntax"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
)
var Calculator = gollm.NewFunction(
"calculator",
"A starlark calculator",
func(ctx *gollm.Context, args struct {
Expression string `description:"The expression to evaluate using starlark"`
}) (any, error) {
val, err := starlark.EvalOptions(&syntax.FileOptions{},
&starlark.Thread{Name: "main"},
"input",
args.Expression,
math.Module.Members)
if err != nil {
return "", err
}
return val.String(), nil
})

View File

@ -1,34 +0,0 @@
package tools
import (
"github.com/Edw590/go-wolfram"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
)
type WolframFunctions struct {
Imperial *gollm.Function
Metric *gollm.Function
}
func CreateWolframFunctions(appId string) WolframFunctions {
client := &wolfram.Client{AppID: appId}
return WolframFunctions{
Imperial: gollm.NewFunction(
"wolfram",
"Query the Wolfram Alpha API",
func(ctx *gollm.Context, args struct {
Question string `description:"The question to ask Wolfram|Alpha"`
}) (any, error) {
return client.GetShortAnswerQuery(args.Question, wolfram.Imperial, 10)
}),
Metric: gollm.NewFunction(
"wolfram",
"Query the Wolfram Alpha API",
func(ctx *gollm.Context, args struct {
Question string `description:"The question to ask Wolfram|Alpha"`
}) (any, error) {
return client.GetShortAnswerQuery(args.Question, wolfram.Metric, 10)
}),
}
}

View File

@ -1,104 +0,0 @@
package agents
import (
"context"
"fmt"
"github.com/asticode/go-astisub"
"github.com/lrstanley/go-ytdlp"
"io"
"log/slog"
"net/url"
"os"
"path/filepath"
)
func init() {
ytdlp.MustInstall(context.Background(), nil)
}
func (a Agent) ReadYouTubeTranscript(ctx context.Context, u *url.URL, questions []string) (Knowledge, error) {
dlp := ytdlp.New()
tmpDir, err := os.MkdirTemp("", "mort-ytdlp-")
if err != nil {
return Knowledge{}, fmt.Errorf("error creating temp dir: %w", err)
}
slog.Info("created temp dir", "path", tmpDir)
defer func(path string) {
err := os.RemoveAll(path)
if err != nil {
slog.Error("error removing temp file", "error", err)
}
}(tmpDir)
subFile := filepath.Join(tmpDir, "subs")
dlp.
SkipDownload().
WriteAutoSubs().
Output(subFile)
res, err := dlp.Run(ctx, u.String())
if err != nil {
return Knowledge{}, fmt.Errorf("error running yt-dlp: %w", err)
}
if res == nil {
return Knowledge{}, fmt.Errorf("yt-dlp returned nil")
}
if res.ExitCode != 0 {
return Knowledge{}, fmt.Errorf("yt-dlp exited with code %d", res.ExitCode)
}
// the transcript for this video now _should_ be at tmpDir/subs.en.vtt, however if it's not then just fine any
// vtt file in the directory
vttFile := filepath.Join(tmpDir, "subs.en.vtt")
_, err = os.Stat(vttFile)
if os.IsNotExist(err) {
vttFile = ""
files, err := os.ReadDir(tmpDir)
if err != nil {
return Knowledge{}, fmt.Errorf("error reading directory: %w", err)
}
for _, file := range files {
if filepath.Ext(file.Name()) == ".vtt" {
vttFile = filepath.Join(tmpDir, file.Name())
break
}
}
}
if vttFile == "" {
return Knowledge{}, fmt.Errorf("no vtt file found")
}
fp, err := os.Open(vttFile)
defer func(cl io.Closer) {
err := cl.Close()
if err != nil {
slog.Error("error closing file", "error", err)
}
}(fp)
if err != nil {
return Knowledge{}, fmt.Errorf("error opening vtt file: %w", err)
}
subs, err := astisub.ReadFromWebVTT(fp)
if err != nil {
return Knowledge{}, fmt.Errorf("error reading vtt file: %w", err)
}
if len(subs.Items) == 0 {
return Knowledge{}, fmt.Errorf("no subtitles found")
}
var ts string
for _, item := range subs.Items {
ts += item.String() + "\n"
}
return a.ExtractKnowledge(ctx, ts, u.String(), questions)
}

View File

@ -2,19 +2,19 @@ package answer
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"net/url"
"strings"
"gitea.stevedudenhoeffer.com/steve/go-extractor"
"github.com/tmc/langchaingo/agents"
"github.com/Edw590/go-wolfram"
"go.starlark.net/lib/math"
"go.starlark.net/starlark"
"go.starlark.net/syntax"
"github.com/tmc/langchaingo/llms"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/cache"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/extractor"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/search"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
)
@ -27,7 +27,7 @@ type Question struct {
// Question is the question to answer
Question string
Model gollm.ChatCompletion
Model llms.Model
Search search.Search
@ -37,19 +37,16 @@ type Question struct {
// Answers is a list of answers to a question
type Answers []string
const DefaultPrompt = `You are being asked to answer a question.
You must respond with a function.
You can answer it if you know the answer, or if some functions exist you can use those to help you find the answer.
You can break up the question into multiple steps, and will learn from questions. Such as, if you need to compute the
exact square root of the powerball jackpot, you could search for the current jackpot and then when search finishes
you could execute calculate sqrt(that value), and respond with that.
`
type Options struct {
// MaxSearches is the maximum possible number of searches to execute for this question. If this is set to 5, the function could
// search up to 5 possible times to find an answer.
MaxSearches int
// MaxThinks is the maximum number of times to think about a question. A "Think" is different than a search in that
// the LLM just breaks the question down into smaller parts and tries to answer them. This is useful for complex
// questions that are hard to answer since LLMs are better at answering smaller questions.
MaxThinks int
// MaxTries is the absolute maximum number of pages to try to get an answer from. For instance, if MaxSearches is 5 and
// 5 pages are tried and no answers are found, the function will return ErrMaxTries.
MaxTries int
@ -57,30 +54,12 @@ type Options struct {
// OnNewFunction is a callback that, if non-nil, will be called when a new function is called by the LLM.
// The "answer" and "no_answer" functions are not included in this callback.
// Return an error to stop the function from being called.
OnNewFunction func(ctx context.Context, funcName string, question string, parameter string) (any, error)
// OnFunctionFinished is a callback that, if non-nil, will be called when a function has finished executing. The
// function name is passed in, as well as the question, the parameter, all similar to OnNewFunction. The result of
// the function is also passed in, as well as any error that occurred. Finally, the result passed from the
// OnNewFunction that preceded this function is passed in as well.
OnFunctionFinished func(ctx context.Context, funcName string, question string, parameter string, result string, err error, newFunctionResult any) error
// SystemPrompt is the prompt to use when asking the system to answer a question.
// If this is empty, DefaultPrompt will be used.
SystemPrompt string
// ExtraSystemPrompts is a list of extra prompts to use when asking the system to answer a question. Use these for
// variety in the prompts, or passing in some useful contextually relevant information.
// All of these will be used in addition to the SystemPrompt.
ExtraSystemPrompts []string
// WolframAppID is the Wolfram Alpha App ID to use when searching Wolfram Alpha for answers. If not set, the
// wolfram function will not be available.
WolframAppID string
OnNewFunction func(ctx context.Context, funcName string, question string, parameter string) error
}
var DefaultOptions = Options{
MaxSearches: 10,
MaxSearches: 5,
MaxThinks: 10,
MaxTries: 5,
}
@ -89,286 +68,310 @@ type Result struct {
Error error
}
func fanExecuteToolCalls(ctx context.Context, toolBox *gollm.ToolBox, calls []gollm.ToolCall) []Result {
var results []Result
var resultsOutput = make(chan Result, len(calls))
fnCall := func(call gollm.ToolCall) Result {
str, err := toolBox.Execute(ctx, call)
if err != nil {
return Result{
Error: err,
}
}
return Result{
Result: str,
}
}
for _, call := range calls {
go func(call gollm.ToolCall) {
resultsOutput <- fnCall(call)
}(call)
}
for i := 0; i < len(calls); i++ {
result := <-resultsOutput
results = append(results, result)
}
close(resultsOutput)
return results
}
type article struct {
URL string
Title string
Body string
}
type Response struct {
Text string
Sources []string
}
func extractArticle(ctx context.Context, c cache.Cache, u *url.URL) (res article, err error) {
defer func() {
e := recover()
func deferClose(cl io.Closer) {
if cl != nil {
_ = cl.Close()
}
}
if e != nil {
if e, ok := e.(error); ok {
err = fmt.Errorf("panic: %w", e)
} else {
err = fmt.Errorf("panic: %v", e)
}
}
}()
func (o Options) Answer(ctx context.Context, q Question) (Response, error) {
var answer Response
extractors := extractor.MultiExtractor(
extractor.CacheExtractor{
Cache: c,
Tag: "goose",
Extractor: extractor.GooseExtractor{},
},
extractor.CacheExtractor{
Cache: c,
Tag: "playwright",
Extractor: extractor.PlaywrightExtractor{},
},
)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
a, err := extractors.Extract(ctx, u.String())
b, err := extractor.NewPlayWrightBrowser(extractor.PlayWrightBrowserOptions{
DarkMode: true,
})
defer deferClose(b)
if err != nil {
return answer, err
return article{
URL: "",
Title: "",
Body: "",
}, err
}
ctx = context.WithValue(ctx, "browser", b)
return article{
URL: a.URL,
Title: a.Title,
Body: a.Body,
}, nil
}
fnSearch := gollm.NewFunction(
"search",
"Search the web for an answer to a question. You can call this function up to "+fmt.Sprint(o.MaxSearches)+` times. The result will be JSON in the format of {"urls": ["https://example.com", "https://url2.com/"], "answer": "the answer to the question"}. If a previous call to search produced no results, do not re-search with just reworded search terms, try a different approach.`,
func(ctx *gollm.Context, args struct {
Query string `description:"search the web with this, such as: 'capital of the united states site:wikipedia.org'"`
Question string `description:"when reading the results, what question(s) are you trying to answer?"`
}) (string, error) {
q2 := q
q2.Question = args.Question
func doesTextAnswerQuestion(ctx context.Context, q Question, text string) (string, error) {
var availableTools = []llms.Tool{
{
Type: "function",
Function: &llms.FunctionDefinition{
Name: "answer",
Description: "The answer from the given text that answers the question.",
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"answer": map[string]any{
"type": "string",
"description": "the answer to the question, the answer should come from the text",
},
},
},
},
},
{
Type: "function",
Function: &llms.FunctionDefinition{
Name: "no_answer",
Description: "Indicate that the text does not answer the question.",
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"ignored": map[string]any{
"type": "string",
"description": "ignored, just here to make sure the function is called. Fill with anything.",
},
},
},
},
},
}
answer := func(ctx context.Context, args string) (string, error) {
type answer struct {
Answer string `json:"answer"`
}
var a answer
err := json.Unmarshal([]byte(args), &a)
if err != nil {
return "", err
}
return a.Answer, nil
}
noAnswer := func(ctx context.Context, ignored string) (string, error) {
return "", nil
}
var req = []llms.MessageContent{
{
Role: llms.ChatMessageTypeSystem,
Parts: []llms.ContentPart{
llms.TextPart("Evaluate the given text to see if it answers the question from the user. The text is as follows:"),
},
},
{
Role: llms.ChatMessageTypeSystem,
Parts: []llms.ContentPart{
llms.TextPart(text),
},
},
{
Role: llms.ChatMessageTypeHuman,
Parts: []llms.ContentPart{
llms.TextPart(q.Question),
},
},
}
res, err := q.Model.GenerateContent(ctx, req, llms.WithTools(availableTools))
if err != nil {
return "", err
}
if len(res.Choices) == 0 {
return "", nil
}
if len(res.Choices[0].ToolCalls) == 0 {
return "", nil
}
for _, call := range res.Choices[0].ToolCalls {
switch call.FunctionCall.Name {
case "answer":
return answer(ctx, call.FunctionCall.Arguments)
case "no_answer":
return noAnswer(ctx, call.FunctionCall.Arguments)
default:
return "", fmt.Errorf("unknown function %s", call.FunctionCall.Name)
}
}
return "", nil
}
func functionSearch(ctx context.Context, q Question, searchTerm string) (string, error) {
slog.Info("searching", "search", searchTerm, "question", q)
res, err := q.Search.Search(ctx, searchTerm)
if err != nil {
return "", err
}
if len(res) == 0 {
return "", nil
}
// first pass try to see if any provide the result without needing archive
for _, r := range res {
trimmed := strings.TrimSpace(r.URL)
if trimmed == "" {
}
slog.Info("extracting article", "url", trimmed)
u, err := url.Parse(trimmed)
if err != nil {
continue
}
a, err := extractArticle(ctx, q.Cache, u)
if err != nil {
continue
}
slog.Info("extracted article", "url", a.URL, "title", a.Title, "body", a.Body)
if a.Title != "" && a.Body != "" {
answer, err := doesTextAnswerQuestion(ctx, q, a.Body)
if o.MaxSearches > 0 {
o.MaxSearches = o.MaxSearches - 1
}
res, err := functionSearch2(ctx, q2, args.Query)
if err != nil {
return "", err
slog.Error("error checking if text answers question", "question", q.Question, "error", err)
continue
}
return res.String()
})
if answer != "" {
return answer, nil
}
}
}
return "", nil
}
func functionThink(ctx context.Context, q Question) (string, error) {
fnAnswer := gollm.NewFunction(
"answer",
"You definitively answer a question, if you call this it means you know the answer and do not need to search for it or use any other function to find it",
func(ctx *gollm.Context, args struct {
Answer string `json:"answer" description:"the answer to the question"`
Sources []string `json:"sources" description:"the sources used to find the answer (e.g.: urls of sites from search)"`
"Answer the question.",
func(ctx context.Context, args struct {
Answer string `description:"the answer to the question"`
}) (string, error) {
answer.Text = args.Answer
answer.Sources = args.Sources
return args.Answer, nil
})
var fnWolfram *gollm.Function
if o.WolframAppID != "" {
fnWolfram = gollm.NewFunction(
"wolfram",
"Search Wolfram Alpha for an answer to a question.",
func(ctx *gollm.Context, args struct {
Question string `description:"the question to search for"`
}) (string, error) {
cl := wolfram.Client{
AppID: o.WolframAppID,
}
unit := wolfram.Imperial
return cl.GetShortAnswerQuery(args.Question, unit, 10)
})
}
fnCalculate := gollm.NewFunction(
"calculate",
"Calculate a mathematical expression using starlark.",
func(ctx *gollm.Context, args struct {
Expression string `description:"the mathematical expression to calculate, in starlark format"`
}) (string, error) {
fileOpts := syntax.FileOptions{}
v, err := starlark.EvalOptions(&fileOpts, &starlark.Thread{Name: "main"}, "input", args.Expression, math.Module.Members)
if err != nil {
return "", err
}
return v.String(), nil
})
fnGiveUp := gollm.NewFunction(
"give_up",
"Indicate that the system has given up on finding an answer.",
func(ctx *gollm.Context, args struct {
Reason string `description:"the reason the system is giving up (e.g.: 'no results found')"`
}) (string, error) {
answer.Text = "given up: " + args.Reason
return "given up", nil
})
var baseFuncs = []*gollm.Function{fnAnswer, fnCalculate, fnGiveUp}
var funcs = baseFuncs
if fnWolfram != nil {
funcs = append(funcs, fnWolfram)
}
if o.MaxSearches > 0 {
funcs = append(funcs, fnSearch)
}
var temp float32 = 0.8
var messages []gollm.Message
if o.SystemPrompt != "" {
messages = append(messages, gollm.Message{
Role: gollm.RoleSystem,
Text: o.SystemPrompt,
})
} else {
messages = append(messages, gollm.Message{
Role: gollm.RoleSystem,
Text: DefaultPrompt,
})
}
for _, prompt := range o.ExtraSystemPrompts {
messages = append(messages, gollm.Message{
Role: gollm.RoleSystem,
Text: prompt,
})
}
if q.Question != "" {
messages = append(messages, gollm.Message{
Role: gollm.RoleUser,
Text: q.Question,
})
}
req := gollm.Request{
Messages: messages,
Toolbox: gollm.NewToolBox(funcs...),
Messages: []gollm.Message{
{
Role: gollm.RoleSystem,
Text: "Answer the given question as accurately and concisely as possible using the answer function.",
},
{
Role: gollm.RoleUser,
Text: q.Question,
},
},
Toolbox: gollm.NewToolBox(fnAnswer),
Temperature: &temp,
}
// runAnswer will run the question and try to find the answer. It will return the next request to ask, if needed
// or any error encountered.
runAnswer := func(o Options, req gollm.Request) (*gollm.Request, error) {
res, err := q.Model.ChatComplete(ctx, req)
slog.Info("running answer", "question", q.Question, "req", req)
for _, c := range req.Conversation {
slog.Info("conversation", "conversation", c)
}
for _, m := range req.Messages {
slog.Info("message", "message", m)
}
res, err := q.Model.ChatComplete(ctx, req)
if err != nil {
return nil, err
}
if len(res.Choices) == 0 {
return nil, fmt.Errorf("no response choices provided")
}
var answers []gollm.ToolCallResponse
choice := res.Choices[0]
var callsOutput = make(chan gollm.ToolCallResponse, len(choice.Calls))
fnCall := func(call gollm.ToolCall) gollm.ToolCallResponse {
str, err := req.Toolbox.Execute(gollm.NewContext(ctx, req, &choice, &call), call)
if err != nil {
return gollm.ToolCallResponse{
ID: call.ID,
Error: err,
}
}
return gollm.ToolCallResponse{
ID: call.ID,
Result: str,
}
}
for _, call := range choice.Calls {
go func(call gollm.ToolCall) {
var arg any
var err error
if o.OnNewFunction != nil {
arg, err = o.OnNewFunction(ctx, call.FunctionCall.Name, q.Question, call.FunctionCall.Arguments)
if err != nil {
callsOutput <- gollm.ToolCallResponse{
ID: call.ID,
Error: err,
}
return
}
}
callRes := fnCall(call)
if o.OnFunctionFinished != nil {
err = o.OnFunctionFinished(ctx, call.FunctionCall.Name, q.Question, call.FunctionCall.Arguments, callRes.Result, callRes.Error, arg)
if err != nil {
callsOutput <- gollm.ToolCallResponse{
ID: call.ID,
Error: err,
}
return
}
}
callsOutput <- callRes
}(call)
}
for i := 0; i < len(choice.Calls); i++ {
result := <-callsOutput
answers = append(answers, result)
}
close(callsOutput)
slog.Info("generating new request", "answers", answers, "choice", choice)
newReq := gollm.NewContext(ctx, req, &choice, nil).ToNewRequest(answers...)
return &newReq, nil
if err != nil {
return "", err
}
maxTries := o.MaxTries
for i := 0; i < maxTries; i++ {
// rework this run's functions incase MaxSearches etc. have changed
var funcs2 = baseFuncs
if fnWolfram != nil {
funcs2 = append(funcs2, fnWolfram)
}
if o.MaxSearches > 0 {
funcs2 = append(funcs2, fnSearch)
}
req.Toolbox = gollm.NewToolBox(funcs2...)
newReq, err := runAnswer(o, req)
if err != nil {
return answer, err
}
if newReq == nil {
break
}
if answer.Text != "" {
break
}
req = *newReq
if len(res.Choices) == 0 {
return "", nil
}
return answer, nil
if len(res.Choices[0].Calls) == 0 {
return "", nil
}
return req.Toolbox.Execute(ctx, res.Choices[0].Calls[0])
}
func Answer(ctx context.Context, q Question) (Response, error) {
func appendResponse(req []llms.MessageContent, response *llms.ContentResponse) ([]llms.MessageContent, error) {
if response == nil {
return req, nil
}
if len(response.Choices) == 0 {
return req, nil
}
choice := response.Choices[0]
assistantResponse := llms.TextParts(llms.ChatMessageTypeAI, choice.Content)
for _, tc := range choice.ToolCalls {
assistantResponse.Parts = append(assistantResponse.Parts, tc)
}
return req, nil
}
func (o Options) Answer(ctx context.Context, q Question) (string, error) {
a := agents.NewConversationalAgent()
}
func Answer(ctx context.Context, q Question) (string, error) {
return DefaultOptions.Answer(ctx, q)
}

View File

@ -1,109 +0,0 @@
package answer
import (
"context"
"fmt"
"net/url"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/cache"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/extractor"
)
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
}
func doesTextAnswerQuestion(ctx *gollm.Context, q Question, text string) (string, error) {
fnAnswer := gollm.NewFunction(
"answer",
"The answer from the given text that answers the question.",
func(ctx *gollm.Context, args struct {
Answer string `description:"the answer to the question, the answer should come from the text"`
}) (string, error) {
return args.Answer, nil
})
fnNoAnswer := gollm.NewFunction(
"no_answer",
"Indicate that the text does not answer the question.",
func(ctx *gollm.Context, args struct {
Ignored string `description:"ignored, just here to make sure the function is called. Fill with anything."`
}) (string, error) {
return "", nil
})
req := gollm.Request{
Messages: []gollm.Message{
{
Role: gollm.RoleSystem,
Text: "Evaluate the given text to see if it answers the question from the user. The text is as follows:",
},
{
Role: gollm.RoleSystem,
Text: text,
},
{
Role: gollm.RoleUser,
Text: q.Question,
},
},
Toolbox: gollm.NewToolBox(fnAnswer, fnNoAnswer),
}
res, err := q.Model.ChatComplete(ctx, req)
if err != nil {
return "", err
}
if len(res.Choices) == 0 {
return "", nil
}
if len(res.Choices[0].Calls) == 0 {
return "", nil
}
return req.Toolbox.Execute(ctx, res.Choices[0].Calls[0])
}

View File

@ -1,653 +0,0 @@
package answer
import (
"encoding/json"
"fmt"
"log/slog"
"net/url"
"slices"
"strings"
"time"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agent"
"gitea.stevedudenhoeffer.com/steve/go-extractor"
"gitea.stevedudenhoeffer.com/steve/go-extractor/sites/duckduckgo"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/search"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
)
const (
kMaxLoops = 10
kMaxReads = 10
kMaxLoadMore = 3
)
type searchResults struct {
Url string `json:"url"`
Answer string `json:"answer"`
}
func (s searchResults) String() (string, error) {
b, err := json.Marshal(s)
if err != nil {
return "", err
}
return string(b), nil
}
func pickResult(ctx *gollm.Context, results []search.Result, q Question) (*search.Result, error) {
// if there's only one result, return it
if len(results) == 1 {
return &results[0], nil
}
// if there are no results, return nil
if len(results) == 0 {
return nil, nil
}
var pick *search.Result
var refused bool
// finally, if there are multiple results then ask the LLM to pick one to read next
fnPick := gollm.NewFunction(
"pick",
"The search result to read next.",
func(ctx *gollm.Context, args struct {
URL string `description:"the url to read next"`
}) (string, error) {
for _, r := range results {
if r.URL == args.URL {
pick = &r
break
}
}
return "", nil
})
fnNoPick := gollm.NewFunction(
"no_pick",
"Indicate that there are no results worth reading.",
func(ctx *gollm.Context, args struct {
Ignored string `description:"ignored, just here to make sure the function is called. Fill with anything."`
}) (string, error) {
refused = true
return "", nil
})
req := gollm.Request{
Messages: []gollm.Message{
{
Role: gollm.RoleSystem,
Text: `You are being given results from a web search. Please select the result you would like to read next to answer the question. Try to pick the most reputable and relevant result.
The results will be in the JSON format of: {"Url": "https://url.here", "Title": "Title Of Search", "Description": "description here"}`,
},
{
Role: gollm.RoleSystem,
Text: "The question you are trying to answer is: " + q.Question,
},
},
Toolbox: gollm.NewToolBox(fnPick, fnNoPick),
}
for _, r := range results {
b, _ := json.Marshal(r)
req.Messages = append(req.Messages, gollm.Message{
Role: gollm.RoleUser,
Text: string(b),
})
}
res, err := q.Model.ChatComplete(ctx, req)
if err != nil {
return nil, err
}
if len(res.Choices) == 0 {
return nil, nil
}
if len(res.Choices[0].Calls) == 0 {
return nil, nil
}
_, _ = req.Toolbox.Execute(ctx, res.Choices[0].Calls[0])
if refused {
return nil, nil
}
return pick, nil
}
func internalSearch(ctx *gollm.Context, q Question, searchTerm string) (searchResults, error) {
slog.Info("searching", "search", searchTerm, "question", q)
results, err := q.Search.Search(ctx, searchTerm)
if err != nil {
return searchResults{}, err
}
if len(results) == 0 {
return searchResults{Url: "not-found", Answer: "no search results found"}, nil
}
for len(results) > 0 {
var pick *search.Result
if len(results) == 1 {
pick = &results[0]
results = results[1:]
} else {
var err error
pick, err = pickResult(ctx, results, q)
slog.Info("picked result", "result", pick, "error", err)
if err != nil {
return searchResults{}, err
}
if pick == nil {
break
}
}
trimmed := strings.TrimSpace(pick.URL)
if trimmed == "" {
}
slog.Info("extracting article", "url", trimmed)
u, err := url.Parse(trimmed)
if err != nil {
continue
}
a, err := extractArticle(ctx, q.Cache, u)
if err != nil {
continue
}
slog.Info("extracted article", "url", a.URL, "title", a.Title, "body", a.Body)
if a.Title != "" && a.Body != "" {
answer, err := doesTextAnswerQuestion(ctx, q, a.Body)
if err != nil {
slog.Error("error checking if text answers question", "question", q.Question, "error", err)
continue
}
if answer != "" {
return searchResults{Url: u.String(), Answer: answer}, nil
}
}
}
return searchResults{Url: "not-found", Answer: "no searched results answered"}, nil
}
type searchResults2 struct {
Answer string `json:"answer"`
Urls []string `json:"urls"`
}
func (r searchResults2) String() (string, error) {
b, err := json.Marshal(r)
if err != nil {
return "", err
}
return string(b), nil
}
func functionSearch2(ctx *gollm.Context, q Question, searchTerm string) (searchResults2, error) {
var res searchResults2
browser, ok := ctx.Value("browser").(extractor.Browser)
if !ok {
return searchResults2{}, fmt.Errorf("browser not found in context")
}
cfg := duckduckgo.Config{
SafeSearch: duckduckgo.SafeSearchOff,
Region: "us-en",
}
page, err := cfg.OpenSearch(ctx, browser, searchTerm)
defer deferClose(page)
if err != nil {
return searchResults2{}, fmt.Errorf("failed to open search page: %w", err)
}
var totalNextPage int
var totalRead int
// oldResults are all the old results from the previous pages, so that when we load more we can filter out
// the old results
var oldResults []duckduckgo.Result
filterResults := func(results []duckduckgo.Result) []duckduckgo.Result {
var res []duckduckgo.Result
for _, r := range results {
if r.Title == "" || r.Description == "" {
continue
}
if slices.Contains(oldResults, r) {
continue
}
res = append(res, r)
}
return res
}
a := agent.NewAgent(gollm.Request{
Messages: []gollm.Message{
{
Role: gollm.RoleSystem,
Text: `You are trying to answer a question by reading pages from a search engine.
Use 'read' to read a page. You can only read 10 pages total, so try to only pick high quality pages. Results of a read will be in the format of {"url": "https://url.here", "answer": "answer here"}.
Additionally, you can use 'next_page' to load more results. You can only use next_page 3 times total.
You can read multiple pages at once, or read one page and continue to the next page if you need more information.
But if you are confident in your answer, you can use 'answer' to provide the answer.
Or you can use 'give_up' to indicate that you cannot find an answer and give up.`,
},
{
Role: gollm.RoleSystem,
Text: "The question you are trying to answer is: " + q.Question,
},
{
Role: gollm.RoleSystem,
Text: "The search terms you used were: " + searchTerm,
},
{
Role: gollm.RoleSystem,
Text: `The search results will be provided by the user in json format of: {"url": "https://url.here", "title": "Title Of Page", "description": "description here"}`,
},
},
})
a.Model = q.Model
var giveup bool
addMessages := func(results []duckduckgo.Result) {
type searchResults struct {
Url string `json:"url"`
Title string `json:"title"`
Desc string `json:"description"`
}
for _, r := range results {
b, _ := json.Marshal(&searchResults{Url: r.URL, Title: r.Title, Desc: r.Description})
a.AddMessage(gollm.Message{
Role: gollm.RoleUser,
Text: string(b),
})
}
}
fnRead := gollm.NewFunction(
"read",
`Read a page from the search results. The results will be in the JSON format of: {"url": "https://url.here", "answer": "answer here"}`,
func(ctx *gollm.Context, args struct {
URL string `description:"the url to read"`
}) (string, error) {
slog.Info("read", "url", args.URL)
if totalRead >= kMaxReads {
return "you have read the maximum number of pages", nil
}
totalRead += 1
u, err := url.Parse(args.URL)
if err != nil {
return "", fmt.Errorf("failed to parse url: %w", err)
}
a, err := extractArticle(ctx, q.Cache, u)
slog.Info("extracted article", "url", a.URL, "title", a.Title, "body", a.Body)
if err != nil {
return "", fmt.Errorf("failed to extract article: %w", err)
}
if a.Title == "" || a.Body == "" {
return "couldn't read the page", nil
}
answer, err := doesTextAnswerQuestion(ctx, q, a.Body)
if err != nil {
return "", fmt.Errorf("failed to check if text answers question: %w", err)
}
var res = searchResults{
Url: u.String(),
Answer: answer,
}
return res.String()
})
fnNextPage := gollm.NewFunction(
"next_page",
"Load more results from the search engine.",
func(ctx *gollm.Context, args struct {
Ignored string `description:"ignored, just here to make sure the function is called. Fill with anything."`
}) (string, error) {
if totalNextPage >= kMaxLoadMore {
return "you have loaded the maximum number of pages", nil
}
totalNextPage += 1
err := page.LoadMore()
if err != nil {
return "", fmt.Errorf("failed to load more results: %w", err)
}
time.Sleep(4 * time.Second)
results := page.GetResults()
// only add the new results here...
filteredResults := filterResults(results)
oldResults = append(oldResults, filteredResults...)
addMessages(filteredResults)
return "ok", nil
})
fnAnswer := gollm.NewFunction(
"answer",
"Provide the answer to the question.",
func(ctx *gollm.Context, args struct {
Answer string `description:"the answer to the question"`
Sources []string `description:"the urls of sources used to find the answer"`
}) (string, error) {
res.Answer = args.Answer
res.Urls = args.Sources
giveup = true
return "ok", nil
})
fnGiveUp := gollm.NewFunction(
"give_up",
"Indicate that you cannot find an answer and give up.",
func(ctx *gollm.Context, args struct {
Ignored string `description:"ignored, just here to make sure the function is called. Fill with anything."`
}) (string, error) {
giveup = true
return "ok", nil
})
// do initial load of results
results := page.GetResults()
filteredResults := filterResults(results)
oldResults = append(oldResults, filteredResults...)
addMessages(filteredResults)
var i = 0
for ; i < kMaxLoops && !giveup; i++ {
// figure out my allowed tools, based on limits
var tools = []*gollm.Function{
fnAnswer,
fnGiveUp,
}
if totalRead < kMaxReads {
tools = append(tools, fnRead)
}
if totalNextPage < kMaxLoadMore {
tools = append(tools, fnNextPage)
}
a.ToolBox = gollm.NewToolBox(tools...)
err = a.Execute(ctx, gollm.Message{Role: gollm.RoleSystem, Text: "Now evaluate if the text answers the question, and use a function to either provide the answer or read more pages."})
if err != nil {
return searchResults2{}, fmt.Errorf("failed to run agent: %w", err)
}
}
if giveup {
return res, fmt.Errorf("gave up: no relevant results found")
}
if res.Answer == "" {
return res, fmt.Errorf("no answer found")
}
return res, nil
}
func functionSearch(ctx *gollm.Context, q Question, searchTerm string) (searchResults2, error) {
var res searchResults2
browser, ok := ctx.Value("browser").(extractor.Browser)
if !ok {
return searchResults2{}, fmt.Errorf("browser not found in context")
}
cfg := duckduckgo.Config{
SafeSearch: duckduckgo.SafeSearchOff,
Region: "us-en",
}
page, err := cfg.OpenSearch(ctx, browser, searchTerm)
defer deferClose(page)
if err != nil {
return searchResults2{}, fmt.Errorf("failed to open search page: %w", err)
}
var totalNextPage int
var totalRead int
// oldResults are all the old results from the previous pages, so that when we load more we can filter out
// the old results
var oldResults []duckduckgo.Result
filterResults := func(results []duckduckgo.Result) []duckduckgo.Result {
var res []duckduckgo.Result
for _, r := range results {
if r.Title == "" || r.Description == "" {
continue
}
if slices.Contains(oldResults, r) {
continue
}
res = append(res, r)
}
return res
}
var giveup bool
req := gollm.Request{
Messages: []gollm.Message{
{
Role: gollm.RoleSystem,
Text: `You are trying to answer a question by reading pages from a search engine.
Use 'read' to read a page. You can only read 10 pages total, so try to only pick high quality pages.
Additionally, you can use 'next_page' to load more results. You can only use next_page 3 times total.
You can read multiple pages at once, or read one page and continue to the next page if you need more information.
But if you are confident in your answer, you can use 'answer' to provide the answer.
Or you can use 'give_up' to indicate that you cannot find an answer and give up.`,
},
{
Role: gollm.RoleSystem,
Text: "The question you are trying to answer is: " + q.Question,
},
{
Role: gollm.RoleSystem,
Text: "The search terms you used were: " + searchTerm,
},
{
Role: gollm.RoleSystem,
Text: `The search results will be provided by the user in json format of: {"url": "https://url.here", "title": "Title Of Page", "description": "description here"}`,
},
},
}
addMessages := func(results []duckduckgo.Result) {
type searchResults struct {
Url string `json:"url"`
Title string `json:"title"`
Desc string `json:"description"`
}
for _, r := range results {
b, _ := json.Marshal(&searchResults{Url: r.URL, Title: r.Title, Desc: r.Description})
req.Messages = append(req.Messages, gollm.Message{
Role: gollm.RoleUser,
Text: string(b),
})
}
}
fnRead := gollm.NewFunction(
"read",
`Read a page from the search results. The results will be in the JSON format of: {"url": "https://url.here", "answer": "answer here"}`,
func(ctx *gollm.Context, args struct {
URL string `description:"the url to read"`
}) (string, error) {
if totalRead >= kMaxReads {
return "you have read the maximum number of pages", nil
}
totalRead += 1
u, err := url.Parse(args.URL)
if err != nil {
return "", fmt.Errorf("failed to parse url: %w", err)
}
a, err := extractArticle(ctx, q.Cache, u)
if err != nil {
return "", fmt.Errorf("failed to extract article: %w", err)
}
if a.Title == "" || a.Body == "" {
return "couldn't read the page", nil
}
answer, err := doesTextAnswerQuestion(ctx, q, a.Body)
if err != nil {
return "", fmt.Errorf("failed to check if text answers question: %w", err)
}
var res = searchResults{
Url: u.String(),
Answer: answer,
}
return res.String()
})
fnNextPage := gollm.NewFunction(
"next_page",
"Load more results from the search engine.",
func(ctx *gollm.Context, args struct {
Ignored string `description:"ignored, just here to make sure the function is called. Fill with anything."`
}) (string, error) {
if totalNextPage >= kMaxLoadMore {
return "you have loaded the maximum number of pages", nil
}
totalNextPage += 1
err := page.LoadMore()
if err != nil {
return "", fmt.Errorf("failed to load more results: %w", err)
}
time.Sleep(4 * time.Second)
results := page.GetResults()
// only add the new results here...
filteredResults := filterResults(results)
oldResults = append(oldResults, filteredResults...)
addMessages(filteredResults)
return "ok", nil
})
fnAnswer := gollm.NewFunction(
"answer",
"Provide the answer to the question.",
func(ctx *gollm.Context, args struct {
Answer string `description:"the answer to the question"`
Sources []string `description:"the urls of sources used to find the answer"`
}) (string, error) {
res.Answer = args.Answer
res.Urls = args.Sources
giveup = true
return "ok", nil
})
fnGiveUp := gollm.NewFunction(
"give_up",
"Indicate that you cannot find an answer and give up.",
func(ctx *gollm.Context, args struct {
Ignored string `description:"ignored, just here to make sure the function is called. Fill with anything."`
}) (string, error) {
giveup = true
return "ok", nil
})
// do initial load of results
results := page.GetResults()
filteredResults := filterResults(results)
oldResults = append(oldResults, filteredResults...)
addMessages(filteredResults)
var i = 0
for ; i < kMaxLoops && !giveup; i++ {
// figure out my allowed tools, based on limits
var tools = []*gollm.Function{
fnAnswer,
fnGiveUp,
}
if totalRead < kMaxReads {
tools = append(tools, fnRead)
}
if totalNextPage < kMaxLoadMore {
tools = append(tools, fnNextPage)
}
req.Toolbox = gollm.NewToolBox(tools...)
res, err := q.Model.ChatComplete(ctx, req)
if err != nil {
return searchResults2{}, fmt.Errorf("failed to chat complete: %w", err)
}
if len(res.Choices) == 0 {
break
}
if len(res.Choices[0].Calls) == 0 {
break
}
_, err = req.Toolbox.Execute(ctx, res.Choices[0].Calls[0])
if err != nil {
return searchResults2{}, fmt.Errorf("failed to execute: %w", err)
}
}
if giveup {
return res, fmt.Errorf("gave up: no relevant results found")
}
if res.Answer == "" {
return res, fmt.Errorf("no answer found")
}
return res, nil
}

17
pkg/toolbox/context.go Normal file
View File

@ -0,0 +1,17 @@
package toolbox
import (
"context"
"time"
"github.com/tmc/langchaingo/llms"
)
type Context interface {
context.Context
WithCancel() (Context, func())
WithTimeout(time.Duration) (Context, func())
WithMessages([]llms.MessageContent) Context
GetMessages() []llms.MessageContent
}

299
pkg/toolbox/function.go Normal file
View File

@ -0,0 +1,299 @@
package toolbox
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"reflect"
"strings"
"sync"
)
type FuncResponse struct {
Result string
Source string
}
type funcCache struct {
sync.RWMutex
m map[reflect.Type]function
}
func (c *funcCache) get(value reflect.Value) (function, bool) {
c.RLock()
defer c.RUnlock()
fn, ok := c.m[value.Type()]
if ok {
slog.Info("cache hit for function", "function", value.Type().String())
}
return fn, ok
}
func (c *funcCache) set(value reflect.Value, fn function) {
c.Lock()
defer c.Unlock()
c.m[value.Type()] = fn
}
var cache = funcCache{m: map[reflect.Type]function{}}
type arg struct {
Name string
Type reflect.Type
Index int
Values []string
Optional bool
Array bool
Description string
}
func (a arg) Schema() map[string]any {
var res = map[string]any{}
if a.Array {
res["type"] = "array"
res["items"] = map[string]any{"type": a.Type.Kind().String()}
} else {
res["type"] = a.Type.Name()
}
if !a.Optional {
res["required"] = true
}
if len(a.Values) > 0 {
res["enum"] = a.Values
}
if a.Description != "" {
res["description"] = a.Description
}
return res
}
type function struct {
fn reflect.Value
argType reflect.Type
args map[string]arg
}
var ErrInvalidFunction = errors.New("invalid function")
// analyzeFuncFromReflect extracts metadata from a reflect.Value representing a function and returns a structured result.
// It maps the function's parameter names to their corresponding reflect.Type and encapsulates them in a function struct.
// Returns a function struct with extracted information and an error if the operation fails.
// The first parameter to the function must be a *Context.
// The second parameter to the function must be a struct, all the fields of which will be passed as arguments to the
// function to be analyzed.
// Struct tags supported are:
// - `name:"<name>"` to specify the name of the parameter (default is the field name)
// - `description:"<description>"` to specify a description of the parameter (default is "")
// - `values:"<value1>,<value2>,..."` to specify a list of possible values for the parameter (default is "") only for
// string, int, and float types
//
// Allowed types on the struct are:
// - string, *string, []string
// - int, *int, []int
// - float64, *float64, []float64
// - bool, *bool, []bool
//
// Pointer types imply that the parameter is optional.
// The function must have at most 2 parameters.
// The function must return a string and an error.
// The function must be of the form `func(*agent.Context, T) (FuncResponse, error)`.
func analyzeFuncFromReflect(fn reflect.Value) (function, error) {
if f, ok := cache.get(fn); ok {
return f, nil
}
var res function
t := fn.Type()
args := map[string]arg{}
for i := 0; i < t.NumIn(); i++ {
if i == 0 {
if t.In(i).String() != "*agent.Context" {
return res, fmt.Errorf("%w: first parameter must be *agent.Context", ErrInvalidFunction)
}
continue
} else if i == 1 {
if t.In(i).Kind() != reflect.Struct {
return res, fmt.Errorf("%w: second parameter must be a struct", ErrInvalidFunction)
}
res.argType = t.In(i)
for j := 0; j < res.argType.NumField(); j++ {
field := res.argType.Field(j)
a := arg{
Name: field.Name,
Type: field.Type,
Index: j,
Description: "",
}
ft := field.Type
// if it's a pointer, it's optional
if ft.Kind() == reflect.Ptr {
a.Optional = true
ft = ft.Elem()
} else if ft.Kind() == reflect.Slice {
a.Array = true
ft = ft.Elem()
}
if ft.Kind() != reflect.String && ft.Kind() != reflect.Int && ft.Kind() != reflect.Float64 && ft.Kind() != reflect.Bool {
return res, fmt.Errorf("%w: unsupported type %s", ErrInvalidFunction, ft.Kind().String())
}
a.Type = ft
if name, ok := field.Tag.Lookup("name"); ok {
a.Name = name
a.Name = strings.TrimSpace(a.Name)
if a.Name == "" {
return res, fmt.Errorf("%w: name tag cannot be empty", ErrInvalidFunction)
}
}
if description, ok := field.Tag.Lookup("description"); ok {
a.Description = description
}
if values, ok := field.Tag.Lookup("values"); ok {
a.Values = strings.Split(values, ",")
for i, v := range a.Values {
a.Values[i] = strings.TrimSpace(v)
}
if ft.Kind() != reflect.String && ft.Kind() != reflect.Int && ft.Kind() != reflect.Float64 {
return res, fmt.Errorf("%w: values tag only supported for string, int, and float types", ErrInvalidFunction)
}
}
args[field.Name] = a
}
} else {
return res, fmt.Errorf("%w: function must have at most 2 parameters", ErrInvalidFunction)
}
}
// finally ensure that the function returns a FuncResponse and an error
if t.NumOut() != 2 || t.Out(0).String() != "agent.FuncResponse" || t.Out(1).String() != "error" {
return res, fmt.Errorf("%w: function must return a FuncResponse and an error", ErrInvalidFunction)
}
cache.set(fn, res)
return res, nil
}
func analyzeFunction[T any, AgentContext any](fn func(AgentContext, T) (FuncResponse, error)) (function, error) {
return analyzeFuncFromReflect(reflect.ValueOf(fn))
}
// Execute will execute the given function with the given context and arguments.
// Returns the result of the execution and an error if the operation fails.
// The arguments must be a JSON-encoded string that represents the struct to be passed to the function.
func (f function) Execute(ctx context.Context, args string) (FuncResponse, error) {
var m = map[string]any{}
err := json.Unmarshal([]byte(args), &m)
if err != nil {
return FuncResponse{}, fmt.Errorf("failed to unmarshal arguments: %w", err)
}
var obj = reflect.New(f.argType).Elem()
// TODO: ensure that "required" fields are present in the arguments
for name, a := range f.args {
if v, ok := m[name]; ok {
if a.Array {
if v == nil {
continue
}
switch a.Type.Kind() {
case reflect.String:
s := v.([]string)
slice := reflect.MakeSlice(reflect.SliceOf(reflect.TypeOf("")), len(s), len(s))
for i, str := range s {
slice.Index(i).SetString(str)
}
obj.Field(a.Index).Set(slice)
case reflect.Int:
i := v.([]int)
slice := reflect.MakeSlice(reflect.SliceOf(reflect.TypeOf(0)), len(i), len(i))
for i, in := range i {
slice.Index(i).SetInt(int64(in))
}
obj.Field(a.Index).Set(slice)
case reflect.Float64:
f := v.([]float64)
slice := reflect.MakeSlice(reflect.SliceOf(reflect.TypeOf(0.0)), len(f), len(f))
for i, fl := range f {
slice.Index(i).SetFloat(fl)
}
obj.Field(a.Index).Set(slice)
case reflect.Bool:
b := v.([]bool)
slice := reflect.MakeSlice(reflect.SliceOf(reflect.TypeOf(false)), len(b), len(b))
for i, b := range b {
slice.Index(i).SetBool(b)
}
obj.Field(a.Index).Set(slice)
default:
return FuncResponse{}, fmt.Errorf("unsupported type %s for field %s", a.Type.Kind().String(), name)
}
} else if a.Optional {
if v == nil {
continue
}
switch a.Type.Kind() {
case reflect.String:
str := v.(string)
obj.Field(a.Index).Set(reflect.ValueOf(&str))
case reflect.Int:
i := v.(int)
obj.Field(a.Index).Set(reflect.ValueOf(&i))
case reflect.Float64:
f := v.(float64)
obj.Field(a.Index).Set(reflect.ValueOf(&f))
case reflect.Bool:
b := v.(bool)
obj.Field(a.Index).Set(reflect.ValueOf(&b))
default:
return FuncResponse{}, fmt.Errorf("unsupported type %s for field %s", a.Type.Kind().String(), name)
}
} else {
switch a.Type.Kind() {
case reflect.String:
obj.Field(a.Index).SetString(v.(string))
case reflect.Int:
obj.Field(a.Index).SetInt(int64(v.(int)))
case reflect.Float64:
obj.Field(a.Index).SetFloat(v.(float64))
case reflect.Bool:
obj.Field(a.Index).SetBool(v.(bool))
default:
return FuncResponse{}, fmt.Errorf("unsupported type %s for field %s", a.Type.Kind().String(), name)
}
}
}
}
res := f.fn.Call([]reflect.Value{reflect.ValueOf(ctx), obj})
if res[1].IsNil() {
return res[0].Interface().(FuncResponse), nil
}
return FuncResponse{}, res[1].Interface().(error)
}

75
pkg/toolbox/tool.go Normal file
View File

@ -0,0 +1,75 @@
package toolbox
import (
"context"
"reflect"
"github.com/tmc/langchaingo/llms"
)
type Tool struct {
Name string
Description string
Function function
}
func (t *Tool) Tool() llms.Tool {
return llms.Tool{
Type: "function",
Function: t.Definition(),
}
}
func (t *Tool) Definition() *llms.FunctionDefinition {
var properties = map[string]any{}
for name, arg := range t.Function.args {
properties[name] = arg.Schema()
}
var res = llms.FunctionDefinition{
Name: t.Name,
Description: t.Description,
Parameters: map[string]any{"type": "object", "properties": properties},
}
return &res
}
// Execute executes the tool with the given context and arguments.
// Returns the result of the execution and an error if the operation fails.
// The arguments must be a JSON-encoded string that represents the struct to be passed to the function.
func (t *Tool) Execute(ctx context.Context, args string) (FuncResponse, error) {
return t.Function.Execute(ctx, args)
}
func FromFunction[T any, AgentContext any](fn func(AgentContext, T) (FuncResponse, error)) *Tool {
f, err := analyzeFunction(fn)
if err != nil {
panic(err)
}
return &Tool{
Name: reflect.TypeOf(fn).Name(),
Description: "This is a tool",
Function: f,
}
}
func (t *Tool) WithName(name string) *Tool {
t.Name = name
return t
}
func (t *Tool) WithDescription(description string) *Tool {
t.Description = description
return t
}
func (t *Tool) WithFunction(fn any) *Tool {
f, err := analyzeFuncFromReflect(reflect.ValueOf(fn))
if err != nil {
panic(err)
}
t.Function = f
return t
}

208
pkg/toolbox/toolbox.go Normal file
View File

@ -0,0 +1,208 @@
package toolbox
import (
"context"
"errors"
"fmt"
"golang.org/x/sync/errgroup"
"github.com/tmc/langchaingo/llms"
)
type ToolBox map[string]*Tool
var (
ErrToolNotFound = errors.New("tool not found")
)
type ToolResults []ToolResult
func (r ToolResults) ToMessageContent() llms.MessageContent {
var res = llms.MessageContent{
Role: llms.ChatMessageTypeTool,
}
for _, v := range r {
res.Parts = append(res.Parts, v.ToToolCallResponse())
}
return res
}
type ToolResult struct {
ID string
Name string
Result string
Source string
Error error
}
func (r ToolResult) ToToolCallResponse() llms.ToolCallResponse {
if r.Error != nil {
return llms.ToolCallResponse{
ToolCallID: r.ID,
Name: r.Name,
Content: "error executing: " + r.Error.Error(),
}
}
return llms.ToolCallResponse{
ToolCallID: r.ID,
Name: r.Name,
Content: r.Result,
}
}
func (tb ToolBox) Execute(ctx context.Context, call llms.ToolCall) (ToolResult, error) {
if call.Type != "function" {
return ToolResult{}, fmt.Errorf("unsupported tool type: %s", call.Type)
}
if call.FunctionCall == nil {
return ToolResult{}, errors.New("function call is nil")
}
tool, ok := tb[call.FunctionCall.Name]
if !ok {
return ToolResult{}, fmt.Errorf("%w: %s", ErrToolNotFound, call.FunctionCall.Name)
}
res, err := tool.Execute(ctx, call.FunctionCall.Arguments)
if err != nil {
return ToolResult{
ID: call.ID,
Name: tool.Name,
Error: err,
Source: res.Source,
}, nil
}
return ToolResult{
ID: call.ID,
Name: tool.Name,
Result: res.Result,
Source: res.Source,
Error: err,
}, nil
}
func (tb ToolBox) ExecuteAll(ctx Context, calls []llms.ToolCall) (ToolResults, error) {
var results []ToolResult
for _, call := range calls {
res, err := tb.Execute(ctx, call)
if err != nil {
return results, err
}
results = append(results, res)
}
return results, nil
}
func (tb ToolBox) ExecuteConcurrent(ctx Context, calls []llms.ToolCall) (ToolResults, error) {
var results []ToolResult
var ch = make(chan ToolResult, len(calls))
var eg = errgroup.Group{}
for _, call := range calls {
eg.Go(func() error {
c, cancel := ctx.WithCancel()
defer cancel()
res, err := tb.Execute(c, call)
if err != nil {
ch <- res
return nil
}
return err
})
}
err := eg.Wait()
if err != nil {
return results, err
}
for range calls {
results = append(results, <-ch)
}
return results, nil
}
type Answers struct {
Response llms.MessageContent
Answers []Answer
}
type Answer struct {
Answer string
Source string
ToolCallResponse llms.ToolCallResponse `json:"-"`
}
func (tb ToolBox) Run(ctx Context, model llms.Model, question string) (Answers, error) {
ctx = ctx.WithMessages([]llms.MessageContent{{
Role: llms.ChatMessageTypeGeneric,
Parts: []llms.ContentPart{llms.TextPart(question)},
}})
res, err := model.GenerateContent(ctx, ctx.GetMessages())
if err != nil {
return Answers{}, err
}
if res == nil {
return Answers{}, errors.New("no response from model")
}
if len(res.Choices) == 0 {
return Answers{}, errors.New("no response from model")
}
choice := res.Choices[0]
response := llms.MessageContent{
Role: llms.ChatMessageTypeAI,
Parts: []llms.ContentPart{llms.TextPart(choice.Content)},
}
for _, c := range choice.ToolCalls {
response.Parts = append(response.Parts, c)
}
results, err := tb.ExecuteConcurrent(ctx, choice.ToolCalls)
if err != nil {
return Answers{}, err
}
var answers []Answer
for _, r := range results {
if r.Error != nil {
answers = append(answers, Answer{
Answer: "error executing: " + r.Error.Error(),
Source: r.Source,
ToolCallResponse: r.ToToolCallResponse(),
})
} else {
answers = append(answers, Answer{
Answer: r.Result,
Source: r.Source,
ToolCallResponse: r.ToToolCallResponse(),
})
}
}
return Answers{
Response: response,
Answers: answers,
}, nil
}
func (tb ToolBox) Register(tool *Tool) {
tb[tool.Name] = tool
}