Add long term memory

This commit is contained in:
mudler
2024-12-17 19:53:07 +01:00
parent d053dc249c
commit 2561f2f175
6 changed files with 104 additions and 47 deletions

View File

@@ -68,6 +68,7 @@ func (a *Agent) decision(
Tools: tools, Tools: tools,
ToolChoice: toolchoice, ToolChoice: toolchoice,
} }
resp, err := a.client.CreateChatCompletion(ctx, decision) resp, err := a.client.CreateChatCompletion(ctx, decision)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -96,6 +97,14 @@ func (m Messages) ToOpenAI() []openai.ChatCompletionMessage {
return []openai.ChatCompletionMessage(m) return []openai.ChatCompletionMessage(m)
} }
func (m Messages) String() string {
s := ""
for _, cc := range m {
s += cc.Role + ": " + cc.Content + "\n"
}
return s
}
func (m Messages) Exist(content string) bool { func (m Messages) Exist(content string) bool {
for _, cc := range m { for _, cc := range m {
if cc.Content == content { if cc.Content == content {

View File

@@ -151,21 +151,59 @@ func (a *Agent) SetConversation(conv []openai.ChatCompletionMessage) {
a.currentConversation = conv a.currentConversation = conv
} }
func (a *Agent) askLLM(ctx context.Context, conversation []openai.ChatCompletionMessage) (openai.ChatCompletionMessage, error) {
resp, err := a.client.CreateChatCompletion(ctx,
openai.ChatCompletionRequest{
Model: a.options.LLMAPI.Model,
Messages: conversation,
},
)
if err != nil {
return openai.ChatCompletionMessage{}, err
}
if len(resp.Choices) != 1 {
return openai.ChatCompletionMessage{}, fmt.Errorf("no enough choices: %w", err)
}
return resp.Choices[0].Message, nil
}
func (a *Agent) ResetConversation() { func (a *Agent) ResetConversation() {
a.Lock() a.Lock()
defer a.Unlock() defer a.Unlock()
xlog.Info("Resetting conversation", "agent", a.Character.Name)
// store into memory the conversation before pruning it // store into memory the conversation before pruning it
// TODO: Shall we summarize the conversation into a bullet list of highlights // TODO: Shall we summarize the conversation into a bullet list of highlights
// using the LLM instead? // using the LLM instead?
if a.options.enableKB { if a.options.enableLongTermMemory {
for _, message := range a.currentConversation { xlog.Info("Saving conversation", "agent", a.Character.Name, "conversation size", len(a.currentConversation))
if message.Role == "user" {
if err := a.options.ragdb.Store(message.Content); err != nil { if a.options.enableSummaryMemory {
xlog.Error("Error storing into memory", "error", err)
msg, err := a.askLLM(a.context.Context, []openai.ChatCompletionMessage{{
Role: "user",
Content: "Summarize the conversation below, keep the highlights as a bullet list:\n" + Messages(a.currentConversation).String(),
}})
if err != nil {
xlog.Error("Error summarizing conversation", "error", err)
}
if err := a.options.ragdb.Store(msg.Content); err != nil {
xlog.Error("Error storing into memory", "error", err)
}
} else {
for _, message := range a.currentConversation {
if message.Role == "user" {
if err := a.options.ragdb.Store(message.Content); err != nil {
xlog.Error("Error storing into memory", "error", err)
}
} }
} }
} }
} }
a.currentConversation = []openai.ChatCompletionMessage{} a.currentConversation = []openai.ChatCompletionMessage{}
@@ -612,34 +650,15 @@ func (a *Agent) consumeJob(job *Job, role string) {
return return
} }
resp, err := a.client.CreateChatCompletion(ctx, msg, err := a.askLLM(ctx, append(a.currentConversation, openai.ChatCompletionMessage{
openai.ChatCompletionRequest{ Role: "system",
Model: a.options.LLMAPI.Model, Content: "The assistant needs to reply without using any tool.",
// Force the AI to response without using any tool }))
// Why: some models might be silly enough to attempt to call tools even if evaluated
// that a reply was not necessary anymore
Messages: append(a.currentConversation, openai.ChatCompletionMessage{
Role: "system",
Content: "The assistant needs to reply without using any tool.",
// + replyResponse.Message,
},
),
//Messages: a.currentConversation,
},
)
if err != nil { if err != nil {
job.Result.Finish(err) job.Result.Finish(err)
return return
} }
if len(resp.Choices) != 1 {
job.Result.Finish(fmt.Errorf("no enough choices: %w", err))
return
}
// display OpenAI's response to the original question utilizing our function
msg := resp.Choices[0].Message
// If we didn't got any message, we can use the response from the action // If we didn't got any message, we can use the response from the action
if chosenAction.Definition().Name.Is(action.ReplyActionName) && msg.Content == "" || if chosenAction.Definition().Name.Is(action.ReplyActionName) && msg.Content == "" ||
strings.Contains(msg.Content, "<tool_call>") { strings.Contains(msg.Content, "<tool_call>") {
@@ -661,9 +680,6 @@ func (a *Agent) periodicallyRun(timer *time.Timer) {
// Remember always to reset the timer - if we don't the agent will stop.. // Remember always to reset the timer - if we don't the agent will stop..
defer timer.Reset(a.options.periodicRuns) defer timer.Reset(a.options.periodicRuns)
if !a.options.standaloneJob {
return
}
a.StopAction() a.StopAction()
xlog.Debug("Agent is running periodically", "agent", a.Character.Name) xlog.Debug("Agent is running periodically", "agent", a.Character.Name)
@@ -675,18 +691,24 @@ func (a *Agent) periodicallyRun(timer *time.Timer) {
xlog.Info("START -- Periodically run is starting") xlog.Info("START -- Periodically run is starting")
if len(a.CurrentConversation()) != 0 { // if len(a.CurrentConversation()) != 0 {
// Here the LLM could decide to store some part of the conversation too in the memory // // Here the LLM could decide to store some part of the conversation too in the memory
evaluateMemory := NewJob( // evaluateMemory := NewJob(
WithText( // WithText(
`Evaluate the current conversation and decide if we need to store some relevant informations from it`, // `Evaluate the current conversation and decide if we need to store some relevant informations from it`,
), // ),
WithReasoningCallback(a.options.reasoningCallback), // WithReasoningCallback(a.options.reasoningCallback),
WithResultCallback(a.options.resultCallback), // WithResultCallback(a.options.resultCallback),
) // )
a.consumeJob(evaluateMemory, SystemRole) // a.consumeJob(evaluateMemory, SystemRole)
// a.ResetConversation()
// }
if !a.options.standaloneJob {
a.ResetConversation() a.ResetConversation()
return
} }
// Here we go in a loop of // Here we go in a loop of

View File

@@ -14,12 +14,12 @@ type llmOptions struct {
} }
type options struct { type options struct {
LLMAPI llmOptions LLMAPI llmOptions
character Character character Character
randomIdentityGuidance string randomIdentityGuidance string
randomIdentity bool randomIdentity bool
userActions Actions userActions Actions
enableHUD, standaloneJob, showCharacter, enableKB bool enableHUD, standaloneJob, showCharacter, enableKB, enableSummaryMemory, enableLongTermMemory bool
canStopItself bool canStopItself bool
initiateConversations bool initiateConversations bool
@@ -127,6 +127,16 @@ var EnablePersonality = func(o *options) error {
return nil return nil
} }
var EnableSummaryMemory = func(o *options) error {
o.enableSummaryMemory = true
return nil
}
var EnableLongTermMemory = func(o *options) error {
o.enableLongTermMemory = true
return nil
}
func WithRAGDB(db RAGDB) Option { func WithRAGDB(db RAGDB) Option {
return func(o *options) error { return func(o *options) error {
o.ragdb = db o.ragdb = db

View File

@@ -28,4 +28,6 @@ type AgentConfig struct {
KnowledgeBaseResults int `json:"kb_results" form:"kb_results"` KnowledgeBaseResults int `json:"kb_results" form:"kb_results"`
CanStopItself bool `json:"can_stop_itself" form:"can_stop_itself"` CanStopItself bool `json:"can_stop_itself" form:"can_stop_itself"`
SystemPrompt string `json:"system_prompt" form:"system_prompt"` SystemPrompt string `json:"system_prompt" form:"system_prompt"`
LongTermMemory bool `json:"long_term_memory" form:"long_term_memory"`
SummaryLongTermMemory bool `json:"summary_long_term_memory" form:"summary_long_term_memory"`
} }

View File

@@ -248,6 +248,15 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
if config.StandaloneJob { if config.StandaloneJob {
opts = append(opts, EnableStandaloneJob) opts = append(opts, EnableStandaloneJob)
} }
if config.LongTermMemory {
opts = append(opts, EnableLongTermMemory)
}
if config.SummaryLongTermMemory {
opts = append(opts, EnableSummaryMemory)
}
if config.CanStopItself { if config.CanStopItself {
opts = append(opts, CanStopItself) opts = append(opts, CanStopItself)
} }

View File

@@ -104,6 +104,11 @@
<label for="random_identity" class="block text-lg font-medium text-gray-400">Random Identity</label> <label for="random_identity" class="block text-lg font-medium text-gray-400">Random Identity</label>
<input type="checkbox" name="random_identity" id="random_identity" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-lg border-gray-300 rounded-md bg-gray-700 text-white"> <input type="checkbox" name="random_identity" id="random_identity" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-lg border-gray-300 rounded-md bg-gray-700 text-white">
<label for="long_term_memory" class="block text-lg font-medium text-gray-400">Long term memory</label>
<input type="checkbox" name="long_term_memory" id="long_term_memory" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-lg border-gray-300 rounded-md bg-gray-700 text-white">
<label for="summary_long_term_memory" class="block text-lg font-medium text-gray-400">Long term memory (summarize!)</label>
<input type="checkbox" name="summary_long_term_memory" id="summary_long_term_memory" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-lg border-gray-300 rounded-md bg-gray-700 text-white">
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label for="identity_guidance" class="block text-lg font-medium text-gray-400">Identity Guidance</label> <label for="identity_guidance" class="block text-lg font-medium text-gray-400">Identity Guidance</label>