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:
46
pkg/agents/reader/agent.go
Normal file
46
pkg/agents/reader/agent.go
Normal 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)
|
||||
}
|
142
pkg/agents/reader/extractor.go
Normal file
142
pkg/agents/reader/extractor.go
Normal 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
|
||||
}
|
Reference in New Issue
Block a user