diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1f47b10..c85d2a6 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 c991610..1ca97d4 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 e746201..0000000 --- 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 229fd07..0000000 --- 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 5685cac..0000000 --- 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 7e94fd9..0000000 --- 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 2fe00a2..0000000 --- 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 59a8d78..5982460 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 e492bec..80fd5c8 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,41 +560,6 @@ func (a *Agent) filterJob(job *types.Job) (ok bool, err error) { return failedBy == "" && (!hasTriggers || triggeredBy != ""), 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() - if len(builtinTools) == 0 { - return - } - - // Get available actions - availableActions := a.mcpActions - - for _, tool := range builtinTools { - functionName := tool.Name - - // Check if this is a web search builtin tool - if strings.HasPrefix(string(functionName), "web_search_") { - // Look for a search action - searchAction := availableActions.Find("search") - if searchAction == nil { - xlog.Warn("Web search builtin tool specified but no 'search' action available", - "function_name", functionName, - "agent", a.Character.Name) - } else { - xlog.Debug("Web search builtin tool matched to search action", - "function_name", functionName, - "agent", a.Character.Name) - } - } else { - // For future builtin tools, add more matching logic here - xlog.Warn("Unknown builtin tool specified", - "function_name", functionName, - "agent", a.Character.Name) - } - } -} - // 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 @@ -748,441 +599,39 @@ func (a *Agent) replyWithToolCall(job *types.Job, conv []openai.ChatCompletionMe job.Result.Finish(nil) } -func (a *Agent) consumeJob(job *types.Job, role string, retries int) { - if err := job.GetContext().Err(); err != nil { - job.Result.Finish(fmt.Errorf("expired")) +// 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() + if len(builtinTools) == 0 { return } - if retries < 1 { - job.Result.Finish(fmt.Errorf("Exceeded recursive retries")) - return - } + // Get available actions + availableActions := a.availableActions() - a.Lock() - paused := a.pause - a.Unlock() + for _, tool := range builtinTools { + functionName := tool.Name - if paused { - xlog.Info("Agent is paused, skipping job", "agent", a.Character.Name) - job.Result.Finish(fmt.Errorf("agent is paused")) - return - } - - // We are self evaluating if we consume the job as a system role - selfEvaluation := role == SystemRole - - conv := job.ConversationHistory - - a.Lock() - a.selfEvaluationInProgress = selfEvaluation - a.Unlock() - defer job.Cancel() - - if selfEvaluation { - defer func() { - a.Lock() - a.selfEvaluationInProgress = false - a.Unlock() - }() - } - - conv = a.processPrompts(job.GetContext(), conv) - if ok, err := a.filterJob(job); !ok || err != nil { - if err != nil { - job.Result.Finish(fmt.Errorf("Error in job filter: %w", err)) - } else { - job.Result.Finish(nil) - } - return - } - conv = a.processUserInputs(conv) - - // RAG - conv = a.knowledgeBaseLookup(job, conv) - - // Validate builtin tools against available actions - a.validateBuiltinTools(job) - - var pickTemplate string - var reEvaluationTemplate string - - if selfEvaluation { - pickTemplate = pickSelfTemplate - reEvaluationTemplate = reSelfEvalTemplate - } else { - pickTemplate = pickActionTemplate - reEvaluationTemplate = reEvalTemplate - } - - // 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 + // Check if this is a web search builtin tool + if strings.HasPrefix(string(functionName), "web_search_") { + // Look for a search action + searchAction := availableActions.Find("search") + if searchAction == nil { + xlog.Warn("Web search builtin tool specified but no 'search' action available", + "function_name", functionName, + "agent", a.Character.Name) + } else { + xlog.Debug("Web search builtin tool matched to search action", + "function_name", functionName, + "agent", a.Character.Name) } - actionParams = p.actionParams } else { - actionParams = *params - } - job.ResetNextAction() - } else { - var err error - chosenAction, actionParams, reasoning, err = a.pickAction(job, pickTemplate, conv, maxRetries) - if err != nil { - xlog.Error("Error picking action", "error", err) - job.Result.Finish(err) - return + // For future builtin tools, add more matching logic here + xlog.Warn("Unknown builtin tool specified", + "function_name", functionName, + "agent", a.Character.Name) } } - - 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) - - 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 - } - - 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 - } - - if !satisfied { - // If not satisfied, continue with the conversation - job.ConversationHistory = conv - job.IncrementEvaluationLoop() - a.consumeJob(job, role, retries) - return - } - - 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 - } - - if chosenAction.Definition().Name.Is(action.StopActionName) { - xlog.Info("LLM decided to stop") - job.Result.Finish(nil) - return - } - - // 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, - ) - - 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.Info( - "Generated parameters", - "agent", a.Character.Name, - "action", chosenAction.Definition().Name, - "reasoning", reasoning, - "params", actionParams.String(), - ) - - if actionParams == nil { - job.Result.Finish(fmt.Errorf("no parameters")) - xlog.Error("No parameters", "agent", a.Character.Name) - return - } - - 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) - } - - 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 - } - - 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 - } - - if selfEvaluation && a.options.initiateConversations && - chosenAction.Definition().Name.Is(action.ConversationActionName) { - - xlog.Info("LLM decided to initiate a new conversation", "agent", a.Character.Name) - - 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 - } - - msg := openai.ChatCompletionMessage{ - Role: "assistant", - Content: message.Message, - } - - 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 = []openai.ChatCompletionMessage{ - msg, - } - 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 !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 - } - - result, err := a.runAction(job, chosenAction, actionParams) - if err != nil { - result.Result = fmt.Sprintf("Error running tool: %v", err) - } - - stateResult := types.ActionState{ - ActionCurrentState: types.ActionCurrentState{ - Job: job, - Action: chosenAction, - Params: actionParams, - Reasoning: reasoning, - }, - ActionResult: result, - } - 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) - } - - // 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)) - 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 - - // 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) - return - } - - // Evaluate the final response - var satisfied bool - satisfied, conv, err = a.handleEvaluation(job, conv, job.GetEvaluationLoop()) - if err != nil { - job.Result.Finish(fmt.Errorf("error evaluating response: %w", err)) - return - } - - if !satisfied { - // If not satisfied, continue with the conversation - job.ConversationHistory = conv - job.IncrementEvaluationLoop() - a.consumeJob(job, role, retries) - return - } - - a.reply(job, role, conv, actionParams, chosenAction, reasoning) -} - -func stripThinkingTags(content string) string { - // Remove content between and (including multi-line) - content = regexp.MustCompile(`(?s).*?`).ReplaceAllString(content, "") - // Remove content between and (including multi-line) - content = regexp.MustCompile(`(?s).*?`).ReplaceAllString(content, "") - // Clean up any extra whitespace - content = strings.TrimSpace(content) - return content -} - -func (a *Agent) cleanupLLMResponse(content string) string { - if a.options.stripThinkingTags { - content = stripThinkingTags(content) - } - // Future post-processing options can be added here - 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 { @@ -1259,6 +708,378 @@ func (a *Agent) addFunctionResultToConversation(ctx context.Context, chosenActio return conv } +func (a *Agent) consumeJob(job *types.Job, role string) { + if err := job.GetContext().Err(); err != nil { + job.Result.Finish(fmt.Errorf("expired")) + return + } + + a.Lock() + paused := a.pause + a.Unlock() + + if paused { + xlog.Info("Agent is paused, skipping job", "agent", a.Character.Name) + job.Result.Finish(fmt.Errorf("agent is paused")) + return + } + + // We are self evaluating if we consume the job as a system role + selfEvaluation := role == SystemRole + + conv := job.ConversationHistory + + a.Lock() + a.selfEvaluationInProgress = selfEvaluation + a.Unlock() + defer job.Cancel() + + if selfEvaluation { + defer func() { + a.Lock() + a.selfEvaluationInProgress = false + a.Unlock() + }() + } + + conv = a.processPrompts(job.GetContext(), conv) + if ok, err := a.filterJob(job); !ok || err != nil { + if err != nil { + job.Result.Finish(fmt.Errorf("Error in job filter: %w", err)) + } else { + job.Result.Finish(nil) + } + return + } + conv = a.processUserInputs(conv) + + // RAG + conv = a.knowledgeBaseLookup(job, conv) + + // Validate builtin tools against available actions + a.validateBuiltinTools(job) + + fragment := cogito.NewFragment(conv...) + + if selfEvaluation { + fragment = fragment.AddStartMessage("system", pickSelfTemplate) + } + + if a.options.enableHUD { + prompt, err := renderTemplate(hudTemplate, a.prepareHUD(), a.availableActions(), "") + if err != nil { + job.Result.Finish(fmt.Errorf("error renderTemplate: %w", err)) + return + } + fragment = fragment.AddStartMessage("system", prompt) + } + + availableActions := a.getAvailableActionsForJob(job) + cogitoTools := availableActions.ToCogitoTools(job.GetContext(), a.sharedState) + allActions := append(availableActions, a.mcpActionDefinitions...) + + 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, + }, + } + } + + defer func() { + if obs != nil && a.observer != nil { + obs.MakeLastProgressCompletion() + a.observer.Update(*obs) + } + }() + + 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) + } + + 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 { + + xlog.Debug("Tool call back", "tool_call", tc) + + // Check if this is a user-defined action + chosenAction := allActions.Find(tc.Name) + + xlog.Debug("Action found", "action", chosenAction) + + 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 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), + } + + a.observer.Update(*obs) + observables[tc.ID] = obs + } + + 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 + } + + msg := openai.ChatCompletionMessage{ + Role: "assistant", + Content: message.Message, + } + + 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 = []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) + } + + // 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 + } + } + + } + + 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"}, + }) + + job.Result.Conversation = conv + job.Result.Finish(nil) + + } + return cont + }, + ), + } + + if a.options.canPlan { + cogitoOpts = append(cogitoOpts, cogito.EnableAutoPlan) + if a.options.enableEvaluation { + cogitoOpts = append(cogitoOpts, cogito.EnableAutoPlanReEvaluator) + } + } + + if a.options.forceReasoning { + cogitoOpts = append(cogitoOpts, cogito.WithForceReasoning()) + } + + if a.options.maxEvaluationLoops > 0 { + cogitoOpts = append(cogitoOpts, + cogito.WithMaxAttempts(a.options.maxEvaluationLoops), + cogito.WithIterations(a.options.maxEvaluationLoops), + ) + } + + fragment, err = cogito.ExecuteTools( + a.llm, fragment, + cogitoOpts..., + ) + + 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) + } + xlog.Error("Error executing cogito", "error", err) + job.Result.Finish(err) + return + } + + if userTool { + return + } + + 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 + } + } + + if len(fragment.Messages) == 0 { + job.Result.Finish(fmt.Errorf("no messages in fragment")) + return + } + + responseFragment, err := a.llm.Ask(job.GetContext(), fragment) + if err != nil { + job.Result.Finish(err) + return + } + + result := a.cleanupLLMResponse(responseFragment.LastMessage().Content) + + conv = append(fragment.Messages, openai.ChatCompletionMessage{ + Role: "assistant", + Content: result, + }) + + 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 { + // Remove content between and (including multi-line) + content = regexp.MustCompile(`(?s).*?`).ReplaceAllString(content, "") + // Remove content between and (including multi-line) + content = regexp.MustCompile(`(?s).*?`).ReplaceAllString(content, "") + // Clean up any extra whitespace + content = strings.TrimSpace(content) + return content +} + +func (a *Agent) cleanupLLMResponse(content string) string { + if a.options.stripThinkingTags { + content = stripThinkingTags(content) + } + // Future post-processing options can be added here + return content +} + // 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 cdc2e8b..c32d5e7 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 7373f91..0000000 --- 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 22ee857..774a9c0 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 62d85b3..7d4176d 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 6243d07..d77b3cc 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 09fe9ac..c98458b 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 8e74b8b..8d4c00e 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 6c9af12..a0f5613 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 ecfda0b..8d65bef 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 30bb94e..29e905c 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 6e520d7..822907c 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 5bd58cc..7067d47 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 295e8ce..4990fa0 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 53df5b9..1725597 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 8881f0c..e252cf4 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 431fc53..a060f15 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 f466cbf..0000000 --- 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 e6ddff7..0000000 --- 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 5c73698..0000000 --- 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 = ` -
- - -
- `; - - // Set the value safely after DOM is created - setTimeout(function() { - const configTextarea = document.getElementById(`connectorConfig${index}`); - if (configTextarea) { - if (typeof configObj === 'object' && configObj !== null) { - configTextarea.value = JSON.stringify(configObj, null, 2); - } else if (typeof config === 'string') { - configTextarea.value = config; - } - } - }, 0); - } - } - } -}; - -// HTML Templates for dynamic elements -const AgentFormTemplates = { - // Connector template - connectorTemplate: function(index, data) { - return ` -
-

Connector ${index + 1}

-
- - -
-
- -
-
Select a connector type to configure
-
-
- -
- `; - }, - - // MCP Server template - mcpServerTemplate: function(index, data) { - return ` -
-

MCP Server ${index + 1}

-
- - -
-
- - -
- -
- `; - }, - - // Action template - actionTemplate: function(index, data) { - return ` -
-

Action ${index + 1}

-
- - -
-
- - -
- -
- `; - }, - - // Prompt Block template - promptBlockTemplate: function(index, data) { - return ` -
-

Prompt Block ${index + 1}

-
- - -
-
- - -
- -
- `; - } -}; - -// Initialize form event listeners -function initAgentFormCommon(options = {}) { - // Add connector button - const addConnectorButton = document.getElementById('addConnectorButton'); - if (addConnectorButton) { - addConnectorButton.addEventListener('click', function() { - // Create options string - let optionsHtml = ''; - if (options.connectors) { - optionsHtml = options.connectors; - } - - // Add new connector form - AgentFormUtils.addDynamicComponent('connectorsSection', AgentFormTemplates.connectorTemplate, { - className: 'connector', - options: optionsHtml - }); - }); - } - - // Add MCP server button - const addMCPButton = document.getElementById('addMCPButton'); - if (addMCPButton) { - addMCPButton.addEventListener('click', function() { - // Add new MCP server form - AgentFormUtils.addDynamicComponent('mcpSection', AgentFormTemplates.mcpServerTemplate, { - className: 'mcp_server' - }); - }); - } - - // Add action button - const actionButton = document.getElementById('action_button'); - if (actionButton) { - actionButton.addEventListener('click', function() { - // Create options string - let optionsHtml = ''; - if (options.actions) { - optionsHtml = options.actions; - } - - // Add new action form - AgentFormUtils.addDynamicComponent('action_box', AgentFormTemplates.actionTemplate, { - className: 'action', - options: optionsHtml - }); - }); - } - - // Add prompt block button - const dynamicButton = document.getElementById('dynamic_button'); - if (dynamicButton) { - dynamicButton.addEventListener('click', function() { - // Create options string - let optionsHtml = ''; - if (options.promptBlocks) { - optionsHtml = options.promptBlocks; - } - - // Add new prompt block form - AgentFormUtils.addDynamicComponent('dynamic_box', AgentFormTemplates.promptBlockTemplate, { - className: 'prompt_block', - options: optionsHtml - }); - }); - } -} - -// Simple toast notification function -function showToast(message, type) { - // Check if toast container exists, if not create it - let toast = document.getElementById('toast'); - if (!toast) { - toast = document.createElement('div'); - toast.id = 'toast'; - toast.className = 'toast'; - - const toastMessage = document.createElement('div'); - toastMessage.id = 'toast-message'; - toast.appendChild(toastMessage); - - document.body.appendChild(toast); - } - - const toastMessage = document.getElementById('toast-message'); - - // Set message - toastMessage.textContent = message; - - // Set type class - toast.className = 'toast'; - toast.classList.add(`toast-${type}`); - - // Show toast - toast.classList.add('show'); - - // Hide after 3 seconds - setTimeout(() => { - toast.classList.remove('show'); - }, 3000); -} diff --git a/webui/old/public/js/common.js b/webui/old/public/js/common.js deleted file mode 100644 index d0cf5eb..0000000 --- a/webui/old/public/js/common.js +++ /dev/null @@ -1,44 +0,0 @@ - // Function to show toast notifications with enhanced animation - function showToast(message, type) { - const toast = document.getElementById('toast'); - const toastMessage = document.getElementById('toast-message'); - - // Set message - toastMessage.textContent = message; - - // Set toast type (success/error) - toast.className = 'toast'; - toast.classList.add(type === 'success' ? 'toast-success' : 'toast-error'); - - // Show toast with enhanced animation - setTimeout(() => { - toast.classList.add('toast-visible'); - }, 100); - - // Hide toast after 3 seconds with animation - setTimeout(() => { - toast.classList.remove('toast-visible'); - - // Clean up after animation completes - setTimeout(() => { - toast.className = 'toast'; - }, 400); - }, 3000); -} - -// Function to create the glitch effect on headings -document.addEventListener('DOMContentLoaded', function() { - const headings = document.querySelectorAll('h1'); - - headings.forEach(heading => { - heading.addEventListener('mouseover', function() { - this.style.animation = 'glitch 0.3s infinite'; - }); - - heading.addEventListener('mouseout', function() { - this.style.animation = 'neonPulse 2s infinite'; - }); - }); - - -}); \ No newline at end of file diff --git a/webui/old/public/js/connector-templates.js b/webui/old/public/js/connector-templates.js deleted file mode 100644 index 3173e71..0000000 --- a/webui/old/public/js/connector-templates.js +++ /dev/null @@ -1,475 +0,0 @@ -/** - * Connector Templates - * - * This file contains templates for all connector types supported by LocalAGI. - * Each template is a function that returns an HTML string for the connector's form. - * - * Note: We don't need to escape HTML in the value attributes because browsers - * handle these values safely when setting them via DOM properties after rendering. - */ - -/** - * Connector Templates - * Each function takes a config object and returns an HTML string - */ -const ConnectorTemplates = { - /** - * Telegram Connector Template - * @param {Object} config - Existing configuration values - * @param {Number} index - Connector index - * @returns {Object} HTML template and setValues function - */ - telegram: function(config = {}, index) { - // Return HTML without values in the template string - const html = ` -
- - - Get this from @BotFather on Telegram -
- `; - - // Function to set values after HTML is added to DOM to avoid XSS - const setValues = function() { - const input = document.getElementById(`telegramToken${index}`); - if (input) input.value = config.token || ''; - }; - - return { html, setValues }; - }, - - /** - * Slack Connector Template - * @param {Object} config - Existing configuration values - * @param {Number} index - Connector index - * @returns {Object} HTML template and setValues function - */ - slack: function(config = {}, index) { - // Return HTML without values in the template string - const html = ` -
- - - App-level token starting with xapp- -
- -
- - - Bot token starting with xoxb- -
- -
- - - Channel ID where the bot will operate -
- -
- - - If checked, the bot will reply to all messages in the channel -
- `; - - // Function to set values after HTML is added to DOM to avoid XSS - const setValues = function() { - const appTokenInput = document.getElementById(`slackAppToken${index}`); - const botTokenInput = document.getElementById(`slackBotToken${index}`); - const channelIDInput = document.getElementById(`slackChannelID${index}`); - const alwaysReplyInput = document.getElementById(`slackAlwaysReply${index}`); - - if (appTokenInput) appTokenInput.value = config.appToken || ''; - if (botTokenInput) botTokenInput.value = config.botToken || ''; - if (channelIDInput) channelIDInput.value = config.channelID || ''; - if (alwaysReplyInput) alwaysReplyInput.checked = config.alwaysReply === 'true'; - }; - - return { html, setValues }; - }, - - /** - * Discord Connector Template - * @param {Object} config - Existing configuration values - * @param {Number} index - Connector index - * @returns {Object} HTML template and setValues function - */ - discord: function(config = {}, index) { - // Return HTML without values in the template string - const html = ` -
- - -
- -
- - -
- `; - - // Function to set values after HTML is added to DOM - const setValues = function() { - const tokenInput = document.getElementById(`discordToken${index}`); - const channelIDInput = document.getElementById(`discordChannelID${index}`); - - if (tokenInput) tokenInput.value = config.token || ''; - if (channelIDInput) channelIDInput.value = config.defaultChannel || ''; - }; - - return { html, setValues }; - }, - - /** - * GitHub Issues Connector Template - * @param {Object} config - Existing configuration values - * @param {Number} index - Connector index - * @returns {Object} HTML template and setValues function - */ - 'github-issues': function(config = {}, index) { - // Return HTML without values in the template string - const html = ` -
- - - Needs repo and read:org permissions -
- -
- - -
- -
- - -
- -
- - - If checked, the bot will reply to issues that have no replies yet -
- -
- - - How often to check for new issues (in seconds) -
- `; - - // Function to set values after HTML is added to DOM to avoid XSS - const setValues = function() { - const tokenInput = document.getElementById(`githubIssuesToken${index}`); - const ownerInput = document.getElementById(`githubIssuesOwner${index}`); - const repoInput = document.getElementById(`githubIssuesRepo${index}`); - const replyIfNoRepliesInput = document.getElementById(`githubIssuesReplyIfNoReplies${index}`); - const pollIntervalInput = document.getElementById(`githubIssuesPollInterval${index}`); - - if (tokenInput) tokenInput.value = config.token || ''; - if (ownerInput) ownerInput.value = config.owner || ''; - if (repoInput) repoInput.value = config.repository || ''; - if (replyIfNoRepliesInput) replyIfNoRepliesInput.checked = config.replyIfNoReplies === 'true'; - if (pollIntervalInput) pollIntervalInput.value = config.pollInterval || '60'; - }; - - return { html, setValues }; - }, - - /** - * GitHub PRs Connector Template - * @param {Object} config - Existing configuration values - * @param {Number} index - Connector index - * @returns {Object} HTML template and setValues function - */ - 'github-prs': function(config = {}, index) { - // Return HTML without values in the template string - const html = ` -
- - - Personal Access Token with repo permissions -
- -
- - -
- -
- - -
- -
- - - If checked, the bot will reply to pull requests that have no replies yet -
- -
- - - How often to check for new pull requests (in seconds) -
- `; - - // Function to set values after HTML is added to DOM to avoid XSS - const setValues = function() { - const tokenInput = document.getElementById(`githubPRsToken${index}`); - const ownerInput = document.getElementById(`githubPRsOwner${index}`); - const repoInput = document.getElementById(`githubPRsRepo${index}`); - const replyIfNoRepliesInput = document.getElementById(`githubPRsReplyIfNoReplies${index}`); - const pollIntervalInput = document.getElementById(`githubPRsPollInterval${index}`); - - if (tokenInput) tokenInput.value = config.token || ''; - if (ownerInput) ownerInput.value = config.owner || ''; - if (repoInput) repoInput.value = config.repository || ''; - if (replyIfNoRepliesInput) replyIfNoRepliesInput.checked = config.replyIfNoReplies === 'true'; - if (pollIntervalInput) pollIntervalInput.value = config.pollInterval || '60'; - }; - - return { html, setValues }; - }, - - /** - * IRC Connector Template - * @param {Object} config - Existing configuration values - * @param {Number} index - Connector index - * @returns {Object} HTML template and setValues function - */ - irc: function(config = {}, index) { - // Return HTML without values in the template string - const html = ` -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - - If checked, the bot will always reply to messages, even if they are not directed at it -
- `; - - // Function to set values after HTML is added to DOM - const setValues = function() { - const serverInput = document.getElementById(`ircServer${index}`); - const portInput = document.getElementById(`ircPort${index}`); - const channelInput = document.getElementById(`ircChannel${index}`); - const nickInput = document.getElementById(`ircNick${index}`); - const alwaysReplyInput = document.getElementById(`ircAlwaysReply${index}`); - - if (serverInput) serverInput.value = config.server || ''; - if (portInput) portInput.value = config.port || '6667'; - if (channelInput) channelInput.value = config.channel || ''; - if (nickInput) nickInput.value = config.nickname || ''; - if (alwaysReplyInput) alwaysReplyInput.checked = config.alwaysReply === 'true'; - }; - - return { html, setValues }; - }, - - /** - * Twitter Connector Template - * @param {Object} config - Existing configuration values - * @param {Number} index - Connector index - * @returns {Object} HTML template and setValues function - */ - twitter: function(config = {}, index) { - // Return HTML without values in the template string - const html = ` -
- - -
- -
- - - Username of your Twitter bot (with or without @) -
- -
- - - If checked, the bot will not enforce Twitter's character limit -
- `; - - // Function to set values after HTML is added to DOM - const setValues = function() { - const tokenInput = document.getElementById(`twitterToken${index}`); - const botUsernameInput = document.getElementById(`twitterBotUsername${index}`); - const noCharLimitInput = document.getElementById(`twitterNoCharLimit${index}`); - - if (tokenInput) tokenInput.value = config.token || ''; - if (botUsernameInput) botUsernameInput.value = config.botUsername || ''; - if (noCharLimitInput) noCharLimitInput.checked = config.noCharacterLimit === 'true'; - }; - - return { html, setValues }; - }, - - /** - * Fallback template for any connector without a specific template - * @param {Object} config - Existing configuration values - * @param {Number} index - Connector index - * @returns {Object} HTML template and setValues function - */ - fallback: function(config = {}, index) { - // Convert config to a pretty-printed JSON string - let configStr = '{}'; - try { - if (typeof config === 'string') { - // If it's already a string, try to parse it first to pretty-print - configStr = JSON.stringify(JSON.parse(config), null, 2); - } else if (typeof config === 'object' && config !== null) { - configStr = JSON.stringify(config, null, 2); - } - } catch (e) { - console.error('Error formatting config:', e); - // If it's a string but not valid JSON, just use it as is - if (typeof config === 'string') { - configStr = config; - } - } - - // Return HTML without values in the template string - const html = ` -
- - - Enter the connector configuration as a JSON object -
- `; - - // Function to set values after HTML is added to DOM - const setValues = function() { - const configInput = document.getElementById(`connectorConfig${index}`); - - if (configInput) configInput.value = configStr; - }; - - return { html, setValues }; - } -}; diff --git a/webui/old/public/js/wizard.js b/webui/old/public/js/wizard.js deleted file mode 100644 index 6be5558..0000000 --- a/webui/old/public/js/wizard.js +++ /dev/null @@ -1,139 +0,0 @@ -/** - * Agent Form Wizard - Navigation and UI functionality - */ -document.addEventListener('DOMContentLoaded', function() { - // Check if the wizard exists on the page - const wizardSidebar = document.querySelector('.wizard-sidebar'); - if (!wizardSidebar) return; - - // Get all sections and nav items - const navItems = document.querySelectorAll('.wizard-nav-item'); - const sections = document.querySelectorAll('.form-section'); - const prevButton = document.getElementById('prevSection'); - const nextButton = document.getElementById('nextSection'); - const currentStepLabelEl = document.getElementById('currentStepLabel'); - const progressDotsContainer = document.getElementById('progressDots'); - - // Create progress dots - const totalSteps = sections.length; - - // Create dots for each section - if (progressDotsContainer) { - for (let i = 0; i < totalSteps; i++) { - const dot = document.createElement('div'); - dot.className = 'progress-dot'; - dot.setAttribute('data-index', i); - dot.addEventListener('click', () => setActiveSection(i)); - progressDotsContainer.appendChild(dot); - } - } - - // Get all progress dots - const progressDots = document.querySelectorAll('.progress-dot'); - - // Track current active section - let currentSectionIndex = 0; - - // Initialize - updateNavigation(); - - // Add click events to nav items - navItems.forEach((item, index) => { - item.addEventListener('click', () => { - setActiveSection(index); - }); - }); - - // Add click events to prev/next buttons - if (prevButton) { - prevButton.addEventListener('click', () => { - if (currentSectionIndex > 0) { - setActiveSection(currentSectionIndex - 1); - } - }); - } - - if (nextButton) { - nextButton.addEventListener('click', () => { - if (currentSectionIndex < sections.length - 1) { - setActiveSection(currentSectionIndex + 1); - } - }); - } - - /** - * Set the active section and update navigation - */ - function setActiveSection(index) { - // Remove active class from all sections and nav items - sections.forEach(section => section.classList.remove('active')); - navItems.forEach(item => item.classList.remove('active')); - progressDots.forEach(dot => dot.classList.remove('active')); - - // Add active class to current section, nav item, and dot - sections[index].classList.add('active'); - navItems[index].classList.add('active'); - if (progressDots[index]) { - progressDots[index].classList.add('active'); - } - - // Update current section index - currentSectionIndex = index; - - // Update navigation state - updateNavigation(); - - // Scroll to top of section - sections[index].scrollIntoView({behavior: 'smooth', block: 'start'}); - } - - /** - * Update navigation buttons and progress - */ - function updateNavigation() { - // Update section label - if (currentStepLabelEl && navItems[currentSectionIndex]) { - // Extract text content without the icon - const navText = navItems[currentSectionIndex].textContent.trim(); - currentStepLabelEl.textContent = navText; - } - - // Update prev/next buttons - if (prevButton) { - prevButton.disabled = currentSectionIndex === 0; - prevButton.style.opacity = currentSectionIndex === 0 ? 0.5 : 1; - } - - if (nextButton) { - nextButton.disabled = currentSectionIndex === sections.length - 1; - nextButton.style.opacity = currentSectionIndex === sections.length - 1 ? 0.5 : 1; - - // Change text for last step - if (currentSectionIndex === sections.length - 2) { - nextButton.innerHTML = 'Finish '; - } else { - nextButton.innerHTML = 'Next '; - } - } - } - - // Helper function to validate current section before proceeding - function validateCurrentSection() { - // Implement validation logic here based on the current section - // Return true if valid, false if not - return true; - } - - // Add to initAgentFormCommon function if it exists - if (typeof window.initAgentFormCommon === 'function') { - const originalInit = window.initAgentFormCommon; - - window.initAgentFormCommon = function(options) { - // Call the original initialization function - originalInit(options); - - // Now initialize the wizard navigation - setActiveSection(0); - }; - } -}); \ No newline at end of file diff --git a/webui/old/public/logo_1.png b/webui/old/public/logo_1.png deleted file mode 100644 index 28c2cfd..0000000 Binary files a/webui/old/public/logo_1.png and /dev/null differ diff --git a/webui/old/public/logo_2.png b/webui/old/public/logo_2.png deleted file mode 100644 index dea225b..0000000 Binary files a/webui/old/public/logo_2.png and /dev/null differ diff --git a/webui/old/views/actions.html b/webui/old/views/actions.html deleted file mode 100644 index 673207a..0000000 --- a/webui/old/views/actions.html +++ /dev/null @@ -1,291 +0,0 @@ - - - - - - Actions Playground - {{template "old/views/partials/header"}} - - - {{template "old/views/partials/menu"}} - - -
- -
- -
-
-

Actions Playground

-

Test and execute actions directly from the UI

-
- -
-

Select an Action

-
- - -
-
- - - - - - -
- - - - - - \ No newline at end of file diff --git a/webui/old/views/agents.html b/webui/old/views/agents.html deleted file mode 100644 index 81e6bac..0000000 --- a/webui/old/views/agents.html +++ /dev/null @@ -1,332 +0,0 @@ - - - - - - Agent List - {{template "old/views/partials/header"}} - - - - {{template "old/views/partials/menu"}} - - -
- -
- -
-
-

Agent List

-

Manage and interact with your AI agents

-
- -
- - Add New Agent - - -
- - - -
- {{ $status := .Status }} - {{ range .Agents }} -
-
-
- -
- -
-
-

{{.}}

-
- - {{ if eq (index $status .) true }}Active{{ else }}Inactive{{ end }} - -
- - - -
- - - - -
-
-
- {{ end }} -
- -
- - -
- - -
- - - - \ No newline at end of file diff --git a/webui/old/views/chat.html b/webui/old/views/chat.html deleted file mode 100644 index fc42b5f..0000000 --- a/webui/old/views/chat.html +++ /dev/null @@ -1,91 +0,0 @@ - - - - Smart Agent Interface - {{template "old/views/partials/header"}} - - - - {{template "old/views/partials/menu"}} -
- - -
-

Talk to '{{.Name}}'

-
- - -
- -
-

Clients:

-
- -
-
-
- -
-

Status:

-
- -
-
-
-
-
- - -
-

Agent:

-
- -
-
-
- - -
-
- -
-
-
- -
- - - - diff --git a/webui/old/views/create.html b/webui/old/views/create.html deleted file mode 100644 index 887fa26..0000000 --- a/webui/old/views/create.html +++ /dev/null @@ -1,154 +0,0 @@ - - - - Create New Agent - {{template "old/views/partials/header"}} - - - - - - - {{template "old/views/partials/menu"}} -
-
-

Create New Agent

- -
- {{template "old/views/partials/agent-form" . }} - - -
-
- - -
- - - - -
-
- - -
- -
- - - - diff --git a/webui/old/views/group-create.html b/webui/old/views/group-create.html deleted file mode 100644 index 2a333ab..0000000 --- a/webui/old/views/group-create.html +++ /dev/null @@ -1,600 +0,0 @@ - - - - Create Agent Group - {{template "old/views/partials/header"}} - - - - - - - - {{template "old/views/partials/menu"}} -
-
-

Create Agent Group

- - -
-
-
1
-
Generate Profiles
-
-
-
2
-
Review & Select
-
-
-
3
-
Configure Settings
-
-
- - -
-

Generate Agent Profiles

-

Describe the group of agents you want to create. Be specific about their roles, relationships, and purpose.

- -
- -
- -
- -
-
- - -
- -

Generating agent profiles...

-
- - -
-

Review & Select Agent Profiles

-

Select the agents you want to create. You can customize their details before creation.

- -
- -
- -
- -
- -
- - -
-
- - -
-

Configure Common Settings

-

Configure common settings for all selected agents. These settings will be applied to each agent.

- -
- -
- -
-

Basic Information from Profiles

-

The name, description, and system prompt for each agent will be taken from the profiles you selected in the previous step.

-
-
- - -
- {{template "old/views/partials/agent-form" . }} -
-
- -
- - -
-
-
- - -
- - - - -
-
- - -
- -
- - - - diff --git a/webui/old/views/index.html b/webui/old/views/index.html deleted file mode 100644 index 8992993..0000000 --- a/webui/old/views/index.html +++ /dev/null @@ -1,224 +0,0 @@ - - - - Smart Assistant Dashboard - {{template "old/views/partials/header"}} - - - - {{template "old/views/partials/menu"}} -
- - -
- Company Logo -
- -

LocalAGI

- - -
-
-
{{.Actions}}
-
Available Actions
-
-
-
{{.Connectors}}
-
Available Connectors
-
-
-
{{ .AgentCount }}
-
Agents
-
-
- -
- - -
-

Agent List

-

View and manage your list of agents, including detailed profiles and statistics.

-
-
- - - -
-

Create Agent

-

Create a new intelligent agent with custom behaviors, connectors, and actions.

-
-
- - - -
-

Analytics

-

View performance metrics and insights from your agent operations.

- Coming Soon -
-
- - -
-

Settings

-

Configure system preferences and global settings for all agents.

- Coming Soon -
-
-
-
- - -
- -
- - - - \ No newline at end of file diff --git a/webui/old/views/login.html b/webui/old/views/login.html deleted file mode 100644 index aedf6b9..0000000 --- a/webui/old/views/login.html +++ /dev/null @@ -1,215 +0,0 @@ - - -{{template "old/views/partials/header" .}} - - -
- - {{template "old/views/partials/menu" .}} - -
- -
-
-
- -
-
- -
-
-

- - Authorization Required - -

-

Please enter your access token to continue

-
- -
-
- -
-
- -
- -
-
- -
- -
-
- -
-
- - Instance is token protected -
-

Current time (UTC): {{.CurrentDate}}

-
-
-
-
- -
- - - - - \ No newline at end of file diff --git a/webui/old/views/partials/agent-form.html b/webui/old/views/partials/agent-form.html deleted file mode 100644 index 42353ad..0000000 --- a/webui/old/views/partials/agent-form.html +++ /dev/null @@ -1,307 +0,0 @@ -
- -
- -
- - -
- -
-

Basic Information

- -
- - {{ if .Name }} - - {{ else }} - - {{ end }} -
- -
- - -
- -
- - -
- -
- -
- -
- -
-
- - -
-

Connectors

- -
- -
- -
- -
-
- - -
-

Actions

- -
- -
- -
- -
-
- - -
-

MCP Servers

- -
- -
- -
- -
-
- - -
-

Memory Settings

- -
- -
- -
- - -
- -
- -
- -
- -
-
- - -
-

Model Settings

- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- -
-
- - -
-

Prompts & Goals

- -
- -
- -
- -
- -
- - -
- -
- - -
-
- - -
-

Advanced Settings

- -
- -
- -
- -
- -
- -
- -
- -
- -
- - -
-
-
-
- - -
-
- -
- -
-
- -
-
- Basic Information -
-
- -
- -
-
\ No newline at end of file diff --git a/webui/old/views/partials/header.html b/webui/old/views/partials/header.html deleted file mode 100644 index 7949c13..0000000 --- a/webui/old/views/partials/header.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/webui/old/views/partials/menu.html b/webui/old/views/partials/menu.html deleted file mode 100644 index 5f22853..0000000 --- a/webui/old/views/partials/menu.html +++ /dev/null @@ -1,105 +0,0 @@ - -
- - \ No newline at end of file diff --git a/webui/old/views/settings.html b/webui/old/views/settings.html deleted file mode 100644 index 3d2b8ef..0000000 --- a/webui/old/views/settings.html +++ /dev/null @@ -1,462 +0,0 @@ - - - - Agent settings {{.Name}} - {{template "old/views/partials/header"}} - - - - - - - {{template "old/views/partials/menu"}} - - -
- -
- -
-
-

Agent settings - {{.Name}}

-
- -
- - -
-

Edit Agent Configuration

-
- - - {{template "old/views/partials/agent-form" .}} - - -
-
- -
-

Agent Control

-
- -
-
- -
-

Export Data

-

Export your agent configuration for backup or transfer.

- -
- -
-

Danger Zone

-

Permanently delete this agent and all associated data. This action cannot be undone.

- -
- - -
-
- - - - \ No newline at end of file diff --git a/webui/old/views/status.html b/webui/old/views/status.html deleted file mode 100644 index fd2a769..0000000 --- a/webui/old/views/status.html +++ /dev/null @@ -1,61 +0,0 @@ - - - - Smart Agent status - {{template "old/views/partials/header"}} - - - - {{template "old/views/partials/menu"}} -
- -
-

{{.Name}}

-
- -
-
- {{ range .History }} - -
-

Agent:

-
- Result: {{.Result}} Action: {{.Action}} Params: {{.Params}} Reasoning: {{.Reasoning}} -
-
- {{end}} -
-
- - diff --git a/webui/routes.go b/webui/routes.go index 6bd57c7..7f44e76 100644 --- a/webui/routes.go +++ b/webui/routes.go @@ -22,12 +22,6 @@ import ( "github.com/mudler/LocalAGI/services" ) -//go:embed old/views/* -var viewsfs embed.FS - -//go:embed old/public/* -var embeddedFiles embed.FS - //go:embed react-ui/dist/* var reactUI embed.FS @@ -40,12 +34,6 @@ func (app *App) registerRoutes(pool *state.AgentPool, webapp *fiber.App) { Browse: true, })) - webapp.Use("/old/public", filesystem.New(filesystem.Config{ - Root: http.FS(embeddedFiles), - PathPrefix: "/old/public", - Browse: true, - })) - if len(app.config.ApiKeys) > 0 { kaConfig, err := GetKeyAuthConfig(app.config.ApiKeys) if err != nil || kaConfig == nil {