Add core implementation for AI-powered question answering

Introduce multiple agents, tools, and utilities for processing, extracting, and answering user-provided questions using LLMs and external data. Key features include knowledge processing, question splitting, search term generation, and contextual knowledge handling.
This commit is contained in:
2025-03-21 11:10:48 -04:00
parent 20bcaefaa2
commit 693ac4e6a7
18 changed files with 1893 additions and 18 deletions

View File

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

View File

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