diff --git a/go.mod b/go.mod index a9361c3..237ab8e 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ replace github.com/rocketlaunchr/google-search => github.com/chrisjoyce911/googl require ( gitea.stevedudenhoeffer.com/steve/go-extractor v0.0.0-20250315044602-7c0e44a22f2c - gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250317023858-7f5e34e437a7 + gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250317041832-2737a5b2be93 github.com/Edw590/go-wolfram v0.0.0-20241010091529-fb9031908c5d github.com/advancedlogic/GoOse v0.0.0-20231203033844-ae6b36caf275 github.com/joho/godotenv v1.5.1 diff --git a/pkg/answer/answer.go b/pkg/answer/answer.go index 7243838..953e63a 100644 --- a/pkg/answer/answer.go +++ b/pkg/answer/answer.go @@ -4,13 +4,13 @@ import ( "context" "errors" "fmt" + "log/slog" + "net/url" + "github.com/Edw590/go-wolfram" "go.starlark.net/lib/math" "go.starlark.net/starlark" "go.starlark.net/syntax" - "log/slog" - "net/url" - "strings" "gitea.stevedudenhoeffer.com/steve/answer/pkg/cache" "gitea.stevedudenhoeffer.com/steve/answer/pkg/extractor" @@ -49,11 +49,6 @@ type Options struct { // search up to 5 possible times to find an answer. MaxSearches int - // MaxThinks is the maximum number of times to think about a question. A "Think" is different than a search in that - // the LLM just breaks the question down into smaller parts and tries to answer them. This is useful for complex - // questions that are hard to answer since LLMs are better at answering smaller questions. - MaxThinks int - // MaxTries is the absolute maximum number of pages to try to get an answer from. For instance, if MaxSearches is 5 and // 5 pages are tried and no answers are found, the function will return ErrMaxTries. MaxTries int @@ -78,8 +73,7 @@ type Options struct { } var DefaultOptions = Options{ - MaxSearches: 5, - MaxThinks: 10, + MaxSearches: 10, MaxTries: 5, } @@ -191,131 +185,23 @@ func doesTextAnswerQuestion(ctx *gollm.Context, q Question, text string) (string return req.Toolbox.Execute(ctx, res.Choices[0].Calls[0]) } -func functionSearch(ctx *gollm.Context, q Question, searchTerm string) (string, error) { - slog.Info("searching", "search", searchTerm, "question", q) - res, err := q.Search.Search(ctx, searchTerm) - if err != nil { - return "", err - } - - if len(res) == 0 { - return "", nil - } - - // first pass try to see if any provide the result without needing archive - for _, r := range res { - trimmed := strings.TrimSpace(r.URL) - if trimmed == "" { - - } - - slog.Info("extracting article", "url", trimmed) - u, err := url.Parse(trimmed) - if err != nil { - continue - } - - a, err := extractArticle(ctx, q.Cache, u) - - if err != nil { - continue - } - - slog.Info("extracted article", "url", a.URL, "title", a.Title, "body", a.Body) - - if a.Title != "" && a.Body != "" { - answer, err := doesTextAnswerQuestion(ctx, q, a.Body) - - if 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 *gollm.Context, q Question) (string, error) { - fnAnswer := gollm.NewFunction( - "answer", - "Answer the question.", - func(ctx *gollm.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) (string, error) { var answer string fnSearch := gollm.NewFunction( "search", - "Search the web for an answer to a question. You can call this function up to "+fmt.Sprint(o.MaxSearches)+" times.", + "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 {"url": "https://example.com", "answer": "the answer to the question"}. If a previous call to search produced no results, do not re-search with just reworded search terms, try a different approach.`, func(ctx *gollm.Context, args struct { - SearchQuery string `description:"what to search the web for for this question"` - Question string `description:"what question(s) you are trying to answer with this search"` + Query string `description:"search the web with this, such as: 'capital of the united states site:wikipedia.org'"` + Question string `description:"when reading the results, what question(s) are you trying to answer?"` }) (string, error) { q2 := q q2.Question = args.Question - if o.MaxThinks > 0 { + if o.MaxSearches > 0 { o.MaxSearches = o.MaxSearches - 1 } - return functionSearch(ctx, q2, args.SearchQuery) - }) - - fnThink := gollm.NewFunction( - "think", - "Think about a question. This is useful for breaking down complex questions into smaller parts that are easier to answer.", - func(ctx *gollm.Context, args struct { - Question string `json:"question" description:"the question to think about"` - }) (string, error) { - q2 := q - q2.Question = args.Question - - if o.MaxThinks > 0 { - o.MaxThinks = o.MaxThinks - 1 - } - - return functionThink(ctx, q2) + return functionSearch(ctx, q2, args.Query) }) fnAnswer := gollm.NewFunction( @@ -361,8 +247,19 @@ func (o Options) Answer(ctx context.Context, q Question) (string, error) { return v.String(), nil }) - var funcs = []*gollm.Function{fnAnswer, fnCalculate} + 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 = "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) } @@ -371,10 +268,6 @@ func (o Options) Answer(ctx context.Context, q Question) (string, error) { funcs = append(funcs, fnSearch) } - if o.MaxThinks > 0 { - funcs = append(funcs, fnThink) - } - var temp float32 = 0.8 var messages []gollm.Message @@ -430,14 +323,15 @@ func (o Options) Answer(ctx context.Context, q Question) (string, error) { if err != nil { return nil, err } - if len(res.Choices) == 0 { - return nil, fmt.Errorf("no response choices provided") - } if len(res.Choices) > o.MaxSearches { res.Choices = res.Choices[:o.MaxSearches] } + if len(res.Choices) == 0 { + return nil, fmt.Errorf("no response choices provided") + } + var answers []gollm.ToolCallResponse choice := res.Choices[0] @@ -490,6 +384,19 @@ func (o Options) Answer(ctx context.Context, q Question) (string, error) { maxTries := o.MaxTries for i := 0; i < maxTries; i++ { + // rework this run's functions incase MaxSearches etc. have changed + var funcs2 = baseFuncs + + if fnWolfram != nil { + funcs2 = append(funcs2, fnWolfram) + } + + if o.MaxSearches > 0 { + funcs2 = append(funcs2, fnSearch) + } + + req.Toolbox = gollm.NewToolBox(funcs2...) + newReq, err := runAnswer(o, req) if err != nil { diff --git a/pkg/answer/search.go b/pkg/answer/search.go new file mode 100644 index 0000000..ee77f47 --- /dev/null +++ b/pkg/answer/search.go @@ -0,0 +1,82 @@ +package answer + +import ( + "encoding/json" + "log/slog" + "net/url" + "strings" + + gollm "gitea.stevedudenhoeffer.com/steve/go-llm" +) + +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 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 + } + + // first pass try to see if any provide the result without needing archive + for _, r := range results { + 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 searchResults{Url: u.String(), Answer: answer}, nil + } + } + } + + return searchResults{Url: "not-found", Answer: "no searched results answered"}, nil +} + +func functionSearch(ctx *gollm.Context, q Question, searchTerm string) (string, error) { + res, err := internalSearch(ctx, q, searchTerm) + if err != nil { + return "", err + } + + return res.String() +}