mirror of
https://github.com/mudler/cogito.git
synced 2026-07-23 10:25:44 -04:00
d1c0dc9bd9
* feat: improve control loop Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix tests Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Drop last message from subtasks in plan execution Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fixup tests Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
755 lines
22 KiB
Go
755 lines
22 KiB
Go
package cogito
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/mudler/cogito/prompt"
|
|
"github.com/mudler/cogito/structures"
|
|
"github.com/mudler/xlog"
|
|
"github.com/sashabaranov/go-openai"
|
|
)
|
|
|
|
type PlanStatus struct {
|
|
Plan structures.Plan
|
|
Tools []ToolStatus
|
|
}
|
|
|
|
var (
|
|
ErrGoalNotAchieved error = errors.New("goal not achieved")
|
|
)
|
|
|
|
// ExtractPlan extracts a plan from a conversation
|
|
// To override the prompt, define a PromptPlanType, PromptReEvaluatePlanType and PromptSubtaskExtractionType
|
|
func ExtractPlan(llm LLM, f Fragment, goal *structures.Goal, opts ...Option) (*structures.Plan, error) {
|
|
o := defaultOptions()
|
|
o.Apply(opts...)
|
|
|
|
// First we ask the LLM to organize subtasks
|
|
prompter := o.prompts.GetPrompt(prompt.PromptPlanType)
|
|
|
|
toolDefs := o.tools.Definitions()
|
|
planOptions := struct {
|
|
Context string
|
|
AdditionalContext string
|
|
Goal *structures.Goal
|
|
Tools []*openai.FunctionDefinition
|
|
FeedbackConversation string
|
|
}{
|
|
Context: f.String(),
|
|
Goal: goal,
|
|
Tools: toolDefs,
|
|
}
|
|
if o.deepContext && f.ParentFragment != nil {
|
|
planOptions.AdditionalContext = f.ParentFragment.AllFragmentsStrings()
|
|
}
|
|
|
|
var feedbackConv *Fragment
|
|
if o.feedbackCallback != nil {
|
|
feedbackConv = o.feedbackCallback()
|
|
planOptions.FeedbackConversation = feedbackConv.String()
|
|
}
|
|
|
|
prompt, err := prompter.Render(planOptions)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to render tool reasoner prompt: %w", err)
|
|
}
|
|
|
|
return applyPlanFromPrompt(llm, o, prompt, feedbackConv)
|
|
}
|
|
|
|
// ExtractPlan extracts a plan from a conversation
|
|
// to override the prompt, define a PromptReEvaluatePlanType and PromptSubtaskExtractionType
|
|
func ReEvaluatePlan(llm LLM, f, subtaskFragment Fragment, goal *structures.Goal, toolStatuses []ToolStatus, subtask string, opts ...Option) (*structures.Plan, error) {
|
|
o := defaultOptions()
|
|
o.Apply(opts...)
|
|
|
|
// First we ask the LLM to organize subtasks
|
|
prompter := o.prompts.GetPrompt(prompt.PromptReEvaluatePlanType)
|
|
|
|
toolDefs := o.tools.Definitions()
|
|
planOptions := struct {
|
|
Context string
|
|
AdditionalContext string
|
|
Subtask string
|
|
SubtaskConversation string
|
|
Goal string
|
|
Tools []*openai.FunctionDefinition
|
|
PastActionHistory []ToolStatus
|
|
FeedbackConversation string
|
|
}{
|
|
Context: f.String(),
|
|
Goal: goal.Goal,
|
|
Subtask: subtask,
|
|
Tools: toolDefs,
|
|
PastActionHistory: toolStatuses,
|
|
SubtaskConversation: subtaskFragment.String(),
|
|
}
|
|
if o.deepContext && f.ParentFragment != nil {
|
|
planOptions.AdditionalContext = f.ParentFragment.AllFragmentsStrings()
|
|
}
|
|
|
|
var feedbackConv *Fragment
|
|
if o.feedbackCallback != nil {
|
|
feedbackConv = o.feedbackCallback()
|
|
planOptions.FeedbackConversation = feedbackConv.String()
|
|
}
|
|
|
|
prompt, err := prompter.Render(planOptions)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to render tool reasoner prompt: %w", err)
|
|
}
|
|
|
|
return applyPlanFromPrompt(llm, o, prompt, feedbackConv)
|
|
}
|
|
|
|
func applyPlanFromPrompt(llm LLM, o *Options, planPrompt string, feedbackConv *Fragment) (*structures.Plan, error) {
|
|
multimedias := []Multimedia{}
|
|
if feedbackConv != nil {
|
|
multimedias = feedbackConv.Multimedia
|
|
}
|
|
planConv := NewEmptyFragment().AddMessage("user", planPrompt, multimedias...)
|
|
reasoningPlan, err := llm.Ask(o.context, planConv)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to ask LLM for plan identification: %w", err)
|
|
}
|
|
|
|
identifiedPlan := reasoningPlan.LastMessage()
|
|
|
|
structure, plan := structures.StructurePlan()
|
|
|
|
prompter := o.prompts.GetPrompt(prompt.PromptSubtaskExtractionType)
|
|
|
|
planOptions := struct {
|
|
Context string
|
|
}{
|
|
Context: identifiedPlan.Content,
|
|
}
|
|
|
|
prompt, err := prompter.Render(planOptions)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to render tool reasoner prompt: %w", err)
|
|
}
|
|
|
|
planConv = NewEmptyFragment().AddMessage("user", prompt)
|
|
|
|
err = planConv.ExtractStructure(o.context, llm, structure)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to extract structure: %w", err)
|
|
}
|
|
|
|
plan.Description = identifiedPlan.Content
|
|
return plan, err
|
|
}
|
|
|
|
// ExtractTODOs generates a TODO list from plan subtasks using the LLM
|
|
func ExtractTODOs(llm LLM, plan *structures.Plan, goal *structures.Goal, opts ...Option) (*structures.TODOList, error) {
|
|
o := defaultOptions()
|
|
o.Apply(opts...)
|
|
|
|
prompter := o.prompts.GetPrompt(prompt.PromptTODOGenerationType)
|
|
|
|
todoOptions := struct {
|
|
Goal *structures.Goal
|
|
Plan *structures.Plan
|
|
}{
|
|
Goal: goal,
|
|
Plan: plan,
|
|
}
|
|
|
|
promptStr, err := prompter.Render(todoOptions)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to render TODO generation prompt: %w", err)
|
|
}
|
|
|
|
todoConv := NewEmptyFragment().AddMessage("user", promptStr)
|
|
reasoningTodo, err := llm.Ask(o.context, todoConv)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to ask LLM for TODO generation: %w", err)
|
|
}
|
|
|
|
identifiedTodo := reasoningTodo.LastMessage()
|
|
|
|
structure, todoList := structures.StructureTODO()
|
|
|
|
todoConv = NewEmptyFragment().AddMessage("user", identifiedTodo.Content)
|
|
|
|
err = todoConv.ExtractStructure(o.context, llm, structure)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to extract TODO structure: %w", err)
|
|
}
|
|
|
|
// Initialize markdown representation
|
|
todoList.ToMarkdown()
|
|
todoList.LastUpdated = time.Now()
|
|
|
|
return todoList, nil
|
|
}
|
|
|
|
// ExecutePlan Executes an already-defined plan with a set of options.
|
|
// To override its prompt, configure PromptPlanExecutionType, PromptPlanType, PromptReEvaluatePlanType and PromptSubtaskExtractionType
|
|
func ExecutePlan(llm LLM, conv Fragment, plan *structures.Plan, goal *structures.Goal, opts ...Option) (Fragment, error) {
|
|
o := defaultOptions()
|
|
o.Apply(opts...)
|
|
|
|
if len(plan.Subtasks) == 0 {
|
|
return NewEmptyFragment(), fmt.Errorf("no subtasks found in plan")
|
|
}
|
|
|
|
// Check if Planning with TODOs is enabled (judge LLM must be set)
|
|
if len(o.reviewerLLMs) > 0 {
|
|
// Generate TODOs from plan if not provided
|
|
if o.todos == nil {
|
|
todoList, err := ExtractTODOs(llm, plan, goal, opts...)
|
|
if err != nil {
|
|
return NewEmptyFragment(), fmt.Errorf("failed to extract TODOs: %w", err)
|
|
}
|
|
o.todos = todoList
|
|
}
|
|
|
|
// Load TODOs from file if persistence path is set
|
|
if o.todoPersistencePath != "" {
|
|
if err := loadTODOsFromFile(o.todoPersistencePath, o.todos); err != nil {
|
|
xlog.Debug("Failed to load TODOs from file, using provided/generated list", "error", err)
|
|
}
|
|
}
|
|
|
|
return executePlanWithTODOs(llm, o.reviewerLLMs, conv, plan, goal, o)
|
|
}
|
|
|
|
xlog.Debug("Executing plan for conversation", "length", len(conv.Messages), "plan", plan.Description, "subtasks", plan.Subtasks)
|
|
|
|
var toolStatuses []ToolStatus
|
|
|
|
conversation := &conv
|
|
|
|
defer func(conversation *Fragment) {
|
|
conversation.Status.Plans = append(conversation.Status.Plans, PlanStatus{
|
|
Plan: *plan,
|
|
Tools: toolStatuses,
|
|
})
|
|
}(conversation)
|
|
|
|
index := 0
|
|
attempts := 1
|
|
for {
|
|
subtask := plan.Subtasks[index]
|
|
|
|
xlog.Debug("Executing subtask", "goal", goal.Goal, "subtask", subtask)
|
|
|
|
prompter := o.prompts.GetPrompt(prompt.PromptPlanExecutionType)
|
|
|
|
subtaskOption := struct {
|
|
Goal string
|
|
Subtask string
|
|
}{
|
|
Goal: goal.Goal,
|
|
Subtask: subtask,
|
|
}
|
|
|
|
prompt, err := prompter.Render(subtaskOption)
|
|
if err != nil {
|
|
return NewEmptyFragment(), fmt.Errorf("failed to render tool reasoner prompt: %w", err)
|
|
}
|
|
|
|
subtaskConv := NewEmptyFragment().AddMessage("user", prompt)
|
|
|
|
subtaskConvResult, err := ExecuteTools(llm, subtaskConv, opts...)
|
|
if err != nil {
|
|
return *conversation, err
|
|
}
|
|
// remove last one as is the answer, not the tool calls
|
|
subtaskConvResult.Messages = subtaskConvResult.Messages[:len(subtaskConvResult.Messages)-1]
|
|
|
|
conversation.Messages = append(conversation.Messages, subtaskConvResult.LastAssistantAndToolMessages()...)
|
|
conversation.Status.Iterations = conversation.Status.Iterations + 1
|
|
conversation.Status.ToolsCalled = append(conversation.Status.ToolsCalled, subtaskConvResult.Status.ToolsCalled...)
|
|
conversation.Status.ToolResults = append(conversation.Status.ToolResults, subtaskConvResult.Status.ToolResults...)
|
|
toolStatuses = append(toolStatuses, subtaskConvResult.Status.ToolResults...)
|
|
|
|
boolean, err := IsGoalAchieved(llm, subtaskConvResult, nil, opts...)
|
|
if err != nil {
|
|
return *conversation, err
|
|
}
|
|
|
|
xlog.Debug("Subtask execution", "achieved", boolean.Boolean, "attempts", attempts, "maxAttempts", o.maxAttempts)
|
|
|
|
toolStatuses := []ToolStatus{}
|
|
for i := range conversation.Status.ToolsCalled {
|
|
toolStatuses = append(toolStatuses, conversation.Status.ToolResults[i])
|
|
}
|
|
|
|
if !boolean.Boolean {
|
|
if attempts >= o.maxAttempts {
|
|
if !o.planReEvaluator {
|
|
return *conversation, ErrGoalNotAchieved
|
|
}
|
|
xlog.Debug("All attempts failed, re-evaluating plan")
|
|
plan, err = ReEvaluatePlan(llm, *conversation, subtaskConv, goal, toolStatuses, subtask, opts...)
|
|
if err != nil {
|
|
return *conversation, err
|
|
}
|
|
|
|
// Start again
|
|
index = 0
|
|
attempts = 1
|
|
} else {
|
|
xlog.Debug("Attempt failed to achieve goal, retrying")
|
|
attempts++
|
|
}
|
|
} else {
|
|
xlog.Debug("Goal correctly achieved")
|
|
attempts = 1 // reset attempts
|
|
if len(plan.Subtasks)-1 > index {
|
|
index++
|
|
} else if !(o.infiniteExecution) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return *conversation, nil
|
|
}
|
|
|
|
// executePlanWithTODOs executes a plan with Planning with TODOs
|
|
func executePlanWithTODOs(workerLLM LLM, reviewerLLMs []LLM, conv Fragment, plan *structures.Plan, goal *structures.Goal, o *Options) (Fragment, error) {
|
|
if len(plan.Subtasks) == 0 {
|
|
return NewEmptyFragment(), fmt.Errorf("no subtasks found in plan")
|
|
}
|
|
|
|
xlog.Debug("Executing plan with TODOs", "plan", plan.Description, "subtasks", plan.Subtasks, "maxIterations", o.maxIterations)
|
|
|
|
conversation := &conv
|
|
if conversation.Status == nil {
|
|
conversation.Status = &Status{}
|
|
}
|
|
conversation.Status.TODOs = o.todos
|
|
|
|
var toolStatuses []ToolStatus
|
|
var previousFeedback string
|
|
|
|
// Outer loop: TODO iterations
|
|
for todoIteration := 1; todoIteration <= o.maxIterations; todoIteration++ {
|
|
conversation.Status.TODOIteration = todoIteration
|
|
xlog.Debug("Starting TODO iteration", "iteration", todoIteration, "maxIterations", o.maxIterations)
|
|
|
|
// Inner loop: execute plan subtasks
|
|
index := 0
|
|
attempts := 1
|
|
for index < len(plan.Subtasks) {
|
|
|
|
subtask := plan.Subtasks[index]
|
|
xlog.Debug("Executing subtask", "goal", goal.Goal, "subtask", subtask, "todoIteration", todoIteration)
|
|
|
|
// WORK PHASE
|
|
conversation.Status.TODOPhase = "work"
|
|
workResult, err := executeWorkPhase(workerLLM, o.todos, goal, subtask, previousFeedback, o)
|
|
if err != nil {
|
|
return *conversation, fmt.Errorf("work phase failed: %w", err)
|
|
}
|
|
|
|
// Update TODOs from work result
|
|
o.todos, err = updateTODOsFromWork(workerLLM, workResult, o.todos, o)
|
|
if err != nil {
|
|
xlog.Debug("Failed to update TODOs from work", "error", err)
|
|
}
|
|
|
|
// REVIEW PHASE
|
|
conversation.Status.TODOPhase = "review"
|
|
reviewResult, goalCompleted, err := executeReviewPhase(reviewerLLMs, workResult, goal, o.todos, o)
|
|
if err != nil {
|
|
return *conversation, fmt.Errorf("review phase failed: %w", err)
|
|
}
|
|
|
|
// Extract feedback from review
|
|
previousFeedback = extractFeedbackFromReview(reviewResult)
|
|
|
|
// Update TODOs from feedback
|
|
o.todos, err = updateTODOsFromFeedback(reviewResult, o.todos, o.todoPersistencePath)
|
|
if err != nil {
|
|
xlog.Debug("Failed to update TODOs from feedback", "error", err)
|
|
}
|
|
|
|
// Save TODOs to file if persistence path is set
|
|
if o.todoPersistencePath != "" {
|
|
if err := saveTODOsToFile(o.todoPersistencePath, o.todos); err != nil {
|
|
xlog.Debug("Failed to save TODOs to file", "error", err)
|
|
}
|
|
}
|
|
|
|
conversation.Status.TODOs = o.todos
|
|
|
|
// last one is the answer, not the tool calls. Remove last message
|
|
workResult.Messages = workResult.Messages[:len(workResult.Messages)-1]
|
|
|
|
conversation.Messages = append(conversation.Messages, workResult.LastAssistantAndToolMessages()...)
|
|
conversation.Status.Iterations = conversation.Status.Iterations + 1
|
|
conversation.Status.ToolsCalled = append(conversation.Status.ToolsCalled, workResult.Status.ToolsCalled...)
|
|
conversation.Status.ToolResults = append(conversation.Status.ToolResults, workResult.Status.ToolResults...)
|
|
toolStatuses = append(toolStatuses, workResult.Status.ToolResults...)
|
|
|
|
if goalCompleted {
|
|
xlog.Debug("Goal execution completed", "subtask", subtask)
|
|
attempts = 1
|
|
if len(plan.Subtasks)-1 > index {
|
|
index++
|
|
} else {
|
|
// All subtasks completed
|
|
conversation.Status.Plans = append(conversation.Status.Plans, PlanStatus{
|
|
Plan: *plan,
|
|
Tools: toolStatuses,
|
|
})
|
|
return *conversation, nil
|
|
}
|
|
} else {
|
|
// Goal execution incomplete: needs rework
|
|
if attempts >= o.maxAttempts {
|
|
if !o.planReEvaluator {
|
|
return *conversation, ErrGoalNotAchieved
|
|
}
|
|
xlog.Debug("All attempts failed, re-evaluating plan")
|
|
// Create a fresh conversation for re-evaluation (fresh context)
|
|
reEvalConv := NewEmptyFragment()
|
|
reEvalConv.Status = conversation.Status
|
|
plan, err = ReEvaluatePlan(workerLLM, reEvalConv, workResult, goal, toolStatuses, subtask)
|
|
if err != nil {
|
|
return *conversation, err
|
|
}
|
|
// Start again with fresh context
|
|
index = 0
|
|
attempts = 1
|
|
} else {
|
|
xlog.Debug("Attempt failed to achieve goal, retrying with feedback", "attempts", attempts)
|
|
attempts++
|
|
// Continue with same subtask but with updated feedback
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
conversation.Status.Plans = append(conversation.Status.Plans, PlanStatus{
|
|
Plan: *plan,
|
|
Tools: toolStatuses,
|
|
})
|
|
|
|
return *conversation, nil
|
|
}
|
|
|
|
// executeWorkPhase executes the work phase with fresh context including TODOs, goal, and feedback
|
|
func executeWorkPhase(workerLLM LLM, todoList *structures.TODOList, goal *structures.Goal, subtask string, previousFeedback string, o *Options) (Fragment, error) {
|
|
prompter := o.prompts.GetPrompt(prompt.PromptTODOWorkType)
|
|
|
|
// Ensure markdown is up to date
|
|
todoMarkdown := todoList.ToMarkdown()
|
|
|
|
workOptions := struct {
|
|
Goal string
|
|
Subtask string
|
|
TODOMarkdown string
|
|
PreviousFeedback string
|
|
}{
|
|
Goal: goal.Goal,
|
|
Subtask: subtask,
|
|
TODOMarkdown: todoMarkdown,
|
|
PreviousFeedback: previousFeedback,
|
|
}
|
|
|
|
promptStr, err := prompter.Render(workOptions)
|
|
if err != nil {
|
|
return NewEmptyFragment(), fmt.Errorf("failed to render work phase prompt: %w", err)
|
|
}
|
|
|
|
// Create fresh fragment with work context
|
|
workFragment := NewEmptyFragment().AddMessage("user", promptStr)
|
|
|
|
// Execute tools with the work fragment
|
|
// Convert Options struct to Option functions for ExecuteTools
|
|
opts := convertOptionsToFunctions(o)
|
|
workResult, err := ExecuteTools(workerLLM, workFragment, opts...)
|
|
if err != nil {
|
|
return NewEmptyFragment(), fmt.Errorf("failed to execute tools in work phase: %w", err)
|
|
}
|
|
|
|
return workResult, nil
|
|
}
|
|
|
|
// executeReviewPhase executes the review phase using the judge LLM
|
|
func executeReviewPhase(reviewerLLMs []LLM, workFragment Fragment, goal *structures.Goal, todoList *structures.TODOList, o *Options) (Fragment, bool, error) {
|
|
prompter := o.prompts.GetPrompt(prompt.PromptTODOReviewType)
|
|
|
|
todoMarkdown := todoList.ToMarkdown()
|
|
|
|
// Get work results as string
|
|
workResults := workFragment.String()
|
|
|
|
reviewOptions := struct {
|
|
Goal string
|
|
WorkResults string
|
|
TODOMarkdown string
|
|
}{
|
|
Goal: goal.Goal,
|
|
WorkResults: workResults,
|
|
TODOMarkdown: todoMarkdown,
|
|
}
|
|
|
|
promptStr, err := prompter.Render(reviewOptions)
|
|
if err != nil {
|
|
return NewEmptyFragment(), false, fmt.Errorf("failed to render review phase prompt: %w", err)
|
|
}
|
|
|
|
// Create review fragment
|
|
reviewFragment := NewEmptyFragment().AddMessage("user", promptStr)
|
|
|
|
// Use IsGoalAchieved to determine if goal execution is completed
|
|
opts := convertOptionsToFunctions(o)
|
|
|
|
reviews := []struct {
|
|
boolean *structures.Boolean
|
|
reviewResult Fragment
|
|
}{}
|
|
|
|
for _, reviewerLLM := range reviewerLLMs {
|
|
|
|
boolean, err := IsGoalAchieved(reviewerLLM, reviewFragment, goal, opts...)
|
|
if err != nil {
|
|
return NewEmptyFragment(), false, fmt.Errorf("failed to check if goal achieved: %w", err)
|
|
}
|
|
|
|
// Get the reasoning from the review
|
|
reviewResult, err := reviewerLLM.Ask(o.context, reviewFragment)
|
|
if err != nil {
|
|
return NewEmptyFragment(), false, fmt.Errorf("failed to get review result: %w", err)
|
|
}
|
|
|
|
reviews = append(reviews, struct {
|
|
boolean *structures.Boolean
|
|
reviewResult Fragment
|
|
}{boolean, reviewResult})
|
|
}
|
|
|
|
boolean := &structures.Boolean{Boolean: false}
|
|
var reviewResult Fragment
|
|
var positiveReview Fragment
|
|
var negativeReview Fragment
|
|
|
|
// Majority vote
|
|
// Count the number of true booleans
|
|
trueCount := 0
|
|
for _, review := range reviews {
|
|
if review.boolean.Boolean {
|
|
trueCount++
|
|
positiveReview = review.reviewResult
|
|
} else {
|
|
negativeReview = review.reviewResult
|
|
}
|
|
}
|
|
|
|
// If the number of true booleans is greater than the number of false booleans, set the boolean to true
|
|
if trueCount > len(reviews)/2 {
|
|
boolean.Boolean = true
|
|
reviewResult = positiveReview
|
|
} else {
|
|
reviewResult = negativeReview
|
|
}
|
|
|
|
goalCompleted := boolean.Boolean
|
|
return reviewResult, goalCompleted, nil
|
|
}
|
|
|
|
// updateTODOsFromWork extracts TODO updates from work results
|
|
func updateTODOsFromWork(workerLLM LLM, workFragment Fragment, todoList *structures.TODOList, o *Options) (*structures.TODOList, error) {
|
|
// Try to extract TODO updates from the work fragment
|
|
prompter := o.prompts.GetPrompt(prompt.PromptTODOTrackingType)
|
|
|
|
todoMarkdown := todoList.ToMarkdown()
|
|
|
|
trackingOptions := struct {
|
|
Context string
|
|
TODOMarkdown string
|
|
}{
|
|
Context: workFragment.String(),
|
|
TODOMarkdown: todoMarkdown,
|
|
}
|
|
|
|
promptStr, err := prompter.Render(trackingOptions)
|
|
if err != nil {
|
|
return todoList, fmt.Errorf("failed to render TODO tracking prompt: %w", err)
|
|
}
|
|
|
|
trackingConv := NewEmptyFragment().AddMessage("user", promptStr)
|
|
structure, updatedTodoList := structures.StructureTODO()
|
|
|
|
// We use the worker LLM here to extract the structure. Maybe we should use the reviewer LLM instead?
|
|
// TODO: Implement a better way to select the LLM to use for extraction?
|
|
err = trackingConv.ExtractStructure(o.context, workerLLM, structure)
|
|
if err != nil {
|
|
// If extraction fails, return original list
|
|
xlog.Debug("Failed to extract TODO updates from work", "error", err)
|
|
return todoList, nil
|
|
}
|
|
|
|
// Update markdown
|
|
updatedTodoList.ToMarkdown()
|
|
updatedTodoList.LastUpdated = time.Now()
|
|
|
|
return updatedTodoList, nil
|
|
}
|
|
|
|
// updateTODOsFromFeedback updates TODOs based on review feedback
|
|
func updateTODOsFromFeedback(reviewFragment Fragment, todoList *structures.TODOList, persistencePath string) (*structures.TODOList, error) {
|
|
// Extract feedback from review
|
|
feedback := extractFeedbackFromReview(reviewFragment)
|
|
|
|
// For now, we'll add feedback to incomplete TODOs
|
|
// In a more sophisticated implementation, we could parse the feedback
|
|
// and update specific TODOs
|
|
incomplete := todoList.GetIncompleteTODOs()
|
|
if len(incomplete) > 0 && feedback != "" {
|
|
// Add feedback to the first incomplete TODO
|
|
todoList.AddFeedback(incomplete[0].ID, feedback)
|
|
}
|
|
|
|
todoList.LastUpdated = time.Now()
|
|
todoList.ToMarkdown()
|
|
|
|
return todoList, nil
|
|
}
|
|
|
|
// extractFeedbackFromReview extracts feedback text from review fragment
|
|
func extractFeedbackFromReview(reviewFragment Fragment) string {
|
|
if len(reviewFragment.Messages) == 0 {
|
|
return ""
|
|
}
|
|
|
|
lastMessage := reviewFragment.LastMessage()
|
|
if lastMessage == nil {
|
|
return ""
|
|
}
|
|
|
|
// Return the content of the last message as feedback
|
|
return lastMessage.Content
|
|
}
|
|
|
|
// saveTODOsToFile saves TODO list to a file
|
|
func saveTODOsToFile(path string, todoList *structures.TODOList) error {
|
|
if todoList == nil {
|
|
return fmt.Errorf("TODO list is nil")
|
|
}
|
|
|
|
data, err := todoList.ToJSON()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to serialize TODOs: %w", err)
|
|
}
|
|
|
|
err = os.WriteFile(path, data, 0644)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write TODOs to file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// convertOptionsToFunctions converts an Options struct to a slice of Option functions
|
|
// This extracts the important options to pass to functions that expect []Option
|
|
func convertOptionsToFunctions(o *Options) []Option {
|
|
var opts []Option
|
|
|
|
// Preserve tools
|
|
if len(o.tools) > 0 {
|
|
opts = append(opts, WithTools(o.tools...))
|
|
}
|
|
|
|
// Preserve context
|
|
if o.context != nil {
|
|
opts = append(opts, WithContext(o.context))
|
|
}
|
|
|
|
// Preserve other important options
|
|
if o.toolReasoner {
|
|
opts = append(opts, EnableToolReasoner)
|
|
}
|
|
|
|
if o.deepContext {
|
|
opts = append(opts, EnableDeepContext)
|
|
}
|
|
if o.planReEvaluator {
|
|
opts = append(opts, EnableAutoPlanReEvaluator)
|
|
}
|
|
if o.strictGuidelines {
|
|
opts = append(opts, EnableStrictGuidelines)
|
|
}
|
|
if len(o.guidelines) > 0 {
|
|
opts = append(opts, WithGuidelines(o.guidelines...))
|
|
}
|
|
if len(o.mcpSessions) > 0 {
|
|
opts = append(opts, WithMCPs(o.mcpSessions...))
|
|
}
|
|
if o.maxAttempts > 0 {
|
|
opts = append(opts, WithMaxAttempts(o.maxAttempts))
|
|
}
|
|
if o.statusCallback != nil {
|
|
opts = append(opts, WithStatusCallback(o.statusCallback))
|
|
}
|
|
if o.reasoningCallback != nil {
|
|
opts = append(opts, WithReasoningCallback(o.reasoningCallback))
|
|
}
|
|
if o.feedbackCallback != nil {
|
|
opts = append(opts, WithFeedbackCallback(o.feedbackCallback))
|
|
}
|
|
if o.toolCallCallback != nil {
|
|
opts = append(opts, WithToolCallBack(o.toolCallCallback))
|
|
}
|
|
if o.maxAdjustmentAttempts > 0 {
|
|
opts = append(opts, WithMaxAdjustmentAttempts(o.maxAdjustmentAttempts))
|
|
}
|
|
if o.forceReasoning {
|
|
opts = append(opts, WithForceReasoning())
|
|
}
|
|
if o.maxRetries > 0 {
|
|
opts = append(opts, WithMaxRetries(o.maxRetries))
|
|
}
|
|
if o.loopDetectionSteps > 0 {
|
|
opts = append(opts, WithLoopDetection(o.loopDetectionSteps))
|
|
}
|
|
if len(o.gaps) > 0 {
|
|
opts = append(opts, WithGaps(o.gaps...))
|
|
}
|
|
if o.prompts != nil {
|
|
for promptType, p := range o.prompts {
|
|
if staticPrompt, ok := p.(prompt.StaticPrompt); ok {
|
|
opts = append(opts, WithPrompt(promptType, staticPrompt))
|
|
}
|
|
}
|
|
}
|
|
|
|
return opts
|
|
}
|
|
|
|
// loadTODOsFromFile loads TODO list from a file
|
|
func loadTODOsFromFile(path string, todoList *structures.TODOList) error {
|
|
if todoList == nil {
|
|
return fmt.Errorf("TODO list is nil")
|
|
}
|
|
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
// File doesn't exist yet, that's okay
|
|
return nil
|
|
}
|
|
return fmt.Errorf("failed to read TODOs from file: %w", err)
|
|
}
|
|
|
|
err = todoList.FromJSON(data)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to deserialize TODOs: %w", err)
|
|
}
|
|
|
|
// Update markdown
|
|
todoList.ToMarkdown()
|
|
|
|
return nil
|
|
}
|