diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1f47b10a..c85d2a67 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,4 +1,4 @@ -name: Run Go Tests +name: Run Tests on: push: @@ -16,43 +16,19 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - - run: | - # Add Docker's official GPG key: - sudo apt-get update - sudo apt-get install ca-certificates curl - sudo install -m 0755 -d /etc/apt/keyrings - sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc - sudo chmod a+r /etc/apt/keyrings/docker.asc - - # Add the repository to Apt sources: - echo \ - "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ - $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \ - sudo tee /etc/apt/sources.list.d/docker.list > /dev/null - sudo apt-get update - sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin make - docker version - - docker run --rm hello-world - uses: actions/setup-go@v5 with: go-version: '>=1.17.0' - - name: Free up disk space - run: | - sudo rm -rf /usr/share/dotnet - sudo rm -rf /usr/local/lib/android - sudo rm -rf /opt/ghc - sudo apt-get clean - docker system prune -af || true - df -h - - name: Run tests run: | make tests - #sudo mv coverage/coverage.txt coverage.txt - #sudo chmod 777 coverage.txt - - # - name: Upload coverage to Codecov - # uses: codecov/codecov-action@v4 - # with: - # token: ${{ secrets.CODECOV_TOKEN }} + test-e2e: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '>=1.17.0' + - run: | + make tests-e2e \ No newline at end of file diff --git a/Makefile b/Makefile index c9916105..1ca97d48 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ cleanup-tests: docker compose down tests: prepare-tests - LOCALAGI_MODEL="gemma-3-4b-it-qat" LOCALAI_API_URL="http://localhost:8081" LOCALAGI_API_URL="http://localhost:8080" $(GOCMD) run github.com/onsi/ginkgo/v2/ginkgo --fail-fast -v -r ./... + LOCALAGI_MODEL="gemma-3-4b-it-qat" LOCALAI_API_URL="http://localhost:8081" LOCALAGI_API_URL="http://localhost:8080" $(GOCMD) run github.com/onsi/ginkgo/v2/ginkgo --label-filter="!E2E" --flake-attempts=5 --fail-fast -v -r ./... run-nokb: $(MAKE) run KBDISABLEINDEX=true @@ -29,4 +29,7 @@ build-image: docker build -t $(IMAGE_NAME) -f Dockerfile.webui . image-push: - docker push $(IMAGE_NAME) \ No newline at end of file + docker push $(IMAGE_NAME) + +tests-e2e: prepare-tests + LOCALAGI_MODEL="gemma-3-4b-it-qat" LOCALAI_API_URL="http://localhost:8081" LOCALAGI_API_URL="http://localhost:8080" $(GOCMD) run github.com/onsi/ginkgo/v2/ginkgo --label-filter="E2E" --flake-attempts=5 --fail-fast -v -r ./tests/e2e/... diff --git a/core/action/goal.go b/core/action/goal.go deleted file mode 100644 index e7462012..00000000 --- a/core/action/goal.go +++ /dev/null @@ -1,48 +0,0 @@ -package action - -import ( - "context" - - "github.com/mudler/LocalAGI/core/types" - "github.com/sashabaranov/go-openai/jsonschema" -) - -// NewGoal creates a new intention action -// The inention action is special as it tries to identify -// a tool to use and a reasoning over to use it -func NewGoal() *GoalAction { - return &GoalAction{} -} - -type GoalAction struct { -} -type GoalResponse struct { - Goal string `json:"goal"` - Achieved bool `json:"achieved"` -} - -func (a *GoalAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) { - return types.ActionResult{}, nil -} - -func (a *GoalAction) Plannable() bool { - return false -} - -func (a *GoalAction) Definition() types.ActionDefinition { - return types.ActionDefinition{ - Name: "goal", - Description: "Check if the goal is achieved", - Properties: map[string]jsonschema.Definition{ - "goal": { - Type: jsonschema.String, - Description: "The goal to check if it is achieved.", - }, - "achieved": { - Type: jsonschema.Boolean, - Description: "Whether the goal is achieved", - }, - }, - Required: []string{"goal", "achieved"}, - } -} diff --git a/core/action/intention.go b/core/action/intention.go deleted file mode 100644 index 229fd079..00000000 --- a/core/action/intention.go +++ /dev/null @@ -1,50 +0,0 @@ -package action - -import ( - "context" - - "github.com/mudler/LocalAGI/core/types" - "github.com/sashabaranov/go-openai/jsonschema" -) - -// NewIntention creates a new intention action -// The inention action is special as it tries to identify -// a tool to use and a reasoning over to use it -func NewIntention(s ...string) *IntentAction { - return &IntentAction{tools: s} -} - -type IntentAction struct { - tools []string -} -type IntentResponse struct { - Tool string `json:"tool"` - Reasoning string `json:"reasoning"` -} - -func (a *IntentAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) { - return types.ActionResult{}, nil -} - -func (a *IntentAction) Plannable() bool { - return false -} - -func (a *IntentAction) Definition() types.ActionDefinition { - return types.ActionDefinition{ - Name: "pick_tool", - Description: "Pick a tool", - Properties: map[string]jsonschema.Definition{ - "reasoning": { - Type: jsonschema.String, - Description: "A detailed reasoning on why you want to call this tool.", - }, - "tool": { - Type: jsonschema.String, - Description: "The tool you want to use", - Enum: a.tools, - }, - }, - Required: []string{"tool", "reasoning"}, - } -} diff --git a/core/action/plan.go b/core/action/plan.go deleted file mode 100644 index 5685cac7..00000000 --- a/core/action/plan.go +++ /dev/null @@ -1,71 +0,0 @@ -package action - -import ( - "context" - - "github.com/mudler/LocalAGI/core/types" - "github.com/sashabaranov/go-openai/jsonschema" -) - -// PlanActionName is the name of the plan action -// used by the LLM to schedule more actions -const PlanActionName = "plan" - -func NewPlan(plannableActions []string) *PlanAction { - return &PlanAction{ - plannables: plannableActions, - } -} - -type PlanAction struct { - plannables []string -} - -type PlanResult struct { - Subtasks []PlanSubtask `json:"subtasks"` - Goal string `json:"goal"` -} -type PlanSubtask struct { - Action string `json:"action"` - Reasoning string `json:"reasoning"` -} - -func (a *PlanAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) { - return types.ActionResult{}, nil -} - -func (a *PlanAction) Plannable() bool { - return false -} - -func (a *PlanAction) Definition() types.ActionDefinition { - return types.ActionDefinition{ - Name: PlanActionName, - Description: "Use it for situations that involves doing more actions in sequence.", - Properties: map[string]jsonschema.Definition{ - "subtasks": { - Type: jsonschema.Array, - Description: "The subtasks to be executed", - Items: &jsonschema.Definition{ - Type: jsonschema.Object, - Properties: map[string]jsonschema.Definition{ - "action": { - Type: jsonschema.String, - Description: "The action to call", - Enum: a.plannables, - }, - "reasoning": { - Type: jsonschema.String, - Description: "The reasoning for calling this action", - }, - }, - }, - }, - "goal": { - Type: jsonschema.String, - Description: "The goal of this plan", - }, - }, - Required: []string{"subtasks", "goal"}, - } -} diff --git a/core/action/reasoning.go b/core/action/reasoning.go deleted file mode 100644 index 7e94fd98..00000000 --- a/core/action/reasoning.go +++ /dev/null @@ -1,43 +0,0 @@ -package action - -import ( - "context" - - "github.com/mudler/LocalAGI/core/types" - "github.com/sashabaranov/go-openai/jsonschema" -) - -// NewReasoning creates a new reasoning action -// The reasoning action is special as it tries to force the LLM -// to think about what to do next -func NewReasoning() *ReasoningAction { - return &ReasoningAction{} -} - -type ReasoningAction struct{} - -type ReasoningResponse struct { - Reasoning string `json:"reasoning"` -} - -func (a *ReasoningAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) { - return types.ActionResult{}, nil -} - -func (a *ReasoningAction) Plannable() bool { - return false -} - -func (a *ReasoningAction) Definition() types.ActionDefinition { - return types.ActionDefinition{ - Name: "pick_action", - Description: "try to understand what's the best thing to do and pick an action with a reasoning", - Properties: map[string]jsonschema.Definition{ - "reasoning": { - Type: jsonschema.String, - Description: "A detailed reasoning on what would you do in this situation.", - }, - }, - Required: []string{"reasoning"}, - } -} diff --git a/core/action/reply.go b/core/action/reply.go deleted file mode 100644 index 2fe00a21..00000000 --- a/core/action/reply.go +++ /dev/null @@ -1,45 +0,0 @@ -package action - -import ( - "context" - - "github.com/mudler/LocalAGI/core/types" - "github.com/sashabaranov/go-openai/jsonschema" -) - -// ReplyActionName is the name of the reply action -// used by the LLM to reply to the user without -// any additional processing -const ReplyActionName = "reply" - -func NewReply() *ReplyAction { - return &ReplyAction{} -} - -type ReplyAction struct{} - -type ReplyResponse struct { - Message string `json:"message"` -} - -func (a *ReplyAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (string, error) { - return "no-op", nil -} - -func (a *ReplyAction) Plannable() bool { - return false -} - -func (a *ReplyAction) Definition() types.ActionDefinition { - return types.ActionDefinition{ - Name: ReplyActionName, - Description: "Use this tool to reply to the user once we have all the informations we need.", - Properties: map[string]jsonschema.Definition{ - "message": { - Type: jsonschema.String, - Description: "The message to reply with", - }, - }, - Required: []string{"message"}, - } -} diff --git a/core/agent/actions.go b/core/agent/actions.go index 59a8d781..5982460c 100644 --- a/core/agent/actions.go +++ b/core/agent/actions.go @@ -1,155 +1,18 @@ package agent import ( - "context" "encoding/json" - "fmt" "os" - "strings" "github.com/mudler/LocalAGI/core/action" "github.com/mudler/LocalAGI/core/types" + "golang.org/x/exp/slices" "github.com/mudler/LocalAGI/pkg/xlog" "github.com/sashabaranov/go-openai" - "github.com/sashabaranov/go-openai/jsonschema" ) -const parameterReasoningPrompt = `You are tasked with generating the optimal parameters for the action "%s". The action requires the following parameters: -%s - -Your task is to: -1. Generate the best possible values for each required parameter -2. If the parameter requires code, provide complete, working code -3. If the parameter requires text or documentation, provide comprehensive, well-structured content -4. Ensure all parameters are complete and ready to be used - -Focus on quality and completeness. Do not explain your reasoning or analyze the action's purpose - just provide the best possible parameter values.` - -type decisionResult struct { - actionParams types.ActionParams - message string - actionName string -} - -// decision forces the agent to take one of the available actions -func (a *Agent) decision( - job *types.Job, - conversation []openai.ChatCompletionMessage, - tools []openai.Tool, toolchoice string, maxRetries int) (*decisionResult, error) { - - var choice *openai.ToolChoice - - if toolchoice != "" { - choice = &openai.ToolChoice{ - Type: openai.ToolTypeFunction, - Function: openai.ToolFunction{Name: toolchoice}, - } - } - - decision := openai.ChatCompletionRequest{ - Model: a.options.LLMAPI.Model, - Messages: conversation, - Tools: tools, - } - - if choice != nil { - decision.ToolChoice = *choice - } - - var obs *types.Observable - if job.Obs != nil { - obs = a.observer.NewObservable() - obs.Name = "decision" - obs.ParentID = job.Obs.ID - obs.Icon = "brain" - obs.Creation = &types.Creation{ - ChatCompletionRequest: &decision, - } - a.observer.Update(*obs) - } - - var lastErr error - for attempts := 0; attempts < maxRetries; attempts++ { - resp, err := a.client.CreateChatCompletion(job.GetContext(), decision) - if err != nil { - lastErr = err - xlog.Warn("Attempt to make a decision failed", "attempt", attempts+1, "error", err) - - if obs != nil { - obs.Progress = append(obs.Progress, types.Progress{ - Error: err.Error(), - }) - a.observer.Update(*obs) - } - - continue - } - - jsonResp, _ := json.Marshal(resp) - xlog.Debug("Decision response", "response", string(jsonResp)) - - if obs != nil { - obs.AddProgress(types.Progress{ - ChatCompletionResponse: &resp, - }) - } - - if len(resp.Choices) != 1 { - lastErr = fmt.Errorf("no choices: %d", len(resp.Choices)) - xlog.Warn("Attempt to make a decision failed", "attempt", attempts+1, "error", lastErr) - - if obs != nil { - obs.Progress[len(obs.Progress)-1].Error = lastErr.Error() - a.observer.Update(*obs) - } - - continue - } - - msg := resp.Choices[0].Message - if len(msg.ToolCalls) != 1 { - if err := a.saveConversation(append(conversation, msg), "decision"); err != nil { - xlog.Error("Error saving conversation", "error", err) - } - - if obs != nil { - obs.MakeLastProgressCompletion() - a.observer.Update(*obs) - } - - return &decisionResult{message: msg.Content}, nil - } - - params := types.ActionParams{} - if err := params.Read(msg.ToolCalls[0].Function.Arguments); err != nil { - lastErr = err - xlog.Warn("Attempt to parse action parameters failed", "attempt", attempts+1, "error", err) - - if obs != nil { - obs.Progress[len(obs.Progress)-1].Error = lastErr.Error() - a.observer.Update(*obs) - } - - continue - } - - if err := a.saveConversation(append(conversation, msg), "decision"); err != nil { - xlog.Error("Error saving conversation", "error", err) - } - - if obs != nil { - obs.MakeLastProgressCompletion() - a.observer.Update(*obs) - } - - return &decisionResult{actionParams: params, actionName: msg.ToolCalls[0].Function.Name, message: msg.Content}, nil - } - - return nil, fmt.Errorf("failed to make a decision after %d attempts: %w", maxRetries, lastErr) -} - type Messages []openai.ChatCompletionMessage func (m Messages) ToOpenAI() []openai.ChatCompletionMessage { @@ -228,205 +91,6 @@ func (m Messages) GetLatestUserMessage() *openai.ChatCompletionMessage { return nil } -func (m Messages) IsLastMessageFromRole(role string) bool { - if len(m) == 0 { - return false - } - - return m[len(m)-1].Role == role -} - -func (a *Agent) generateParameters(job *types.Job, pickTemplate string, act types.Action, c []openai.ChatCompletionMessage, reasoning string, maxAttempts int) (*decisionResult, error) { - if act == nil { - return nil, fmt.Errorf("action is nil") - } - if len(act.Definition().Properties) > 0 { - xlog.Debug("Action has properties", "action", act.Definition().Name, "properties", act.Definition().Properties) - } else { - xlog.Debug("Action has no properties", "action", act.Definition().Name) - return &decisionResult{actionParams: types.ActionParams{}}, nil - } - - stateHUD, err := renderTemplate(pickTemplate, a.prepareHUD(), a.availableActions(), reasoning) - if err != nil { - return nil, err - } - - conversation := c - if !Messages(c).Exist(stateHUD) && a.options.enableHUD { - conversation = append([]openai.ChatCompletionMessage{ - { - Role: "system", - Content: stateHUD, - }, - }, conversation...) - } - - cc := conversation - if a.options.forceReasoning { - // First, get the LLM to reason about optimal parameter usage - parameterReasoningPrompt := fmt.Sprintf(parameterReasoningPrompt, - act.Definition().Name, - formatProperties(act.Definition().Properties)) - - // Get initial reasoning about parameters using askLLM - paramReasoningMsg, err := a.askLLM(job.GetContext(), - append(conversation, openai.ChatCompletionMessage{ - Role: "system", - Content: parameterReasoningPrompt, - }), - maxAttempts, - ) - if err != nil { - xlog.Warn("Failed to get parameter reasoning", "error", err) - } - - // Combine original reasoning with parameter-specific reasoning - enhancedReasoning := reasoning - if paramReasoningMsg.Content != "" { - enhancedReasoning = fmt.Sprintf("%s\n\nParameter Analysis:\n%s", reasoning, paramReasoningMsg.Content) - } - - cc = append(conversation, openai.ChatCompletionMessage{ - Role: "system", - Content: fmt.Sprintf("The agent decided to use the tool %s with the following reasoning: %s", act.Definition().Name, enhancedReasoning), - }) - } - - var result *decisionResult - var attemptErr error - - for attempts := 0; attempts < maxAttempts; attempts++ { - result, attemptErr = a.decision(job, - cc, - a.availableActions().ToTools(), - act.Definition().Name.String(), - maxAttempts, - ) - if attemptErr == nil && result.actionParams != nil { - return result, nil - } - xlog.Warn("Attempt to generate parameters failed", "attempt", attempts+1, "error", attemptErr) - } - - return nil, fmt.Errorf("failed to generate parameters after %d attempts: %w", maxAttempts, attemptErr) -} - -// Helper function to format properties for the prompt -func formatProperties(props map[string]jsonschema.Definition) string { - var result strings.Builder - for name, prop := range props { - result.WriteString(fmt.Sprintf("- %s: %s\n", name, prop.Description)) - } - return result.String() -} - -func (a *Agent) handlePlanning(ctx context.Context, job *types.Job, chosenAction types.Action, actionParams types.ActionParams, reasoning string, pickTemplate string, conv Messages) (Messages, error) { - // Planning: run all the actions in sequence - if !chosenAction.Definition().Name.Is(action.PlanActionName) { - xlog.Debug("no plan action") - return conv, nil - } - - xlog.Debug("[planning]...") - planResult := action.PlanResult{} - if err := actionParams.Unmarshal(&planResult); err != nil { - return conv, fmt.Errorf("error unmarshalling plan result: %w", err) - } - - stateResult := types.ActionState{ - ActionCurrentState: types.ActionCurrentState{ - Job: job, - Action: chosenAction, - Params: actionParams, - Reasoning: reasoning, - }, - ActionResult: types.ActionResult{ - Result: fmt.Sprintf("planning %s, subtasks: %+v", planResult.Goal, planResult.Subtasks), - }, - } - job.Result.SetResult(stateResult) - job.CallbackWithResult(stateResult) - - xlog.Info("[Planning] starts", "agent", a.Character.Name, "goal", planResult.Goal) - for _, s := range planResult.Subtasks { - xlog.Info("[Planning] subtask", "agent", a.Character.Name, "action", s.Action, "reasoning", s.Reasoning) - } - - if len(planResult.Subtasks) == 0 { - return conv, fmt.Errorf("no subtasks") - } - - // Execute all subtasks in sequence - for _, subtask := range planResult.Subtasks { - xlog.Info("[subtask] Generating parameters", - "agent", a.Character.Name, - "action", subtask.Action, - "reasoning", reasoning, - ) - - subTaskAction := a.availableActions().Find(subtask.Action) - if subTaskAction == nil { - xlog.Error("Action not found: %s", subtask.Action) - return conv, fmt.Errorf("action %s not found", subtask.Action) - } - - subTaskReasoning := fmt.Sprintf("%s Overall goal is: %s", subtask.Reasoning, planResult.Goal) - - params, err := a.generateParameters(job, pickTemplate, subTaskAction, conv, subTaskReasoning, maxRetries) - if err != nil { - xlog.Error("error generating action's parameters", "error", err) - return conv, fmt.Errorf("error generating action's parameters: %w", err) - - } - actionParams = params.actionParams - - if !job.Callback(types.ActionCurrentState{ - Job: job, - Action: subTaskAction, - Params: actionParams, - Reasoning: subTaskReasoning, - }) { - job.Result.SetResult(types.ActionState{ - ActionCurrentState: types.ActionCurrentState{ - Job: job, - Action: chosenAction, - Params: actionParams, - Reasoning: subTaskReasoning, - }, - ActionResult: types.ActionResult{ - Result: "stopped by callback", - }, - }) - job.Result.Conversation = conv - job.Result.Finish(nil) - break - } - - result, err := a.runAction(job, subTaskAction, actionParams) - if err != nil { - xlog.Error("error running action", "error", err) - return conv, fmt.Errorf("error running action: %w", err) - } - - stateResult := types.ActionState{ - ActionCurrentState: types.ActionCurrentState{ - Job: job, - Action: subTaskAction, - Params: actionParams, - Reasoning: subTaskReasoning, - }, - ActionResult: result, - } - job.Result.SetResult(stateResult) - job.CallbackWithResult(stateResult) - xlog.Debug("[subtask] Action executed", "agent", a.Character.Name, "action", subTaskAction.Definition().Name, "result", result) - conv = a.addFunctionResultToConversation(job.GetContext(), subTaskAction, actionParams, result, conv) - } - - return conv, nil -} - // getAvailableActionsForJob returns available actions including user-defined ones for a specific job func (a *Agent) getAvailableActionsForJob(job *types.Job) types.Actions { // Start with regular available actions @@ -446,22 +110,7 @@ func (a *Agent) getAvailableActionsForJob(job *types.Job) types.Actions { func (a *Agent) availableActions() types.Actions { // defaultActions := append(a.options.userActions, action.NewReply()) - addPlanAction := func(actions types.Actions) types.Actions { - if !a.options.canPlan { - return actions - } - plannablesActions := []string{} - for _, a := range actions { - if a.Plannable() { - plannablesActions = append(plannablesActions, a.Definition().Name.String()) - } - } - planAction := action.NewPlan(plannablesActions) - actions = append(actions, planAction) - return actions - } - - defaultActions := append(a.mcpActions, a.options.userActions...) + defaultActions := slices.Clone(a.options.userActions) if a.options.initiateConversations && a.selfEvaluationInProgress { // && self-evaluation.. acts := append(defaultActions, action.NewConversation()) @@ -472,7 +121,7 @@ func (a *Agent) availableActions() types.Actions { // acts = append(acts, action.NewStop()) // } - return addPlanAction(acts) + return acts } if a.options.canStopItself { @@ -480,14 +129,14 @@ func (a *Agent) availableActions() types.Actions { if a.options.enableHUD { acts = append(acts, action.NewState()) } - return addPlanAction(acts) + return acts } if a.options.enableHUD { - return addPlanAction(append(defaultActions, action.NewState())) + return append(defaultActions, action.NewState()) } - return addPlanAction(defaultActions) + return defaultActions } func (a *Agent) prepareHUD() (promptHUD *PromptHUD) { @@ -502,150 +151,3 @@ func (a *Agent) prepareHUD() (promptHUD *PromptHUD) { ShowCharacter: a.options.showCharacter, } } - -// pickAction picks an action based on the conversation -func (a *Agent) pickAction(job *types.Job, templ string, messages []openai.ChatCompletionMessage, maxRetries int) (types.Action, types.ActionParams, string, error) { - c := messages - - xlog.Debug("[pickAction] picking action starts", "messages", messages) - - // Get available actions including user-defined ones - availableActions := a.getAvailableActionsForJob(job) - - // Identify the goal of this conversation - - if !a.options.forceReasoning || job.ToolChoice != "" { - xlog.Debug("not forcing reasoning", "forceReasoning", a.options.forceReasoning, "ToolChoice", job.ToolChoice) - // We also could avoid to use functions here and get just a reply from the LLM - // and then use the reply to get the action - thought, err := a.decision(job, - messages, - availableActions.ToTools(), - job.ToolChoice, - maxRetries) - if err != nil { - return nil, nil, "", err - } - - xlog.Debug("thought action Name", "actionName", thought.actionName) - xlog.Debug("thought message", "message", thought.message) - - // Find the action - chosenAction := availableActions.Find(thought.actionName) - if chosenAction == nil || thought.actionName == "" { - xlog.Debug("no answer") - - // LLM replied with an answer? - //fmt.Errorf("no action found for intent:" + thought.actioName) - return nil, nil, thought.message, nil - } - xlog.Debug(fmt.Sprintf("chosenAction: %v", chosenAction.Definition().Name)) - return chosenAction, thought.actionParams, thought.message, nil - } - - // Force the LLM to think and we extract a "reasoning" to pick a specific action and with which parameters - xlog.Debug("[pickAction] forcing reasoning") - - prompt, err := renderTemplate(templ, a.prepareHUD(), a.availableActions(), "") - if err != nil { - return nil, nil, "", err - } - // Get the LLM to think on what to do - // and have a thought - if !Messages(c).Exist(prompt) { - c = append([]openai.ChatCompletionMessage{ - { - Role: "system", - Content: prompt, - }, - }, c...) - } - - // Create a detailed prompt for reasoning that includes available actions and their properties - reasoningPrompt := "Analyze the current situation and determine the best course of action. Consider the following:\n\n" - reasoningPrompt += "Available Actions:\n" - for _, act := range a.availableActions() { - reasoningPrompt += fmt.Sprintf("- %s: %s\n", act.Definition().Name, act.Definition().Description) - if len(act.Definition().Properties) > 0 { - reasoningPrompt += " Properties:\n" - for name, prop := range act.Definition().Properties { - reasoningPrompt += fmt.Sprintf(" - %s: %s\n", name, prop.Description) - } - } - reasoningPrompt += "\n" - } - reasoningPrompt += "\nProvide a detailed reasoning about what action would be most appropriate in this situation and why. You can also just reply with a simple message by choosing the 'reply' or 'answer' action." - - // Get reasoning using askLLM - reasoningMsg, err := a.askLLM(job.GetContext(), - append(c, openai.ChatCompletionMessage{ - Role: "system", - Content: reasoningPrompt, - }), - maxRetries) - if err != nil { - return nil, nil, "", fmt.Errorf("failed to get reasoning: %w", err) - } - - originalReasoning := reasoningMsg.Content - - xlog.Debug("[pickAction] picking action", "messages", c) - - actionsID := []string{"reply"} - for _, m := range a.availableActions() { - actionsID = append(actionsID, m.Definition().Name.String()) - } - - xlog.Debug("[pickAction] actionsID", "actionsID", actionsID) - - intentionsTools := action.NewIntention(actionsID...) - // TODO: FORCE to select ana ction here - // NOTE: we do not give the full conversation here to pick the action - // to avoid hallucinations - - // Extract an action - params, err := a.decision(job, - append(c, openai.ChatCompletionMessage{ - Role: "system", - Content: "Pick the relevant action given the following reasoning: " + originalReasoning, - }), - types.Actions{intentionsTools}.ToTools(), - intentionsTools.Definition().Name.String(), maxRetries) - if err != nil { - return nil, nil, "", fmt.Errorf("failed to get the action tool parameters: %v", err) - } - - if params.actionParams == nil { - xlog.Debug("[pickAction] no action params found") - return nil, nil, params.message, nil - } - - actionChoice := action.IntentResponse{} - err = params.actionParams.Unmarshal(&actionChoice) - if err != nil { - return nil, nil, "", err - } - - if actionChoice.Tool == "" || actionChoice.Tool == "reply" { - xlog.Debug("[pickAction] no action found, replying") - return nil, nil, "", nil - } - - chosenAction := a.availableActions().Find(actionChoice.Tool) - - xlog.Debug("[pickAction] chosenAction", "chosenAction", chosenAction, "actionName", actionChoice.Tool) - - // // Let's double check if the action is correct by asking the LLM to judge it - - // if chosenAction!= nil { - // promptString:= "Given the following goal and thoughts, is the action correct? \n\n" - // promptString+= fmt.Sprintf("Goal: %s\n", goalResponse.Goal) - // promptString+= fmt.Sprintf("Thoughts: %s\n", originalReasoning) - // promptString+= fmt.Sprintf("Action: %s\n", chosenAction.Definition().Name.String()) - // promptString+= fmt.Sprintf("Action description: %s\n", chosenAction.Definition().Description) - // promptString+= fmt.Sprintf("Action parameters: %s\n", params.actionParams) - - // } - - return chosenAction, nil, originalReasoning, nil -} diff --git a/core/agent/agent.go b/core/agent/agent.go index e492becb..80fd5c88 100644 --- a/core/agent/agent.go +++ b/core/agent/agent.go @@ -3,6 +3,7 @@ package agent import ( "bytes" "context" + "encoding/json" "errors" "fmt" "io" @@ -14,6 +15,7 @@ import ( "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/mudler/LocalAGI/pkg/xlog" + "github.com/mudler/cogito" "github.com/mudler/LocalAGI/core/action" "github.com/mudler/LocalAGI/core/types" @@ -26,7 +28,6 @@ const ( UserRole = "user" AssistantRole = "assistant" SystemRole = "system" - maxRetries = 5 ) type Agent struct { @@ -44,14 +45,16 @@ type Agent struct { newConversations chan openai.ChatCompletionMessage - mcpActions types.Actions mcpSessions []*mcp.ClientSession + // only contains the MCP action definitions for observables + mcpActionDefinitions types.Actions subscriberMutex sync.Mutex newMessagesSubscribers []func(openai.ChatCompletionMessage) observer Observer + llm cogito.LLM sharedState *types.AgentSharedState } @@ -69,7 +72,7 @@ func New(opts ...Option) (*Agent, error) { } client := llm.NewClient(options.LLMAPI.APIKey, options.LLMAPI.APIURL, options.timeout) - + llmClient := cogito.NewOpenAILLM(options.LLMAPI.Model, options.LLMAPI.APIKey, options.LLMAPI.APIURL) c := context.Background() if options.context != nil { c = options.context @@ -82,6 +85,7 @@ func New(opts ...Option) (*Agent, error) { client: client, Character: options.character, currentState: &types.AgentInternalState{}, + llm: llmClient, context: types.NewActionContext(ctx, cancel), newConversations: make(chan openai.ChatCompletionMessage), newMessagesSubscribers: options.newConversationsSubscribers, @@ -260,37 +264,6 @@ func (a *Agent) TTS(ctx context.Context, text string) ([]byte, error) { return buf.Bytes(), nil } -func (a *Agent) askLLM(ctx context.Context, conversation []openai.ChatCompletionMessage, maxRetries int) (openai.ChatCompletionMessage, error) { - var resp openai.ChatCompletionResponse - var err error - - for attempt := 0; attempt <= maxRetries; attempt++ { - resp, err = a.client.CreateChatCompletion(ctx, - openai.ChatCompletionRequest{ - Model: a.options.LLMAPI.Model, - Messages: conversation, - }, - ) - if err == nil && len(resp.Choices) == 1 && resp.Choices[0].Message.Content != "" { - break - } - xlog.Warn("Error asking LLM, retrying", "attempt", attempt+1, "error", err) - if attempt < maxRetries { - time.Sleep(2 * time.Second) // Optional: Add a delay between retries - } - } - - if err != nil { - return openai.ChatCompletionMessage{}, err - } - - if len(resp.Choices) != 1 { - return openai.ChatCompletionMessage{}, fmt.Errorf("not enough choices: %w", err) - } - - return resp.Choices[0].Message, nil -} - var ErrContextCanceled = fmt.Errorf("context canceled") func (a *Agent) Stop() { @@ -324,93 +297,6 @@ func (a *Agent) Memory() RAGDB { return a.options.ragdb } -func (a *Agent) runAction(job *types.Job, chosenAction types.Action, params types.ActionParams) (result types.ActionResult, err error) { - var obs *types.Observable - if job.Obs != nil { - obs = a.observer.NewObservable() - obs.Name = "action" - obs.Icon = "bolt" - obs.ParentID = job.Obs.ID - obs.Creation = &types.Creation{ - FunctionDefinition: chosenAction.Definition().ToFunctionDefinition(), - FunctionParams: params, - } - a.observer.Update(*obs) - } - - xlog.Info("[runAction] Running action", "action", chosenAction.Definition().Name, "agent", a.Character.Name, "params", params.String()) - - for _, act := range a.availableActions() { - if act.Definition().Name == chosenAction.Definition().Name { - res, err := act.Run(job.GetContext(), a.sharedState, params) - if err != nil { - if obs != nil { - obs.Completion = &types.Completion{ - Error: err.Error(), - } - } - - return types.ActionResult{}, fmt.Errorf("error running action: %w", err) - } - - if obs != nil { - obs.Progress = append(obs.Progress, types.Progress{ - ActionResult: res.Result, - }) - a.observer.Update(*obs) - } - - result = res - } - } - - if chosenAction.Definition().Name.Is(action.StateActionName) { - // We need to store the result in the state - state := types.AgentInternalState{} - - err = params.Unmarshal(&state) - if err != nil { - werr := fmt.Errorf("error unmarshalling state of the agent: %w", err) - if obs != nil { - obs.Completion = &types.Completion{ - Error: werr.Error(), - } - } - return types.ActionResult{}, werr - } - // update the current state with the one we just got from the action - a.currentState = &state - if obs != nil { - obs.Progress = append(obs.Progress, types.Progress{ - AgentState: &state, - }) - a.observer.Update(*obs) - } - - // update the state file - if a.options.statefile != "" { - if err := a.SaveState(a.options.statefile); err != nil { - if obs != nil { - obs.Completion = &types.Completion{ - Error: err.Error(), - } - } - - return types.ActionResult{}, err - } - } - } - - xlog.Debug("[runAction] Action result", "action", chosenAction.Definition().Name, "params", params.String(), "result", result.Result) - - if obs != nil { - obs.MakeLastProgressCompletion() - a.observer.Update(*obs) - } - - return result, nil -} - func (a *Agent) processPrompts(ctx context.Context, conversation Messages) Messages { // Add custom prompts for _, prompt := range a.options.prompts { @@ -674,6 +560,45 @@ func (a *Agent) filterJob(job *types.Job) (ok bool, err error) { return failedBy == "" && (!hasTriggers || triggeredBy != ""), nil } +// replyWithToolCall handles user-defined actions by recording the action state without setting Response +func (a *Agent) replyWithToolCall(job *types.Job, conv []openai.ChatCompletionMessage, params types.ActionParams, chosenAction types.Action, reasoning string) { + // Record the action state so the webui can detect this is a user-defined action + stateResult := types.ActionState{ + ActionCurrentState: types.ActionCurrentState{ + Job: job, + Action: chosenAction, + Params: params, + Reasoning: reasoning, + }, + ActionResult: types.ActionResult{ + Result: reasoning, // The reasoning/message to show to user + }, + } + + // Add the action state to the job result + job.Result.SetResult(stateResult) + + // Used by the observer + conv = append(conv, openai.ChatCompletionMessage{ + Role: "assistant", + ToolCalls: []openai.ToolCall{ + { + Type: openai.ToolTypeFunction, + Function: openai.FunctionCall{ + Name: chosenAction.Definition().ToFunctionDefinition().Name, + Arguments: params.String(), + }, + }, + }, + }) + + // Set conversation but leave Response empty + // The webui will detect the user-defined action and generate the proper tool call response + job.Result.Conversation = conv + // job.Result.Response remains empty - this signals to webui that it should check State + job.Result.Finish(nil) +} + // validateBuiltinTools checks that builtin tools specified by the user can be matched to available actions func (a *Agent) validateBuiltinTools(job *types.Job) { builtinTools := job.GetBuiltinTools() @@ -682,7 +607,7 @@ func (a *Agent) validateBuiltinTools(job *types.Job) { } // Get available actions - availableActions := a.mcpActions + availableActions := a.availableActions() for _, tool := range builtinTools { functionName := tool.Name @@ -709,56 +634,86 @@ func (a *Agent) validateBuiltinTools(job *types.Job) { } } -// replyWithToolCall handles user-defined actions by recording the action state without setting Response -func (a *Agent) replyWithToolCall(job *types.Job, conv []openai.ChatCompletionMessage, params types.ActionParams, chosenAction types.Action, reasoning string) { - // Record the action state so the webui can detect this is a user-defined action - stateResult := types.ActionState{ - ActionCurrentState: types.ActionCurrentState{ - Job: job, - Action: chosenAction, - Params: params, - Reasoning: reasoning, - }, - ActionResult: types.ActionResult{ - Result: reasoning, // The reasoning/message to show to user - }, - } - - // Add the action state to the job result - job.Result.SetResult(stateResult) - - // Used by the observer +func (a *Agent) addFunctionResultToConversation(ctx context.Context, chosenAction types.Action, actionParams types.ActionParams, result types.ActionResult, conv Messages) Messages { + // calling the function conv = append(conv, openai.ChatCompletionMessage{ Role: "assistant", ToolCalls: []openai.ToolCall{ { Type: openai.ToolTypeFunction, Function: openai.FunctionCall{ - Name: chosenAction.Definition().ToFunctionDefinition().Name, - Arguments: params.String(), + Name: chosenAction.Definition().Name.String(), + Arguments: actionParams.String(), }, }, }, }) - // Set conversation but leave Response empty - // The webui will detect the user-defined action and generate the proper tool call response - job.Result.Conversation = conv - // job.Result.Response remains empty - this signals to webui that it should check State - job.Result.Finish(nil) + // result of calling the function + + // If it contains an image, we need to put it in the conversation (if supported by the model) + if result.ImageBase64Result != "" { + // iF model support both images and text, process it as a single multicontent message and return + if !a.options.SeparatedMultimodalModel() { + conv = append(conv, openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleTool, + MultiContent: []openai.ChatMessagePart{ + { + Type: openai.ChatMessagePartTypeText, + Text: result.Result, + }, + { + Type: openai.ChatMessagePartTypeImageURL, + ImageURL: &openai.ChatMessageImageURL{ + URL: result.ImageBase64Result, + }, + }, + }, + Name: chosenAction.Definition().Name.String(), + ToolCallID: chosenAction.Definition().Name.String(), + }) + + return conv + } else { + // We need to describe the image first, and we will process the text separately (we do not return here) + imageDescription, err := a.describeImage(ctx, a.options.LLMAPI.MultimodalModel, result.ImageBase64Result) + if err != nil { + xlog.Error("Error describing image", "error", err) + } else { + conv = append(conv, openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleTool, + Content: fmt.Sprintf("Tool generated an image, the description of the image is: %s", imageDescription), + Name: chosenAction.Definition().Name.String(), + ToolCallID: chosenAction.Definition().Name.String(), + }) + if result.Result != "" { + conv = append(conv, openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleTool, + Content: result.Result, + Name: chosenAction.Definition().Name.String(), + ToolCallID: chosenAction.Definition().Name.String(), + }) + } + } + } + } else { + conv = append(conv, openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleTool, + Content: result.Result, + Name: chosenAction.Definition().Name.String(), + ToolCallID: chosenAction.Definition().Name.String(), + }) + } + + return conv } -func (a *Agent) consumeJob(job *types.Job, role string, retries int) { +func (a *Agent) consumeJob(job *types.Job, role string) { if err := job.GetContext().Err(); err != nil { job.Result.Finish(fmt.Errorf("expired")) return } - if retries < 1 { - job.Result.Finish(fmt.Errorf("Exceeded recursive retries")) - return - } - a.Lock() paused := a.pause a.Unlock() @@ -804,292 +759,307 @@ func (a *Agent) consumeJob(job *types.Job, role string, retries int) { // Validate builtin tools against available actions a.validateBuiltinTools(job) - var pickTemplate string - var reEvaluationTemplate string + fragment := cogito.NewFragment(conv...) if selfEvaluation { - pickTemplate = pickSelfTemplate - reEvaluationTemplate = reSelfEvalTemplate - } else { - pickTemplate = pickActionTemplate - reEvaluationTemplate = reEvalTemplate + fragment = fragment.AddStartMessage("system", pickSelfTemplate) } - // choose an action first - var chosenAction types.Action - var reasoning string - var actionParams types.ActionParams - - if job.HasNextAction() { - // if we are being re-evaluated, we already have the action - // and the reasoning. Consume it here and reset it - action, params, reason := job.GetNextAction() - chosenAction = *action - reasoning = reason - if params == nil { - p, err := a.generateParameters(job, pickTemplate, chosenAction, conv, reasoning, maxRetries) - if err != nil { - xlog.Error("Error generating parameters, trying again", "error", err) - // try again - job.SetNextAction(&chosenAction, nil, reasoning) - a.consumeJob(job, role, retries-1) - return - } - actionParams = p.actionParams - } else { - actionParams = *params - } - job.ResetNextAction() - } else { - var err error - chosenAction, actionParams, reasoning, err = a.pickAction(job, pickTemplate, conv, maxRetries) + if a.options.enableHUD { + prompt, err := renderTemplate(hudTemplate, a.prepareHUD(), a.availableActions(), "") if err != nil { - xlog.Error("Error picking action", "error", err) - job.Result.Finish(err) + job.Result.Finish(fmt.Errorf("error renderTemplate: %w", err)) return } + fragment = fragment.AddStartMessage("system", prompt) } - if chosenAction == nil { - // If no action was picked up, the reasoning is the message returned by the assistant - // so we can consume it as if it was a reply. - xlog.Info("No action to do, just reply", "agent", a.Character.Name, "reasoning", reasoning) + availableActions := a.getAvailableActionsForJob(job) + cogitoTools := availableActions.ToCogitoTools(job.GetContext(), a.sharedState) + allActions := append(availableActions, a.mcpActionDefinitions...) - if reasoning != "" { - conv = append(conv, openai.ChatCompletionMessage{ - Role: "assistant", - Content: a.cleanupLLMResponse(reasoning), - }) - } else { - xlog.Info("No reasoning, just reply", "agent", a.Character.Name) - msg, err := a.askLLM(job.GetContext(), conv, maxRetries) - if err != nil { - job.Result.Finish(fmt.Errorf("error asking LLM for a reply: %w", err)) - return - } - msg.Content = a.cleanupLLMResponse(msg.Content) - conv = append(conv, msg) - reasoning = msg.Content + obs := job.Obs + if obs == nil && a.observer != nil && job.Obs != nil { + obs = a.observer.NewObservable() + obs.Name = "decision" + obs.Icon = "brain" + obs.ParentID = job.Obs.ID + obs.Creation = &types.Creation{ + ChatCompletionRequest: &openai.ChatCompletionRequest{ + Model: a.options.LLMAPI.Model, + Messages: conv, + }, } + } - var satisfied bool - var err error - // Evaluate the response - satisfied, conv, err = a.handleEvaluation(job, conv, job.GetEvaluationLoop()) - if err != nil { - job.Result.Finish(fmt.Errorf("error evaluating response: %w", err)) - return + defer func() { + if obs != nil && a.observer != nil { + obs.MakeLastProgressCompletion() + a.observer.Update(*obs) } + }() - if !satisfied { - // If not satisfied, continue with the conversation - job.ConversationHistory = conv - job.IncrementEvaluationLoop() - a.consumeJob(job, role, retries) - return - } + var err error + var userTool bool + + var observables = make(map[string]*types.Observable) + + cogitoOpts := []cogito.Option{ + cogito.WithMCPs(a.mcpSessions...), + cogito.WithReasoningCallback(func(s string) { + xlog.Debug("Cogito reasoning callback", "status", s) + + if a.observer != nil && job.Obs != nil { + job.Obs.AddProgress( + types.Progress{ + ChatCompletionResponse: &openai.ChatCompletionResponse{ + Choices: []openai.ChatCompletionChoice{ + { + Message: openai.ChatCompletionMessage{ + Role: "assistant", + Content: s, + }, + }, + }, + }, + }) + a.observer.Update(*job.Obs) + } + }), + cogito.WithTools( + cogitoTools..., + ), + cogito.WithToolCallResultCallback(func(t cogito.ToolStatus) { + if a.observer != nil && obs != nil { + obs := observables[t.ToolArguments.ID] + obs.Progress = append(obs.Progress, types.Progress{ + ActionResult: t.Result, + }) + obs.Name = "action" + obs.Icon = "bolt" + obs.MakeLastProgressCompletion() + a.observer.Update(*obs) + } - xlog.Debug("Finish job with reasoning", "reasoning", reasoning, "agent", a.Character.Name, "conversation", fmt.Sprintf("%+v", conv)) - job.Result.Conversation = conv - job.Result.AddFinalizer(func(conv []openai.ChatCompletionMessage) { - a.saveCurrentConversation(conv) - }) - job.Result.SetResponse(reasoning) - job.Result.Finish(nil) - return - } + aa := allActions.Find(t.Name) + state := types.ActionState{ + ActionCurrentState: types.ActionCurrentState{ + Job: job, + Action: aa, + Params: types.ActionParams(t.ToolArguments.Arguments), + Reasoning: t.ToolArguments.Reasoning, + }, + ActionResult: types.ActionResult{Result: t.Result}, + } + job.Result.SetResult(state) + job.CallbackWithResult(state) + conv = a.addFunctionResultToConversation(job.GetContext(), aa, types.ActionParams(t.ToolArguments.Arguments), types.ActionResult{Result: t.Result}, conv) + }), + cogito.WithToolCallBack( + func(tc *cogito.ToolChoice) bool { - if chosenAction.Definition().Name.Is(action.StopActionName) { - xlog.Info("LLM decided to stop") - job.Result.Finish(nil) - return - } + xlog.Debug("Tool call back", "tool_call", tc) - // if we force a reasoning, we need to generate the parameters - if a.options.forceReasoning || actionParams == nil { - xlog.Info("Generating parameters", - "agent", a.Character.Name, - "action", chosenAction.Definition().Name, - "reasoning", reasoning, - ) + // Check if this is a user-defined action + chosenAction := allActions.Find(tc.Name) - params, err := a.generateParameters(job, pickTemplate, chosenAction, conv, reasoning, maxRetries) - if err != nil { - xlog.Error("Error generating parameters, trying again", "error", err) - // try again - job.SetNextAction(&chosenAction, nil, reasoning) - a.consumeJob(job, role, retries-1) - return - } - actionParams = params.actionParams - } + xlog.Debug("Action found", "action", chosenAction) - xlog.Info( - "Generated parameters", - "agent", a.Character.Name, - "action", chosenAction.Definition().Name, - "reasoning", reasoning, - "params", actionParams.String(), - ) + if chosenAction != nil && types.IsActionUserDefined(chosenAction) { + xlog.Debug("User-defined action chosen, returning tool call", "action", chosenAction.Definition().Name) + a.replyWithToolCall(job, conv, tc.Arguments, chosenAction, tc.Reasoning) + userTool = true + return false + } - if actionParams == nil { - job.Result.Finish(fmt.Errorf("no parameters")) - xlog.Error("No parameters", "agent", a.Character.Name) - return - } + if a.observer != nil && job.Obs != nil { + obs := a.observer.NewObservable() + obs.Name = "decision" + obs.ParentID = job.Obs.ID + obs.Icon = "brain" + obs.Creation = &types.Creation{ + ChatCompletionRequest: &openai.ChatCompletionRequest{ + Model: a.options.LLMAPI.Model, + Messages: conv, + }, + FunctionDefinition: chosenAction.Definition().ToFunctionDefinition(), + FunctionParams: types.ActionParams(tc.Arguments), + } - if a.options.loopDetectionSteps > 0 && len(job.GetPastActions()) > 0 { - count := 0 - for _, pastAction := range job.GetPastActions() { - if pastAction.Action.Definition().Name == chosenAction.Definition().Name && - pastAction.Params.String() == actionParams.String() { - count++ - } - } - if count > a.options.loopDetectionSteps { - xlog.Info("Loop detected, stopping agent", "agent", a.Character.Name, "action", chosenAction.Definition().Name) - a.reply(job, role, conv, actionParams, chosenAction, reasoning) - return - } - xlog.Debug("Checked for loops", "action", chosenAction.Definition().Name, "count", count) - } + a.observer.Update(*obs) + observables[tc.ID] = obs + } - job.AddPastAction(chosenAction, &actionParams) - - if !job.Callback(types.ActionCurrentState{ - Job: job, - Action: chosenAction, - Params: actionParams, - Reasoning: reasoning}) { - job.Result.SetResult(types.ActionState{ - ActionCurrentState: types.ActionCurrentState{ - Job: job, - Action: chosenAction, - Params: actionParams, - Reasoning: reasoning, - }, - ActionResult: types.ActionResult{Result: "stopped by callback"}}) - job.Result.Conversation = conv - job.Result.Finish(nil) - return - } + switch tc.Name { + case action.StopActionName: + return false + case action.ConversationActionName: + message := action.ConversationActionResponse{} + toolArgs, _ := json.Marshal(tc.Arguments) + if err := json.Unmarshal([]byte(toolArgs), &message); err != nil { + xlog.Error("Error unmarshalling conversation response", "error", err) + job.Result.Finish(fmt.Errorf("error unmarshalling conversation response: %w", err)) + return false + } - var err error - conv, err = a.handlePlanning(job.GetContext(), job, chosenAction, actionParams, reasoning, pickTemplate, conv) - if err != nil { - xlog.Error("error handling planning", "error", err) - a.reply(job, role, append(conv, openai.ChatCompletionMessage{ - Role: "assistant", - Content: fmt.Sprintf("Error handling planning: %v", err), - }), actionParams, chosenAction, reasoning) - return - } + msg := openai.ChatCompletionMessage{ + Role: "assistant", + Content: message.Message, + } - if selfEvaluation && a.options.initiateConversations && - chosenAction.Definition().Name.Is(action.ConversationActionName) { + go func(agent *Agent) { + xlog.Info("Sending new conversation to channel", "agent", agent.Character.Name, "message", msg.Content) + agent.newConversations <- msg + }(a) - xlog.Info("LLM decided to initiate a new conversation", "agent", a.Character.Name) + job.Result.Conversation = []openai.ChatCompletionMessage{ + msg, + } + job.Result.SetResponse("decided to initiate a new conversation") + job.Result.Finish(nil) + return true + case action.StateActionName: + // We need to store the result in the state + state := types.AgentInternalState{} + dat, _ := json.Marshal(tc.Arguments) + err = json.Unmarshal(dat, &state) + if err != nil { + werr := fmt.Errorf("error unmarshalling state of the agent: %w", err) + if obs != nil && a.observer != nil { + obs.Completion = &types.Completion{ + Error: werr.Error(), + } + a.observer.Update(*obs) + } + return false + } + // update the current state with the one we just got from the action + a.currentState = &state + if obs != nil && a.observer != nil { + obs.Progress = append(obs.Progress, types.Progress{ + AgentState: &state, + }) + a.observer.Update(*obs) + } - message := action.ConversationActionResponse{} - if err := actionParams.Unmarshal(&message); err != nil { - xlog.Error("Error unmarshalling conversation response", "error", err) - job.Result.Finish(fmt.Errorf("error unmarshalling conversation response: %w", err)) - return - } + // update the state file + if a.options.statefile != "" { + if err := a.SaveState(a.options.statefile); err != nil { + if obs != nil && a.observer != nil { + obs.Completion = &types.Completion{ + Error: err.Error(), + } + a.observer.Update(*obs) + } + + return false + } + } - msg := openai.ChatCompletionMessage{ - Role: "assistant", - Content: message.Message, - } + } + + cont := job.Callback(types.ActionCurrentState{ + Job: job, + Action: chosenAction, + Params: types.ActionParams(tc.Arguments), + Reasoning: tc.Reasoning}) + + if !cont { + job.Result.SetResult( + types.ActionState{ + ActionCurrentState: types.ActionCurrentState{ + Job: job, + Action: chosenAction, + Params: types.ActionParams(tc.Arguments), + Reasoning: tc.Reasoning, + }, + ActionResult: types.ActionResult{Result: "stopped by callback"}, + }) - go func(agent *Agent) { - xlog.Info("Sending new conversation to channel", "agent", agent.Character.Name, "message", msg.Content) - agent.newConversations <- msg - }(a) + job.Result.Conversation = conv + job.Result.Finish(nil) - job.Result.Conversation = []openai.ChatCompletionMessage{ - msg, + } + return cont + }, + ), + } + + if a.options.canPlan { + cogitoOpts = append(cogitoOpts, cogito.EnableAutoPlan) + if a.options.enableEvaluation { + cogitoOpts = append(cogitoOpts, cogito.EnableAutoPlanReEvaluator) } - job.Result.SetResponse("decided to initiate a new conversation") - job.Result.Finish(nil) - return } - // if we have a reply action, we need to run it - if chosenAction.Definition().Name.Is(action.ReplyActionName) { - a.reply(job, role, conv, actionParams, chosenAction, reasoning) - return + if a.options.forceReasoning { + cogitoOpts = append(cogitoOpts, cogito.WithForceReasoning()) } - if !chosenAction.Definition().Name.Is(action.PlanActionName) { - // Check if this is a user-defined action - if types.IsActionUserDefined(chosenAction) { - xlog.Debug("User-defined action chosen, returning tool call", "action", chosenAction.Definition().Name) - a.replyWithToolCall(job, conv, actionParams, chosenAction, reasoning) - return - } + if a.options.maxEvaluationLoops > 0 { + cogitoOpts = append(cogitoOpts, + cogito.WithMaxAttempts(a.options.maxEvaluationLoops), + cogito.WithIterations(a.options.maxEvaluationLoops), + ) + } - result, err := a.runAction(job, chosenAction, actionParams) - if err != nil { - result.Result = fmt.Sprintf("Error running tool: %v", err) - } + fragment, err = cogito.ExecuteTools( + a.llm, fragment, + cogitoOpts..., + ) - stateResult := types.ActionState{ - ActionCurrentState: types.ActionCurrentState{ - Job: job, - Action: chosenAction, - Params: actionParams, - Reasoning: reasoning, - }, - ActionResult: result, + if err != nil && !errors.Is(err, cogito.ErrNoToolSelected) && !errors.Is(err, cogito.ErrGoalNotAchieved) && !userTool { + if obs != nil { + obs.Completion = &types.Completion{ + Error: err.Error(), + } + a.observer.Update(*obs) } - job.Result.SetResult(stateResult) - job.CallbackWithResult(stateResult) - xlog.Debug("Action executed", "agent", a.Character.Name, "action", chosenAction.Definition().Name, "result", result) - - conv = a.addFunctionResultToConversation(job.GetContext(), chosenAction, actionParams, result, conv) + xlog.Error("Error executing cogito", "error", err) + job.Result.Finish(err) + return } - // given the result, we can now re-evaluate the conversation - followingAction, followingParams, reasoning, err := a.pickAction(job, reEvaluationTemplate, conv, maxRetries) - if err != nil { - job.Result.Conversation = conv - job.Result.Finish(fmt.Errorf("error picking action: %w", err)) + if userTool { return } - if followingAction != nil && - !followingAction.Definition().Name.Is(action.ReplyActionName) && - !chosenAction.Definition().Name.Is(action.ReplyActionName) { - - xlog.Info("Following action", "action", followingAction.Definition().Name, "agent", a.Character.Name) - job.ConversationHistory = conv + if len(fragment.Messages) > 0 && + fragment.LastMessage().Role == "tool" { + toolToCall := fragment.Messages[len(fragment.Messages)-2].ToolCalls[0].Function.Name + switch toolToCall { + case action.StopActionName: + job.Result.Finish(nil) + return + } + } - // We need to do another action (?) - // The agent decided to do another action - // call ourselves again - job.SetNextAction(&followingAction, &followingParams, reasoning) - a.consumeJob(job, role, retries) + if len(fragment.Messages) == 0 { + job.Result.Finish(fmt.Errorf("no messages in fragment")) return } - // Evaluate the final response - var satisfied bool - satisfied, conv, err = a.handleEvaluation(job, conv, job.GetEvaluationLoop()) + responseFragment, err := a.llm.Ask(job.GetContext(), fragment) if err != nil { - job.Result.Finish(fmt.Errorf("error evaluating response: %w", err)) + job.Result.Finish(err) return } - if !satisfied { - // If not satisfied, continue with the conversation - job.ConversationHistory = conv - job.IncrementEvaluationLoop() - a.consumeJob(job, role, retries) - return - } + result := a.cleanupLLMResponse(responseFragment.LastMessage().Content) + + conv = append(fragment.Messages, openai.ChatCompletionMessage{ + Role: "assistant", + Content: result, + }) - a.reply(job, role, conv, actionParams, chosenAction, reasoning) + job.Result.Plans = fragment.Status.Plans + job.Result.Conversation = conv + job.ConversationHistory = conv + job.Result.AddFinalizer(func(conv []openai.ChatCompletionMessage) { + a.saveCurrentConversation(conv) + }) + job.Result.SetResponse(result) + job.Result.Finish(nil) } func stripThinkingTags(content string) string { @@ -1110,155 +1080,6 @@ func (a *Agent) cleanupLLMResponse(content string) string { return content } -func (a *Agent) reply(job *types.Job, role string, conv Messages, actionParams types.ActionParams, chosenAction types.Action, reasoning string) { - job.Result.Conversation = conv - - // At this point can only be a reply action - xlog.Info("Computing reply", "agent", a.Character.Name) - - forceResponsePrompt := "Reply to the user without using any tools or function calls. Just reply with the message." - - // If we have a hud, display it when answering normally - if a.options.enableHUD { - prompt, err := renderTemplate(hudTemplate, a.prepareHUD(), a.availableActions(), reasoning) - if err != nil { - job.Result.Conversation = conv - job.Result.Finish(fmt.Errorf("error renderTemplate: %w", err)) - return - } - if !Messages(conv).Exist(prompt) { - conv = append([]openai.ChatCompletionMessage{ - { - Role: "system", - Content: prompt, - }, - { - Role: "system", - Content: forceResponsePrompt, - }, - }, conv...) - } - } else { - conv = append([]openai.ChatCompletionMessage{ - { - Role: "system", - Content: forceResponsePrompt, - }, - }, conv...) - } - - xlog.Info("Reasoning, ask LLM for a reply", "agent", a.Character.Name) - xlog.Debug("Conversation", "conversation", fmt.Sprintf("%+v", conv)) - msg, err := a.askLLM(job.GetContext(), conv, maxRetries) - if err != nil { - job.Result.Conversation = conv - job.Result.Finish(err) - xlog.Error("Error asking LLM for a reply", "error", err) - return - } - - msg.Content = a.cleanupLLMResponse(msg.Content) - - if msg.Content == "" { - // If we didn't got any message, we can use the response from the action (it should be a reply) - - replyResponse := action.ReplyResponse{} - if err := actionParams.Unmarshal(&replyResponse); err != nil { - job.Result.Conversation = conv - job.Result.Finish(fmt.Errorf("error unmarshalling reply response: %w", err)) - return - } - - if chosenAction.Definition().Name.Is(action.ReplyActionName) && replyResponse.Message != "" { - xlog.Info("No output returned from conversation, using the action response as a reply " + replyResponse.Message) - msg.Content = a.cleanupLLMResponse(replyResponse.Message) - } - } - - conv = append(conv, msg) - job.Result.SetResponse(msg.Content) - xlog.Info("Response from LLM", "response", msg.Content, "agent", a.Character.Name) - job.Result.Conversation = conv - job.Result.AddFinalizer(func(conv []openai.ChatCompletionMessage) { - a.saveCurrentConversation(conv) - }) - job.Result.Finish(nil) -} - -func (a *Agent) addFunctionResultToConversation(ctx context.Context, chosenAction types.Action, actionParams types.ActionParams, result types.ActionResult, conv Messages) Messages { - // calling the function - conv = append(conv, openai.ChatCompletionMessage{ - Role: "assistant", - ToolCalls: []openai.ToolCall{ - { - Type: openai.ToolTypeFunction, - Function: openai.FunctionCall{ - Name: chosenAction.Definition().Name.String(), - Arguments: actionParams.String(), - }, - }, - }, - }) - - // result of calling the function - - // If it contains an image, we need to put it in the conversation (if supported by the model) - if result.ImageBase64Result != "" { - // iF model support both images and text, process it as a single multicontent message and return - if !a.options.SeparatedMultimodalModel() { - conv = append(conv, openai.ChatCompletionMessage{ - Role: openai.ChatMessageRoleTool, - MultiContent: []openai.ChatMessagePart{ - { - Type: openai.ChatMessagePartTypeText, - Text: result.Result, - }, - { - Type: openai.ChatMessagePartTypeImageURL, - ImageURL: &openai.ChatMessageImageURL{ - URL: result.ImageBase64Result, - }, - }, - }, - Name: chosenAction.Definition().Name.String(), - ToolCallID: chosenAction.Definition().Name.String(), - }) - - return conv - } else { - // We need to describe the image first, and we will process the text separately (we do not return here) - imageDescription, err := a.describeImage(ctx, a.options.LLMAPI.MultimodalModel, result.ImageBase64Result) - if err != nil { - xlog.Error("Error describing image", "error", err) - } else { - conv = append(conv, openai.ChatCompletionMessage{ - Role: openai.ChatMessageRoleTool, - Content: fmt.Sprintf("Tool generated an image, the description of the image is: %s", imageDescription), - Name: chosenAction.Definition().Name.String(), - ToolCallID: chosenAction.Definition().Name.String(), - }) - if result.Result != "" { - conv = append(conv, openai.ChatCompletionMessage{ - Role: openai.ChatMessageRoleTool, - Content: result.Result, - Name: chosenAction.Definition().Name.String(), - ToolCallID: chosenAction.Definition().Name.String(), - }) - } - } - } - } else { - conv = append(conv, openai.ChatCompletionMessage{ - Role: openai.ChatMessageRoleTool, - Content: result.Result, - Name: chosenAction.Definition().Name.String(), - ToolCallID: chosenAction.Definition().Name.String(), - }) - } - - return conv -} - // This is running in the background. func (a *Agent) periodicallyRun(timer *time.Timer) { // Remember always to reset the timer - if we don't the agent will stop.. @@ -1316,7 +1137,7 @@ func (a *Agent) periodicallyRun(timer *time.Timer) { } // Process the reminder as a normal conversation - a.consumeJob(reminderJob, UserRole, a.options.loopDetectionSteps) + a.consumeJob(reminderJob, UserRole) // After the reminder job is complete, ensure the user is notified if reminderJob.Result != nil && reminderJob.Result.Conversation != nil { @@ -1359,7 +1180,7 @@ func (a *Agent) periodicallyRun(timer *time.Timer) { types.WithReasoningCallback(a.options.reasoningCallback), types.WithResultCallback(a.options.resultCallback), ) - a.consumeJob(whatNext, SystemRole, a.options.loopDetectionSteps) + a.consumeJob(whatNext, SystemRole) xlog.Info("STOP -- Periodically run is done", "agent", a.Character.Name) } @@ -1418,7 +1239,7 @@ func (a *Agent) run(timer *time.Timer) error { <-timer.C } xlog.Debug("Agent is consuming a job", "agent", a.Character.Name, "job", job) - a.consumeJob(job, UserRole, a.options.loopDetectionSteps) + a.consumeJob(job, UserRole) timer.Reset(a.options.periodicRuns) case <-a.context.Done(): // Agent has been canceled, return error diff --git a/core/agent/agent_test.go b/core/agent/agent_test.go index cdc2e8bd..c32d5e7b 100644 --- a/core/agent/agent_test.go +++ b/core/agent/agent_test.go @@ -37,7 +37,8 @@ var debugOptions = []types.JobOption{ } type TestAction struct { - response map[string]string + response map[string]string + definition *types.ActionDefinition } func (a *TestAction) Plannable() bool { @@ -55,7 +56,7 @@ func (a *TestAction) Run(c context.Context, sharedState *types.AgentSharedState, } func (a *TestAction) Definition() types.ActionDefinition { - return types.ActionDefinition{ + def := types.ActionDefinition{ Name: "get_weather", Description: "get current weather", Properties: map[string]jsonschema.Definition{ @@ -71,6 +72,11 @@ func (a *TestAction) Definition() types.ActionDefinition { Required: []string{"location"}, } + + if a.definition != nil { + def = *a.definition + } + return def } type FakeStoreResultAction struct { @@ -128,7 +134,6 @@ var _ = Describe("Agent test", func() { WithModel(testModel), EnableForceReasoning, WithTimeout("10m"), - WithLoopDetectionSteps(3), // WithRandomIdentity(), WithActions(&TestAction{response: map[string]string{ "boston": testActionResult, @@ -153,6 +158,9 @@ var _ = Describe("Agent test", func() { } Expect(reasons).To(ContainElement(testActionResult), fmt.Sprint(res)) Expect(reasons).To(ContainElement(testActionResult2), fmt.Sprint(res)) + + Expect(len(res.Conversation)).To(BeNumerically(">", 1), fmt.Sprint(res.Conversation)) + reasons = []string{} res = agent.Ask( @@ -225,11 +233,33 @@ var _ = Describe("Agent test", func() { WithModel(testModel), WithLLMAPIKey(apiKeyURL), WithTimeout("10m"), + WithMaxEvaluationLoops(1), WithActions( - &TestAction{response: map[string]string{ - "boston": testActionResult, - "milan": testActionResult2, - }}, + &TestAction{ + response: map[string]string{ + "boston": testActionResult, + "milan": testActionResult2, + }, + }, + &TestAction{ + response: map[string]string{ + "flight": "Flight options from Boston to Milan (April 22-26, 2025):\n• Outbound: Boston Logan (BOS) → Milan Malpensa (MXP), April 22, 2025\n - Economy: $450-650 (Alitalia, Delta, Lufthansa)\n - Business: $1,200-1,800\n - Flight time: 8h 15m (1 stop) or 9h 45m (direct)\n• Return: Milan Malpensa (MXP) → Boston Logan (BOS), April 26, 2025\n - Economy: $420-580\n - Business: $1,100-1,600\n• Total estimated cost: $870-1,230 per person\n• Best booking window: 2-3 months in advance for optimal prices", + "hotel": "Hotel recommendations in Milan for April 22-26, 2025:\n• Luxury (4-5 stars): $200-400/night\n - Hotel Principe di Savoia: $380/night (central location)\n - Mandarin Oriental: $420/night (luxury amenities)\n• Mid-range (3-4 stars): $120-200/night\n - Hotel Spadari al Duomo: $160/night (near cathedral)\n - Hotel Milano Scala: $140/night (theater district)\n• Budget (2-3 stars): $80-120/night\n - Hotel Bernina: $95/night (near train station)\n• Total 4-night stay: $320-1,680 depending on category\n• Booking tip: Reserve early for spring season discounts", + "car": "Car rental options in Milan for April 22-26, 2025:\n• Economy cars: $35-50/day (Fiat 500, VW Polo)\n• Compact cars: $45-65/day (Ford Focus, Opel Astra)\n• Mid-size cars: $60-85/day (BMW 3 Series, Audi A4)\n• SUV/Luxury: $90-150/day (BMW X3, Mercedes E-Class)\n• Total 4-day rental: $140-600\n• Pickup locations: Milan Malpensa Airport, Milan Central Station, city center\n• Insurance: $15-25/day additional\n• Fuel: ~$60-80 for 4 days of city driving\n• Parking: $20-40/day in city center hotels", + "food": "Dining budget and recommendations for Milan (April 22-26, 2025):\n• Fine dining: $80-150/person (Michelin-starred restaurants)\n - Cracco: $120/person (2 Michelin stars)\n - Trussardi alla Scala: $100/person\n• Mid-range restaurants: $40-80/person\n - Luini: $15/person (famous panzerotti)\n - Piz: $25/person (authentic pizza)\n• Casual dining: $20-40/person\n - Aperitivo bars: $15-25/person\n - Street food: $8-15/person\n• Daily food budget: $60-120/person\n• Total 4-day food cost: $240-480/person\n• Must-try: Risotto alla Milanese, Osso Buco, Panettone", + }, + definition: &types.ActionDefinition{ + Name: "search_internet", + Description: "search the internet for information", + Properties: map[string]jsonschema.Definition{ + "query": { + Type: jsonschema.String, + Description: "The query to search for", + }, + }, + Required: []string{"query"}, + }, + }, ), EnablePlanning, EnableForceReasoning, @@ -241,21 +271,20 @@ var _ = Describe("Agent test", func() { defer agent.Stop() result := agent.Ask( - types.WithText("Use the plan tool to do two actions in sequence: search for the weather in boston and search for the weather in milan"), + types.WithText("Create a plan for my 4-day trip from Boston to milan in April of this year (2025). I'm not sure about the dates yet, I want you to find out the best dates also according to what you find."), ) - Expect(len(result.State)).To(BeNumerically(">", 1)) + + Expect(len(result.Conversation)).To(BeNumerically(">", 1), fmt.Sprint(result.Conversation)) + + Expect(len(result.Plans)).To(BeNumerically(">=", 1), fmt.Sprintf("%+v", result)) + Expect(len(result.State)).To(BeNumerically(">=", 1)) actionsExecuted := []string{} - actionResults := []string{} for _, r := range result.State { xlog.Info(r.Result) actionsExecuted = append(actionsExecuted, r.Action.Definition().Name.String()) - actionResults = append(actionResults, r.ActionResult.Result) } - Expect(actionsExecuted).To(ContainElement("get_weather"), fmt.Sprint(result)) - Expect(actionsExecuted).To(ContainElement("plan"), fmt.Sprint(result)) - Expect(actionResults).To(ContainElement(testActionResult), fmt.Sprint(result)) - Expect(actionResults).To(ContainElement(testActionResult2), fmt.Sprint(result)) + Expect(actionsExecuted).To(Or(ContainElement("search_internet"), ContainElement("get_weather")), fmt.Sprint(result)) }) It("Can initiate conversations", func() { diff --git a/core/agent/evaluation.go b/core/agent/evaluation.go deleted file mode 100644 index 7373f91f..00000000 --- a/core/agent/evaluation.go +++ /dev/null @@ -1,162 +0,0 @@ -package agent - -import ( - "fmt" - - "github.com/mudler/LocalAGI/core/types" - "github.com/mudler/LocalAGI/pkg/llm" - "github.com/mudler/LocalAGI/pkg/xlog" - "github.com/sashabaranov/go-openai" - "github.com/sashabaranov/go-openai/jsonschema" -) - -type EvaluationResult struct { - Satisfied bool `json:"satisfied"` - Gaps []string `json:"gaps"` - Reasoning string `json:"reasoning"` -} - -type GoalExtraction struct { - Goal string `json:"goal"` - Constraints []string `json:"constraints"` - Context string `json:"context"` -} - -func (a *Agent) extractGoal(job *types.Job, conv []openai.ChatCompletionMessage) (*GoalExtraction, error) { - // Create the goal extraction schema - schema := jsonschema.Definition{ - Type: jsonschema.Object, - Properties: map[string]jsonschema.Definition{ - "goal": { - Type: jsonschema.String, - Description: "The main goal or request from the user", - }, - "constraints": { - Type: jsonschema.Array, - Items: &jsonschema.Definition{ - Type: jsonschema.String, - }, - Description: "Any constraints or requirements specified by the user", - }, - "context": { - Type: jsonschema.String, - Description: "Additional context that might be relevant for understanding the goal", - }, - }, - Required: []string{"goal", "constraints", "context"}, - } - - // Create the goal extraction prompt - prompt := `Analyze the conversation and extract the user's main goal, any constraints, and relevant context. -Consider the entire conversation history to understand the complete context and requirements. -Focus on identifying the primary objective and any specific requirements or limitations mentioned.` - - var result GoalExtraction - err := llm.GenerateTypedJSONWithConversation(job.GetContext(), a.client, - append( - []openai.ChatCompletionMessage{ - { - Role: "system", - Content: prompt, - }, - }, - conv...), a.options.LLMAPI.Model, schema, &result) - if err != nil { - return nil, fmt.Errorf("error extracting goal: %w", err) - } - - return &result, nil -} - -func (a *Agent) evaluateJob(job *types.Job, conv []openai.ChatCompletionMessage) (*EvaluationResult, error) { - if !a.options.enableEvaluation { - return &EvaluationResult{Satisfied: true}, nil - } - - // Extract the goal first - goal, err := a.extractGoal(job, conv) - if err != nil { - return nil, fmt.Errorf("error extracting goal: %w", err) - } - - // Create the evaluation schema - schema := jsonschema.Definition{ - Type: jsonschema.Object, - Properties: map[string]jsonschema.Definition{ - "satisfied": { - Type: jsonschema.Boolean, - }, - "gaps": { - Type: jsonschema.Array, - Items: &jsonschema.Definition{ - Type: jsonschema.String, - }, - }, - "reasoning": { - Type: jsonschema.String, - }, - }, - Required: []string{"satisfied", "gaps", "reasoning"}, - } - - // Create the evaluation prompt - prompt := fmt.Sprintf(`Evaluate if the assistant has satisfied the user's request. Consider: -1. The identified goal: %s -2. Constraints and requirements: %v -3. Context: %s -4. The conversation history -5. Any gaps or missing information -6. Whether the response fully addresses the user's needs - -Provide a detailed evaluation with specific gaps if any are found.`, - goal.Goal, - goal.Constraints, - goal.Context) - - var result EvaluationResult - err = llm.GenerateTypedJSONWithConversation(job.GetContext(), a.client, - append( - []openai.ChatCompletionMessage{ - { - Role: "system", - Content: prompt, - }, - }, - conv...), - a.options.LLMAPI.Model, schema, &result) - if err != nil { - return nil, fmt.Errorf("error generating evaluation: %w", err) - } - - return &result, nil -} - -func (a *Agent) handleEvaluation(job *types.Job, conv []openai.ChatCompletionMessage, currentLoop int) (bool, []openai.ChatCompletionMessage, error) { - if !a.options.enableEvaluation || currentLoop >= a.options.maxEvaluationLoops { - return true, conv, nil - } - - result, err := a.evaluateJob(job, conv) - if err != nil { - return false, conv, err - } - - if result.Satisfied { - return true, conv, nil - } - - // If there are gaps, we need to address them - if len(result.Gaps) > 0 { - // Add the evaluation result to the conversation - conv = append(conv, openai.ChatCompletionMessage{ - Role: "system", - Content: fmt.Sprintf("Evaluation found gaps that need to be addressed:\n%s\nReasoning: %s", - result.Gaps, result.Reasoning), - }) - - xlog.Debug("Evaluation found gaps, incrementing loop count", "loop", currentLoop+1) - return false, conv, nil - } - - return true, conv, nil -} diff --git a/core/agent/knowledgebase.go b/core/agent/knowledgebase.go index 22ee8574..774a9c00 100644 --- a/core/agent/knowledgebase.go +++ b/core/agent/knowledgebase.go @@ -8,6 +8,7 @@ import ( "github.com/mudler/LocalAGI/core/types" "github.com/mudler/LocalAGI/pkg/xlog" + "github.com/mudler/cogito" "github.com/sashabaranov/go-openai" ) @@ -17,7 +18,7 @@ func (a *Agent) knowledgeBaseLookup(job *types.Job, conv Messages) Messages { xlog.Debug("[Knowledge Base Lookup] Disabled, skipping", "agent", a.Character.Name) return conv } - + var obs *types.Observable if job != nil && job.Obs != nil && a.observer != nil { obs = a.observer.NewObservable() @@ -90,7 +91,7 @@ func (a *Agent) knowledgeBaseLookup(job *types.Job, conv Messages) Messages { if obs != nil { obs.Completion = &types.Completion{ - Conversation: []openai.ChatCompletionMessage{systemMessage}, + Conversation: []openai.ChatCompletionMessage{systemMessage}, } a.observer.Update(*obs) } @@ -125,13 +126,12 @@ func (a *Agent) saveCurrentConversation(conv Messages) { xlog.Info("Saving conversation", "agent", a.Character.Name, "conversation size", len(conv)) if a.options.enableSummaryMemory && len(conv) > 0 { - 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(conv).String(), - }}, maxRetries) + fragment := cogito.NewEmptyFragment().AddStartMessage("user", "Summarize the conversation below, keep the highlights as a bullet list:\n"+Messages(conv).String()) + fragment, err := a.llm.Ask(a.context.Context, fragment) if err != nil { xlog.Error("Error summarizing conversation", "error", err) } + msg := fragment.LastMessage() if err := a.options.ragdb.Store(msg.Content); err != nil { xlog.Error("Error storing into memory", "error", err) diff --git a/core/agent/mcp.go b/core/agent/mcp.go index 62d85b37..7d4176d0 100644 --- a/core/agent/mcp.go +++ b/core/agent/mcp.go @@ -3,7 +3,7 @@ package agent import ( "context" "encoding/json" - "errors" + "fmt" "os" "os/exec" "time" @@ -13,12 +13,11 @@ import ( "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/mudler/LocalAGI/core/types" "github.com/mudler/LocalAGI/pkg/xlog" - "github.com/rs/zerolog/log" "github.com/sashabaranov/go-openai/jsonschema" ) -var _ types.Action = &mcpAction{} +var _ types.Action = &mcpWrapperAction{} type MCPServer struct { URL string `json:"url"` @@ -31,44 +30,20 @@ type MCPSTDIOServer struct { Cmd string `json:"cmd"` } -type mcpAction struct { +type mcpWrapperAction struct { mcpClient *mcp.ClientSession inputSchema ToolInputSchema toolName string toolDescription string } -func (a *mcpAction) Plannable() bool { - return true +func (m *mcpWrapperAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) { + // We don't call the method here, it is used by cogito. + // We will just use these to have a list of actions that MCP server provides for resolving internal states + return types.ActionResult{Result: "MCP action called"}, fmt.Errorf("not implemented") } -func (m *mcpAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) { - p := &mcp.CallToolParams{ - Name: m.toolName, - Arguments: params, - } - resp, err := m.mcpClient.CallTool(ctx, p) - if err != nil { - xlog.Error("Failed to call tool", "error", err.Error()) - return types.ActionResult{}, err - } - - xlog.Debug("MCP response", "response", resp) - - if resp.IsError { - log.Error().Msgf("tool failed") - return types.ActionResult{}, errors.New("tool failed") - } - - result := "" - for _, c := range resp.Content { - result += c.(*mcp.TextContent).Text - } - - return types.ActionResult{Result: result}, nil -} - -func (m *mcpAction) Definition() types.ActionDefinition { +func (m *mcpWrapperAction) Definition() types.ActionDefinition { props := map[string]jsonschema.Definition{} dat, err := json.Marshal(m.inputSchema.Properties) if err != nil { @@ -92,7 +67,6 @@ type ToolInputSchema struct { } func (a *Agent) addTools(client *mcp.ClientSession) (types.Actions, error) { - var generatedActions types.Actions tools, err := client.ListTools(a.context, nil) @@ -124,7 +98,7 @@ func (a *Agent) addTools(client *mcp.ClientSession) (types.Actions, error) { } // Create a new action with Client + tool - generatedActions = append(generatedActions, &mcpAction{ + generatedActions = append(generatedActions, &mcpWrapperAction{ mcpClient: client, toolName: t.Name, inputSchema: inputSchema, @@ -133,7 +107,6 @@ func (a *Agent) addTools(client *mcp.ClientSession) (types.Actions, error) { } return generatedActions, nil - } // bearerTokenRoundTripper is a custom roundtripper that injects a bearer token @@ -163,8 +136,9 @@ func newBearerTokenRoundTripper(token string, base http.RoundTripper) http.Round } func (a *Agent) initMCPActions() error { + a.closeMCPSTDIOServers() // Make sure we stop all previous servers if any is active - a.mcpActions = nil + a.mcpActionDefinitions = nil var err error generatedActions := types.Actions{} @@ -235,7 +209,7 @@ func (a *Agent) initMCPActions() error { generatedActions = append(generatedActions, actions...) } - a.mcpActions = generatedActions + a.mcpActionDefinitions = generatedActions return err } diff --git a/core/agent/options.go b/core/agent/options.go index 6243d073..d77b3cc9 100644 --- a/core/agent/options.go +++ b/core/agent/options.go @@ -33,7 +33,6 @@ type options struct { canStopItself bool initiateConversations bool - loopDetectionSteps int forceReasoning bool canPlan bool characterfile string @@ -78,7 +77,6 @@ func defaultOptions() *options { return &options{ parallelJobs: 1, periodicRuns: 15 * time.Minute, - loopDetectionSteps: 10, maxEvaluationLoops: 2, enableEvaluation: false, LLMAPI: llmOptions{ @@ -136,13 +134,6 @@ func WithTimeout(timeout string) Option { } } -func WithLoopDetectionSteps(steps int) Option { - return func(o *options) error { - o.loopDetectionSteps = steps - return nil - } -} - func WithConversationsPath(path string) Option { return func(o *options) error { o.conversationsPath = path diff --git a/core/agent/templates.go b/core/agent/templates.go index 09fe9ace..c98458bb 100644 --- a/core/agent/templates.go +++ b/core/agent/templates.go @@ -94,7 +94,6 @@ Guidelines: 4. Update your state appropriately When making decisions: -- Use the "reply" tool to provide final responses - Update your state using appropriate tools - Plan complex tasks using the planning tool - Consider both immediate and long-term goals @@ -105,43 +104,4 @@ Remember: - Keep track of your progress and state - Be proactive in addressing potential issues -Available Tools: -{{range .Actions -}} -- {{.Name}}: {{.Description }} -{{ end }} - -{{if .Reasoning}}Previous Reasoning: {{.Reasoning}}{{end}} ` + hudTemplate - -const reSelfEvalTemplate = pickSelfTemplate - -const pickActionTemplate = hudTemplate + ` -Your only task is to analyze the conversation and determine a goal and the best tool to use, or just a final response if we have fullfilled the goal. - -Guidelines: -1. Review the current state, what was done already and context -2. Consider available tools and their purposes -3. Plan your approach carefully -4. Explain your reasoning clearly - -When choosing actions: -- Use "reply" or "answer" tools for direct responses -- Select appropriate tools for specific tasks -- Consider the impact of each action -- Plan for potential challenges - -Decision Process: -1. Analyze the situation -2. Consider available options -3. Choose the best course of action -4. Explain your reasoning -5. Execute the chosen action - -Available Tools: -{{range .Actions -}} -- {{.Name}}: {{.Description }} -{{ end }} - -{{if .Reasoning}}Previous Reasoning: {{.Reasoning}}{{end}}` - -const reEvalTemplate = pickActionTemplate diff --git a/core/state/config.go b/core/state/config.go index 8e74b8b3..8d4c00e4 100644 --- a/core/state/config.go +++ b/core/state/config.go @@ -70,7 +70,6 @@ type AgentConfig struct { EnableKnowledgeBase bool `json:"enable_kb" form:"enable_kb"` EnableReasoning bool `json:"enable_reasoning" form:"enable_reasoning"` KnowledgeBaseResults int `json:"kb_results" form:"kb_results"` - LoopDetectionSteps int `json:"loop_detection_steps" form:"loop_detection_steps"` CanStopItself bool `json:"can_stop_itself" form:"can_stop_itself"` SystemPrompt string `json:"system_prompt" form:"system_prompt"` LongTermMemory bool `json:"long_term_memory" form:"long_term_memory"` @@ -292,15 +291,6 @@ func NewAgentConfigMeta( HelpText: "Enable agent to explain its reasoning process", Tags: config.Tags{Section: "AdvancedSettings"}, }, - { - Name: "loop_detection_steps", - Label: "Max Loop Detection Steps", - Type: "number", - DefaultValue: 5, - Min: 1, - Step: 1, - Tags: config.Tags{Section: "AdvancedSettings"}, - }, { Name: "parallel_jobs", Label: "Parallel Jobs", diff --git a/core/state/pool.go b/core/state/pool.go index 6c9af12b..a0f56133 100644 --- a/core/state/pool.go +++ b/core/state/pool.go @@ -570,19 +570,16 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O opts = append(opts, EnableKnowledgeBaseWithResults(config.KnowledgeBaseResults)) } - if config.LoopDetectionSteps > 0 { - opts = append(opts, WithLoopDetectionSteps(config.LoopDetectionSteps)) - } - if config.ParallelJobs > 0 { opts = append(opts, WithParallelJobs(config.ParallelJobs)) } if config.EnableEvaluation { opts = append(opts, EnableEvaluation()) - if config.MaxEvaluationLoops > 0 { - opts = append(opts, WithMaxEvaluationLoops(config.MaxEvaluationLoops)) - } + } + + if config.MaxEvaluationLoops > 0 { + opts = append(opts, WithMaxEvaluationLoops(config.MaxEvaluationLoops)) } xlog.Info("Starting agent", "name", name, "config", config) diff --git a/core/types/actions.go b/core/types/actions.go index ecfda0b1..8d65bef1 100644 --- a/core/types/actions.go +++ b/core/types/actions.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/mudler/cogito" "github.com/sashabaranov/go-openai" "github.com/sashabaranov/go-openai/jsonschema" ) @@ -88,11 +89,35 @@ func (a ActionDefinition) ToFunctionDefinition() *openai.FunctionDefinition { } } +type cogitoWrapper struct { + action Action + ctx context.Context + sharedState *AgentSharedState +} + +func (c *cogitoWrapper) Tool() openai.Tool { + return openai.Tool{ + Type: openai.ToolTypeFunction, + Function: c.action.Definition().ToFunctionDefinition(), + } +} + +func (c *cogitoWrapper) Run(args map[string]any) (string, error) { + ctx := c.ctx + if ctx == nil { + ctx = context.Background() + } + result, err := c.action.Run(ctx, c.sharedState, ActionParams(args)) + if err != nil { + return "", err + } + return result.Result, nil +} + // Actions is something the agent can do type Action interface { Run(ctx context.Context, sharedState *AgentSharedState, action ActionParams) (ActionResult, error) Definition() ActionDefinition - Plannable() bool } // UserDefinedChecker interface to identify user-defined actions @@ -162,6 +187,14 @@ func (a Actions) ToTools() []openai.Tool { return tools } +func (a Actions) ToCogitoTools(ctx context.Context, sharedState *AgentSharedState) []cogito.Tool { + tools := []cogito.Tool{} + for _, action := range a { + tools = append(tools, &cogitoWrapper{action: action, ctx: ctx, sharedState: sharedState}) + } + return tools +} + func (a Actions) Find(name string) Action { for _, action := range a { if action.Definition().Name.Is(name) { diff --git a/core/types/job.go b/core/types/job.go index 30bb94ef..29e905ce 100644 --- a/core/types/job.go +++ b/core/types/job.go @@ -5,6 +5,7 @@ import ( "log" "github.com/google/uuid" + "github.com/mudler/cogito" "github.com/sashabaranov/go-openai" ) @@ -20,19 +21,15 @@ type Job struct { UUID string Metadata map[string]interface{} DoneFilter bool - + // Tools available for this job BuiltinTools []ActionDefinition // Built-in tools like web search UserTools []ActionDefinition // User-defined function tools ToolChoice string - pastActions []*ActionRequest - nextAction *Action - nextActionParams *ActionParams - nextActionReasoning string - - context context.Context - cancel context.CancelFunc + context context.Context + fragment *cogito.Fragment + cancel context.CancelFunc Obs *Observable } @@ -108,37 +105,6 @@ func (j *Job) CallbackWithResult(stateResult ActionState) { j.ResultCallback(stateResult) } -func (j *Job) SetNextAction(action *Action, params *ActionParams, reasoning string) { - j.nextAction = action - j.nextActionParams = params - j.nextActionReasoning = reasoning -} - -func (j *Job) AddPastAction(action Action, params *ActionParams) { - j.pastActions = append(j.pastActions, &ActionRequest{ - Action: action, - Params: params, - }) -} - -func (j *Job) GetPastActions() []*ActionRequest { - return j.pastActions -} - -func (j *Job) GetNextAction() (*Action, *ActionParams, string) { - return j.nextAction, j.nextActionParams, j.nextActionReasoning -} - -func (j *Job) HasNextAction() bool { - return j.nextAction != nil -} - -func (j *Job) ResetNextAction() { - j.nextAction = nil - j.nextActionParams = nil - j.nextActionReasoning = "" -} - func WithTextImage(text, image string) JobOption { return func(j *Job) { j.ConversationHistory = append(j.ConversationHistory, openai.ChatCompletionMessage{ diff --git a/core/types/result.go b/core/types/result.go index 6e520d71..822907cb 100644 --- a/core/types/result.go +++ b/core/types/result.go @@ -3,6 +3,7 @@ package types import ( "sync" + "github.com/mudler/cogito" "github.com/sashabaranov/go-openai" ) @@ -11,6 +12,7 @@ type JobResult struct { sync.Mutex // The result of a job State []ActionState + Plans []cogito.PlanStatus Conversation []openai.ChatCompletionMessage Finalizers []func([]openai.ChatCompletionMessage) diff --git a/go.mod b/go.mod index 5bd58cc3..7067d471 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/mudler/LocalAGI -go 1.24 +go 1.24.1 toolchain go1.24.2 @@ -15,8 +15,8 @@ require ( github.com/gofiber/template/html/v2 v2.1.3 github.com/google/go-github/v69 v69.2.0 github.com/google/uuid v1.6.0 - github.com/gorilla/websocket v1.5.3 github.com/modelcontextprotocol/go-sdk v1.0.0 + github.com/mudler/cogito v0.4.1-0.20251027122454-3f982f8fe711 github.com/onsi/ginkgo/v2 v2.25.3 github.com/onsi/gomega v1.38.2 github.com/philippgille/chromem-go v0.7.0 @@ -34,9 +34,18 @@ require ( ) require ( + dario.cat/mergo v1.0.1 // indirect github.com/JohannesKaufmann/dom v0.2.0 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/Masterminds/sprig/v3 v3.3.0 // indirect github.com/google/jsonschema-go v0.3.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/huandu/xstrings v1.5.0 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/spf13/cast v1.7.0 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect ) @@ -92,6 +101,6 @@ require ( golang.org/x/text v0.28.0 // indirect golang.org/x/tools v0.36.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/protobuf v1.36.7 // indirect + google.golang.org/protobuf v1.36.8 // indirect maunium.net/go/maulogger/v2 v2.4.1 // indirect ) diff --git a/go.sum b/go.sum index 295e8ce6..4990fa00 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,19 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/JohannesKaufmann/dom v0.2.0 h1:1bragmEb19K8lHAqgFgqCpiPCFEZMTXzOIEjuxkUfLQ= github.com/JohannesKaufmann/dom v0.2.0/go.mod h1:57iSUl5RKric4bUkgos4zu6Xt5LMHUnw3TF1l5CbGZo= github.com/JohannesKaufmann/html-to-markdown/v2 v2.4.0 h1:C0/TerKdQX9Y9pbYi1EsLr5LDNANsqunyI/btpyfCg8= github.com/JohannesKaufmann/html-to-markdown/v2 v2.4.0/go.mod h1:OLaKh+giepO8j7teevrNwiy/fwf8LXgoc9g7rwaE1jk= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo= github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= @@ -19,18 +29,41 @@ github.com/antchfx/xpath v1.3.4 h1:1ixrW1VnXd4HurCj7qnqnR0jo14g8JMe20Fshg1Vgz4= github.com/antchfx/xpath v1.3.4/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= github.com/bwmarrin/discordgo v0.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+EgjDno= github.com/bwmarrin/discordgo v0.29.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/chasefleming/elem-go v0.30.0 h1:BlhV1ekv1RbFiM8XZUQeln1Ikb4D+bu2eDO4agREvok= github.com/chasefleming/elem-go v0.30.0/go.mod h1:hz73qILBIKnTgOujnSMtEj20/epI+f6vg71RUilJAA4= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2 h1:flLYmnQFZNo04x2NPehMbf30m7Pli57xwZ0NFqR/hb0= github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2/go.mod h1:NtWqRzAp/1tw+twkW8uuBenEVVYndEAZACWU3F3xdoQ= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw= +github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/donseba/go-htmx v1.12.0 h1:7tESER0uxaqsuGMv3yP3pK1drfBUXM6apG4H7/3+IgE= github.com/donseba/go-htmx v1.12.0/go.mod h1:8PTAYvNKf8+QYis+DpAsggKz+sa2qljtMgvdAeNBh5s= +github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= +github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/emersion/go-imap/v2 v2.0.0-beta.5 h1:H3858DNmBuXyMK1++YrQIRdpKE1MwBc+ywBtg3n+0wA= github.com/emersion/go-imap/v2 v2.0.0-beta.5/go.mod h1:BZTFHsS1hmgBkFlHqbxGLXk2hnRqTItUgwjSSCsYNAk= github.com/emersion/go-message v0.18.2 h1:rl55SQdjd9oJcIoQNhubD2Acs1E6IzlZISRTK7x/Lpg= @@ -41,8 +74,16 @@ github.com/emersion/go-smtp v0.24.0 h1:g6AfoF140mvW0vLNPD/LuCBLEAdlxOjIXqbIkJIS6 github.com/emersion/go-smtp v0.24.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4McvHxCU2gsQ= github.com/eritikass/githubmarkdownconvertergo v0.1.10 h1:mL93ADvYMOeT15DcGtK9AaFFc+RcWcy6kQBC6yS/5f4= github.com/eritikass/githubmarkdownconvertergo v0.1.10/go.mod h1:BdpHs6imOtzE5KorbUtKa6bZ0ZBh1yFcrTTAL8FwDKY= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-telegram/bot v1.17.0 h1:Hs0kGxSj97QFqOQP0zxduY/4tSx8QDzvNI9uVRS+zmY= @@ -62,6 +103,8 @@ github.com/gofiber/template/html/v2 v2.1.3 h1:n1LYBtmr9C0V/k/3qBblXyMxV5B0o/gpb6 github.com/gofiber/template/html/v2 v2.1.3/go.mod h1:U5Fxgc5KpyujU9OqKzy6Kn6Qup6Tm7zdsISR+VpnHRE= github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM= github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= @@ -89,6 +132,8 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o= github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= @@ -97,6 +142,10 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= @@ -107,21 +156,116 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= +github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modelcontextprotocol/go-sdk v1.0.0 h1:Z4MSjLi38bTgLrd/LjSmofqRqyBiVKRyQSJgw8q8V74= github.com/modelcontextprotocol/go-sdk v1.0.0/go.mod h1:nYtYQroQ2KQiM0/SbyEPUWQ6xs4B95gJjEalc9AQyOs= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/mudler/cogito v0.2.0 h1:UzowMlP6kiDLnuwQikac9yUOhI6Qe2tW1jZP5gHQvaY= +github.com/mudler/cogito v0.2.0/go.mod h1:abMwl+CUjCp87IufA2quZdZt0bbLaHHN79o17HbUKxU= +github.com/mudler/cogito v0.3.0 h1:NbVAO3bLkK5oGSY0xq87jlz8C9OIsLW55s+8Hfzeu9s= +github.com/mudler/cogito v0.3.0/go.mod h1:abMwl+CUjCp87IufA2quZdZt0bbLaHHN79o17HbUKxU= +github.com/mudler/cogito v0.3.1 h1:VdoWbZ4Vpxpx/g5blvdMMBHmFykz7Ik2aK5+zecVp8o= +github.com/mudler/cogito v0.3.1/go.mod h1:abMwl+CUjCp87IufA2quZdZt0bbLaHHN79o17HbUKxU= +github.com/mudler/cogito v0.3.2-0.20251020080418-a22dd87a3120 h1:CYhwOgljaenDQC1yLavGLhD+uN0GXjCofol5aj2BGSk= +github.com/mudler/cogito v0.3.2-0.20251020080418-a22dd87a3120/go.mod h1:abMwl+CUjCp87IufA2quZdZt0bbLaHHN79o17HbUKxU= +github.com/mudler/cogito v0.3.2-0.20251020090902-940ed4660784 h1:xRMrA7KQf+MKNTaAlcajlbblpvokGXOr05Nnngq27dU= +github.com/mudler/cogito v0.3.2-0.20251020090902-940ed4660784/go.mod h1:abMwl+CUjCp87IufA2quZdZt0bbLaHHN79o17HbUKxU= +github.com/mudler/cogito v0.3.2-0.20251020203303-3832318bdfc0 h1:E8pn4QCxcsBNpkk5pMQRR9V4aXV1wufLykq8MQu3JHs= +github.com/mudler/cogito v0.3.2-0.20251020203303-3832318bdfc0/go.mod h1:abMwl+CUjCp87IufA2quZdZt0bbLaHHN79o17HbUKxU= +github.com/mudler/cogito v0.3.2-0.20251020204043-f21710b138ec h1:F3gaZIl7bnMvKQx57qFjOqUydVz73ViQ8ZB76LNodgo= +github.com/mudler/cogito v0.3.2-0.20251020204043-f21710b138ec/go.mod h1:abMwl+CUjCp87IufA2quZdZt0bbLaHHN79o17HbUKxU= +github.com/mudler/cogito v0.3.2-0.20251021104853-9c04ac0b45f1 h1:m5cvRFZhlEypGUy4zgW50Yuep/2+fJNfTcsltoc3k9o= +github.com/mudler/cogito v0.3.2-0.20251021104853-9c04ac0b45f1/go.mod h1:abMwl+CUjCp87IufA2quZdZt0bbLaHHN79o17HbUKxU= +github.com/mudler/cogito v0.3.2 h1:VcMlAq4umf3FCixb6Tm0RLwjAS0majmQaXHPcz4nlM0= +github.com/mudler/cogito v0.3.2/go.mod h1:abMwl+CUjCp87IufA2quZdZt0bbLaHHN79o17HbUKxU= +github.com/mudler/cogito v0.3.3-0.20251021164911-a49cf79c2eda h1:Au1tRnahUHDJi9i2IgI1rNpbSjIWeG/ZIlWWuW/94Hg= +github.com/mudler/cogito v0.3.3-0.20251021164911-a49cf79c2eda/go.mod h1:abMwl+CUjCp87IufA2quZdZt0bbLaHHN79o17HbUKxU= +github.com/mudler/cogito v0.3.3-0.20251021180018-921ed81d33e7 h1:Lm+KFRq1ghI7WHjzOlaoq3alidhPcHbSxbiX+YpowTI= +github.com/mudler/cogito v0.3.3-0.20251021180018-921ed81d33e7/go.mod h1:abMwl+CUjCp87IufA2quZdZt0bbLaHHN79o17HbUKxU= +github.com/mudler/cogito v0.3.3-0.20251021201604-550aad0fa074 h1:6Qs9sRadiW5SS6tbZvhw/GnWnHi7RXWrF732eihaFRU= +github.com/mudler/cogito v0.3.3-0.20251021201604-550aad0fa074/go.mod h1:abMwl+CUjCp87IufA2quZdZt0bbLaHHN79o17HbUKxU= +github.com/mudler/cogito v0.3.4-0.20251021203110-1f801b935405 h1:RkqVi/FS0K4zThO61GbyEJW4dGjZ2CDAw//KtzOgZhM= +github.com/mudler/cogito v0.3.4-0.20251021203110-1f801b935405/go.mod h1:abMwl+CUjCp87IufA2quZdZt0bbLaHHN79o17HbUKxU= +github.com/mudler/cogito v0.3.4-0.20251021205807-8b24a604fc4f h1:C+cRbVD8qDTgfOjQIs0kAWmlgteODxhx308Z+CGOO4I= +github.com/mudler/cogito v0.3.4-0.20251021205807-8b24a604fc4f/go.mod h1:abMwl+CUjCp87IufA2quZdZt0bbLaHHN79o17HbUKxU= +github.com/mudler/cogito v0.3.4-0.20251022151954-5f46a5b9e3c4 h1:VwEEePUqeL+Lwu0nxMc2oRxlMx4dm5MZ48CyorpiYEA= +github.com/mudler/cogito v0.3.4-0.20251022151954-5f46a5b9e3c4/go.mod h1:abMwl+CUjCp87IufA2quZdZt0bbLaHHN79o17HbUKxU= +github.com/mudler/cogito v0.3.4-0.20251023130443-7ff6cae505b1 h1:JdB5ttL4P6N+oaVdYZZCZfos3SKOHiyhBTmMIGAiWbU= +github.com/mudler/cogito v0.3.4-0.20251023130443-7ff6cae505b1/go.mod h1:abMwl+CUjCp87IufA2quZdZt0bbLaHHN79o17HbUKxU= +github.com/mudler/cogito v0.3.4-0.20251023134830-411b39f3a81b h1:aGCkFEzNru0ltfv4t8Boysyzs3qngmxOYUAS5Vy6J5I= +github.com/mudler/cogito v0.3.4-0.20251023134830-411b39f3a81b/go.mod h1:abMwl+CUjCp87IufA2quZdZt0bbLaHHN79o17HbUKxU= +github.com/mudler/cogito v0.3.4-0.20251023202832-731ee424d0b8 h1:k1IspOjikHDP9KPuHXF6b8Ea9FTG8Mr6FqGJB970xqc= +github.com/mudler/cogito v0.3.4-0.20251023202832-731ee424d0b8/go.mod h1:abMwl+CUjCp87IufA2quZdZt0bbLaHHN79o17HbUKxU= +github.com/mudler/cogito v0.3.4-0.20251023203233-e2b86b295d73 h1:hXIGdK5jwkpUdg5rtU0ATEoaxEv35Xib9yLDnn/jFZU= +github.com/mudler/cogito v0.3.4-0.20251023203233-e2b86b295d73/go.mod h1:abMwl+CUjCp87IufA2quZdZt0bbLaHHN79o17HbUKxU= +github.com/mudler/cogito v0.3.4-0.20251024080815-78e62ba45d36 h1:PTNz1lu9XISminQxswbQrqNtTmOKJoN6Vu1vQYzmWt4= +github.com/mudler/cogito v0.3.4-0.20251024080815-78e62ba45d36/go.mod h1:abMwl+CUjCp87IufA2quZdZt0bbLaHHN79o17HbUKxU= +github.com/mudler/cogito v0.3.4-0.20251024085926-f6cadae5e7d3 h1:oxj/4bxncH0caQiQjvfOs1dbG7/tA4nYIRKulJ7O+PQ= +github.com/mudler/cogito v0.3.4-0.20251024085926-f6cadae5e7d3/go.mod h1:abMwl+CUjCp87IufA2quZdZt0bbLaHHN79o17HbUKxU= +github.com/mudler/cogito v0.3.4-0.20251024131236-a8bd497a67f4 h1:4EK8BTyoEe4gMQdrkBOmDZtKuFH1pUS0JwdX6qZBZOg= +github.com/mudler/cogito v0.3.4-0.20251024131236-a8bd497a67f4/go.mod h1:abMwl+CUjCp87IufA2quZdZt0bbLaHHN79o17HbUKxU= +github.com/mudler/cogito v0.3.4-0.20251024134814-312dfa8c7614 h1:zdsMzwk7zT7mJKEOwopzKP9h+9MXV5jRSU5CxGHT5qM= +github.com/mudler/cogito v0.3.4-0.20251024134814-312dfa8c7614/go.mod h1:abMwl+CUjCp87IufA2quZdZt0bbLaHHN79o17HbUKxU= +github.com/mudler/cogito v0.3.4-0.20251024135733-ffb57e1ae14f h1:gcby9JLkkTx051yBwOQ3ptqyAt58kN2x5crGEaP5Ns8= +github.com/mudler/cogito v0.3.4-0.20251024135733-ffb57e1ae14f/go.mod h1:abMwl+CUjCp87IufA2quZdZt0bbLaHHN79o17HbUKxU= +github.com/mudler/cogito v0.3.4-0.20251024141958-2cfbe4694d3c h1:qs0S0mmSeawfklmqR0FEiDePpo53TQ+sSqC9jQSB1Lk= +github.com/mudler/cogito v0.3.4-0.20251024141958-2cfbe4694d3c/go.mod h1:abMwl+CUjCp87IufA2quZdZt0bbLaHHN79o17HbUKxU= +github.com/mudler/cogito v0.3.4-0.20251024152704-187d58ed8f15 h1:WQGU1/sE0Owzn5c7TwWvbrRMTiy5Qw9beKNbVPpn+SA= +github.com/mudler/cogito v0.3.4-0.20251024152704-187d58ed8f15/go.mod h1:abMwl+CUjCp87IufA2quZdZt0bbLaHHN79o17HbUKxU= +github.com/mudler/cogito v0.3.4-0.20251024155416-968d509d8b56 h1:s/RudMZPVVFkbceaTadKh+4F4cgODEQ3RqYCLIGmxVE= +github.com/mudler/cogito v0.3.4-0.20251024155416-968d509d8b56/go.mod h1:abMwl+CUjCp87IufA2quZdZt0bbLaHHN79o17HbUKxU= +github.com/mudler/cogito v0.3.4-0.20251024155826-3b6b1f383521 h1:6Zx3GIKmCgtpbz8QkAunqfbn4aHkRoHKSmT7rbpqkJo= +github.com/mudler/cogito v0.3.4-0.20251024155826-3b6b1f383521/go.mod h1:abMwl+CUjCp87IufA2quZdZt0bbLaHHN79o17HbUKxU= +github.com/mudler/cogito v0.3.4-0.20251024192107-6d29a316e1eb h1:qD2x82Sl8U80r9LxHQLYDBlWVzmISDv0VAmV8lZ7xVI= +github.com/mudler/cogito v0.3.4-0.20251024192107-6d29a316e1eb/go.mod h1:abMwl+CUjCp87IufA2quZdZt0bbLaHHN79o17HbUKxU= +github.com/mudler/cogito v0.3.4-0.20251024193252-575a1339dc5a h1:7NG3biLSygaw3nCg9FcVWR5lHvMJs4m1CjYRuUNPTps= +github.com/mudler/cogito v0.3.4-0.20251024193252-575a1339dc5a/go.mod h1:abMwl+CUjCp87IufA2quZdZt0bbLaHHN79o17HbUKxU= +github.com/mudler/cogito v0.4.0 h1:CkdzbQplQW6LDUM6mTqupGFiQVluy0nx7xbYgAfBVKs= +github.com/mudler/cogito v0.4.0/go.mod h1:abMwl+CUjCp87IufA2quZdZt0bbLaHHN79o17HbUKxU= +github.com/mudler/cogito v0.4.1-0.20251027114500-2d074204974a h1:EMhksAuNmgWdouHlx2C4/T0k8KON7L1y8gwphCs+LIY= +github.com/mudler/cogito v0.4.1-0.20251027114500-2d074204974a/go.mod h1:abMwl+CUjCp87IufA2quZdZt0bbLaHHN79o17HbUKxU= +github.com/mudler/cogito v0.4.1-0.20251027121752-933e11d826e2 h1:nyMkF/vlkk7JlyPKWkWMDUzWIIm4Hj6goQKf7L7lmGc= +github.com/mudler/cogito v0.4.1-0.20251027121752-933e11d826e2/go.mod h1:2uhEElCTq8eXSsqJ1JF01oA5h9niXSELVKqCF1PqjEw= +github.com/mudler/cogito v0.4.1-0.20251027122454-3f982f8fe711 h1:poqzlrxY6DhxzlKSL65pXeTydPAdCHWsVBm/FCgnwFU= +github.com/mudler/cogito v0.4.1-0.20251027122454-3f982f8fe711/go.mod h1:2uhEElCTq8eXSsqJ1JF01oA5h9niXSELVKqCF1PqjEw= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo/v2 v2.25.3 h1:Ty8+Yi/ayDAGtk4XxmmfUy4GabvM+MegeB4cDLRi6nw= github.com/onsi/ginkgo/v2 v2.25.3/go.mod h1:43uiyQC4Ed2tkOzLsEYm7hnrb7UJTWHYNsuy3bG/snE= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/philippgille/chromem-go v0.7.0 h1:4jfvfyKymjKNfGxBUhHUcj1kp7B17NL/I1P+vGh1RvY= github.com/philippgille/chromem-go v0.7.0/go.mod h1:hTd+wGEm/fFPQl7ilfCwQXkgEUxceYh86iIdoKMolPo= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkoukk/tiktoken-go v0.1.7 h1:qOBHXX4PHtvIvmOtyg1EeKlwFRiMKAcoMp4Q+bLQDmw= github.com/pkoukk/tiktoken-go v0.1.7/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -142,16 +286,26 @@ github.com/sebdah/goldie/v2 v2.7.1 h1:PkBHymaYdtvEkZV7TmyqKxdmn5/Vcj+8TpATWZjnG5 github.com/sebdah/goldie/v2 v2.7.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc= +github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/slack-go/slack v0.17.3 h1:zV5qO3Q+WJAQ/XwbGfNFrRMaJ5T/naqaonyPV/1TP4g= github.com/slack-go/slack v0.17.3/go.mod h1:X+UqOufi3LYQHDnMG1vxf0J8asC6+WllXrVrhl8/Prk= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fxg= github.com/temoto/robotstxt v1.1.2/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo= +github.com/testcontainers/testcontainers-go v0.38.0 h1:d7uEapLcv2P8AvH8ahLqDMMxda2W9gQN1nRbHS28HBw= +github.com/testcontainers/testcontainers-go v0.38.0/go.mod h1:C52c9MoHpWO+C4aqmgSU+hxlR5jlEayWtgYrb8Pzz1w= github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64 h1:l/T7dYuJEQZOwVOpjIXr1180aM9PZL/d1MnMVIxefX4= github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64/go.mod h1:Q1NAJOuRdQCqN/VIWdnaaEhV8LpeO2rtlBP7/iDJNII= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -164,6 +318,10 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/tmc/langchaingo v0.1.13 h1:rcpMWBIi2y3B90XxfE4Ao8dhCQPVDMaNPnN5cGB1CaA= github.com/tmc/langchaingo v0.1.13/go.mod h1:vpQ5NOIhpzxDfTZK9B6tf2GM/MoaHewPWM5KXXGh7hg= github.com/traefik/yaegi v0.16.1 h1:f1De3DVJqIDKmnasUF6MwmWv1dSEEat0wcpXhD2On3E= @@ -179,8 +337,20 @@ github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT0 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.mau.fi/util v0.3.0 h1:Lt3lbRXP6ZBqTINK0EieRWor3zEwwwrDT14Z5N8RUCs= go.mau.fi/util v0.3.0/go.mod h1:9dGsBCCbZJstx16YgnVMVi3O2bOizELoKpugLD4FoGs= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.starlark.net v0.0.0-20250417143717-f57e51f710eb h1:zOg9DxxrorEmgGUr5UPdCEwKqiqG0MlZciuCuA3XiDE= go.starlark.net v0.0.0-20250417143717-f57e51f710eb/go.mod h1:YKMCv9b1WrfWmeqdV5MAuEHWsu5iC+fe6kYl2sQjdI8= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= @@ -276,8 +446,8 @@ google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAs google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= -google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/client/agents.go b/pkg/client/agents.go index 53df5b92..17255975 100644 --- a/pkg/client/agents.go +++ b/pkg/client/agents.go @@ -8,13 +8,13 @@ import ( // AgentConfig represents the configuration for an agent type AgentConfig struct { - Name string `json:"name"` - Actions []string `json:"actions,omitempty"` - Connectors []string `json:"connectors,omitempty"` - PromptBlocks []string `json:"prompt_blocks,omitempty"` - InitialPrompt string `json:"initial_prompt,omitempty"` - Parallel bool `json:"parallel,omitempty"` - Config map[string]interface{} `json:"config,omitempty"` + Name string `json:"name"` + Actions []string `json:"actions,omitempty"` + Connectors []string `json:"connectors,omitempty"` + PromptBlocks []string `json:"prompt_blocks,omitempty"` + InitialPrompt string `json:"initial_prompt,omitempty"` + Parallel bool `json:"parallel,omitempty"` + EnableReasoning bool `json:"enable_reasoning,omitempty"` } // AgentStatus represents the status of an agent diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index 8881f0c5..e252cf40 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -15,8 +15,8 @@ import ( . "github.com/onsi/gomega" ) -var _ = Describe("Agent test", func() { - Context("Creates an agent and it answers", func() { +var _ = Describe("E2E test", func() { + Context("Creates an agent and it answers", Label("E2E"), func() { BeforeEach(func() { Eventually(func() error { // test apiURL is working and available @@ -24,7 +24,7 @@ var _ = Describe("Agent test", func() { return err }, "10m", "10s").ShouldNot(HaveOccurred()) - client := localagi.NewClient(localagiURL, "", time.Minute) + client := localagi.NewClient(localagiURL, "", 5*time.Minute) err := client.DeleteAgent("testagent1") Expect(err).ToNot(HaveOccurred()) }) @@ -44,7 +44,7 @@ var _ = Describe("Agent test", func() { }) }) - Context("Can do user defined tool calls", Ordered, func() { + Context("Can do user defined tool calls", Ordered, Label("E2E"), func() { BeforeAll(func() { Eventually(func() error { // test apiURL is working and available @@ -56,13 +56,14 @@ var _ = Describe("Agent test", func() { err := client.DeleteAgent("testagent2") Expect(err).ToNot(HaveOccurred()) err = client.CreateAgent(&localagi.AgentConfig{ - Name: "testagent2", + Name: "testagent2", + EnableReasoning: true, }) Expect(err).ToNot(HaveOccurred()) }) It("can create a task", func() { - client := localagi.NewClient(localagiURL, "", 5*time.Minute) + client := localagi.NewClient(localagiURL, "", 10*time.Minute) req := localagi.RequestBody{ Model: "testagent2", @@ -170,7 +171,7 @@ var _ = Describe("Agent test", func() { msg, err := out.ToMessage() if err == nil && msg.Role == "assistant" { xlog.Info("Agent returned message", "message", msg) - Expect(len(result.Output)).To(BeNumerically(">", 1)) + Expect(len(result.Output)).To(BeNumerically(">", 1), fmt.Sprintf("%+v", result.Output)) continue } fnc, err := out.ToFunctionToolCall() @@ -204,7 +205,7 @@ var _ = Describe("Agent test", func() { localagi.InputMessage{ Type: "message", Role: "user", - Content: "Was the appointment created?", + Content: "Was the appointment created? Reply using the ChooseAnswer tool.", }, }, Tools: []localagi.Tool{ @@ -235,12 +236,12 @@ var _ = Describe("Agent test", func() { Expect(err).ToNot(HaveOccurred()) Expect(len(result.Output)).To(BeNumerically(">", 0)) fnc, err := result.Output[len(result.Output)-1].ToFunctionToolCall() - Expect(err).ToNot(HaveOccurred()) + Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("%+v", result)) Expect(fnc.Arguments).To(ContainSubstring("true")) }) It("can tool call; web search", func() { - client := localagi.NewClient(localagiURL, "", 5*time.Minute) + client := localagi.NewClient(localagiURL, "", 10*time.Minute) req := localagi.RequestBody{ Model: "testagent2", diff --git a/webui/app.go b/webui/app.go index 431fc537..a060f15e 100644 --- a/webui/app.go +++ b/webui/app.go @@ -25,14 +25,11 @@ import ( "github.com/mudler/LocalAGI/core/sse" "github.com/mudler/LocalAGI/core/state" - "github.com/donseba/go-htmx" fiber "github.com/gofiber/fiber/v2" - "github.com/gofiber/template/html/v2" ) type ( App struct { - htmx *htmx.HTMX config *Config *fiber.App sharedState *internalTypes.AgentSharedState @@ -41,16 +38,12 @@ type ( func NewApp(opts ...Option) *App { config := NewConfig(opts...) - engine := html.NewFileSystem(http.FS(viewsfs), ".html") // Initialize a new Fiber app // Pass the engine to the Views - webapp := fiber.New(fiber.Config{ - Views: engine, - }) + webapp := fiber.New(fiber.Config{}) a := &App{ - htmx: htmx.New(), config: config, App: webapp, sharedState: internalTypes.NewAgentSharedState(5 * time.Minute), diff --git a/webui/old/public/css/styles.css b/webui/old/public/css/styles.css deleted file mode 100644 index f466cbf4..00000000 --- a/webui/old/public/css/styles.css +++ /dev/null @@ -1,714 +0,0 @@ -:root { - --primary: #00ff95; - --secondary: #ff00b1; - --tertiary: #5e00ff; - --dark-bg: #111111; - --darker-bg: #0a0a0a; - --medium-bg: #222222; - --light-bg: #333333; - --neon-glow: 0 0 8px rgba(0, 255, 149, 0.7); - --pink-glow: 0 0 8px rgba(255, 0, 177, 0.7); - --purple-glow: 0 0 8px rgba(94, 0, 255, 0.7); -} - -/* Glitch effect animation */ -@keyframes glitch { - 0% { transform: translate(0); } - 20% { transform: translate(-2px, 2px); } - 40% { transform: translate(-2px, -2px); } - 60% { transform: translate(2px, 2px); } - 80% { transform: translate(2px, -2px); } - 100% { transform: translate(0); } -} - -/* Neon pulse animation */ -@keyframes neonPulse { - 0% { text-shadow: 0 0 7px var(--primary), 0 0 10px var(--primary); } - 50% { text-shadow: 0 0 15px var(--primary), 0 0 25px var(--primary); } - 100% { text-shadow: 0 0 7px var(--primary), 0 0 10px var(--primary); } -} - -/* Scanning line effect */ -@keyframes scanline { - 0% { transform: translateY(-100%); } - 100% { transform: translateY(100%); } -} - -body { - font-family: 'Outfit', sans-serif; - background-color: var(--dark-bg); - color: #ffffff; - padding: 20px; - position: relative; - overflow-x: hidden; - background-image: - radial-gradient(circle at 10% 20%, rgba(0, 255, 149, 0.05) 0%, transparent 20%), - radial-gradient(circle at 90% 80%, rgba(255, 0, 177, 0.05) 0%, transparent 20%), - radial-gradient(circle at 50% 50%, rgba(94, 0, 255, 0.05) 0%, transparent 30%), - linear-gradient(180deg, var(--darker-bg) 0%, var(--dark-bg) 100%); - background-attachment: fixed; -} - -body::before { - content: ""; - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: repeating-linear-gradient( - transparent, - transparent 2px, - rgba(0, 0, 0, 0.1) 2px, - rgba(0, 0, 0, 0.1) 4px - ); - pointer-events: none; - z-index: 1000; - opacity: 0.3; -} - -body::after { - content: ""; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 4px; - background: linear-gradient(90deg, var(--primary), var(--secondary)); - opacity: 0.7; - z-index: 1001; - animation: scanline 6s linear infinite; - pointer-events: none; -} - -h1, h2, h3, h4, h5, h6 { - font-weight: 700; -} - -h1 { - font-family: 'Permanent Marker', cursive; - color: var(--primary); - text-shadow: var(--neon-glow); - margin-bottom: 1rem; - position: relative; - animation: neonPulse 2s infinite; -} - -h1:hover { - animation: glitch 0.3s infinite; -} - -h2 { - font-size: 1.5rem; - color: var(--secondary); - text-shadow: var(--pink-glow); - margin-bottom: 0.5rem; -} - -.section-box { - background-color: rgba(17, 17, 17, 0.85); - border: 1px solid var(--primary); - padding: 25px; - margin-bottom: 20px; - border-radius: 6px; - box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4), 0 0 0 1px var(--primary), inset 0 0 20px rgba(0, 0, 0, 0.3); - position: relative; - overflow: hidden; -} - -.section-box::before { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 2px; - background: linear-gradient(90deg, var(--primary), var(--secondary), var(--tertiary), var(--primary)); - background-size: 200% 100%; - animation: gradientMove 3s linear infinite; -} - -@keyframes gradientMove { - 0% { background-position: 0% 50%; } - 100% { background-position: 100% 50%; } -} - -input, button, textarea, select { - width: 100%; - padding: 12px; - margin-top: 8px; - border-radius: 4px; - border: 1px solid var(--medium-bg); - background-color: var(--light-bg); - color: white; - transition: all 0.3s ease; -} - -input[type="text"], input[type="file"], textarea { - background-color: var(--light-bg); - border-left: 3px solid var(--primary); - color: white; -} - -input:focus, textarea:focus, select:focus { - outline: none; - border-color: var(--primary); - box-shadow: var(--neon-glow); -} - -button { - background: linear-gradient(135deg, var(--tertiary), var(--secondary)); - color: white; - cursor: pointer; - border: none; - position: relative; - overflow: hidden; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 1px; - transition: all 0.3s ease; -} - -button::before { - content: ""; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); - transition: all 0.5s; -} - -button:hover { - transform: translateY(-3px); - box-shadow: 0 7px 14px rgba(0, 0, 0, 0.3), 0 0 10px rgba(94, 0, 255, 0.5); -} - -button:hover::before { - left: 100%; -} - -textarea { - height: 200px; - resize: vertical; -} - -/* Select styling */ -select { - appearance: none; - background-color: var(--light-bg); - border-left: 3px solid var(--tertiary); - color: white; - padding: 12px; - border-radius: 4px; - background-image: url('data:image/svg+xml;utf8,'); - background-repeat: no-repeat; - background-position: right 10px center; - background-size: 12px; - cursor: pointer; -} - -select:hover { - border-color: var(--secondary); - box-shadow: 0 0 0 1px var(--secondary); -} - -select:focus { - border-color: var(--tertiary); - box-shadow: var(--purple-glow); -} - -select { - overflow-y: auto; -} - -option { - background-color: var(--medium-bg); - color: white; - padding: 8px 10px; -} - -/* Custom Scrollbars */ -::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -::-webkit-scrollbar-track { - background: var(--medium-bg); - border-radius: 10px; -} - -::-webkit-scrollbar-thumb { - background: linear-gradient(var(--primary), var(--secondary)); - border-radius: 10px; -} - -::-webkit-scrollbar-thumb:hover { - background: var(--tertiary); -} - -/* Checkbox styling */ -.checkbox-custom { - position: relative; - display: inline-block; - width: 22px; - height: 22px; - margin: 5px; - cursor: pointer; - vertical-align: middle; -} - -.checkbox-custom input { - opacity: 0; - width: 0; - height: 0; -} - -.checkbox-custom .checkmark { - position: absolute; - top: 0; - left: 0; - height: 22px; - width: 22px; - background-color: var(--light-bg); - border-radius: 4px; - border: 1px solid var(--medium-bg); - transition: all 0.3s ease; -} - -.checkbox-custom:hover .checkmark { - border-color: var(--primary); - box-shadow: var(--neon-glow); -} - -.checkbox-custom input:checked ~ .checkmark { - background: linear-gradient(135deg, var(--primary), var(--tertiary)); - border-color: transparent; -} - -.checkbox-custom .checkmark:after { - content: ""; - position: absolute; - display: none; -} - -.checkbox-custom input:checked ~ .checkmark:after { - display: block; -} - -.checkbox-custom .checkmark:after { - left: 8px; - top: 4px; - width: 6px; - height: 12px; - border: solid white; - border-width: 0 2px 2px 0; - transform: rotate(45deg); -} - -/* Card styling */ -.container { - max-width: 1200px; - margin: 0 auto; - padding: 20px; -} - -.card-link { - text-decoration: none; - display: block; -} - -.card { - background: linear-gradient(145deg, rgba(34, 34, 34, 0.9), rgba(17, 17, 17, 0.9)); - border: 1px solid rgba(94, 0, 255, 0.2); - border-radius: 8px; - padding: 25px; - margin: 25px auto; - text-align: left; - width: 90%; - transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); - position: relative; - overflow: hidden; -} - -.card::before { - content: ""; - position: absolute; - left: 0; - bottom: 0; - width: 100%; - height: 3px; - background: linear-gradient(90deg, var(--primary), var(--secondary), var(--tertiary)); - transform: scaleX(0); - transform-origin: left; - transition: transform 0.4s ease-out; -} - -.card:hover { - transform: translateY(-8px) scale(1.02); - box-shadow: 0 15px 30px rgba(0, 0, 0, 0.4), 0 0 15px rgba(94, 0, 255, 0.3); -} - -.card:hover::before { - transform: scaleX(1); -} - -.card h2 { - font-family: 'Outfit', sans-serif; - font-size: 1.5em; - font-weight: 600; - color: var(--primary); - margin-bottom: 0.8em; - position: relative; - display: inline-block; -} - -.card a { - color: var(--secondary); - transition: color 0.3s; - text-decoration: none; - position: relative; -} - -.card a:hover { - color: var(--primary); -} - -.card a::after { - content: ""; - position: absolute; - bottom: -2px; - left: 0; - width: 100%; - height: 1px; - background: var(--primary); - transform: scaleX(0); - transform-origin: right; - transition: transform 0.3s ease; -} - -.card a:hover::after { - transform: scaleX(1); - transform-origin: left; -} - -.card p { - color: #cccccc; - font-size: 1em; - line-height: 1.6; -} - -/* Button container */ -.button-container { - display: flex; - justify-content: flex-end; - gap: 10px; - margin-bottom: 12px; -} - -/* Alert and Toast styling */ -.alert { - padding: 12px 15px; - border-radius: 4px; - margin: 15px 0; - display: none; - position: relative; - border-left: 4px solid; - animation: fadeIn 0.3s ease-in; -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(-10px); } - to { opacity: 1; transform: translateY(0); } -} - -.alert-success { - background-color: rgba(0, 255, 149, 0.1); - border-color: var(--primary); - color: var(--primary); -} - -.alert-error { - background-color: rgba(255, 0, 177, 0.1); - border-color: var(--secondary); - color: var(--secondary); -} - -.toast { - position: fixed; - top: 30px; - right: 30px; - max-width: 350px; - padding: 15px 20px; - border-radius: 6px; - box-shadow: 0 10px 20px rgba(0, 0, 0, 0.5); - z-index: 2000; - opacity: 0; - transform: translateX(30px); - transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); - display: flex; - align-items: center; -} - -.toast::before { - content: ""; - width: 20px; - height: 20px; - margin-right: 15px; - background-position: center; - background-repeat: no-repeat; - background-size: contain; -} - -.toast-success { - background: linear-gradient(135deg, rgba(0, 255, 149, 0.9), rgba(0, 255, 149, 0.7)); - color: #111111; - border-left: 4px solid var(--primary); -} - -.toast-success::before { - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23111111'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E"); -} - -.toast-error { - background: linear-gradient(135deg, rgba(255, 0, 177, 0.9), rgba(255, 0, 177, 0.7)); - color: #ffffff; - border-left: 4px solid var(--secondary); -} - -.toast-error::before { - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23ffffff'%3E%3Cpath d='M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'/%3E%3C/svg%3E"); -} - -.toast-visible { - opacity: 1; - transform: translateX(0); -} - -/* Action buttons */ -.action-btn { - background: var(--medium-bg); - color: white; - border: 1px solid rgba(255, 255, 255, 0.1); - padding: 8px 15px; - border-radius: 4px; - cursor: pointer; - transition: all 0.3s ease; - font-weight: 500; - font-size: 0.9rem; - display: inline-flex; - align-items: center; - gap: 8px; -} - -.action-btn i { - font-size: 1rem; -} - -.action-btn:hover { - transform: translateY(-2px); -} - -.start-btn { - background: linear-gradient(135deg, var(--primary), rgba(0, 255, 149, 0.7)); - color: #111111; - border: none; -} - -.start-btn:hover { - box-shadow: 0 0 15px rgba(0, 255, 149, 0.5); - background: var(--primary); -} - -.pause-btn { - background: linear-gradient(135deg, var(--tertiary), rgba(94, 0, 255, 0.7)); - color: white; - border: none; -} - -.pause-btn:hover { - box-shadow: 0 0 15px rgba(94, 0, 255, 0.5); - background: var(--tertiary); -} - -/* Badge styling */ -.badge { - display: inline-block; - padding: 3px 10px; - border-radius: 12px; - font-size: 0.75rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.badge-primary { - background-color: var(--primary); - color: #111111; -} - -.badge-secondary { - background-color: var(--secondary); - color: white; -} - -.badge-tertiary { - background-color: var(--tertiary); - color: white; -} - -/* Data display tables */ -.data-table { - width: 100%; - border-collapse: separate; - border-spacing: 0; - margin: 20px 0; - border-radius: 6px; - overflow: hidden; -} - -.data-table th, .data-table td { - text-align: left; - padding: 12px 15px; - border-bottom: 1px solid var(--medium-bg); -} - -.data-table th { - background-color: rgba(94, 0, 255, 0.2); - color: var(--tertiary); - font-weight: 600; - text-transform: uppercase; - letter-spacing: 1px; - font-size: 0.85rem; -} - -.data-table tr:last-child td { - border-bottom: none; -} - -.data-table tr:nth-child(odd) td { - background-color: rgba(17, 17, 17, 0.6); -} - -.data-table tr:nth-child(even) td { - background-color: rgba(34, 34, 34, 0.6); -} - -.data-table tr:hover td { - background-color: rgba(94, 0, 255, 0.1); -} - -/* Terminal-style code display */ -.code-terminal { - background-color: #0a0a0a; - border-radius: 6px; - padding: 15px; - font-family: 'Courier New', monospace; - color: #00ff95; - margin: 20px 0; - position: relative; - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.4); - overflow: hidden; -} - -.code-terminal::before { - content: ""; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 25px; - background: #222; - display: flex; - align-items: center; - padding: 0 10px; -} - -.code-terminal::after { - content: "• • •"; - position: absolute; - top: 0; - left: 12px; - height: 25px; - display: flex; - align-items: center; - color: #666; - font-size: 20px; - letter-spacing: -2px; -} - -.code-terminal pre { - margin-top: 25px; - white-space: pre-wrap; - word-break: break-word; - line-height: 1.5; -} - -.code-terminal .prompt { - color: var(--secondary); - user-select: none; -} - -/* User info badge */ -.user-info { - display: flex; - align-items: center; - background: linear-gradient(135deg, rgba(17, 17, 17, 0.8), rgba(34, 34, 34, 0.8)); - border: 1px solid var(--tertiary); - border-radius: 30px; - padding: 6px 15px; - margin: 10px 0; - font-size: 0.9rem; - box-shadow: var(--purple-glow); -} - -.user-info::before { - content: ""; - width: 10px; - height: 10px; - background-color: var(--primary); - border-radius: 50%; - margin-right: 10px; - animation: pulse 2s infinite; -} - -@keyframes pulse { - 0% { box-shadow: 0 0 0 0 rgba(0, 255, 149, 0.7); } - 70% { box-shadow: 0 0 0 10px rgba(0, 255, 149, 0); } - 100% { box-shadow: 0 0 0 0 rgba(0, 255, 149, 0); } -} - -.timestamp { - margin-left: auto; - font-family: 'Courier New', monospace; - color: var(--secondary); -} - -/* Responsive design adjustments */ -@media (max-width: 768px) { - .container { - padding: 10px; - } - - .card { - width: 100%; - padding: 15px; - } - - .section-box { - padding: 15px; - } - - .button-container { - flex-direction: column; - } - - .toast { - top: 10px; - right: 10px; - left: 10px; - max-width: none; - } -} \ No newline at end of file diff --git a/webui/old/public/css/wizard.css b/webui/old/public/css/wizard.css deleted file mode 100644 index e6ddff7f..00000000 --- a/webui/old/public/css/wizard.css +++ /dev/null @@ -1,254 +0,0 @@ -/* Agent Form Wizard Styles */ -.agent-form-container { - display: flex; - gap: 2rem; - margin-bottom: 2rem; -} - -/* Wizard Sidebar */ -.wizard-sidebar { - width: 250px; - background: var(--surface); - border-radius: 8px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); - padding: 1.5rem 0; - flex-shrink: 0; -} - -.wizard-nav { - list-style: none; - padding: 0; - margin: 0; -} - -.wizard-nav-item { - padding: 12px 20px; - cursor: pointer; - transition: all 0.2s ease; - border-left: 4px solid transparent; - display: flex; - align-items: center; -} - -.wizard-nav-item i { - margin-right: 10px; - width: 20px; - text-align: center; -} - -.wizard-nav-item:hover { - background: rgba(var(--primary-rgb), 0.1); -} - -.wizard-nav-item.active { - background: rgba(var(--primary-rgb), 0.15); - border-left-color: var(--primary); - color: var(--primary); - font-weight: 600; -} - -/* Form Content Area */ -.form-content-area { - flex: 1; - padding: 1.5rem; - background: var(--surface); - border-radius: 8px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); -} - -.section-title { - font-size: 1.5rem; - margin-bottom: 1.5rem; - padding-bottom: 0.75rem; - border-bottom: 1px solid rgba(var(--border-rgb), 0.5); - color: var(--text); -} - -.form-section { - display: none; -} - -.form-section.active { - display: block; - animation: fadeIn 0.3s ease; -} - -@keyframes fadeIn { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -/* Improved input styles */ -.mb-4 { - margin-bottom: 1.5rem; -} - -.form-section label { - display: block; - margin-bottom: 0.5rem; - font-weight: 500; - color: var(--text); -} - -.form-section input[type="text"], -.form-section input[type="number"], -.form-section textarea, -.form-section select { - width: 100%; - padding: 10px 12px; - border-radius: 6px; - border: 1px solid rgba(var(--border-rgb), 0.8); - background-color: var(--input-bg); - color: var(--text); - font-size: 16px; - transition: border-color 0.2s ease; -} - -.form-section textarea { - min-height: 120px; - resize: vertical; -} - -.form-section input[type="text"]:focus, -.form-section input[type="number"]:focus, -.form-section textarea:focus, -.form-section select:focus { - border-color: var(--primary); - outline: none; - box-shadow: 0 0 0 2px rgba(var(--primary-rgb), 0.2); -} - -/* Button Styles */ -.button-container { - margin: 1.5rem 0; -} - -.action-btn { - background: linear-gradient(135deg, var(--primary), var(--secondary)); - color: white; - border: none; - padding: 10px 16px; - border-radius: 6px; - cursor: pointer; - font-weight: 500; - transition: all 0.2s ease; - display: inline-flex; - align-items: center; - justify-content: center; -} - -.action-btn i { - margin-right: 6px; -} - -.action-btn:hover { - transform: translateY(-2px); - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); -} - -.action-btn:active { - transform: translateY(0); -} -/* Navigation controls - improved layout */ -.wizard-controls { - display: flex; - justify-content: space-between; - align-items: center; - margin-top: 1.5rem; - padding-top: 1.5rem; - border-top: 1px solid rgba(var(--border-rgb), 0.5); -} - -.wizard-controls-left { - flex: 1; - text-align: left; -} - -.wizard-controls-center { - flex: 2; - text-align: center; -} - -.wizard-controls-right { - flex: 1; - text-align: right; -} - -.nav-btn { - background: var(--surface); - color: var(--text); - border: 1px solid rgba(var(--border-rgb), 0.8); - padding: 8px 16px; - border-radius: 6px; - cursor: pointer; - font-weight: 500; - transition: all 0.2s ease; - display: inline-flex; - align-items: center; -} - -.nav-btn i { - margin-right: 6px; -} - -.nav-btn:last-child i { - margin-right: 0; - margin-left: 6px; -} - -.nav-btn:hover { - background: rgba(var(--primary-rgb), 0.1); -} - -.progress-indicator { - display: inline-block; - font-size: 0.9rem; - color: var(--text-muted); - font-weight: 500; - margin-top: 0.5rem; -} - -.progress-dots { - display: flex; - justify-content: center; - margin-bottom: 8px; - gap: 6px; -} - -.progress-dot { - width: 10px; - height: 10px; - border-radius: 50%; - background-color: rgba(var(--border-rgb), 0.4); - transition: all 0.2s ease; -} - -.progress-dot.active { - background-color: var(--primary); - transform: scale(1.2); -} - -/* Responsive adjustments */ -@media (max-width: 768px) { - .wizard-controls { - flex-direction: column; - gap: 1rem; - } - - .wizard-controls-left, - .wizard-controls-center, - .wizard-controls-right { - width: 100%; - text-align: center; - } - - .progress-dots { - margin: 12px 0; - } -} \ No newline at end of file diff --git a/webui/old/public/js/agent-form.js b/webui/old/public/js/agent-form.js deleted file mode 100644 index 5c73698e..00000000 --- a/webui/old/public/js/agent-form.js +++ /dev/null @@ -1,564 +0,0 @@ -// Common utility functions for agent forms -const AgentFormUtils = { - // Add dynamic component based on template - addDynamicComponent: function(sectionId, templateFunction, dataItems) { - const section = document.getElementById(sectionId); - const newIndex = section.getElementsByClassName(dataItems.className).length; - - // Generate HTML from template function - const newHtml = templateFunction(newIndex, dataItems); - - // Add to DOM - section.insertAdjacentHTML('beforeend', newHtml); - }, - - // Process form data into JSON structure - processFormData: function(formData) { - const jsonData = {}; - - // Process basic form fields - for (const [key, value] of formData.entries()) { - // Skip the array fields as they'll be processed separately - if (!key.includes('[') && !key.includes('].')) { - // Handle checkboxes - if (value === 'on') { - jsonData[key] = true; - } - // Handle numeric fields - specifically kb_results - else if (key === 'kb_results') { - // Convert to integer or default to 3 if empty - jsonData[key] = value ? parseInt(value, 10) : 3; - - // Check if the parse was successful - if (isNaN(jsonData[key])) { - showToast('Knowledge Base Results must be a number', 'error'); - return null; // Indicate validation error - } - } - // Handle other numeric fields if needed - else if (key === 'periodic_runs' && value) { - // Try to parse as number if it looks like one - const numValue = parseInt(value, 10); - if (!isNaN(numValue) && String(numValue) === value) { - jsonData[key] = numValue; - } else { - jsonData[key] = value; - } - } - else { - jsonData[key] = value; - } - } - } - - return jsonData; - }, - - // Process connectors from form - processConnectors: function(button) { - const connectors = []; - const connectorElements = document.querySelectorAll('.connector'); - - for (let i = 0; i < connectorElements.length; i++) { - const typeSelect = document.getElementById(`connectorType${i}`); - if (!typeSelect) { - showToast(`Error: Could not find connector type select for index ${i}`, 'error'); - button.innerHTML = button.getAttribute('data-original-text'); - button.disabled = false; - return null; // Validation failed - } - - const type = typeSelect.value; - if (!type) { - showToast(`Please select a connector type for connector ${i+1}`, 'error'); - button.innerHTML = button.getAttribute('data-original-text'); - button.disabled = false; - return null; // Validation failed - } - - // Get all config fields for this connector - const connector = { - type: type, - config: {} - }; - - // Find all config inputs for this connector - const configInputs = document.querySelectorAll(`[name^="connectors[${i}][config]"]`); - - // Check if we have a JSON textarea (fallback template) - const jsonTextarea = document.getElementById(`connectorConfig${i}`); - if (jsonTextarea && jsonTextarea.value) { - try { - // If it's a JSON textarea, parse it and use the result - const jsonConfig = JSON.parse(jsonTextarea.value); - // Convert the parsed JSON back to a string for the backend - connector.config = JSON.stringify(jsonConfig); - } catch (e) { - // If it's not valid JSON, use it as is - connector.config = jsonTextarea.value; - } - } else { - // Process individual form fields - configInputs.forEach(input => { - // Extract the key from the name attribute - // Format: connectors[0][config][key] - const keyMatch = input.name.match(/\[config\]\[([^\]]+)\]/); - if (keyMatch && keyMatch[1]) { - const key = keyMatch[1]; - // For checkboxes, set true/false based on checked state - if (input.type === 'checkbox') { - connector.config[key] = input.checked ? 'true' : 'false'; - } else { - connector.config[key] = input.value; - } - } - }); - - // Convert the config object to a JSON string for the backend - connector.config = JSON.stringify(connector.config); - } - - connectors.push(connector); - } - - return connectors; - }, - - // Process MCP servers from form - processMCPServers: function() { - const mcpServers = []; - const mcpElements = document.querySelectorAll('.mcp_server'); - - for (let i = 0; i < mcpElements.length; i++) { - const urlInput = document.getElementById(`mcpURL${i}`); - const tokenInput = document.getElementById(`mcpToken${i}`); - - if (urlInput && urlInput.value) { - const server = { - url: urlInput.value - }; - - // Add token if present - if (tokenInput && tokenInput.value) { - server.token = tokenInput.value; - } - - mcpServers.push(server); - } - } - - return mcpServers; - }, - - // Process actions from form - processActions: function(button) { - const actions = []; - const actionElements = document.querySelectorAll('.action'); - - for (let i = 0; i < actionElements.length; i++) { - const nameSelect = document.getElementById(`actionsName${i}`); - const configTextarea = document.getElementById(`actionsConfig${i}`); - - if (!nameSelect) { - showToast(`Error: Could not find action name select for index ${i}`, 'error'); - button.innerHTML = button.getAttribute('data-original-text'); - button.disabled = false; - return null; // Validation failed - } - - const name = nameSelect.value; - if (!name) { - showToast(`Please select an action type for action ${i+1}`, 'error'); - button.innerHTML = button.getAttribute('data-original-text'); - button.disabled = false; - return null; // Validation failed - } - - let config = {}; - if (configTextarea && configTextarea.value) { - try { - config = JSON.parse(configTextarea.value); - } catch (e) { - showToast(`Invalid JSON in action ${i+1} config: ${e.message}`, 'error'); - button.innerHTML = button.getAttribute('data-original-text'); - button.disabled = false; - return null; // Validation failed - } - } - - actions.push({ - name: name, - config: JSON.stringify(config) // Convert to JSON string for backend - }); - } - - return actions; - }, - - // Process prompt blocks from form - processPromptBlocks: function(button) { - const promptBlocks = []; - const promptElements = document.querySelectorAll('.prompt_block'); - - for (let i = 0; i < promptElements.length; i++) { - const nameSelect = document.getElementById(`promptName${i}`); - const configTextarea = document.getElementById(`promptConfig${i}`); - - if (!nameSelect) { - showToast(`Error: Could not find prompt block name select for index ${i}`, 'error'); - button.innerHTML = button.getAttribute('data-original-text'); - button.disabled = false; - return null; // Validation failed - } - - const name = nameSelect.value; - if (!name) { - showToast(`Please select a prompt block type for block ${i+1}`, 'error'); - button.innerHTML = button.getAttribute('data-original-text'); - button.disabled = false; - return null; // Validation failed - } - - let config = {}; - if (configTextarea && configTextarea.value) { - try { - config = JSON.parse(configTextarea.value); - } catch (e) { - showToast(`Invalid JSON in prompt block ${i+1} config: ${e.message}`, 'error'); - button.innerHTML = button.getAttribute('data-original-text'); - button.disabled = false; - return null; // Validation failed - } - } - - promptBlocks.push({ - name: name, - config: JSON.stringify(config) // Convert to JSON string for backend - }); - } - - return promptBlocks; - }, - - // Helper function to format config values (for edit form) - formatConfigValue: function(configElement, configValue) { - if (!configElement) return; - - // If configValue is an object, stringify it - if (typeof configValue === 'object' && configValue !== null) { - try { - configElement.value = JSON.stringify(configValue, null, 2); - } catch (e) { - console.error('Error stringifying config value:', e); - configElement.value = '{}'; - } - } - // If it's a string that looks like JSON, try to parse and pretty print it - else if (typeof configValue === 'string' && (configValue.startsWith('{') || configValue.startsWith('['))) { - try { - const parsed = JSON.parse(configValue); - configElement.value = JSON.stringify(parsed, null, 2); - } catch (e) { - // If it's not valid JSON, just use the string as is - configElement.value = configValue; - } - } - // Otherwise, just use the value as is - else { - configElement.value = configValue || ''; - } - }, - - // Helper function to set select value (with fallback if option doesn't exist) - setSelectValue: function(selectElement, value) { - if (!selectElement) return; - - // Check if the option exists - let optionExists = false; - for (let i = 0; i < selectElement.options.length; i++) { - if (selectElement.options[i].value === value) { - optionExists = true; - break; - } - } - - // Set the value if the option exists - if (optionExists) { - selectElement.value = value; - } else if (selectElement.options.length > 0) { - // Otherwise, select the first option - selectElement.selectedIndex = 0; - } - }, - - // Render connector form based on type - renderConnectorForm: function(index, type, config = {}) { - const formContainer = document.getElementById(`connectorFormContainer${index}`); - if (!formContainer) return; - - // Clear existing form - formContainer.innerHTML = ''; - - // Debug log to see what's happening - console.log(`Rendering connector form for type: ${type}`); - console.log(`Config for connector:`, config); - console.log(`Available templates:`, ConnectorTemplates ? Object.keys(ConnectorTemplates) : 'None'); - - // Ensure config is an object - let configObj = config; - if (typeof config === 'string') { - try { - configObj = JSON.parse(config); - } catch (e) { - console.error('Error parsing connector config string:', e); - configObj = {}; - } - } - - // If we have a template for this connector type in the global ConnectorTemplates object - if (ConnectorTemplates && type && ConnectorTemplates[type]) { - console.log(`Found template for ${type}`); - // Get the template result which contains HTML and setValues function - const templateResult = ConnectorTemplates[type](configObj, index); - - // Set the HTML content - formContainer.innerHTML = templateResult.html; - - // Call the setValues function to set input values safely - if (typeof templateResult.setValues === 'function') { - setTimeout(templateResult.setValues, 0); - } - } else { - console.log(`No template found for ${type}, using fallback`); - // Use the fallback template - if (ConnectorTemplates && ConnectorTemplates.fallback) { - const fallbackResult = ConnectorTemplates.fallback(configObj, index); - formContainer.innerHTML = fallbackResult.html; - - if (typeof fallbackResult.setValues === 'function') { - setTimeout(fallbackResult.setValues, 0); - } - } else { - // Fallback to generic JSON textarea if no fallback template - formContainer.innerHTML = ` -
Test and execute actions directly from the UI
-Manage and interact with your AI agents
-Describe the group of agents you want to create. Be specific about their roles, relationships, and purpose.
- -Generating agent profiles...
-Select the agents you want to create. You can customize their details before creation.
- -Configure common settings for all selected agents. These settings will be applied to each agent.
- - - - -
- View and manage your list of agents, including detailed profiles and statistics.
-Create a new intelligent agent with custom behaviors, connectors, and actions.
-View performance metrics and insights from your agent operations.
- Coming Soon -Configure system preferences and global settings for all agents.
- Coming Soon -Please enter your access token to continue
-Current time (UTC): {{.CurrentDate}}
-Export your agent configuration for backup or transfer.
- -Permanently delete this agent and all associated data. This action cannot be undone.
- -