Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
82580a5a7a | |||
ab1434d671 | |||
1447b73dfb | |||
1be5cc047c | |||
7a77d74624 | |||
e874791f66 | |||
f5a3f97cfe | |||
38b1e4dcbd | |||
a0bc467bc4 | |||
22589dd49c | |||
4f1a7e5b80 | |||
1aaed4ea28 | |||
5d2c350acf | |||
5407c1a7cc | |||
693ac4e6a7 | |||
20bcaefaa2 | |||
b3df0fc902 | |||
bc56213d5a | |||
a5a669f9a6 | |||
5b338d4129 | |||
bdaa3b7d96 | |||
bf16b4b0cd | |||
568a0e99e4 | |||
4c106d32cb | |||
1d47cf5758 |
125
cmd/agent/cmd.go
Normal file
125
cmd/agent/cmd.go
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@ -162,9 +163,7 @@ func main() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, a := range answers {
|
fmt.Println(fmt.Sprintf("Question: %s\nAnswer: %q", question.Question, answers))
|
||||||
slog.Info("answer", "index", i, "answer", a)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
151
cmd/console/cmd.go
Normal file
151
cmd/console/cmd.go
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents"
|
||||||
|
|
||||||
|
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents/console"
|
||||||
|
|
||||||
|
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getKey(key string, env string) string {
|
||||||
|
if key != "" {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.Getenv(env)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Usage: go run cmd/answer.go question...
|
||||||
|
// - flags:
|
||||||
|
// --model=[model string such as openai/gpt-4o, anthropic/claude..., google/gemini-1.5. Default: openai/gpt-4o]
|
||||||
|
// --search-provider=[search provider string such as google, duckduckgo. Default: google]
|
||||||
|
// --cache-provider=[cache provider string such as memory, redis, file. Default: memory]
|
||||||
|
|
||||||
|
var app = cli.App{
|
||||||
|
Name: "console",
|
||||||
|
Usage: "has an llm control a console for you",
|
||||||
|
Version: "0.1",
|
||||||
|
Description: "",
|
||||||
|
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "env-file",
|
||||||
|
Value: ".env",
|
||||||
|
Usage: "file to read environment variables from",
|
||||||
|
},
|
||||||
|
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "model",
|
||||||
|
Value: "openai/gpt-4o-mini",
|
||||||
|
Usage: "model to use for answering the question, syntax: provider/model such as openai/gpt-4o",
|
||||||
|
},
|
||||||
|
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "llm-key",
|
||||||
|
Value: "",
|
||||||
|
Usage: "key for the llm model (if empty, will use env var of PROVIDER_API_KEY, such as OPENAI_API_KEY)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Action: func(ctx *cli.Context) error {
|
||||||
|
// if there is no question to answer, print usage
|
||||||
|
if ctx.NArg() == 0 {
|
||||||
|
return cli.ShowAppHelp(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.String("env-file") != "" {
|
||||||
|
_ = godotenv.Load(ctx.String("env-file"))
|
||||||
|
}
|
||||||
|
|
||||||
|
var llm gollm.LLM
|
||||||
|
|
||||||
|
model := ctx.String("model")
|
||||||
|
|
||||||
|
a := strings.Split(model, "/")
|
||||||
|
|
||||||
|
if len(a) != 2 {
|
||||||
|
panic("invalid model, expected: provider/model (such as openai/gpt-4o)")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch a[0] {
|
||||||
|
case "openai":
|
||||||
|
llm = gollm.OpenAI(getKey(ctx.String("llm-key"), "OPENAI_API_KEY"))
|
||||||
|
|
||||||
|
case "anthropic":
|
||||||
|
llm = gollm.Anthropic(getKey(ctx.String("llm-key"), "ANTHROPI_API_KEY"))
|
||||||
|
|
||||||
|
case "google":
|
||||||
|
llm = gollm.Google(getKey(ctx.String("llm-key"), "GOOGLE_API_KEY"))
|
||||||
|
|
||||||
|
default:
|
||||||
|
panic("unknown model provider")
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := llm.ModelVersion(a[1])
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
question := strings.Join(ctx.Args(), " ")
|
||||||
|
|
||||||
|
c := console.Agent{
|
||||||
|
Model: m,
|
||||||
|
OnDone: func(ctx context.Context, knowledge agents.Knowledge) error { return nil },
|
||||||
|
OnCommandStart: func(ctx context.Context, command string) error {
|
||||||
|
slog.Info("command", "command", command)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
OnCommandDone: func(ctx context.Context, command string, output string, err error) error {
|
||||||
|
slog.Info("command done", "command", command, "output", output, "err", err)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ContextualInformation: []string{
|
||||||
|
fmt.Sprintf("The current time is %s", time.Now().Format(time.RFC3339)),
|
||||||
|
},
|
||||||
|
MaxCommands: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := c.Answer(context.Background(), []string{question})
|
||||||
|
|
||||||
|
if res.Directory != "" {
|
||||||
|
defer func() {
|
||||||
|
_ = os.RemoveAll(res.Directory)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Results: ", fmt.Sprintf("%+v", res))
|
||||||
|
|
||||||
|
answer, err := agents.AnswerQuestionWithKnowledge(context.Background(), res.Knowledge, m, []string{
|
||||||
|
fmt.Sprintf("The current time is %s", time.Now().Format(time.RFC3339)),
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Answer: ", answer)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := app.Run(os.Args)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Error: ", err)
|
||||||
|
}
|
||||||
|
}
|
118
cmd/steps/cmd.go
Normal file
118
cmd/steps/cmd.go
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getKey(key string, env string) string {
|
||||||
|
if key != "" {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.Getenv(env)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Usage: go run cmd/answer.go question...
|
||||||
|
// - flags:
|
||||||
|
// --model=[model string such as openai/gpt-4o, anthropic/claude..., google/gemini-1.5. Default: openai/gpt-4o]
|
||||||
|
// --search-provider=[search provider string such as google, duckduckgo. Default: google]
|
||||||
|
// --cache-provider=[cache provider string such as memory, redis, file. Default: memory]
|
||||||
|
|
||||||
|
var app = cli.App{
|
||||||
|
Name: "console",
|
||||||
|
Usage: "has an llm control a console for you",
|
||||||
|
Version: "0.1",
|
||||||
|
Description: "",
|
||||||
|
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "env-file",
|
||||||
|
Value: ".env",
|
||||||
|
Usage: "file to read environment variables from",
|
||||||
|
},
|
||||||
|
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "model",
|
||||||
|
Value: "openai/gpt-4o-mini",
|
||||||
|
Usage: "model to use for answering the question, syntax: provider/model such as openai/gpt-4o",
|
||||||
|
},
|
||||||
|
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "llm-key",
|
||||||
|
Value: "",
|
||||||
|
Usage: "key for the llm model (if empty, will use env var of PROVIDER_API_KEY, such as OPENAI_API_KEY)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Action: func(ctx *cli.Context) error {
|
||||||
|
// if there is no question to answer, print usage
|
||||||
|
if ctx.NArg() == 0 {
|
||||||
|
return cli.ShowAppHelp(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.String("env-file") != "" {
|
||||||
|
_ = godotenv.Load(ctx.String("env-file"))
|
||||||
|
}
|
||||||
|
|
||||||
|
var llm gollm.LLM
|
||||||
|
|
||||||
|
model := ctx.String("model")
|
||||||
|
|
||||||
|
a := strings.Split(model, "/")
|
||||||
|
|
||||||
|
if len(a) != 2 {
|
||||||
|
panic("invalid model, expected: provider/model (such as openai/gpt-4o)")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch a[0] {
|
||||||
|
case "openai":
|
||||||
|
llm = gollm.OpenAI(getKey(ctx.String("llm-key"), "OPENAI_API_KEY"))
|
||||||
|
|
||||||
|
case "anthropic":
|
||||||
|
llm = gollm.Anthropic(getKey(ctx.String("llm-key"), "ANTHROPI_API_KEY"))
|
||||||
|
|
||||||
|
case "google":
|
||||||
|
llm = gollm.Google(getKey(ctx.String("llm-key"), "GOOGLE_API_KEY"))
|
||||||
|
|
||||||
|
default:
|
||||||
|
panic("unknown model provider")
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := llm.ModelVersion(a[1])
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
question := strings.Join(ctx.Args(), " ")
|
||||||
|
|
||||||
|
ag := agents.NewAgent(m, nil)
|
||||||
|
|
||||||
|
steps, err := ag.SplitQuestion(context.Background(), question)
|
||||||
|
|
||||||
|
fmt.Println("Input question: ", question)
|
||||||
|
fmt.Println("Steps: ")
|
||||||
|
for i, s := range steps {
|
||||||
|
fmt.Println(" - Step ", i+1, ": ", s)
|
||||||
|
}
|
||||||
|
fmt.Println("Error: ", err)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := app.Run(os.Args)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Error: ", err)
|
||||||
|
}
|
||||||
|
}
|
93
go.mod
93
go.mod
@ -1,40 +1,56 @@
|
|||||||
module gitea.stevedudenhoeffer.com/steve/answer
|
module gitea.stevedudenhoeffer.com/steve/answer
|
||||||
|
|
||||||
go 1.23.2
|
go 1.24.1
|
||||||
|
|
||||||
replace github.com/rocketlaunchr/google-search => github.com/chrisjoyce911/google-search v0.0.0-20230910003754-e501aedf805a
|
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 (
|
require (
|
||||||
gitea.stevedudenhoeffer.com/steve/go-extractor v0.0.0-20250123020607-964a98a5a884
|
gitea.stevedudenhoeffer.com/steve/go-extractor v0.0.0-20250318064250-39453288ce2a
|
||||||
gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250123045620-0d909edd44d9
|
gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250412190744-39ffb8223775
|
||||||
|
github.com/Edw590/go-wolfram v0.0.0-20241010091529-fb9031908c5d
|
||||||
github.com/advancedlogic/GoOse v0.0.0-20231203033844-ae6b36caf275
|
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/joho/godotenv v1.5.1
|
||||||
github.com/playwright-community/playwright-go v0.5001.0
|
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/rocketlaunchr/google-search v1.1.6
|
github.com/rocketlaunchr/google-search v1.1.6
|
||||||
github.com/urfave/cli v1.22.16
|
github.com/urfave/cli v1.22.16
|
||||||
|
go.starlark.net v0.0.0-20250318223901-d9371fef63fe
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go v0.118.3 // indirect
|
cloud.google.com/go v0.120.0 // indirect
|
||||||
cloud.google.com/go/ai v0.10.0 // indirect
|
cloud.google.com/go/ai v0.10.1 // indirect
|
||||||
cloud.google.com/go/auth v0.15.0 // indirect
|
cloud.google.com/go/auth v0.15.0 // indirect
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||||
cloud.google.com/go/compute/metadata v0.6.0 // indirect
|
cloud.google.com/go/compute/metadata v0.6.0 // indirect
|
||||||
cloud.google.com/go/longrunning v0.6.4 // indirect
|
cloud.google.com/go/longrunning v0.6.6 // indirect
|
||||||
github.com/PuerkitoBio/goquery v1.10.2 // 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
|
||||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||||
github.com/antchfx/htmlquery v1.3.4 // indirect
|
github.com/antchfx/htmlquery v1.3.4 // indirect
|
||||||
github.com/antchfx/xmlquery v1.4.4 // indirect
|
github.com/antchfx/xmlquery v1.4.4 // indirect
|
||||||
github.com/antchfx/xpath v1.3.3 // indirect
|
github.com/antchfx/xpath v1.3.3 // indirect
|
||||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de // 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/cpuguy83/go-md2man/v2 v2.0.6 // indirect
|
||||||
github.com/deckarep/golang-set/v2 v2.7.0 // indirect
|
github.com/deckarep/golang-set/v2 v2.8.0 // indirect
|
||||||
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
|
github.com/docker/go-connections v0.5.0 // indirect
|
||||||
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/fatih/set v0.2.1 // indirect
|
github.com/fatih/set v0.2.1 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/gigawattio/window v0.0.0-20180317192513-0f5467e35573 // indirect
|
github.com/gigawattio/window v0.0.0-20180317192513-0f5467e35573 // indirect
|
||||||
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
|
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
|
||||||
github.com/go-logr/logr v1.4.2 // indirect
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-resty/resty/v2 v2.16.5 // indirect
|
github.com/go-resty/resty/v2 v2.16.5 // indirect
|
||||||
@ -43,44 +59,55 @@ require (
|
|||||||
github.com/go-stack/stack v1.8.1 // indirect
|
github.com/go-stack/stack v1.8.1 // indirect
|
||||||
github.com/gobwas/glob v0.2.3 // indirect
|
github.com/gobwas/glob v0.2.3 // indirect
|
||||||
github.com/gocolly/colly/v2 v2.1.0 // 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/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
github.com/golang/protobuf v1.5.4 // indirect
|
github.com/golang/protobuf v1.5.4 // indirect
|
||||||
github.com/google/generative-ai-go v0.19.0 // indirect
|
github.com/google/generative-ai-go v0.19.0 // indirect
|
||||||
github.com/google/s2a-go v0.1.9 // indirect
|
github.com/google/s2a-go v0.1.9 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||||
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
|
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
|
||||||
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 // indirect
|
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 // indirect
|
||||||
github.com/kennygrant/sanitize v1.2.4 // indirect
|
github.com/kennygrant/sanitize v1.2.4 // indirect
|
||||||
github.com/liushuangls/go-anthropic/v2 v2.13.1 // indirect
|
github.com/liushuangls/go-anthropic/v2 v2.15.0 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
|
github.com/moby/term v0.5.2 // indirect
|
||||||
|
github.com/morikuni/aec v1.0.0 // indirect
|
||||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
github.com/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/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
|
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
|
||||||
github.com/sashabaranov/go-openai v1.37.0 // indirect
|
github.com/sashabaranov/go-openai v1.38.1 // indirect
|
||||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||||
github.com/temoto/robotstxt v1.1.2 // 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
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.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.59.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.34.0 // indirect
|
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.34.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.34.0 // indirect
|
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||||
golang.org/x/crypto v0.34.0 // indirect
|
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect
|
golang.org/x/crypto v0.37.0 // indirect
|
||||||
golang.org/x/net v0.35.0 // indirect
|
golang.org/x/net v0.39.0 // indirect
|
||||||
golang.org/x/oauth2 v0.26.0 // indirect
|
golang.org/x/oauth2 v0.29.0 // indirect
|
||||||
golang.org/x/sync v0.11.0 // indirect
|
golang.org/x/sync v0.13.0 // indirect
|
||||||
golang.org/x/sys v0.30.0 // indirect
|
golang.org/x/sys v0.32.0 // indirect
|
||||||
golang.org/x/text v0.22.0 // indirect
|
golang.org/x/text v0.24.0 // indirect
|
||||||
golang.org/x/time v0.10.0 // indirect
|
golang.org/x/time v0.11.0 // indirect
|
||||||
google.golang.org/api v0.222.0 // indirect
|
google.golang.org/api v0.228.0 // indirect
|
||||||
google.golang.org/appengine v1.6.8 // indirect
|
google.golang.org/appengine v1.6.8 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250219182151-9fdb1cabc7b2 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20250409194420-de1ac958c67a // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250409194420-de1ac958c67a // indirect
|
||||||
google.golang.org/grpc v1.70.0 // indirect
|
google.golang.org/grpc v1.71.1 // indirect
|
||||||
google.golang.org/protobuf v1.36.5 // indirect
|
google.golang.org/protobuf v1.36.6 // indirect
|
||||||
|
gotest.tools/v3 v3.5.2 // indirect
|
||||||
)
|
)
|
||||||
|
502
go.sum
Normal file
502
go.sum
Normal file
@ -0,0 +1,502 @@
|
|||||||
|
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=
|
153
pkg/agent/agent.go
Normal file
153
pkg/agent/agent.go
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
|
||||||
|
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAgent(req gollm.Request) *Agent {
|
||||||
|
return &Agent{req: req}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
Text string
|
||||||
|
Sources []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func deferClose(cl io.Closer) {
|
||||||
|
if cl != nil {
|
||||||
|
_ = cl.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) AddConversation(in gollm.Input) {
|
||||||
|
a.req.Conversation = append(a.req.Conversation, in)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) AddMessage(msg gollm.Message) {
|
||||||
|
slog.Info("adding message", "message", msg)
|
||||||
|
a.req.Messages = append(a.req.Messages, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(res.Choices) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
242
pkg/agents/agent.go
Normal file
242
pkg/agents/agent.go
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
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
|
||||||
|
}
|
3
pkg/agents/consensus.go
Normal file
3
pkg/agents/consensus.go
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
package agents
|
||||||
|
|
||||||
|
// TODO: Consensus system?
|
302
pkg/agents/console-new/agent.go
Normal file
302
pkg/agents/console-new/agent.go
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
package console_new
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/mount"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
|
||||||
|
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents"
|
||||||
|
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents/shared"
|
||||||
|
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Agent struct {
|
||||||
|
// Model is the chat completion model to use
|
||||||
|
Model gollm.ChatCompletion
|
||||||
|
|
||||||
|
OnLoopComplete func(ctx context.Context, knowledge agents.Knowledge) error
|
||||||
|
|
||||||
|
OnCommandStart func(ctx context.Context, command string) error
|
||||||
|
OnCommandDone func(ctx context.Context, command string, output string, err error) error
|
||||||
|
|
||||||
|
OnDone func(ctx context.Context, knowledge agents.Knowledge) error
|
||||||
|
|
||||||
|
ContextualInformation []string
|
||||||
|
|
||||||
|
MaxCommands int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
Knowledge agents.Knowledge
|
||||||
|
Directory string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Answer will give the model access to an ubuntu console with python and pip installed, and then ask the model to
|
||||||
|
// do what is necessary to answer the question.
|
||||||
|
func (a Agent) Answer(ctx context.Context, questions []string) (Response, error) {
|
||||||
|
var res Response
|
||||||
|
|
||||||
|
if a.MaxCommands <= 0 {
|
||||||
|
a.MaxCommands = 10000
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Knowledge = agents.Knowledge{
|
||||||
|
OriginalQuestions: questions,
|
||||||
|
RemainingQuestions: questions,
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a temporary scratch directory
|
||||||
|
dir, err := os.MkdirTemp("", "console-")
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Directory = dir
|
||||||
|
|
||||||
|
cl, err := client.NewClientWithOpts(client.FromEnv)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
defer cl.Close()
|
||||||
|
|
||||||
|
mounts := []mount.Mount{
|
||||||
|
{
|
||||||
|
Type: mount.TypeBind,
|
||||||
|
Source: dir,
|
||||||
|
Target: "/home/user",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := CreateContainer(ctx, cl, ContainerConfig{
|
||||||
|
Config: &container.Config{
|
||||||
|
Image: "ubuntu:latest",
|
||||||
|
Cmd: []string{"tail", "-f", "/dev/null"},
|
||||||
|
Tty: true,
|
||||||
|
WorkingDir: "/home/user",
|
||||||
|
},
|
||||||
|
HostConfig: &container.HostConfig{
|
||||||
|
AutoRemove: true,
|
||||||
|
Mounts: mounts,
|
||||||
|
},
|
||||||
|
Name: filepath.Base(dir),
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return res, fmt.Errorf("failed to create container: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = c.Close(ctx)
|
||||||
|
}()
|
||||||
|
|
||||||
|
slog.Info("starting container", "dir", dir, "container", fmt.Sprintf("%+v", c))
|
||||||
|
err = c.Start(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return res, fmt.Errorf("failed to start container: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the model
|
||||||
|
|
||||||
|
var history executions
|
||||||
|
var keepGoing = true
|
||||||
|
|
||||||
|
opwd, epwd := c.Execute(ctx, "ls -al /home")
|
||||||
|
|
||||||
|
fmt.Println(opwd)
|
||||||
|
slog.Info("pwd", "pwd", opwd, "epwd", epwd)
|
||||||
|
|
||||||
|
tools := map[string]*gollm.Function{
|
||||||
|
"exit": gollm.NewFunction(
|
||||||
|
"exit",
|
||||||
|
"exit the container",
|
||||||
|
func(ctx *gollm.Context, args struct {
|
||||||
|
RemainingQuestions []string `description:"any remaining questions that remain unanswered"`
|
||||||
|
}) (any, error) {
|
||||||
|
keepGoing = false
|
||||||
|
return "exiting", nil
|
||||||
|
}),
|
||||||
|
|
||||||
|
"write": gollm.NewFunction(
|
||||||
|
"write",
|
||||||
|
"write a file in the /root directory",
|
||||||
|
func(ctx *gollm.Context, args struct {
|
||||||
|
Filename string `description:"The name of the file to write"`
|
||||||
|
Content string `description:"The content of the file to write"`
|
||||||
|
}) (any, error) {
|
||||||
|
target, err := SafeJoinPath(dir, args.Filename)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Create(target)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
_, err = f.WriteString(args.Content)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return "wrote file", nil
|
||||||
|
}),
|
||||||
|
|
||||||
|
"read": gollm.NewFunction(
|
||||||
|
"read",
|
||||||
|
"read a file in the /root directory",
|
||||||
|
func(ctx *gollm.Context, args struct {
|
||||||
|
Filename string `description:"The name of the file to read"`
|
||||||
|
}) (any, error) {
|
||||||
|
target, err := SafeJoinPath(dir, args.Filename)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := os.ReadFile(target)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(b), nil
|
||||||
|
}),
|
||||||
|
|
||||||
|
"execute": gollm.NewFunction(
|
||||||
|
"execute",
|
||||||
|
"execute a command in the container",
|
||||||
|
func(ctx *gollm.Context, args struct {
|
||||||
|
Command string `description:"The command to execute"`
|
||||||
|
}) (any, error) {
|
||||||
|
if len(history) >= a.MaxCommands {
|
||||||
|
return "too many commands", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.OnCommandStart != nil {
|
||||||
|
err := a.OnCommandStart(ctx, args.Command)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var res string
|
||||||
|
// if the command starts with sudo then we need to use the sudo function
|
||||||
|
if strings.HasPrefix(args.Command, "sudo ") {
|
||||||
|
res, err = c.Sudo(ctx, args.Command[5:])
|
||||||
|
} else {
|
||||||
|
res, err = c.Execute(ctx, args.Command)
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.OnCommandDone != nil {
|
||||||
|
err = a.OnCommandDone(ctx, args.Command, res, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
history = append(history, execution{
|
||||||
|
Command: args.Command,
|
||||||
|
Output: res,
|
||||||
|
})
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}),
|
||||||
|
|
||||||
|
"sudo": gollm.NewFunction(
|
||||||
|
"sudo",
|
||||||
|
"execute a command in the container",
|
||||||
|
func(ctx *gollm.Context, args struct {
|
||||||
|
Command string `description:"The command to execute"`
|
||||||
|
}) (any, error) {
|
||||||
|
if len(history) >= a.MaxCommands {
|
||||||
|
return "too many commands", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.OnCommandStart != nil {
|
||||||
|
err := a.OnCommandStart(ctx, args.Command)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := c.Sudo(ctx, args.Command)
|
||||||
|
|
||||||
|
if a.OnCommandDone != nil {
|
||||||
|
err = a.OnCommandDone(ctx, args.Command, res, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
res = "error executing: " + err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
history = append(history, execution{
|
||||||
|
Command: "sudo " + args.Command,
|
||||||
|
Output: res,
|
||||||
|
})
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < a.MaxCommands && len(history) < a.MaxCommands && keepGoing; i++ {
|
||||||
|
|
||||||
|
systemPrompt := `You are now in a shell in a container of the ubuntu:latest image to answer a question asked by the user, it is very basic install of ubuntu, simple things (like python) are not preinstalled but can be installed via apt. You will be run multiple times and gain knowledge throughout the process.`
|
||||||
|
|
||||||
|
if len(history) < a.MaxCommands {
|
||||||
|
systemPrompt += `You can run any command you like to get to the needed results.`
|
||||||
|
}
|
||||||
|
|
||||||
|
systemPrompt += `Alternatively, you can use the tool "write" to write a file in the home directory, and also the tool "read" to read a file in the home directory.
|
||||||
|
When you are done, please use "exit" to exit the container.
|
||||||
|
Respond with any number of commands to answer the question, they will be executed in order.`
|
||||||
|
|
||||||
|
var toolbox []*gollm.Function
|
||||||
|
|
||||||
|
// add unrestricted tools
|
||||||
|
toolbox = append(toolbox, tools["exit"], tools["write"], tools["read"])
|
||||||
|
|
||||||
|
if len(history) < a.MaxCommands {
|
||||||
|
toolbox = append(toolbox, tools["execute"], tools["sudo"])
|
||||||
|
}
|
||||||
|
|
||||||
|
kw := shared.KnowledgeWorker{
|
||||||
|
Model: a.Model,
|
||||||
|
ToolBox: gollm.NewToolBox(toolbox...),
|
||||||
|
ContextualInformation: a.ContextualInformation,
|
||||||
|
OnNewFunction: func(ctx context.Context, funcName string, args string) (any, error) {
|
||||||
|
slog.Info("new function called", "function name", funcName, "args", args)
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("answering question", "question", questions[0])
|
||||||
|
r, err := kw.Answer(ctx, &res.Knowledge, systemPrompt, "", "", history.ToGeneralButLastMessageHistory(), func(res gollm.ToolCallResponse) {
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return res, fmt.Errorf("error answering question: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(r.Knowledge) > 0 {
|
||||||
|
slog.Info("answered question and learned", "knowledge", r.Knowledge)
|
||||||
|
} else {
|
||||||
|
slog.Info("answered question and learned nothing")
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Knowledge, err = agents.KnowledgeIntegrate(ctx, a.Model, res.Knowledge, r)
|
||||||
|
if err != nil {
|
||||||
|
return res, fmt.Errorf("error integrating knowledge: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("knowledge integrated", "question", questions[0], "knowledge", res.Knowledge)
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
115
pkg/agents/console-new/container.go
Normal file
115
pkg/agents/console-new/container.go
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
package console_new
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/network"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Container struct {
|
||||||
|
client *client.Client
|
||||||
|
id string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Container) Start(ctx context.Context) error {
|
||||||
|
return c.client.ContainerStart(ctx, c.id, container.StartOptions{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Container) Close(ctx context.Context) error {
|
||||||
|
timeout := 10
|
||||||
|
if err := c.client.ContainerStop(ctx, c.id, container.StopOptions{
|
||||||
|
Timeout: &timeout,
|
||||||
|
}); err != nil {
|
||||||
|
// If stop fails, force kill
|
||||||
|
if err := c.client.ContainerKill(ctx, c.id, "SIGKILL"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove container and volumes
|
||||||
|
removeOptions := container.RemoveOptions{
|
||||||
|
RemoveVolumes: true,
|
||||||
|
Force: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.client.ContainerRemove(ctx, c.id, removeOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Container) execUserCmd(ctx context.Context, user string, cmd string) (string, error) {
|
||||||
|
slog.Info("executing command", "user", user, "cmd", cmd)
|
||||||
|
var cmdStr = []string{"/bin/bash", "-c"}
|
||||||
|
|
||||||
|
cmdStr = append(cmdStr, cmd)
|
||||||
|
|
||||||
|
slog.Info("executing command", "user", user, "cmd", fmt.Sprintf("%#v", cmdStr))
|
||||||
|
exec, err := c.client.ContainerExecCreate(ctx, c.id, container.ExecOptions{
|
||||||
|
Cmd: cmdStr,
|
||||||
|
User: user,
|
||||||
|
AttachStdout: true,
|
||||||
|
AttachStderr: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.client.ContainerExecAttach(ctx, exec.ID, container.ExecAttachOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Close()
|
||||||
|
|
||||||
|
output, err := io.ReadAll(resp.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for command to finish and get exit code
|
||||||
|
inspectResp, err := c.client.ContainerExecInspect(ctx, exec.ID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("command finished", "output", string(output), "err", err, "resp", inspectResp)
|
||||||
|
|
||||||
|
if inspectResp.ExitCode != 0 {
|
||||||
|
return string(output), fmt.Errorf("command exited with code %d", inspectResp.ExitCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(output), nil
|
||||||
|
}
|
||||||
|
func (c Container) Execute(ctx context.Context, cmd string) (string, error) {
|
||||||
|
return c.execUserCmd(ctx, "", cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Container) ExecuteAs(ctx context.Context, user string, cmd string) (string, error) {
|
||||||
|
return c.execUserCmd(ctx, user, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Container) Sudo(ctx context.Context, cmd string) (string, error) {
|
||||||
|
return c.execUserCmd(ctx, "root", cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContainerConfig struct {
|
||||||
|
Config *container.Config
|
||||||
|
HostConfig *container.HostConfig
|
||||||
|
NetConfig *network.NetworkingConfig
|
||||||
|
Platform *v1.Platform
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateContainer(ctx context.Context, cl *client.Client, cfg ContainerConfig) (*Container, error) {
|
||||||
|
resp, err := cl.ContainerCreate(ctx, cfg.Config, cfg.HostConfig, cfg.NetConfig, cfg.Platform, cfg.Name)
|
||||||
|
|
||||||
|
slog.Info("creating container", "resp", resp, "err", err)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Container{client: cl, id: resp.ID}, nil
|
||||||
|
}
|
97
pkg/agents/console-new/execution.go
Normal file
97
pkg/agents/console-new/execution.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
package console_new
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type execution struct {
|
||||||
|
Command string
|
||||||
|
Output string
|
||||||
|
WhatILearned []string
|
||||||
|
WhatIStillNeedToLearn []string
|
||||||
|
}
|
||||||
|
|
||||||
|
const kMaxLenCommandSummary = 200
|
||||||
|
const kMaxLenCommandOutputSummary = 200
|
||||||
|
|
||||||
|
func (e execution) ToGeneralMessageHistory() gollm.Message {
|
||||||
|
if len(e.Command) > kMaxLenCommandSummary {
|
||||||
|
e.Command = e.Command[:kMaxLenCommandSummary] + "... (truncated)"
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(e.Output) > kMaxLenCommandOutputSummary {
|
||||||
|
e.Output = e.Output[:kMaxLenCommandOutputSummary] + "... (truncated)"
|
||||||
|
}
|
||||||
|
|
||||||
|
text := "# " + e.Command + "\n" + e.Output
|
||||||
|
|
||||||
|
return gollm.Message{
|
||||||
|
Role: gollm.RoleUser,
|
||||||
|
Text: text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e execution) ToDetailedMessageHistory() gollm.Message {
|
||||||
|
prompt := "$ "
|
||||||
|
if strings.HasPrefix(e.Command, "sudo ") {
|
||||||
|
prompt = "# "
|
||||||
|
e.Command = e.Command[5:]
|
||||||
|
}
|
||||||
|
|
||||||
|
text := prompt + strings.TrimSpace(e.Command) + "\n" + e.Output
|
||||||
|
|
||||||
|
if len(e.WhatILearned) > 0 {
|
||||||
|
text += "\n\nWhat I learned:\n" + strings.Join(e.WhatILearned, "\n")
|
||||||
|
} else {
|
||||||
|
text += "\n\nI didn't learn anything new."
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(e.WhatIStillNeedToLearn) > 0 {
|
||||||
|
text += "\n\nWhat I still need to learn:\n" + strings.Join(e.WhatIStillNeedToLearn, "\n")
|
||||||
|
} else {
|
||||||
|
text += "\n\nI don't need to learn anything else."
|
||||||
|
}
|
||||||
|
|
||||||
|
return gollm.Message{
|
||||||
|
Role: gollm.RoleUser,
|
||||||
|
Text: text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type executions []execution
|
||||||
|
|
||||||
|
func (e executions) ToGeneralMessageHistory() []gollm.Message {
|
||||||
|
var messages []gollm.Message
|
||||||
|
|
||||||
|
for _, v := range e {
|
||||||
|
messages = append(messages, v.ToGeneralMessageHistory())
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e executions) ToGeneralButLastMessageHistory() []gollm.Message {
|
||||||
|
var messages []gollm.Message
|
||||||
|
|
||||||
|
for i, v := range e {
|
||||||
|
if i == len(e)-1 {
|
||||||
|
messages = append(messages, v.ToDetailedMessageHistory())
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
messages = append(messages, v.ToGeneralMessageHistory())
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages
|
||||||
|
}
|
||||||
|
func (e executions) ToDetailedMessageHistory() []gollm.Message {
|
||||||
|
var messages []gollm.Message
|
||||||
|
|
||||||
|
for _, v := range e {
|
||||||
|
messages = append(messages, v.ToDetailedMessageHistory())
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages
|
||||||
|
}
|
23
pkg/agents/console-new/util.go
Normal file
23
pkg/agents/console-new/util.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package console_new
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SafeJoinPath(tempDir, fileName string) (string, error) {
|
||||||
|
// Clean both paths
|
||||||
|
tempDir = filepath.Clean(tempDir)
|
||||||
|
fileName = filepath.Clean(fileName)
|
||||||
|
|
||||||
|
// Join paths and clean result
|
||||||
|
fullPath := filepath.Clean(filepath.Join(tempDir, fileName))
|
||||||
|
|
||||||
|
// Verify the path is still within tempDir
|
||||||
|
if !strings.HasPrefix(fullPath, tempDir+string(filepath.Separator)) {
|
||||||
|
return "", fmt.Errorf("invalid path")
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullPath, nil
|
||||||
|
}
|
302
pkg/agents/console/agent.go
Normal file
302
pkg/agents/console/agent.go
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
package console
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/mount"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
|
||||||
|
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents"
|
||||||
|
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents/shared"
|
||||||
|
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Agent struct {
|
||||||
|
// Model is the chat completion model to use
|
||||||
|
Model gollm.ChatCompletion
|
||||||
|
|
||||||
|
OnLoopComplete func(ctx context.Context, knowledge agents.Knowledge) error
|
||||||
|
|
||||||
|
OnCommandStart func(ctx context.Context, command string) error
|
||||||
|
OnCommandDone func(ctx context.Context, command string, output string, err error) error
|
||||||
|
|
||||||
|
OnDone func(ctx context.Context, knowledge agents.Knowledge) error
|
||||||
|
|
||||||
|
ContextualInformation []string
|
||||||
|
|
||||||
|
MaxCommands int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
Knowledge agents.Knowledge
|
||||||
|
Directory string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Answer will give the model access to an ubuntu console with python and pip installed, and then ask the model to
|
||||||
|
// do what is necessary to answer the question.
|
||||||
|
func (a Agent) Answer(ctx context.Context, questions []string) (Response, error) {
|
||||||
|
var res Response
|
||||||
|
|
||||||
|
if a.MaxCommands <= 0 {
|
||||||
|
a.MaxCommands = 10000
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Knowledge = agents.Knowledge{
|
||||||
|
OriginalQuestions: questions,
|
||||||
|
RemainingQuestions: questions,
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a temporary scratch directory
|
||||||
|
dir, err := os.MkdirTemp("", "console-")
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Directory = dir
|
||||||
|
|
||||||
|
cl, err := client.NewClientWithOpts(client.FromEnv)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
defer cl.Close()
|
||||||
|
|
||||||
|
mounts := []mount.Mount{
|
||||||
|
{
|
||||||
|
Type: mount.TypeBind,
|
||||||
|
Source: dir,
|
||||||
|
Target: "/home/user",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := CreateContainer(ctx, cl, ContainerConfig{
|
||||||
|
Config: &container.Config{
|
||||||
|
Image: "ubuntu:latest",
|
||||||
|
Cmd: []string{"tail", "-f", "/dev/null"},
|
||||||
|
Tty: true,
|
||||||
|
WorkingDir: "/home/user",
|
||||||
|
},
|
||||||
|
HostConfig: &container.HostConfig{
|
||||||
|
AutoRemove: true,
|
||||||
|
Mounts: mounts,
|
||||||
|
},
|
||||||
|
Name: filepath.Base(dir),
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return res, fmt.Errorf("failed to create container: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = c.Close(ctx)
|
||||||
|
}()
|
||||||
|
|
||||||
|
slog.Info("starting container", "dir", dir, "container", fmt.Sprintf("%+v", c))
|
||||||
|
err = c.Start(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return res, fmt.Errorf("failed to start container: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the model
|
||||||
|
|
||||||
|
var history executions
|
||||||
|
var keepGoing = true
|
||||||
|
|
||||||
|
opwd, epwd := c.Execute(ctx, "ls -al /home")
|
||||||
|
|
||||||
|
fmt.Println(opwd)
|
||||||
|
slog.Info("pwd", "pwd", opwd, "epwd", epwd)
|
||||||
|
|
||||||
|
tools := map[string]*gollm.Function{
|
||||||
|
"exit": gollm.NewFunction(
|
||||||
|
"exit",
|
||||||
|
"exit the container",
|
||||||
|
func(ctx *gollm.Context, args struct {
|
||||||
|
RemainingQuestions []string `description:"any remaining questions that remain unanswered"`
|
||||||
|
}) (any, error) {
|
||||||
|
keepGoing = false
|
||||||
|
return "exiting", nil
|
||||||
|
}),
|
||||||
|
|
||||||
|
"write": gollm.NewFunction(
|
||||||
|
"write",
|
||||||
|
"write a file in the /root directory",
|
||||||
|
func(ctx *gollm.Context, args struct {
|
||||||
|
Filename string `description:"The name of the file to write"`
|
||||||
|
Content string `description:"The content of the file to write"`
|
||||||
|
}) (any, error) {
|
||||||
|
target, err := SafeJoinPath(dir, args.Filename)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Create(target)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
_, err = f.WriteString(args.Content)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return "wrote file", nil
|
||||||
|
}),
|
||||||
|
|
||||||
|
"read": gollm.NewFunction(
|
||||||
|
"read",
|
||||||
|
"read a file in the /root directory",
|
||||||
|
func(ctx *gollm.Context, args struct {
|
||||||
|
Filename string `description:"The name of the file to read"`
|
||||||
|
}) (any, error) {
|
||||||
|
target, err := SafeJoinPath(dir, args.Filename)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := os.ReadFile(target)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(b), nil
|
||||||
|
}),
|
||||||
|
|
||||||
|
"execute": gollm.NewFunction(
|
||||||
|
"execute",
|
||||||
|
"execute a command in the container",
|
||||||
|
func(ctx *gollm.Context, args struct {
|
||||||
|
Command string `description:"The command to execute"`
|
||||||
|
}) (any, error) {
|
||||||
|
if len(history) >= a.MaxCommands {
|
||||||
|
return "too many commands", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.OnCommandStart != nil {
|
||||||
|
err := a.OnCommandStart(ctx, args.Command)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var res string
|
||||||
|
// if the command starts with sudo then we need to use the sudo function
|
||||||
|
if strings.HasPrefix(args.Command, "sudo ") {
|
||||||
|
res, err = c.Sudo(ctx, args.Command[5:])
|
||||||
|
} else {
|
||||||
|
res, err = c.Execute(ctx, args.Command)
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.OnCommandDone != nil {
|
||||||
|
err = a.OnCommandDone(ctx, args.Command, res, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
history = append(history, execution{
|
||||||
|
Command: args.Command,
|
||||||
|
Output: res,
|
||||||
|
})
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}),
|
||||||
|
|
||||||
|
"sudo": gollm.NewFunction(
|
||||||
|
"sudo",
|
||||||
|
"execute a command in the container",
|
||||||
|
func(ctx *gollm.Context, args struct {
|
||||||
|
Command string `description:"The command to execute"`
|
||||||
|
}) (any, error) {
|
||||||
|
if len(history) >= a.MaxCommands {
|
||||||
|
return "too many commands", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.OnCommandStart != nil {
|
||||||
|
err := a.OnCommandStart(ctx, args.Command)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := c.Sudo(ctx, args.Command)
|
||||||
|
|
||||||
|
if a.OnCommandDone != nil {
|
||||||
|
err = a.OnCommandDone(ctx, args.Command, res, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
res = "error executing: " + err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
history = append(history, execution{
|
||||||
|
Command: "sudo " + args.Command,
|
||||||
|
Output: res,
|
||||||
|
})
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < a.MaxCommands && len(history) < a.MaxCommands && keepGoing; i++ {
|
||||||
|
|
||||||
|
systemPrompt := `You are now in a shell in a container of the ubuntu:latest image to answer a question asked by the user, it is very basic install of ubuntu, simple things (like python) are not preinstalled but can be installed via apt. You will be run multiple times and gain knowledge throughout the process.`
|
||||||
|
|
||||||
|
if len(history) < a.MaxCommands {
|
||||||
|
systemPrompt += `You can run any command you like to get to the needed results.`
|
||||||
|
}
|
||||||
|
|
||||||
|
systemPrompt += `Alternatively, you can use the tool "write" to write a file in the home directory, and also the tool "read" to read a file in the home directory.
|
||||||
|
When you are done, please use "exit" to exit the container.
|
||||||
|
Respond with any number of commands to answer the question, they will be executed in order.`
|
||||||
|
|
||||||
|
var toolbox []*gollm.Function
|
||||||
|
|
||||||
|
// add unrestricted tools
|
||||||
|
toolbox = append(toolbox, tools["exit"], tools["write"], tools["read"])
|
||||||
|
|
||||||
|
if len(history) < a.MaxCommands {
|
||||||
|
toolbox = append(toolbox, tools["execute"], tools["sudo"])
|
||||||
|
}
|
||||||
|
|
||||||
|
kw := shared.KnowledgeWorker{
|
||||||
|
Model: a.Model,
|
||||||
|
ToolBox: gollm.NewToolBox(toolbox...),
|
||||||
|
ContextualInformation: a.ContextualInformation,
|
||||||
|
OnNewFunction: func(ctx context.Context, funcName string, args string) (any, error) {
|
||||||
|
slog.Info("new function called", "function name", funcName, "args", args)
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("answering question", "question", questions[0])
|
||||||
|
r, err := kw.Answer(ctx, &res.Knowledge, systemPrompt, "", "", history.ToGeneralButLastMessageHistory(), func(res gollm.ToolCallResponse) {
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return res, fmt.Errorf("error answering question: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(r.Knowledge) > 0 {
|
||||||
|
slog.Info("answered question and learned", "knowledge", r.Knowledge)
|
||||||
|
} else {
|
||||||
|
slog.Info("answered question and learned nothing")
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Knowledge, err = agents.KnowledgeIntegrate(ctx, a.Model, res.Knowledge, r)
|
||||||
|
if err != nil {
|
||||||
|
return res, fmt.Errorf("error integrating knowledge: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("knowledge integrated", "question", questions[0], "knowledge", res.Knowledge)
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
115
pkg/agents/console/container.go
Normal file
115
pkg/agents/console/container.go
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
package console
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/network"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Container struct {
|
||||||
|
client *client.Client
|
||||||
|
id string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Container) Start(ctx context.Context) error {
|
||||||
|
return c.client.ContainerStart(ctx, c.id, container.StartOptions{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Container) Close(ctx context.Context) error {
|
||||||
|
timeout := 10
|
||||||
|
if err := c.client.ContainerStop(ctx, c.id, container.StopOptions{
|
||||||
|
Timeout: &timeout,
|
||||||
|
}); err != nil {
|
||||||
|
// If stop fails, force kill
|
||||||
|
if err := c.client.ContainerKill(ctx, c.id, "SIGKILL"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove container and volumes
|
||||||
|
removeOptions := container.RemoveOptions{
|
||||||
|
RemoveVolumes: true,
|
||||||
|
Force: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.client.ContainerRemove(ctx, c.id, removeOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Container) execUserCmd(ctx context.Context, user string, cmd string) (string, error) {
|
||||||
|
slog.Info("executing command", "user", user, "cmd", cmd)
|
||||||
|
var cmdStr = []string{"/bin/bash", "-c"}
|
||||||
|
|
||||||
|
cmdStr = append(cmdStr, cmd)
|
||||||
|
|
||||||
|
slog.Info("executing command", "user", user, "cmd", fmt.Sprintf("%#v", cmdStr))
|
||||||
|
exec, err := c.client.ContainerExecCreate(ctx, c.id, container.ExecOptions{
|
||||||
|
Cmd: cmdStr,
|
||||||
|
User: user,
|
||||||
|
AttachStdout: true,
|
||||||
|
AttachStderr: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.client.ContainerExecAttach(ctx, exec.ID, container.ExecAttachOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Close()
|
||||||
|
|
||||||
|
output, err := io.ReadAll(resp.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for command to finish and get exit code
|
||||||
|
inspectResp, err := c.client.ContainerExecInspect(ctx, exec.ID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("command finished", "output", string(output), "err", err, "resp", inspectResp)
|
||||||
|
|
||||||
|
if inspectResp.ExitCode != 0 {
|
||||||
|
return string(output), fmt.Errorf("command exited with code %d", inspectResp.ExitCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(output), nil
|
||||||
|
}
|
||||||
|
func (c Container) Execute(ctx context.Context, cmd string) (string, error) {
|
||||||
|
return c.execUserCmd(ctx, "", cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Container) ExecuteAs(ctx context.Context, user string, cmd string) (string, error) {
|
||||||
|
return c.execUserCmd(ctx, user, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Container) Sudo(ctx context.Context, cmd string) (string, error) {
|
||||||
|
return c.execUserCmd(ctx, "root", cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContainerConfig struct {
|
||||||
|
Config *container.Config
|
||||||
|
HostConfig *container.HostConfig
|
||||||
|
NetConfig *network.NetworkingConfig
|
||||||
|
Platform *v1.Platform
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateContainer(ctx context.Context, cl *client.Client, cfg ContainerConfig) (*Container, error) {
|
||||||
|
resp, err := cl.ContainerCreate(ctx, cfg.Config, cfg.HostConfig, cfg.NetConfig, cfg.Platform, cfg.Name)
|
||||||
|
|
||||||
|
slog.Info("creating container", "resp", resp, "err", err)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Container{client: cl, id: resp.ID}, nil
|
||||||
|
}
|
97
pkg/agents/console/execution.go
Normal file
97
pkg/agents/console/execution.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
package console
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type execution struct {
|
||||||
|
Command string
|
||||||
|
Output string
|
||||||
|
WhatILearned []string
|
||||||
|
WhatIStillNeedToLearn []string
|
||||||
|
}
|
||||||
|
|
||||||
|
const kMaxLenCommandSummary = 200
|
||||||
|
const kMaxLenCommandOutputSummary = 200
|
||||||
|
|
||||||
|
func (e execution) ToGeneralMessageHistory() gollm.Message {
|
||||||
|
if len(e.Command) > kMaxLenCommandSummary {
|
||||||
|
e.Command = e.Command[:kMaxLenCommandSummary] + "... (truncated)"
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(e.Output) > kMaxLenCommandOutputSummary {
|
||||||
|
e.Output = e.Output[:kMaxLenCommandOutputSummary] + "... (truncated)"
|
||||||
|
}
|
||||||
|
|
||||||
|
text := "# " + e.Command + "\n" + e.Output
|
||||||
|
|
||||||
|
return gollm.Message{
|
||||||
|
Role: gollm.RoleUser,
|
||||||
|
Text: text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e execution) ToDetailedMessageHistory() gollm.Message {
|
||||||
|
prompt := "$ "
|
||||||
|
if strings.HasPrefix(e.Command, "sudo ") {
|
||||||
|
prompt = "# "
|
||||||
|
e.Command = e.Command[5:]
|
||||||
|
}
|
||||||
|
|
||||||
|
text := prompt + strings.TrimSpace(e.Command) + "\n" + e.Output
|
||||||
|
|
||||||
|
if len(e.WhatILearned) > 0 {
|
||||||
|
text += "\n\nWhat I learned:\n" + strings.Join(e.WhatILearned, "\n")
|
||||||
|
} else {
|
||||||
|
text += "\n\nI didn't learn anything new."
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(e.WhatIStillNeedToLearn) > 0 {
|
||||||
|
text += "\n\nWhat I still need to learn:\n" + strings.Join(e.WhatIStillNeedToLearn, "\n")
|
||||||
|
} else {
|
||||||
|
text += "\n\nI don't need to learn anything else."
|
||||||
|
}
|
||||||
|
|
||||||
|
return gollm.Message{
|
||||||
|
Role: gollm.RoleUser,
|
||||||
|
Text: text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type executions []execution
|
||||||
|
|
||||||
|
func (e executions) ToGeneralMessageHistory() []gollm.Message {
|
||||||
|
var messages []gollm.Message
|
||||||
|
|
||||||
|
for _, v := range e {
|
||||||
|
messages = append(messages, v.ToGeneralMessageHistory())
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e executions) ToGeneralButLastMessageHistory() []gollm.Message {
|
||||||
|
var messages []gollm.Message
|
||||||
|
|
||||||
|
for i, v := range e {
|
||||||
|
if i == len(e)-1 {
|
||||||
|
messages = append(messages, v.ToDetailedMessageHistory())
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
messages = append(messages, v.ToGeneralMessageHistory())
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages
|
||||||
|
}
|
||||||
|
func (e executions) ToDetailedMessageHistory() []gollm.Message {
|
||||||
|
var messages []gollm.Message
|
||||||
|
|
||||||
|
for _, v := range e {
|
||||||
|
messages = append(messages, v.ToDetailedMessageHistory())
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages
|
||||||
|
}
|
23
pkg/agents/console/util.go
Normal file
23
pkg/agents/console/util.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package console
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SafeJoinPath(tempDir, fileName string) (string, error) {
|
||||||
|
// Clean both paths
|
||||||
|
tempDir = filepath.Clean(tempDir)
|
||||||
|
fileName = filepath.Clean(fileName)
|
||||||
|
|
||||||
|
// Join paths and clean result
|
||||||
|
fullPath := filepath.Clean(filepath.Join(tempDir, fileName))
|
||||||
|
|
||||||
|
// Verify the path is still within tempDir
|
||||||
|
if !strings.HasPrefix(fullPath, tempDir+string(filepath.Separator)) {
|
||||||
|
return "", fmt.Errorf("invalid path")
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullPath, nil
|
||||||
|
}
|
59
pkg/agents/extract_knowledge.go
Normal file
59
pkg/agents/extract_knowledge.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package agents
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExtractKnowledge will take a knowledge object and use the gained knowledge to extract the knowledge relevant to the
|
||||||
|
// questions provided.
|
||||||
|
// sourceData is the raw text to analyze for the knowledge.
|
||||||
|
// source is the source of the information, such as a URL.
|
||||||
|
// questions are the questions that the knowledge is trying to answer.
|
||||||
|
// model is the chat completion model to use.
|
||||||
|
// contextualInformation is any contextual information that should be provided to the model.
|
||||||
|
// It will return the knowledge extracted from the sourceData along with any remaining questions.
|
||||||
|
// This agent call will not use the Agent's system prompts, but will instead form its own. The contextual information will be used.
|
||||||
|
func (a Agent) ExtractKnowledge(ctx context.Context, sourceData string, source string, questions []string) (Knowledge, error) {
|
||||||
|
|
||||||
|
var knowledge Knowledge
|
||||||
|
fnAnswer := gollm.NewFunction(
|
||||||
|
"learn",
|
||||||
|
`Use learn to pass some relevant information to the model. The model will use this information to answer the question. Use it to learn relevant information from the text. Keep these concise and relevant to the question.`,
|
||||||
|
func(ctx *gollm.Context, args struct {
|
||||||
|
Info string `description:"The information to learn from the text."`
|
||||||
|
}) (any, error) {
|
||||||
|
knowledge.Knowledge = append(knowledge.Knowledge, TidBit{Info: args.Info, Source: source})
|
||||||
|
return "", nil
|
||||||
|
})
|
||||||
|
|
||||||
|
fnNoAnswer := gollm.NewFunction(
|
||||||
|
"finished",
|
||||||
|
"Indicate that the text does not answer the question.",
|
||||||
|
func(ctx *gollm.Context, args struct {
|
||||||
|
Remaining string `description:"After all the knowledge has been learned, this is the parts of the question that are not answered, if any. Leave this blank if the text fully answers the question."`
|
||||||
|
}) (any, error) {
|
||||||
|
knowledge.RemainingQuestions = []string{args.Remaining}
|
||||||
|
return "", nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Overwrite this agent's system prompts with the ones needed for this function.
|
||||||
|
|
||||||
|
var questionPrompt = "The questions you are trying to answer using the text are:\n" + strings.Join(questions, "\n")
|
||||||
|
if len(questions) == 1 {
|
||||||
|
questionPrompt = "The question you are trying to answer using the text is: " + questions[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := a.
|
||||||
|
WithSystemPrompt(`Evaluate the given text to see if you can answer any information from it relevant to the question that the user asks.
|
||||||
|
Use the "learn" function to pass relevant information to the model. You can use the "learn" function multiple times to pass multiple pieces of relevant information to the model.
|
||||||
|
If the text does not answer the question or you are done using "learn" to pass on knowledge then use the "finished" function and indicate the parts of the question that are not answered by anything learned.
|
||||||
|
You can call "learn" multiple times before calling "finished".`).
|
||||||
|
WithSystemPromptSuffix(``).
|
||||||
|
WithToolbox(gollm.NewToolBox(fnAnswer, fnNoAnswer)).
|
||||||
|
CallAndExecute(ctx, "The text for you to evaluate is: "+sourceData, questionPrompt)
|
||||||
|
|
||||||
|
return knowledge, err
|
||||||
|
}
|
108
pkg/agents/knowledge.go
Normal file
108
pkg/agents/knowledge.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
package agents
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TidBit is a small piece of information that the AI has learned.
|
||||||
|
type TidBit struct {
|
||||||
|
Info string `json:"info"`
|
||||||
|
Source string `json:"source"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Knowledge struct {
|
||||||
|
// OriginalQuestions are the questions that was asked first to the AI before any processing was done.
|
||||||
|
OriginalQuestions []string `json:"originalQuestions"`
|
||||||
|
|
||||||
|
// RemainingQuestions is the questions that are left to find answers for.
|
||||||
|
RemainingQuestions []string `json:"remainingQuestions"`
|
||||||
|
|
||||||
|
// NotesToSelf are notes that the AI has made for itself.
|
||||||
|
NotesToSelf []string `json:"notesToSelf"`
|
||||||
|
|
||||||
|
// CurrentObjectives are the objectives that the AI is currently working on.
|
||||||
|
CurrentObjectives []string `json:"currentObjectives"`
|
||||||
|
|
||||||
|
// Knowledge are the tidbits of information that the AI has learned.
|
||||||
|
Knowledge []TidBit `json:"knowledge"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k Knowledge) ToSystemMessage() gollm.Message {
|
||||||
|
var sources = map[string][]string{}
|
||||||
|
|
||||||
|
for _, t := range k.Knowledge {
|
||||||
|
sources[t.Source] = append(sources[t.Source], t.Info)
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg string
|
||||||
|
|
||||||
|
if len(k.OriginalQuestions) > 0 {
|
||||||
|
msg += "Original questions asked:\n - " + strings.Join(k.OriginalQuestions, "\n - ") + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(k.NotesToSelf) > 0 {
|
||||||
|
msg += "Notes to self:\n - " + strings.Join(k.NotesToSelf, "\n - ") + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(sources) > 0 {
|
||||||
|
msg += "Learned information:\n"
|
||||||
|
for source, info := range sources {
|
||||||
|
if source == "" {
|
||||||
|
source = "(unsourced)"
|
||||||
|
}
|
||||||
|
msg += " - From " + source + ":\n - " + strings.Join(info, "\n - ") + "\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(k.CurrentObjectives) > 0 {
|
||||||
|
msg += "Current objectives:\n - " + strings.Join(k.CurrentObjectives, "\n - ") + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(k.RemainingQuestions) > 0 {
|
||||||
|
msg += "Remaining questions:\n - " + strings.Join(k.RemainingQuestions, "\n - ") + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
return gollm.Message{
|
||||||
|
Role: gollm.RoleSystem,
|
||||||
|
Text: msg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToMessage converts the knowledge to a message that can be sent to the LLM.
|
||||||
|
func (k Knowledge) ToMessage() string {
|
||||||
|
var learned []string
|
||||||
|
for _, t := range k.Knowledge {
|
||||||
|
learned = append(learned, t.Info)
|
||||||
|
}
|
||||||
|
return "Original questions asked:\n" + strings.Join(k.OriginalQuestions, "\n") + "\n" +
|
||||||
|
"Learned information:\n" + strings.Join(learned, "\n") + "\n" +
|
||||||
|
"Remaining questions:\n" + strings.Join(k.RemainingQuestions, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Absorb adds the knowledge from the other knowledge objects to this one.
|
||||||
|
// The OriginalQuestions returned will be from value of k's OriginalQuestions.
|
||||||
|
// If any absorbed knowledge has any "CurrentObjectives" set they will explicitly overwrite the current objectives in
|
||||||
|
// aggregate, all CurrentObjectives from all absorbed knowledge will be used.
|
||||||
|
// Any new Knowledge or NotesToSelf will be appended to the current Knowledge and NotesToSelf.
|
||||||
|
func (k Knowledge) Absorb(o ...Knowledge) Knowledge {
|
||||||
|
res := k
|
||||||
|
|
||||||
|
var newObjectives []string
|
||||||
|
|
||||||
|
for _, ok := range o {
|
||||||
|
if len(ok.CurrentObjectives) > 0 {
|
||||||
|
newObjectives = append(newObjectives, ok.CurrentObjectives...)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.NotesToSelf = append(res.NotesToSelf, ok.NotesToSelf...)
|
||||||
|
res.Knowledge = append(res.Knowledge, ok.Knowledge...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newObjectives) > 0 {
|
||||||
|
res.CurrentObjectives = newObjectives
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
87
pkg/agents/knowledge_integrate.go
Normal file
87
pkg/agents/knowledge_integrate.go
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
}
|
150
pkg/agents/knowledge_processor.go
Normal file
150
pkg/agents/knowledge_processor.go
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
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
|
||||||
|
}
|
32
pkg/agents/question_splitter.go
Normal file
32
pkg/agents/question_splitter.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
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
|
||||||
|
}
|
72
pkg/agents/read_page.go
Normal file
72
pkg/agents/read_page.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package agents
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"gitea.stevedudenhoeffer.com/steve/answer/pkg/cache"
|
||||||
|
"gitea.stevedudenhoeffer.com/steve/answer/pkg/extractor"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a Agent) ReadPage(ctx context.Context, u *url.URL, questions []string) (Knowledge, error) {
|
||||||
|
ar, err := extractArticle(ctx, u)
|
||||||
|
if err != nil {
|
||||||
|
return Knowledge{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ar.Body == "" {
|
||||||
|
return Knowledge{}, fmt.Errorf("could not extract body from page")
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.ExtractKnowledge(ctx, ar.Body, u.String(), questions)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type article struct {
|
||||||
|
URL string
|
||||||
|
Title string
|
||||||
|
Body string
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractArticle(ctx context.Context, u *url.URL) (res article, err error) {
|
||||||
|
defer func() {
|
||||||
|
e := recover()
|
||||||
|
|
||||||
|
if e != nil {
|
||||||
|
if e, ok := e.(error); ok {
|
||||||
|
err = fmt.Errorf("panic: %w", e)
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("panic: %v", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
extractors := extractor.MultiExtractor(
|
||||||
|
extractor.CacheExtractor{
|
||||||
|
Cache: cache.Nop{},
|
||||||
|
Tag: "goose",
|
||||||
|
Extractor: extractor.GooseExtractor{},
|
||||||
|
},
|
||||||
|
extractor.CacheExtractor{
|
||||||
|
Cache: cache.Nop{},
|
||||||
|
Tag: "playwright",
|
||||||
|
Extractor: extractor.PlaywrightExtractor{},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
a, err := extractors.Extract(ctx, u.String())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return article{
|
||||||
|
URL: "",
|
||||||
|
Title: "",
|
||||||
|
Body: "",
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return article{
|
||||||
|
URL: a.URL,
|
||||||
|
Title: a.Title,
|
||||||
|
Body: a.Body,
|
||||||
|
}, nil
|
||||||
|
}
|
227
pkg/agents/search.go
Normal file
227
pkg/agents/search.go
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
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."})
|
||||||
|
}
|
54
pkg/agents/search_terms.go
Normal file
54
pkg/agents/search_terms.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
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
|
||||||
|
}
|
163
pkg/agents/shared/knowledgeworker.go
Normal file
163
pkg/agents/shared/knowledgeworker.go
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
}
|
42
pkg/agents/shared/modeltracker.go
Normal file
42
pkg/agents/shared/modeltracker.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
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)
|
||||||
|
}
|
29
pkg/agents/steps.go
Normal file
29
pkg/agents/steps.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package agents
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Steps is a utility function that will ask the LLM to split a goal into smaller steps.
|
||||||
|
// It is a utility function and as such it does not use the agent's set system prompts, it will however use any
|
||||||
|
// contextual information that it has.
|
||||||
|
func (a Agent) Steps(ctx context.Context, goal string) ([]string, error) {
|
||||||
|
var res []string
|
||||||
|
fnSteps := gollm.NewFunction(
|
||||||
|
"steps",
|
||||||
|
"split the provided goal by the user into sub-steps",
|
||||||
|
func(ctx *gollm.Context, args struct {
|
||||||
|
Steps []string `description:"The steps that should be taken to achieve the goal"`
|
||||||
|
}) (any, error) {
|
||||||
|
res = append(res, args.Steps...)
|
||||||
|
return "", nil
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err := a.WithSystemPrompt(`The user is going to mention a goal to you, and you are to respond using only one call of the "steps" function. You should provide the "steps" function with steps that should be taken to achieve the goal the user requests.`).
|
||||||
|
WithSystemPromptSuffix(``).
|
||||||
|
WithToolbox(gollm.NewToolBox(fnSteps)).
|
||||||
|
CallAndExecute(ctx, goal)
|
||||||
|
|
||||||
|
return res, err
|
||||||
|
}
|
27
pkg/agents/tools/calculator.go
Normal file
27
pkg/agents/tools/calculator.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
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
|
||||||
|
})
|
34
pkg/agents/tools/wolfram.go
Normal file
34
pkg/agents/tools/wolfram.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
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)
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
104
pkg/agents/youtube.go
Normal file
104
pkg/agents/youtube.go
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
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)
|
||||||
|
}
|
@ -4,12 +4,17 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/url"
|
|
||||||
"strings"
|
"gitea.stevedudenhoeffer.com/steve/go-extractor"
|
||||||
|
|
||||||
|
"github.com/Edw590/go-wolfram"
|
||||||
|
"go.starlark.net/lib/math"
|
||||||
|
"go.starlark.net/starlark"
|
||||||
|
"go.starlark.net/syntax"
|
||||||
|
|
||||||
"gitea.stevedudenhoeffer.com/steve/answer/pkg/cache"
|
"gitea.stevedudenhoeffer.com/steve/answer/pkg/cache"
|
||||||
"gitea.stevedudenhoeffer.com/steve/answer/pkg/extractor"
|
|
||||||
"gitea.stevedudenhoeffer.com/steve/answer/pkg/search"
|
"gitea.stevedudenhoeffer.com/steve/answer/pkg/search"
|
||||||
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
|
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
|
||||||
)
|
)
|
||||||
@ -32,16 +37,19 @@ type Question struct {
|
|||||||
// Answers is a list of answers to a question
|
// Answers is a list of answers to a question
|
||||||
type Answers []string
|
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 {
|
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
|
// 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.
|
// search up to 5 possible times to find an answer.
|
||||||
MaxSearches int
|
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
|
// 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.
|
// 5 pages are tried and no answers are found, the function will return ErrMaxTries.
|
||||||
MaxTries int
|
MaxTries int
|
||||||
@ -49,12 +57,30 @@ type Options struct {
|
|||||||
// OnNewFunction is a callback that, if non-nil, will be called when a new function is called by the LLM.
|
// 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.
|
// The "answer" and "no_answer" functions are not included in this callback.
|
||||||
// Return an error to stop the function from being called.
|
// Return an error to stop the function from being called.
|
||||||
OnNewFunction func(ctx context.Context, funcName string, question string, parameter string) error
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
var DefaultOptions = Options{
|
var DefaultOptions = Options{
|
||||||
MaxSearches: 5,
|
MaxSearches: 10,
|
||||||
MaxThinks: 10,
|
|
||||||
MaxTries: 5,
|
MaxTries: 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,379 +89,286 @@ type Result struct {
|
|||||||
Error error
|
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 {
|
type article struct {
|
||||||
URL string
|
URL string
|
||||||
Title string
|
Title string
|
||||||
Body string
|
Body string
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractArticle(ctx context.Context, c cache.Cache, u *url.URL) (res article, err error) {
|
type Response struct {
|
||||||
defer func() {
|
Text string
|
||||||
e := recover()
|
Sources []string
|
||||||
|
|
||||||
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 context.Context, q Question, text string) (string, error) {
|
func deferClose(cl io.Closer) {
|
||||||
fnAnswer := gollm.NewFunction(
|
if cl != nil {
|
||||||
"answer",
|
_ = cl.Close()
|
||||||
"The answer from the given text that answers the question.",
|
|
||||||
func(ctx context.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 context.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])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func functionSearch(ctx context.Context, q Question, searchTerm string) (string, error) {
|
func (o Options) Answer(ctx context.Context, q Question) (Response, error) {
|
||||||
|
var answer Response
|
||||||
|
|
||||||
slog.Info("searching", "search", searchTerm, "question", q)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
res, err := q.Search.Search(ctx, searchTerm)
|
defer cancel()
|
||||||
|
|
||||||
|
b, err := extractor.NewPlayWrightBrowser(extractor.PlayWrightBrowserOptions{
|
||||||
|
DarkMode: true,
|
||||||
|
})
|
||||||
|
defer deferClose(b)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return answer, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(res) == 0 {
|
ctx = context.WithValue(ctx, "browser", b)
|
||||||
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 err != nil {
|
|
||||||
slog.Error("error checking if text answers question", "question", q.Question, "error", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if answer != "" {
|
|
||||||
return answer, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func functionThink(ctx context.Context, q Question) (string, error) {
|
|
||||||
fnAnswer := gollm.NewFunction(
|
|
||||||
"answer",
|
|
||||||
"Answer the question.",
|
|
||||||
func(ctx context.Context, args struct {
|
|
||||||
Answer string `description:"the answer to the question"`
|
|
||||||
}) (string, error) {
|
|
||||||
return args.Answer, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
var temp float32 = 0.8
|
|
||||||
req := gollm.Request{
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
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])
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o Options) Answer(ctx context.Context, q Question) (Answers, error) {
|
|
||||||
fnSearch := gollm.NewFunction(
|
fnSearch := gollm.NewFunction(
|
||||||
"search",
|
"search",
|
||||||
"Search the web for an answer to a question. You can call this function up to "+fmt.Sprint(o.MaxSearches)+" times.",
|
"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 context.Context, args struct {
|
func(ctx *gollm.Context, args struct {
|
||||||
SearchQuery string `description:"what to search the web for for this question"`
|
Query string `description:"search the web with this, such as: 'capital of the united states site:wikipedia.org'"`
|
||||||
Question string `description:"what question(s) you are trying to answer with this search"`
|
Question string `description:"when reading the results, what question(s) are you trying to answer?"`
|
||||||
}) (string, error) {
|
}) (string, error) {
|
||||||
q2 := q
|
q2 := q
|
||||||
q2.Question = args.Question
|
q2.Question = args.Question
|
||||||
|
|
||||||
return functionSearch(ctx, q2, args.SearchQuery)
|
if o.MaxSearches > 0 {
|
||||||
})
|
o.MaxSearches = o.MaxSearches - 1
|
||||||
|
}
|
||||||
|
res, err := functionSearch2(ctx, q2, args.Query)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
fnThink := gollm.NewFunction(
|
return res.String()
|
||||||
"think",
|
|
||||||
"Think about a question. This is useful for breaking down complex questions into smaller parts that are easier to answer.",
|
|
||||||
func(ctx context.Context, args struct {
|
|
||||||
Question string `json:"question" description:"the question to think about"`
|
|
||||||
}) (string, error) {
|
|
||||||
q2 := q
|
|
||||||
q2.Question = args.Question
|
|
||||||
|
|
||||||
return functionThink(ctx, q2)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
fnAnswer := gollm.NewFunction(
|
fnAnswer := gollm.NewFunction(
|
||||||
"answer",
|
"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",
|
"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 context.Context, args struct {
|
func(ctx *gollm.Context, args struct {
|
||||||
Answer string `json:"answer" description:"the answer to the question"`
|
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)"`
|
||||||
}) (string, error) {
|
}) (string, error) {
|
||||||
|
answer.Text = args.Answer
|
||||||
|
answer.Sources = args.Sources
|
||||||
return args.Answer, nil
|
return args.Answer, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
var funcs = []*gollm.Function{fnAnswer}
|
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 {
|
if o.MaxSearches > 0 {
|
||||||
funcs = append(funcs, fnSearch)
|
funcs = append(funcs, fnSearch)
|
||||||
}
|
}
|
||||||
|
|
||||||
if o.MaxThinks > 0 {
|
|
||||||
funcs = append(funcs, fnThink)
|
|
||||||
}
|
|
||||||
|
|
||||||
var temp float32 = 0.8
|
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{
|
req := gollm.Request{
|
||||||
Messages: []gollm.Message{
|
Messages: messages,
|
||||||
{
|
|
||||||
Role: gollm.RoleSystem,
|
|
||||||
Text: "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.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Role: gollm.RoleUser,
|
|
||||||
Text: q.Question,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Toolbox: gollm.NewToolBox(funcs...),
|
Toolbox: gollm.NewToolBox(funcs...),
|
||||||
Temperature: &temp,
|
Temperature: &temp,
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := q.Model.ChatComplete(ctx, req)
|
// 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) {
|
||||||
|
|
||||||
if err != nil {
|
slog.Info("running answer", "question", q.Question, "req", req)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(res.Choices) == 0 {
|
for _, c := range req.Conversation {
|
||||||
return nil, nil
|
slog.Info("conversation", "conversation", c)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(res.Choices) > o.MaxSearches {
|
for _, m := range req.Messages {
|
||||||
res.Choices = res.Choices[:o.MaxSearches]
|
slog.Info("message", "message", m)
|
||||||
}
|
}
|
||||||
|
|
||||||
var answers []Result
|
res, err := q.Model.ChatComplete(ctx, req)
|
||||||
for _, choice := range res.Choices {
|
|
||||||
fnChoice := func(choice gollm.ResponseChoice) []Result {
|
|
||||||
var calls []Result
|
|
||||||
var callsOutput = make(chan Result, len(choice.Calls))
|
|
||||||
fnCall := func(call gollm.ToolCall) Result {
|
|
||||||
str, err := req.Toolbox.Execute(ctx, call)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Result{
|
return nil, err
|
||||||
Error: err,
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result{
|
if len(res.Choices) == 0 {
|
||||||
Result: str,
|
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, call := range choice.Calls {
|
return gollm.ToolCallResponse{
|
||||||
go func(call gollm.ToolCall) {
|
ID: call.ID,
|
||||||
if o.OnNewFunction != nil {
|
Result: str,
|
||||||
err := o.OnNewFunction(ctx, call.FunctionCall.Name, q.Question, call.FunctionCall.Arguments)
|
}
|
||||||
if err != nil {
|
}
|
||||||
callsOutput <- Result{
|
|
||||||
Error: err,
|
for _, call := range choice.Calls {
|
||||||
}
|
go func(call gollm.ToolCall) {
|
||||||
return
|
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
|
||||||
}
|
}
|
||||||
callsOutput <- fnCall(call)
|
}
|
||||||
}(call)
|
callRes := fnCall(call)
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < len(choice.Calls); i++ {
|
if o.OnFunctionFinished != nil {
|
||||||
result := <-callsOutput
|
err = o.OnFunctionFinished(ctx, call.FunctionCall.Name, q.Question, call.FunctionCall.Arguments, callRes.Result, callRes.Error, arg)
|
||||||
calls = append(calls, result)
|
if err != nil {
|
||||||
}
|
callsOutput <- gollm.ToolCallResponse{
|
||||||
|
ID: call.ID,
|
||||||
|
Error: err,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
close(callsOutput)
|
callsOutput <- callRes
|
||||||
|
}(call)
|
||||||
slog.Info("calls", "calls", calls)
|
|
||||||
return calls
|
|
||||||
}
|
}
|
||||||
|
|
||||||
answers = append(answers, fnChoice(choice)...)
|
for i := 0; i < len(choice.Calls); i++ {
|
||||||
}
|
result := <-callsOutput
|
||||||
|
answers = append(answers, result)
|
||||||
var errs []error
|
|
||||||
var results []string
|
|
||||||
|
|
||||||
for _, answer := range answers {
|
|
||||||
if answer.Error != nil {
|
|
||||||
errs = append(errs, answer.Error)
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
results = append(results, answer.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 len(errs) > 0 {
|
maxTries := o.MaxTries
|
||||||
return nil, errors.Join(errs...)
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
return results, nil
|
return answer, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Answer(ctx context.Context, q Question) (Answers, error) {
|
func Answer(ctx context.Context, q Question) (Response, error) {
|
||||||
return DefaultOptions.Answer(ctx, q)
|
return DefaultOptions.Answer(ctx, q)
|
||||||
}
|
}
|
||||||
|
109
pkg/answer/extractor.go
Normal file
109
pkg/answer/extractor.go
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
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])
|
||||||
|
}
|
653
pkg/answer/search.go
Normal file
653
pkg/answer/search.go
Normal file
@ -0,0 +1,653 @@
|
|||||||
|
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
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user