restructured answers a bit

This commit is contained in:
Steve Dudenhoeffer 2025-03-01 01:25:34 -05:00
parent 090b28d956
commit ff1c369772
11 changed files with 114 additions and 43 deletions

26
go.mod
View File

@ -9,11 +9,14 @@ 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-20250123020607-964a98a5a884
gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250123045620-0d909edd44d9 gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250123045620-0d909edd44d9
github.com/Edw590/go-wolfram v0.0.0-20241010091529-fb9031908c5d
github.com/advancedlogic/GoOse v0.0.0-20231203033844-ae6b36caf275 github.com/advancedlogic/GoOse v0.0.0-20231203033844-ae6b36caf275
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/playwright-community/playwright-go v0.5001.0
github.com/rocketlaunchr/google-search v1.1.6 github.com/rocketlaunchr/google-search v1.1.6
github.com/tmc/langchaingo v0.1.13
github.com/urfave/cli v1.22.16 github.com/urfave/cli v1.22.16
golang.org/x/sync v0.11.0
) )
require ( require (
@ -23,7 +26,9 @@ require (
cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect
cloud.google.com/go/compute/metadata v0.6.0 // indirect cloud.google.com/go/compute/metadata v0.6.0 // indirect
cloud.google.com/go/longrunning v0.6.4 // indirect cloud.google.com/go/longrunning v0.6.4 // indirect
github.com/Edw590/go-wolfram v0.0.0-20241010091529-fb9031908c5d // indirect github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.0 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/PuerkitoBio/goquery v1.10.2 // indirect github.com/PuerkitoBio/goquery v1.10.2 // 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
@ -33,6 +38,7 @@ require (
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.7.0 // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect github.com/dlclark/regexp2 v1.10.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/set v0.2.1 // indirect github.com/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
@ -53,31 +59,44 @@ require (
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.4 // indirect
github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/googleapis/gax-go/v2 v2.14.1 // indirect
github.com/goph/emperror v0.17.2 // indirect
github.com/huandu/xstrings v1.3.3 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 // indirect github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kennygrant/sanitize v1.2.4 // indirect github.com/kennygrant/sanitize v1.2.4 // indirect
github.com/liushuangls/go-anthropic/v2 v2.13.1 // indirect github.com/liushuangls/go-anthropic/v2 v2.13.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mitchellh/copystructure v1.0.0 // indirect
github.com/mitchellh/reflectwalk v1.0.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/nikolalohinski/gonja v1.5.3 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pkoukk/tiktoken-go v0.1.6 // indirect github.com/pkoukk/tiktoken-go v0.1.6 // 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.37.0 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/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/tmc/langchaingo v0.1.13 // indirect github.com/yargevad/filepathx v1.0.0 // 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.59.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect
go.opentelemetry.io/otel v1.34.0 // indirect go.opentelemetry.io/otel v1.34.0 // indirect
go.opentelemetry.io/otel/metric v1.34.0 // indirect go.opentelemetry.io/otel/metric v1.34.0 // indirect
go.opentelemetry.io/otel/trace v1.34.0 // indirect go.opentelemetry.io/otel/trace v1.34.0 // indirect
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 // indirect
golang.org/x/crypto v0.34.0 // indirect golang.org/x/crypto v0.34.0 // indirect
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect
golang.org/x/net v0.35.0 // indirect golang.org/x/net v0.35.0 // indirect
golang.org/x/oauth2 v0.26.0 // indirect golang.org/x/oauth2 v0.26.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect golang.org/x/text v0.22.0 // indirect
golang.org/x/time v0.10.0 // indirect golang.org/x/time v0.10.0 // indirect
@ -87,4 +106,5 @@ require (
google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 // indirect
google.golang.org/grpc v1.70.0 // indirect google.golang.org/grpc v1.70.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

View File

@ -6,6 +6,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/toolbox"
"github.com/tmc/langchaingo/llms" "github.com/tmc/langchaingo/llms"
cache2 "gitea.stevedudenhoeffer.com/steve/answer/pkg/cache" cache2 "gitea.stevedudenhoeffer.com/steve/answer/pkg/cache"
@ -60,7 +62,7 @@ var (
) )
func ask(ctx *Context, q Question) (Answers, error) { func ask(ctx *Context, q Question) (Answers, error) {
var tb = ToolBox{} var tb = toolbox.ToolBox{}
if ctx.Agent.RemainingSearches.Load() > 0 { if ctx.Agent.RemainingSearches.Load() > 0 {
tb.Register(SearchTool) tb.Register(SearchTool)
@ -71,11 +73,11 @@ func ask(ctx *Context, q Question) (Answers, error) {
return tb.Run(ctx, q) return tb.Run(ctx, q)
} }
var SummarizeAnswers = FromFunction( var SummarizeAnswers = toolbox.FromFunction(
func(ctx *Context, args struct { func(ctx *Context, args struct {
Summary string `description:"the summary of the answers"` Summary string `description:"the summary of the answers"`
}) (FuncResponse, error) { }) (toolbox.FuncResponse, error) {
return FuncResponse{Result: args.Summary}, nil return toolbox.FuncResponse{Result: args.Summary}, nil
}). }).
WithName("summarize_answers"). WithName("summarize_answers").
WithDescription(`You are given previously figured out answers and they are in the format of: [ { "answer": "the answer", "source": "the source of the answer" }, { "answer": "answer 2", "source": "the source for answer2" } ]. You need to summarize the answers into a single string. Be sure to make the summary clear and concise, but include the sources at some point.`) WithDescription(`You are given previously figured out answers and they are in the format of: [ { "answer": "the answer", "source": "the source of the answer" }, { "answer": "answer 2", "source": "the source for answer2" } ]. You need to summarize the answers into a single string. Be sure to make the summary clear and concise, but include the sources at some point.`)
@ -92,7 +94,7 @@ func (a *Agent) Ask(ctx context.Context, q Question) (string, error) {
return "", err return "", err
} }
tb := ToolBox{} tb := toolbox.ToolBox{}
tb.Register(SummarizeAnswers) tb.Register(SummarizeAnswers)

View File

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

View File

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

View File

@ -4,6 +4,8 @@ import (
"context" "context"
"time" "time"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/toolbox"
"github.com/tmc/langchaingo/llms" "github.com/tmc/langchaingo/llms"
) )
@ -35,7 +37,7 @@ func (c *Context) WithMessages(m ...llms.MessageContent) *Context {
return c return c
} }
func (c *Context) WithToolResults(r ...ToolResult) *Context { func (c *Context) WithToolResults(r ...toolbox.ToolResult) *Context {
msg := llms.MessageContent{ msg := llms.MessageContent{
Role: llms.ChatMessageTypeTool, Role: llms.ChatMessageTypeTool,
} }

View File

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

View File

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

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

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

View File

@ -1,6 +1,7 @@
package agent package toolbox
import ( import (
"context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@ -190,14 +191,14 @@ func analyzeFuncFromReflect(fn reflect.Value) (function, error) {
return res, nil return res, nil
} }
func analyzeFunction[T any](fn func(*Context, T) (FuncResponse, error)) (function, error) { func analyzeFunction[T any, AgentContext any](fn func(AgentContext, T) (FuncResponse, error)) (function, error) {
return analyzeFuncFromReflect(reflect.ValueOf(fn)) return analyzeFuncFromReflect(reflect.ValueOf(fn))
} }
// Execute will execute the given function with the given context and arguments. // Execute will execute the given function with the given context and arguments.
// Returns the result of the execution and an error if the operation fails. // Returns the result of the execution and an error if the operation fails.
// The arguments must be a JSON-encoded string that represents the struct to be passed to the function. // The arguments must be a JSON-encoded string that represents the struct to be passed to the function.
func (f function) Execute(ctx *Context, args string) (FuncResponse, error) { func (f function) Execute(ctx context.Context, args string) (FuncResponse, error) {
var m = map[string]any{} var m = map[string]any{}
err := json.Unmarshal([]byte(args), &m) err := json.Unmarshal([]byte(args), &m)

View File

@ -1,6 +1,7 @@
package agent package toolbox
import ( import (
"context"
"reflect" "reflect"
"github.com/tmc/langchaingo/llms" "github.com/tmc/langchaingo/llms"
@ -36,11 +37,11 @@ func (t *Tool) Definition() *llms.FunctionDefinition {
// Execute executes the tool with the given context and arguments. // Execute executes the tool with the given context and arguments.
// Returns the result of the execution and an error if the operation fails. // Returns the result of the execution and an error if the operation fails.
// The arguments must be a JSON-encoded string that represents the struct to be passed to the function. // The arguments must be a JSON-encoded string that represents the struct to be passed to the function.
func (t *Tool) Execute(ctx *Context, args string) (FuncResponse, error) { func (t *Tool) Execute(ctx context.Context, args string) (FuncResponse, error) {
return t.Function.Execute(ctx, args) return t.Function.Execute(ctx, args)
} }
func FromFunction[T any](fn func(*Context, T) (FuncResponse, error)) *Tool { func FromFunction[T any, AgentContext any](fn func(AgentContext, T) (FuncResponse, error)) *Tool {
f, err := analyzeFunction(fn) f, err := analyzeFunction(fn)
if err != nil { if err != nil {
panic(err) panic(err)

View File

@ -1,6 +1,7 @@
package agent package toolbox
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
@ -52,7 +53,7 @@ func (r ToolResult) ToToolCallResponse() llms.ToolCallResponse {
Content: r.Result, Content: r.Result,
} }
} }
func (tb ToolBox) Execute(ctx *Context, call llms.ToolCall) (ToolResult, error) { func (tb ToolBox) Execute(ctx context.Context, call llms.ToolCall) (ToolResult, error) {
if call.Type != "function" { if call.Type != "function" {
return ToolResult{}, fmt.Errorf("unsupported tool type: %s", call.Type) return ToolResult{}, fmt.Errorf("unsupported tool type: %s", call.Type)
} }
@ -67,6 +68,14 @@ func (tb ToolBox) Execute(ctx *Context, call llms.ToolCall) (ToolResult, error)
} }
res, err := tool.Execute(ctx, call.FunctionCall.Arguments) res, err := tool.Execute(ctx, call.FunctionCall.Arguments)
if err != nil {
return ToolResult{
ID: call.ID,
Name: tool.Name,
Error: err,
Source: res.Source,
}, nil
}
return ToolResult{ return ToolResult{
ID: call.ID, ID: call.ID,
@ -77,7 +86,7 @@ func (tb ToolBox) Execute(ctx *Context, call llms.ToolCall) (ToolResult, error)
}, nil }, nil
} }
func (tb ToolBox) ExecuteAll(ctx *Context, calls []llms.ToolCall) (ToolResults, error) { func (tb ToolBox) ExecuteAll(ctx Context, calls []llms.ToolCall) (ToolResults, error) {
var results []ToolResult var results []ToolResult
for _, call := range calls { for _, call := range calls {
@ -92,7 +101,7 @@ func (tb ToolBox) ExecuteAll(ctx *Context, calls []llms.ToolCall) (ToolResults,
return results, nil return results, nil
} }
func (tb ToolBox) ExecuteConcurrent(ctx *Context, calls []llms.ToolCall) (ToolResults, error) { func (tb ToolBox) ExecuteConcurrent(ctx Context, calls []llms.ToolCall) (ToolResults, error) {
var results []ToolResult var results []ToolResult
var ch = make(chan ToolResult, len(calls)) var ch = make(chan ToolResult, len(calls))
var eg = errgroup.Group{} var eg = errgroup.Group{}
@ -124,13 +133,24 @@ func (tb ToolBox) ExecuteConcurrent(ctx *Context, calls []llms.ToolCall) (ToolRe
return results, nil return results, nil
} }
func (tb ToolBox) Run(ctx *Context, q Question) (Answers, error) { type Answers struct {
ctx.Messages = append(ctx.Messages, llms.MessageContent{ Response llms.MessageContent
Role: llms.ChatMessageTypeGeneric, Answers []Answer
Parts: []llms.ContentPart{llms.TextPart(q.Question)}, }
})
res, err := ctx.Agent.Model.GenerateContent(ctx, ctx.Messages) type Answer struct {
Answer string
Source string
ToolCallResponse llms.ToolCallResponse `json:"-"`
}
func (tb ToolBox) Run(ctx Context, model llms.Model, question string) (Answers, error) {
ctx = ctx.WithMessages([]llms.MessageContent{{
Role: llms.ChatMessageTypeGeneric,
Parts: []llms.ContentPart{llms.TextPart(question)},
}})
res, err := model.GenerateContent(ctx, ctx.GetMessages())
if err != nil { if err != nil {
return Answers{}, err return Answers{}, err
} }