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}
-
-
-
-
-
-
-
- `;
- },
-
- // 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
-
-
-
-
-
-
-
- Action Configuration
-
-
-
-
-
-
-
-
-
-
-
-
-
\ 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
-
-
-
-
-
-
-
Import Agent
-
-
-
-
-
- Agent imported successfully! The page will refresh in a moment.
-
-
-
-
- Error importing 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}}'
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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
-
-
-
-
-
-
-
-
- Agent created successfully! Redirecting to agent list...
-
-
-
- Error creating agent.
-
-
-
-
-
-
-
-
-
-
-
-
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
-
-
-
-
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.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Agents created successfully! Redirecting to agent list...
-
-
-
- Error creating agents.
-
-
-
-
-
-
-
-
-
-
-
-
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"}}
-
-
-
-
-

-
-
-
LocalAGI
-
-
-
-
-
{{.Actions}}
-
Available Actions
-
-
-
{{.Connectors}}
-
Available Connectors
-
-
-
{{ .AgentCount }}
-
Agents
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ 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
-
-
-
-
-
-
-
\ 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
-
-
-
-
-
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.
-
-
-
-
- Agent: {{.Name}}
- Last modified:
-
-
-
-
-
-
-
\ 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 {