feat: consume cogito for agent reasoning (#320)

* WIP

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Drop old webui

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Almost there

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* It builds

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Make it build

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fixups, still doesn't work

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* unused now

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Send result before closing

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Fix observability

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Drop MCP code and wire-up in cogito

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Drop some templates

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Keep reporting into conv

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* tests fixups

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* tests fixups

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fixups

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Do not complete observable during thought process

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Update cogito

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Fixups

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Fixups

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Fixups

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Drop unneded option now

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Fixups

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Better handling of user tools

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* TEST

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Add flake attempts

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Revert "TEST"

This reverts commit 8b12a9fd03.

* tKeep indexing MCP actions

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Split CI jobs to improve speed

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* CI optimizations

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Bump timeout

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Bump cogito

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix: always commit last progress

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* chore: better management of observables

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
Ettore Di Giacinto
2025-10-27 18:41:28 +01:00
committed by GitHub
parent 84f21c6ab4
commit 73a6be8264
46 changed files with 740 additions and 6796 deletions
+11 -35
View File
@@ -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
+5 -2
View File
@@ -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)
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/...
-48
View File
@@ -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"},
}
}
-50
View File
@@ -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"},
}
}
-71
View File
@@ -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"},
}
}
-43
View File
@@ -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"},
}
}
-45
View File
@@ -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"},
}
}
+6 -504
View File
@@ -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
}
+406 -585
View File
File diff suppressed because it is too large Load Diff
+44 -15
View File
@@ -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() {
-162
View File
@@ -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
}
+6 -6
View File
@@ -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)
+12 -38
View File
@@ -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
}
-9
View File
@@ -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
-40
View File
@@ -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
-10
View File
@@ -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",
+4 -7
View File
@@ -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)
+34 -1
View File
@@ -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) {
+5 -39
View File
@@ -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{
+2
View File
@@ -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)
+12 -3
View File
@@ -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
)
+174 -4
View File
@@ -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=
+7 -7
View File
@@ -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
+11 -10
View File
@@ -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",
+1 -8
View File
@@ -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),
-714
View File
@@ -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,<svg fill="%23ffffff" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M7 10l5 5 5-5z"/></svg>');
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;
}
}
-254
View File
@@ -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;
}
}
-564
View File
@@ -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 = `
<div class="form-group">
<label for="connectorConfig${index}">Connector Config (JSON)</label>
<textarea id="connectorConfig${index}"
name="connectors[${index}][config]"
class="form-control"
placeholder='{"key":"value"}'></textarea>
</div>
`;
// 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 `
<div class="connector mb-4 section-box" style="margin-top: 15px; padding: 15px;">
<h2>Connector ${index + 1}</h2>
<div class="mb-4">
<label for="connectorType${index}">Connector Type</label>
<select name="connectors[${index}][type]"
id="connectorType${index}"
class="form-control"
onchange="AgentFormUtils.renderConnectorForm(${index}, this.value)">
<option value="">Select Connector Type</option>
${data.options}
</select>
</div>
<div id="connectorFormContainer${index}">
<!-- Connector form will be dynamically inserted here -->
<div class="form-group">
<div class="placeholder-text">Select a connector type to configure</div>
</div>
</div>
<button type="button" class="remove-btn" onclick="this.closest('.connector').remove()">
<i class="fas fa-trash"></i> Remove Connector
</button>
</div>
`;
},
// MCP Server template
mcpServerTemplate: function(index, data) {
return `
<div class="mcp_server mb-4 section-box" style="margin-top: 15px; padding: 15px;">
<h2>MCP Server ${index + 1}</h2>
<div class="mb-4">
<label for="mcpURL${index}">Server URL</label>
<input type="text" id="mcpURL${index}" name="mcp_servers[${index}][url]" placeholder="https://example.com">
</div>
<div class="mb-4">
<label for="mcpToken${index}">API Token (Optional)</label>
<input type="text" id="mcpToken${index}" name="mcp_servers[${index}][token]" placeholder="API token">
</div>
<button type="button" class="remove-btn" onclick="this.closest('.mcp_server').remove()">
<i class="fas fa-trash"></i> Remove Server
</button>
</div>
`;
},
// Action template
actionTemplate: function(index, data) {
return `
<div class="action mb-4 section-box" style="margin-top: 15px; padding: 15px;">
<h2>Action ${index + 1}</h2>
<div class="mb-4">
<label for="actionsName${index}">Action Type</label>
<select name="actions[${index}][name]" id="actionsName${index}">
${data.options}
</select>
</div>
<div class="mb-4">
<label for="actionsConfig${index}">Action Config (JSON)</label>
<textarea id="actionsConfig${index}" name="actions[${index}][config]" placeholder='{"key":"value"}'>{}</textarea>
</div>
<button type="button" class="remove-btn" onclick="this.closest('.action').remove()">
<i class="fas fa-trash"></i> Remove Action
</button>
</div>
`;
},
// Prompt Block template
promptBlockTemplate: function(index, data) {
return `
<div class="prompt_block mb-4 section-box" style="margin-top: 15px; padding: 15px;">
<h2>Prompt Block ${index + 1}</h2>
<div class="mb-4">
<label for="promptName${index}">Prompt Block Type</label>
<select name="promptblocks[${index}][name]" id="promptName${index}">
${data.options}
</select>
</div>
<div class="mb-4">
<label for="promptConfig${index}">Prompt Block Config (JSON)</label>
<textarea id="promptConfig${index}" name="promptblocks[${index}][config]" placeholder='{"key":"value"}'>{}</textarea>
</div>
<button type="button" class="remove-btn" onclick="this.closest('.prompt_block').remove()">
<i class="fas fa-trash"></i> Remove Prompt Block
</button>
</div>
`;
}
};
// 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);
}
-44
View File
@@ -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';
});
});
});
-475
View File
@@ -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 = `
<div class="form-group">
<label for="telegramToken${index}">Telegram Bot Token</label>
<input type="text"
id="telegramToken${index}"
name="connectors[${index}][config][token]"
class="form-control"
placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11">
<small class="form-text text-muted">Get this from @BotFather on Telegram</small>
</div>
`;
// 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 = `
<div class="form-group">
<label for="slackAppToken${index}">Slack App Token</label>
<input type="text"
id="slackAppToken${index}"
name="connectors[${index}][config][appToken]"
class="form-control"
placeholder="xapp-...">
<small class="form-text text-muted">App-level token starting with xapp-</small>
</div>
<div class="form-group">
<label for="slackBotToken${index}">Slack Bot Token</label>
<input type="text"
id="slackBotToken${index}"
name="connectors[${index}][config][botToken]"
class="form-control"
placeholder="xoxb-...">
<small class="form-text text-muted">Bot token starting with xoxb-</small>
</div>
<div class="form-group">
<label for="slackChannelID${index}">Slack Channel ID</label>
<input type="text"
id="slackChannelID${index}"
name="connectors[${index}][config][channelID]"
class="form-control"
placeholder="C012345678">
<small class="form-text text-muted">Channel ID where the bot will operate</small>
</div>
<div class="form-group form-check">
<input type="checkbox"
class="form-check-input"
id="slackAlwaysReply${index}"
name="connectors[${index}][config][alwaysReply]">
<label class="form-check-label" for="slackAlwaysReply${index}">Always Reply</label>
<small class="form-text text-muted">If checked, the bot will reply to all messages in the channel</small>
</div>
`;
// 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 = `
<div class="form-group">
<label for="discordToken${index}">Discord Bot Token</label>
<input type="text"
id="discordToken${index}"
name="connectors[${index}][config][token]"
class="form-control"
placeholder="Bot token from Discord Developer Portal">
</div>
<div class="form-group">
<label for="discordChannelID${index}">Default Channel ID</label>
<input type="text"
id="discordChannelID${index}"
name="connectors[${index}][config][defaultChannel]"
class="form-control"
placeholder="Channel ID where the bot will operate">
</div>
`;
// 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 = `
<div class="form-group">
<label for="githubIssuesToken${index}">GitHub Personal Access Token</label>
<input type="text"
id="githubIssuesToken${index}"
name="connectors[${index}][config][token]"
class="form-control"
placeholder="ghp_...">
<small class="form-text text-muted">Needs repo and read:org permissions</small>
</div>
<div class="form-group">
<label for="githubIssuesOwner${index}">Repository Owner</label>
<input type="text"
id="githubIssuesOwner${index}"
name="connectors[${index}][config][owner]"
class="form-control"
placeholder="username or organization">
</div>
<div class="form-group">
<label for="githubIssuesRepo${index}">Repository Name</label>
<input type="text"
id="githubIssuesRepo${index}"
name="connectors[${index}][config][repository]"
class="form-control"
placeholder="repository-name">
</div>
<div class="form-group form-check">
<input type="checkbox"
class="form-check-input"
id="githubIssuesReplyIfNoReplies${index}"
name="connectors[${index}][config][replyIfNoReplies]"
value="true">
<label class="form-check-label" for="githubIssuesReplyIfNoReplies${index}">Reply to issues with no replies</label>
<small class="form-text text-muted">If checked, the bot will reply to issues that have no replies yet</small>
</div>
<div class="form-group">
<label for="githubIssuesPollInterval${index}">Poll Interval (seconds)</label>
<input type="number"
id="githubIssuesPollInterval${index}"
name="connectors[${index}][config][pollInterval]"
class="form-control"
placeholder="60">
<small class="form-text text-muted">How often to check for new issues (in seconds)</small>
</div>
`;
// 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 = `
<div class="form-group">
<label for="githubPRsToken${index}">GitHub Token</label>
<input type="text"
id="githubPRsToken${index}"
name="connectors[${index}][config][token]"
class="form-control"
placeholder="ghp_...">
<small class="form-text text-muted">Personal Access Token with repo permissions</small>
</div>
<div class="form-group">
<label for="githubPRsOwner${index}">Repository Owner</label>
<input type="text"
id="githubPRsOwner${index}"
name="connectors[${index}][config][owner]"
class="form-control"
placeholder="username or organization">
</div>
<div class="form-group">
<label for="githubPRsRepo${index}">Repository Name</label>
<input type="text"
id="githubPRsRepo${index}"
name="connectors[${index}][config][repository]"
class="form-control"
placeholder="repository-name">
</div>
<div class="form-group form-check">
<input type="checkbox"
class="form-check-input"
id="githubPRsReplyIfNoReplies${index}"
name="connectors[${index}][config][replyIfNoReplies]"
value="true">
<label class="form-check-label" for="githubPRsReplyIfNoReplies${index}">Reply to PRs with no replies</label>
<small class="form-text text-muted">If checked, the bot will reply to pull requests that have no replies yet</small>
</div>
<div class="form-group">
<label for="githubPRsPollInterval${index}">Poll Interval (seconds)</label>
<input type="number"
id="githubPRsPollInterval${index}"
name="connectors[${index}][config][pollInterval]"
class="form-control"
placeholder="60">
<small class="form-text text-muted">How often to check for new pull requests (in seconds)</small>
</div>
`;
// 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 = `
<div class="form-group">
<label for="ircServer${index}">IRC Server</label>
<input type="text"
id="ircServer${index}"
name="connectors[${index}][config][server]"
class="form-control"
placeholder="irc.libera.chat">
</div>
<div class="form-group">
<label for="ircPort${index}">Port</label>
<input type="text"
id="ircPort${index}"
name="connectors[${index}][config][port]"
class="form-control"
placeholder="6667">
</div>
<div class="form-group">
<label for="ircChannel${index}">Channel</label>
<input type="text"
id="ircChannel${index}"
name="connectors[${index}][config][channel]"
class="form-control"
placeholder="#channel">
</div>
<div class="form-group">
<label for="ircNick${index}">Nickname</label>
<input type="text"
id="ircNick${index}"
name="connectors[${index}][config][nickname]"
class="form-control"
placeholder="MyBot">
</div>
<div class="form-group form-check">
<input type="checkbox"
class="form-check-input"
id="ircAlwaysReply${index}"
name="connectors[${index}][config][alwaysReply]"
value="true">
<label class="form-check-label" for="ircAlwaysReply${index}">Always reply to messages</label>
<small class="form-text text-muted">If checked, the bot will always reply to messages, even if they are not directed at it</small>
</div>
`;
// 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 = `
<div class="form-group">
<label for="twitterToken${index}">Twitter API Token</label>
<input type="text"
id="twitterToken${index}"
name="connectors[${index}][config][token]"
class="form-control"
placeholder="Your Twitter API token">
</div>
<div class="form-group">
<label for="twitterBotUsername${index}">Bot Username</label>
<input type="text"
id="twitterBotUsername${index}"
name="connectors[${index}][config][botUsername]"
class="form-control"
placeholder="@YourBotUsername">
<small class="form-text text-muted">Username of your Twitter bot (with or without @)</small>
</div>
<div class="form-group form-check">
<input type="checkbox"
class="form-check-input"
id="twitterNoCharLimit${index}"
name="connectors[${index}][config][noCharacterLimit]"
value="true">
<label class="form-check-label" for="twitterNoCharLimit${index}">Disable character limit</label>
<small class="form-text text-muted">If checked, the bot will not enforce Twitter's character limit</small>
</div>
`;
// 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 = `
<div class="form-group">
<label for="connectorConfig${index}">Connector Configuration (JSON)</label>
<textarea id="connectorConfig${index}"
name="connectors[${index}][config]"
class="form-control"
rows="10"
placeholder='{"key":"value"}'>${escapeHTML(configStr)}</textarea>
<small class="form-text text-muted">Enter the connector configuration as a JSON object</small>
</div>
`;
// 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 };
}
};
-139
View File
@@ -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 <i class="fas fa-check"></i>';
} else {
nextButton.innerHTML = 'Next <i class="fas fa-arrow-right"></i>';
}
}
}
// 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);
};
}
});
Binary file not shown.

Before

Width:  |  Height:  |  Size: 933 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

-291
View File
@@ -1,291 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Actions Playground</title>
{{template "old/views/partials/header"}}
</head>
<body>
{{template "old/views/partials/menu"}}
<!-- Toast for notifications -->
<div id="toast" class="toast">
<span id="toast-message"></span>
</div>
<div class="container mx-auto">
<header class="text-center mb-8">
<h1 class="text-4xl md:text-6xl font-bold">Actions Playground</h1>
<p class="mt-4 text-gray-400">Test and execute actions directly from the UI</p>
</header>
<section class="section-box mb-8">
<h2 class="mb-4">Select an Action</h2>
<div class="mb-4">
<label for="action-select" class="block mb-2">Available Actions:</label>
<select id="action-select" class="w-full">
<option value="">-- Select an action --</option>
<!-- Actions will be loaded here -->
</select>
</div>
</section>
<section id="config-section" class="section-box mb-8 hidden">
<h2 class="mb-4">Action Configuration</h2>
<form id="action-form">
<div class="mb-6">
<label for="config-json" class="block mb-2">Configuration (JSON):</label>
<textarea id="config-json" class="w-full" rows="5" placeholder='{"key": "value"}'>{}</textarea>
<p class="text-xs text-gray-400 mt-1">Enter JSON configuration for the action</p>
</div>
<div class="mb-6">
<label for="params-json" class="block mb-2">Parameters (JSON):</label>
<textarea id="params-json" class="w-full" rows="5" placeholder='{"key": "value"}'>{}</textarea>
<p class="text-xs text-gray-400 mt-1">Enter JSON parameters for the action</p>
</div>
<div class="flex justify-end">
<button type="submit" class="action-btn start-btn">
<i class="fas fa-play"></i> Execute Action
</button>
</div>
</form>
</section>
<section id="results-section" class="section-box mb-8 hidden">
<h2 class="mb-4">Action Results</h2>
<div id="action-results">
<!-- Results will appear here -->
</div>
</section>
<footer class="text-center text-gray-500 text-sm mb-8">
<p>&copy; 2025 LocalAGI.</p>
</footer>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Load available actions
fetchActions();
// Handle action selection
document.getElementById('action-select').addEventListener('change', function() {
const actionId = this.value;
if (actionId) {
document.getElementById('config-section').classList.remove('hidden');
} else {
document.getElementById('config-section').classList.add('hidden');
}
// Hide results when changing actions
document.getElementById('results-section').classList.add('hidden');
});
// Handle form submission
document.getElementById('action-form').addEventListener('submit', function(e) {
e.preventDefault();
const actionId = document.getElementById('action-select').value;
if (actionId) {
executeAction(actionId);
} else {
showToast('Please select an action first', 'error');
}
});
});
function fetchActions() {
fetch('/api/actions')
.then(response => response.json())
.then(actions => {
const select = document.getElementById('action-select');
// Clear existing options except the first one
while (select.options.length > 1) {
select.remove(1);
}
if (actions.length === 0) {
const option = document.createElement('option');
option.text = 'No actions available';
option.disabled = true;
select.add(option);
return;
}
// Add options for each action
actions.forEach(actionId => {
const option = document.createElement('option');
option.value = actionId;
option.text = actionId; // Using actionId as display text
select.add(option);
});
})
.catch(error => {
console.error('Error fetching actions:', error);
showToast('Failed to load actions: ' + error.message, 'error');
const select = document.getElementById('action-select');
const option = document.createElement('option');
option.text = 'Error loading actions';
option.disabled = true;
// Clear existing options except the first one
while (select.options.length > 1) {
select.remove(1);
}
select.add(option);
});
}
function executeAction(actionId) {
// Get the JSON data from textareas
let config = {};
let params = {};
try {
const configText = document.getElementById('config-json').value.trim();
if (configText && configText !== '{}') {
config = JSON.parse(configText);
}
const paramsText = document.getElementById('params-json').value.trim();
if (paramsText && paramsText !== '{}') {
params = JSON.parse(paramsText);
}
} catch (error) {
showToast('Invalid JSON: ' + error.message, 'error');
return;
}
// Show the results section with loading indicator
const resultsSection = document.getElementById('results-section');
resultsSection.classList.remove('hidden');
const resultDiv = document.getElementById('action-results');
resultDiv.innerHTML = `
<div class="flex justify-center items-center py-8">
<div class="loader"></div>
</div>
`;
// Execute the action
fetch(`/api/action/${actionId}/run`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
config: config,
params: params
})
})
.then(response => {
return response.json();
})
.then(result => {
if (result.error) {
throw new Error(result.error);
}
// Display the results
showActionResult(result);
showToast('Action executed successfully!', 'success');
})
.catch(error => {
resultDiv.innerHTML = `
<div class="alert alert-error" style="display: block;">
<i class="fas fa-exclamation-circle mr-2"></i> Error: ${error.message}
</div>
`;
showToast('Error executing action', 'error');
});
}
function showActionResult(result) {
const resultDiv = document.getElementById('action-results');
let html = '';
// Display result
if (result.Result) {
html += `
<div class="mb-4">
<h4 class="text-lg mb-2" style="color: var(--secondary);">Result:</h4>
<div class="code-terminal">
<pre>${escapeHtml(result.Result)}</pre>
</div>
</div>
`;
}
// Display metadata if available
if (result.Metadata && Object.keys(result.Metadata).length > 0) {
html += `
<div class="mb-4">
<h4 class="text-lg mb-2" style="color: var(--secondary);">Metadata:</h4>
<div class="code-terminal">
<pre>${escapeHtml(JSON.stringify(result.Metadata, null, 2))}</pre>
</div>
</div>
`;
}
if (!html) {
html = '<p class="text-gray-400">No results returned from the action.</p>';
}
resultDiv.innerHTML = html;
// Scroll to results
resultDiv.scrollIntoView({ behavior: 'smooth' });
}
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function showToast(message, type) {
const toast = document.getElementById('toast');
const toastMessage = document.getElementById('toast-message');
toastMessage.textContent = message;
toast.className = 'toast toast-' + type;
toast.classList.add('toast-visible');
setTimeout(() => {
toast.classList.remove('toast-visible');
}, 3000);
}
</script>
<style>
.loader {
width: 48px;
height: 48px;
border: 5px solid var(--tertiary);
border-bottom-color: transparent;
border-radius: 50%;
display: inline-block;
box-sizing: border-box;
animation: rotation 1s linear infinite;
}
@keyframes rotation {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.code-terminal {
margin-top: 0;
}
</style>
</body>
</html>
-332
View File
@@ -1,332 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Agent List</title>
{{template "old/views/partials/header"}}
<style>
.avatar-placeholder {
width: 96px;
height: 96px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #2a2a2a, #3a3a3a);
color: var(--primary);
font-size: 1.5rem;
margin-bottom: 1rem;
border: 2px solid var(--primary);
box-shadow: var(--neon-glow);
position: relative;
overflow: hidden;
}
.avatar-placeholder::after {
content: "";
position: absolute;
width: 100%;
height: 4px;
background: var(--primary);
bottom: 0;
left: 0;
animation: loading-progress 2s infinite linear;
}
@keyframes loading-progress {
0% { width: 0; }
50% { width: 100%; }
100% { width: 0; }
}
.placeholder-text {
z-index: 1;
}
</style>
</head>
<body>
{{template "old/views/partials/menu"}}
<!-- Toast for notifications -->
<div id="toast" class="toast">
<span id="toast-message"></span>
</div>
<div class="container mx-auto">
<header class="text-center mb-8">
<h1 class="text-4xl md:text-6xl font-bold">Agent List</h1>
<p class="mt-4 text-gray-400">Manage and interact with your AI agents</p>
</header>
<div class="button-container justify-center mb-6">
<a href="/old/create" class="action-btn start-btn">
<i class="fas fa-plus-circle"></i> Add New Agent
</a>
<button id="toggle-import" class="action-btn" style="background: linear-gradient(135deg, var(--tertiary), #4a76a8);">
<i class="fas fa-file-import"></i> Import Agent
</button>
</div>
<section id="import-section" class="hidden mb-8">
<div class="section-box">
<h2>Import Agent</h2>
<!-- Response Messages Container -->
<div id="response-container">
<!-- Success Alert -->
<div id="success-alert" class="alert alert-success" style="display: none;">
Agent imported successfully! The page will refresh in a moment.
</div>
<!-- Error Alert -->
<div id="error-alert" class="alert alert-error" style="display: none;">
<span id="error-message">Error importing agent.</span>
</div>
</div>
<form id='import-form' hx-encoding='multipart/form-data' hx-post='/settings/import' hx-target="#response-container" hx-swap="none">
<div class="mb-4">
<label for="file" class="block mb-2">Select Agent File:</label>
<input type='file' name='file' id='file'>
</div>
<div class="flex items-center">
<button id="import-button" type="submit" class="action-btn">
<i class="fas fa-cloud-upload-alt"></i> Import Agent
</button>
</div>
</form>
</div>
</section>
<section class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{{ $status := .Status }}
{{ range .Agents }}
<div hx-ext="sse" data-agent-name="{{.}}" class="card">
<div class="flex flex-col items-center text-center p-4">
<div class="avatar-container mb-4">
<img src="/avatars/{{.}}.png" alt="{{.}}" class="w-24 h-24 rounded-full"
style="border: 2px solid var(--primary); box-shadow: var(--neon-glow); display: none;"
onload="this.style.display = 'block'; this.nextElementSibling.style.display = 'none';"
onerror="this.style.display = 'none'; this.nextElementSibling.style.display = 'flex';">
<div class="avatar-placeholder">
<span class="placeholder-text"><i class="fas fa-sync fa-spin"></i></span>
</div>
</div>
<h2>{{.}}</h2>
<div class="mb-4 flex items-center justify-center">
<span class="badge {{ if eq (index $status .) true }}badge-primary{{ else }}badge-secondary{{ end }} mr-2">
{{ if eq (index $status .) true }}Active{{ else }}Inactive{{ end }}
</span>
</div>
<div class="grid grid-cols-2 gap-2 w-full mb-4">
<a href="/old/status/{{.}}" class="action-btn flex items-center justify-center"
style="background: linear-gradient(135deg, #2a2a2a, #3a3a3a);">
<i class="fas fa-info-circle mr-2"></i> Status
</a>
<a href="/old/talk/{{.}}" class="action-btn flex items-center justify-center"
style="background: linear-gradient(135deg, #2a2a2a, #3a3a3a);">
<i class="fas fa-comments mr-2"></i> Talk
</a>
</div>
<div class="grid grid-cols-2 gap-2 w-full">
<button class="action-btn toggle-btn col-span-1"
data-agent="{{.}}"
data-active="{{ if eq (index $status .) true }}true{{ else }}false{{ end }}">
{{ if eq (index $status .) true }}
<i class="fas fa-pause"></i> Pause
{{ else }}
<i class="fas fa-play"></i> Start
{{ end }}
</button>
<a href="/old/settings/{{.}}" class="action-btn col-span-1 flex items-center justify-center"
style="background: linear-gradient(135deg, #2a2a2a, #3a3a3a);">
<i class="fas fa-cog"></i>
</a>
</div>
</div>
</div>
{{ end }}
</section>
<div class="user-info mt-8 mb-4">
<span></span>
<span class="timestamp"></span>
</div>
<footer class="text-center text-gray-500 text-sm mb-8">
<p>&copy; 2025 LocalAGI.</p>
</footer>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Image loading handler
document.querySelectorAll('.avatar-container img').forEach(img => {
// Check if image is already cached
if (img.complete) {
if (img.naturalHeight === 0) {
// Image failed to load
img.style.display = 'none';
img.nextElementSibling.style.display = 'flex';
} else {
// Image loaded successfully
img.style.display = 'block';
img.nextElementSibling.style.display = 'none';
}
}
// onload and onerror handlers are already in the HTML
});
const importSection = document.getElementById('import-section');
const toggleImport = document.getElementById('toggle-import');
// Toggle import section visibility
toggleImport.addEventListener('click', function() {
importSection.classList.toggle('hidden');
// Add glitch effect when showing
if (!importSection.classList.contains('hidden')) {
importSection.style.animation = 'glitch 0.3s';
setTimeout(() => {
importSection.style.animation = '';
}, 300);
}
});
// Handle import form submission
document.getElementById('import-form').addEventListener('htmx:afterRequest', function(event) {
const xhr = event.detail.xhr;
const successAlert = document.getElementById('success-alert');
const errorAlert = document.getElementById('error-alert');
const errorMessage = document.getElementById('error-message');
// Hide both alerts initially
successAlert.style.display = 'none';
errorAlert.style.display = 'none';
if (xhr.status === 200) {
try {
const response = JSON.parse(xhr.responseText);
if (response.status === "ok") {
// Show success message
successAlert.style.display = 'block';
showToast("Agent imported successfully!", "success");
// Refresh the page after a short delay
setTimeout(() => {
window.location.reload();
}, 2000);
} else if (response.error) {
// Show error message
errorMessage.textContent = response.error;
errorAlert.style.display = 'block';
showToast("Import failed: " + response.error, "error");
}
} catch (e) {
// Handle parsing error
errorMessage.textContent = "Invalid response format";
errorAlert.style.display = 'block';
showToast("Invalid response format", "error");
}
} else {
// Handle HTTP error
errorMessage.textContent = "Server error: " + xhr.status;
errorAlert.style.display = 'block';
showToast("Server error: " + xhr.status, "error");
}
});
// Handle toggle buttons - using pure JavaScript
document.querySelectorAll('.toggle-btn').forEach(button => {
button.addEventListener('click', function() {
const agent = this.getAttribute('data-agent');
const isActive = this.getAttribute('data-active') === 'true';
const endpoint = isActive ? `/api/agent/${agent}/pause` : `/api/agent/${agent}/start`;
// Add animation
this.style.animation = 'pulse 0.5s';
// Create a new XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.open('PUT', endpoint);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = () => {
// Clear animation
this.style.animation = '';
if (xhr.status === 200) {
try {
const response = JSON.parse(xhr.responseText);
if (response.status === "ok") {
// Toggle the button state
const newState = !isActive;
this.setAttribute('data-active', newState ? 'true' : 'false');
// Update button text and icon
if (newState) {
this.innerHTML = '<i class="fas fa-pause"></i> Pause';
} else {
this.innerHTML = '<i class="fas fa-play"></i> Start';
}
// Show success toast
const action = isActive ? 'pause' : 'start';
showToast(`Agent "${agent}" ${action}ed successfully`, 'success');
// Update the status badge
updateAgentStatus(agent, newState);
} else if (response.error) {
// Show error toast
showToast(`Error: ${response.error}`, 'error');
}
} catch (e) {
// Handle parsing error
showToast("Invalid response format", 'error');
console.error("Error parsing response:", e);
}
} else {
// Handle HTTP error
showToast(`Server error: ${xhr.status}`, 'error');
}
};
xhr.onerror = () => {
// Clear animation
this.style.animation = '';
showToast("Network error occurred", 'error');
console.error("Network error occurred");
};
// Send the request
xhr.send(JSON.stringify({}));
});
});
});
// Function to update agent status in the UI
function updateAgentStatus(agentName, isOnline) {
// Find the card for this agent
const cards = document.querySelectorAll('.card');
cards.forEach(card => {
const agentTitle = card.querySelector('h2').textContent;
if (agentTitle === agentName) {
// Update the badge
const badge = card.querySelector('.badge');
if (isOnline) {
badge.className = 'badge badge-primary mr-2';
badge.textContent = 'Active';
} else {
badge.className = 'badge badge-secondary mr-2';
badge.textContent = 'Inactive';
}
}
});
}
</script>
</body>
</html>
-91
View File
@@ -1,91 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Smart Agent Interface</title>
{{template "old/views/partials/header"}}
<style>
body { overflow: hidden; }
.chat-container { height: 90vh; display: flex; flex-direction: column; }
.chat-messages { overflow-y: auto; flex-grow: 1; }
.htmx-indicator{
opacity:0;
transition: opacity 10ms ease-in;
}
.htmx-request .htmx-indicator{
opacity:1
}
/* Loader (https://cssloaders.github.io/) */
.loader {
width: 12px;
height: 12px;
border-radius: 50%;
display: block;
margin:15px auto;
position: relative;
color: #FFF;
box-sizing: border-box;
animation: animloader 2s linear infinite;
}
@keyframes animloader {
0% { box-shadow: 14px 0 0 -2px, 38px 0 0 -2px, -14px 0 0 -2px, -38px 0 0 -2px; }
25% { box-shadow: 14px 0 0 -2px, 38px 0 0 -2px, -14px 0 0 -2px, -38px 0 0 2px; }
50% { box-shadow: 14px 0 0 -2px, 38px 0 0 -2px, -14px 0 0 2px, -38px 0 0 -2px; }
75% { box-shadow: 14px 0 0 2px, 38px 0 0 -2px, -14px 0 0 -2px, -38px 0 0 -2px; }
100% { box-shadow: 14px 0 0 -2px, 38px 0 0 2px, -14px 0 0 -2px, -38px 0 0 -2px; }
}
</style>
</head>
<body class="bg-gray-900 p-4 text-white font-sans" hx-ext="sse" sse-connect="/sse/{{.Name}}">
{{template "old/views/partials/menu"}}
<div class="chat-container bg-gray-800 shadow-lg rounded-lg" >
<!-- Chat Header -->
<div class="border-b border-gray-700 p-4">
<h1 class="text-lg font-semibold">Talk to '{{.Name}}'</h1>
</div>
<!-- Chat Messages -->
<div class="chat-messages p-4">
<!-- Client Box -->
<div class="bg-gray-700 p-4">
<h2 class="text-sm font-semibold">Clients:</h2>
<div id="clients" class="text-sm text-gray-300">
<!-- Status updates dynamically here -->
<div sse-swap="clients"></div>
</div>
</div>
<!-- HUD Box -->
<div class="bg-gray-700 p-4">
<h2 class="text-sm font-semibold">Status:</h2>
<div id="hud" class="text-sm text-gray-300">
<!-- Status updates dynamically here -->
<div sse-swap="hud"></div>
</div>
</div>
<div sse-swap="messages" hx-swap="beforeend" id="messages" hx-on:htmx:after-settle="document.getElementById('messages').scrollIntoView(false)"></div>
</div>
<!-- Agent Status Box -->
<div class="bg-gray-700 p-4">
<h2 class="text-sm font-semibold">Agent:</h2>
<div id="agentStatus" class="text-sm text-gray-300">
<!-- Status updates dynamically here -->
<div sse-swap="status" ></div>
</div>
</div>
<!-- Message Input -->
<div class="p-4 border-t border-gray-700">
<div sse-swap="message_status"></div>
<input id="inputMessage" name="message" type="text" hx-post="/old/chat/{{.Name}}" hx-target="#results" hx-indicator=".htmx-indicator"
class="p-2 border rounded w-full bg-gray-600 text-white placeholder-gray-300" placeholder="Type a message..." _="on htmx:afterRequest set my value to ''">
<div class="my-2 htmx-indicator" ></div>
<div id="results" class="flex justify-center"></div>
</div>
</div>
</body>
</html>
-154
View File
@@ -1,154 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Create New Agent</title>
{{template "old/views/partials/header"}}
<script src="/old/public/js/wizard.js"></script>
<link rel="stylesheet" href="/old/public/css/wizard.css">
<script src="/old/public/js/connector-templates.js"></script>
<script src="/old/public/js/agent-form.js"></script>
</head>
<body>
{{template "old/views/partials/menu"}}
<div class="container">
<div class="section-box">
<h1>Create New Agent</h1>
<form id="create-agent-form" action="/api/agent/create" method="POST">
{{template "old/views/partials/agent-form" . }}
<button type="submit" id="create-button" data-original-text="<i class='fas fa-robot'></i> Create Agent">
<i class="fas fa-robot"></i> Create Agent
</button>
</form>
</div>
<!-- Response Messages Container -->
<div id="response-container">
<!-- Alert messages will be shown here -->
<div id="success-alert" class="alert alert-success" style="display: none;">
Agent created successfully! Redirecting to agent list...
</div>
<div id="error-alert" class="alert alert-error" style="display: none;">
<span id="error-message">Error creating agent.</span>
</div>
</div>
</div>
<!-- Toast notification container -->
<div id="toast" class="toast">
<span id="toast-message"></span>
</div>
<script>
const actions = `{{ range .Actions }}<option value="{{.}}">{{.}}</option>{{ end }}`;
const connectors = `{{ range .Connectors }}<option value="{{.}}">{{.}}</option>{{ end }}`;
const promptBlocks = `{{ range .PromptBlocks }}<option value="{{.}}">{{.}}</option>{{ end }}`;
document.addEventListener('DOMContentLoaded', function() {
// Initialize common form components
initAgentFormCommon({
actions: actions,
connectors: connectors,
promptBlocks: promptBlocks
});
// Form submission handling
const form = document.getElementById('create-agent-form');
form.addEventListener('submit', function(e) {
e.preventDefault();
// Show loading state
const createButton = document.getElementById('create-button');
const originalButtonText = createButton.innerHTML;
createButton.setAttribute('data-original-text', originalButtonText);
createButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Creating...';
createButton.disabled = true;
// Build a structured data object
const formData = new FormData(form);
const jsonData = AgentFormUtils.processFormData(formData);
// Process special fields
jsonData.connectors = AgentFormUtils.processConnectors(createButton);
if (jsonData.connectors === null) return; // Validation failed
jsonData.mcp_servers = AgentFormUtils.processMCPServers();
jsonData.actions = AgentFormUtils.processActions(createButton);
if (jsonData.actions === null) return; // Validation failed
jsonData.promptblocks = AgentFormUtils.processPromptBlocks(createButton);
if (jsonData.promptblocks === null) return; // Validation failed
// Send the structured data as JSON
fetch('/api/agent/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(jsonData)
})
.then(response => response.json())
.then(data => {
const successAlert = document.getElementById('success-alert');
const errorAlert = document.getElementById('error-alert');
const errorMessage = document.getElementById('error-message');
// Hide both alerts initially
successAlert.style.display = 'none';
errorAlert.style.display = 'none';
if (data.status === "ok") {
// Show success toast
showToast('Agent created successfully!', 'success');
// Show success message
successAlert.style.display = 'block';
// Redirect to agent list page after a delay
setTimeout(() => {
window.location.href = '/old/agents';
}, 2000);
} else if (data.error) {
// Show error toast
showToast('Error: ' + data.error, 'error');
// Show error message
errorMessage.textContent = data.error;
errorAlert.style.display = 'block';
// Restore button state
createButton.innerHTML = originalButtonText;
createButton.disabled = false;
} else {
// Handle unexpected response format
showToast('Unexpected response format', 'error');
errorMessage.textContent = "Unexpected response format";
errorAlert.style.display = 'block';
// Restore button state
createButton.innerHTML = originalButtonText;
createButton.disabled = false;
}
})
.catch(error => {
// Handle network or other errors
showToast('Network error: ' + error.message, 'error');
const errorAlert = document.getElementById('error-alert');
const errorMessage = document.getElementById('error-message');
errorMessage.textContent = "Network error: " + error.message;
errorAlert.style.display = 'block';
// Restore button state
createButton.innerHTML = originalButtonText;
createButton.disabled = false;
});
});
});
</script>
</body>
</html>
-600
View File
@@ -1,600 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Create Agent Group</title>
{{template "old/views/partials/header"}}
<script src="/old/public/js/wizard.js"></script>
<link rel="stylesheet" href="/old/public/css/wizard.css">
<script src="/old/public/js/connector-templates.js"></script>
<script src="/old/public/js/agent-form.js"></script>
<style>
.agent-profile {
border: 1px solid var(--medium-bg);
border-radius: 8px;
padding: 15px;
margin-bottom: 20px;
background-color: var(--lighter-bg);
position: relative;
transition: all 0.3s ease;
}
.agent-profile:hover {
transform: translateY(-3px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
}
.agent-profile h3 {
color: var(--primary);
text-shadow: var(--neon-glow);
margin-top: 0;
margin-bottom: 15px;
border-bottom: 1px solid var(--medium-bg);
padding-bottom: 10px;
}
.agent-profile .description {
color: var(--text);
font-size: 0.9rem;
margin-bottom: 15px;
}
.agent-profile .system-prompt {
background-color: var(--darker-bg);
border-radius: 6px;
padding: 10px;
font-size: 0.85rem;
max-height: 150px;
overflow-y: auto;
margin-bottom: 10px;
white-space: pre-wrap;
}
.agent-profile.selected {
border: 2px solid var(--primary);
background-color: rgba(var(--primary-rgb), 0.1);
}
.agent-profile .select-checkbox {
position: absolute;
top: 10px;
right: 10px;
}
.page-section {
display: none;
animation: fadeIn 0.5s;
}
.page-section.active {
display: block;
}
.progress-container {
display: flex;
justify-content: center;
margin-bottom: 30px;
}
.progress-step {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
padding: 0 20px;
}
.progress-step:not(:last-child)::after {
content: '';
position: absolute;
top: 12px;
right: -30px;
width: 60px;
height: 3px;
background-color: var(--medium-bg);
}
.progress-step.active:not(:last-child)::after {
background-color: var(--primary);
}
.step-circle {
width: 28px;
height: 28px;
border-radius: 50%;
background-color: var(--medium-bg);
display: flex;
justify-content: center;
align-items: center;
color: var(--text);
margin-bottom: 8px;
transition: all 0.3s ease;
}
.progress-step.active .step-circle {
background-color: var(--primary);
box-shadow: 0 0 10px var(--primary);
}
.step-label {
font-size: 0.9rem;
color: var(--muted-text);
transition: all 0.3s ease;
}
.progress-step.active .step-label {
color: var(--primary);
font-weight: bold;
}
.prompt-container {
margin-bottom: 30px;
}
.prompt-container textarea {
width: 100%;
min-height: 120px;
padding: 15px;
border-radius: 6px;
background-color: var(--lighter-bg);
border: 1px solid var(--medium-bg);
color: var(--text);
font-size: 1rem;
resize: vertical;
}
.action-buttons {
display: flex;
justify-content: space-between;
margin-top: 30px;
}
.select-all-container {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.loader {
display: none;
text-align: center;
margin: 40px 0;
}
.loader i {
color: var(--primary);
font-size: 2rem;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* Make form elements auto in group mode */
#wizard-container .form-section {
height: auto;
overflow: visible;
}
.info-message {
background-color: rgba(var(--primary-rgb), 0.1);
border-left: 4px solid var(--primary);
padding: 15px;
margin: 20px 0;
border-radius: 0 8px 8px 0;
display: flex;
align-items: center;
}
.info-message i {
font-size: 1.5rem;
color: var(--primary);
margin-right: 15px;
}
.info-message-content {
flex: 1;
}
.info-message-content h4 {
margin-top: 0;
margin-bottom: 5px;
color: var(--primary);
}
.info-message-content p {
margin-bottom: 0;
}
</style>
</head>
<body>
{{template "old/views/partials/menu"}}
<div class="container">
<div class="section-box">
<h1>Create Agent Group</h1>
<!-- Progress Bar -->
<div class="progress-container">
<div class="progress-step active" data-step="1">
<div class="step-circle">1</div>
<div class="step-label">Generate Profiles</div>
</div>
<div class="progress-step" data-step="2">
<div class="step-circle">2</div>
<div class="step-label">Review & Select</div>
</div>
<div class="progress-step" data-step="3">
<div class="step-circle">3</div>
<div class="step-label">Configure Settings</div>
</div>
</div>
<!-- Step 1: Generate Profiles -->
<div id="step1" class="page-section active">
<h2>Generate Agent Profiles</h2>
<p>Describe the group of agents you want to create. Be specific about their roles, relationships, and purpose.</p>
<div class="prompt-container">
<textarea id="group-description" placeholder="Example: Create a team of agents for a software development project including a project manager, developer, tester, and designer. They should collaborate to build web applications."></textarea>
</div>
<div class="action-buttons">
<button type="button" id="generate-profiles-btn" class="action-btn">
<i class="fas fa-magic"></i> Generate Profiles
</button>
</div>
</div>
<!-- Loader -->
<div id="loader" class="loader">
<i class="fas fa-spinner fa-spin"></i>
<p>Generating agent profiles...</p>
</div>
<!-- Step 2: Review & Select Profiles -->
<div id="step2" class="page-section">
<h2>Review & Select Agent Profiles</h2>
<p>Select the agents you want to create. You can customize their details before creation.</p>
<div class="select-all-container">
<label for="select-all" class="checkbox-label">
<span class="checkbox-custom">
<input type="checkbox" id="select-all">
<span class="checkmark"></span>
</span>
Select All
</label>
</div>
<div id="agent-profiles-container">
<!-- Agent profiles will be generated here -->
</div>
<div class="action-buttons">
<button type="button" id="back-to-step1-btn" class="nav-btn">
<i class="fas fa-arrow-left"></i> Back
</button>
<button type="button" id="to-step3-btn" class="action-btn">
Continue <i class="fas fa-arrow-right"></i>
</button>
</div>
</div>
<!-- Step 3: Common Settings -->
<div id="step3" class="page-section">
<h2>Configure Common Settings</h2>
<p>Configure common settings for all selected agents. These settings will be applied to each agent.</p>
<form id="group-settings-form">
<!-- Informative message about profile data -->
<div class="info-message">
<i class="fas fa-info-circle"></i>
<div class="info-message-content">
<h4>Basic Information from Profiles</h4>
<p>The name, description, and system prompt for each agent will be taken from the profiles you selected in the previous step.</p>
</div>
</div>
<!-- Use the existing agent-form partial -->
<div id="group-agent-form">
{{template "old/views/partials/agent-form" . }}
</div>
</form>
<div class="action-buttons">
<button type="button" id="back-to-step2-btn" class="nav-btn">
<i class="fas fa-arrow-left"></i> Back
</button>
<button type="button" id="create-group-btn" class="action-btn" data-original-text="<i class='fas fa-users'></i> Create Agent Group">
<i class="fas fa-users"></i> Create Agent Group
</button>
</div>
</div>
</div>
<!-- Response Messages Container -->
<div id="response-container">
<!-- Alert messages will be shown here -->
<div id="success-alert" class="alert alert-success" style="display: none;">
Agents created successfully! Redirecting to agent list...
</div>
<div id="error-alert" class="alert alert-error" style="display: none;">
<span id="error-message">Error creating agents.</span>
</div>
</div>
</div>
<!-- Toast notification container -->
<div id="toast" class="toast">
<span id="toast-message"></span>
</div>
<script>
const actions = `{{ range .Actions }}<option value="{{.}}">{{.}}</option>{{ end }}`;
const connectors = `{{ range .Connectors }}<option value="{{.}}">{{.}}</option>{{ end }}`;
const promptBlocks = `{{ range .PromptBlocks }}<option value="{{.}}">{{.}}</option>{{ end }}`;
// Store generated agent profiles
let agentProfiles = [];
document.addEventListener('DOMContentLoaded', function() {
// Initialize the form components
initAgentFormCommon({
actions: actions,
connectors: connectors,
promptBlocks: promptBlocks
});
// Hide the Basic Information section
const basicSection = document.getElementById('basic-section');
if (basicSection) {
basicSection.style.display = 'none';
}
// Update the wizard navigation items to skip Basic Information
const basicNavItem = document.querySelector('.wizard-nav-item[data-target="basic-section"]');
if (basicNavItem) {
basicNavItem.style.display = 'none';
}
// Make sure Connectors section is active by default
const connectorsSection = document.getElementById('connectors-section');
if (connectorsSection) {
document.querySelectorAll('.form-section').forEach(section => {
section.classList.remove('active');
});
connectorsSection.classList.add('active');
}
// Update the active nav item
const connectorsNavItem = document.querySelector('.wizard-nav-item[data-target="connectors-section"]');
if (connectorsNavItem) {
document.querySelectorAll('.wizard-nav-item').forEach(item => {
item.classList.remove('active');
});
connectorsNavItem.classList.add('active');
}
// Update the current step label
const currentStepLabel = document.getElementById('currentStepLabel');
if (currentStepLabel) {
currentStepLabel.textContent = 'Connectors';
}
// Navigation between steps
const goToStep = (stepNumber) => {
// Hide all steps
document.querySelectorAll('.page-section').forEach(section => {
section.classList.remove('active');
});
// Show the target step
document.getElementById(`step${stepNumber}`).classList.add('active');
// Update progress bar
document.querySelectorAll('.progress-step').forEach(step => {
step.classList.remove('active');
if (parseInt(step.dataset.step) <= stepNumber) {
step.classList.add('active');
}
});
};
// Step 1: Generate Profiles
document.getElementById('generate-profiles-btn').addEventListener('click', function() {
const description = document.getElementById('group-description').value.trim();
if (!description) {
showToast('Please enter a description for your agent group', 'error');
return;
}
// Show loader
document.getElementById('loader').style.display = 'block';
document.getElementById('step1').style.display = 'none';
// Send request to generate profiles
fetch('/api/agent/group/generateProfiles', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ description: description })
})
.then(response => response.json())
.then(data => {
// Hide loader
document.getElementById('loader').style.display = 'none';
agentProfiles = Array.isArray(data) ? data : [];
if (agentProfiles.length === 0) {
showToast('No agent profiles were generated. Please try again with a more detailed description.', 'error');
document.getElementById('step1').style.display = 'block';
return;
}
// Render agent profiles
renderAgentProfiles();
// Go to step 2
goToStep(2);
})
.catch(error => {
document.getElementById('loader').style.display = 'none';
document.getElementById('step1').style.display = 'block';
showToast('Error generating profiles: ' + error.message, 'error');
console.error('Error:', error);
});
});
// Render agent profiles in step 2
function renderAgentProfiles() {
const container = document.getElementById('agent-profiles-container');
container.innerHTML = '';
agentProfiles.forEach((profile, index) => {
const profileElement = document.createElement('div');
profileElement.className = 'agent-profile';
profileElement.dataset.index = index;
profileElement.innerHTML = `
<label class="select-checkbox checkbox-label">
<span class="checkbox-custom">
<input type="checkbox" class="profile-checkbox" checked>
<span class="checkmark"></span>
</span>
</label>
<h3>${profile.name}</h3>
<div class="description">${profile.description}</div>
<div class="system-prompt">${profile.system_prompt}</div>
`;
profileElement.querySelector('.profile-checkbox').addEventListener('change', function() {
profileElement.classList.toggle('selected', this.checked);
updateSelectAllCheckbox();
});
// Initially set as selected
profileElement.classList.add('selected');
container.appendChild(profileElement);
});
}
// Select all checkbox functionality
document.getElementById('select-all').addEventListener('change', function() {
const isChecked = this.checked;
document.querySelectorAll('.profile-checkbox').forEach(checkbox => {
checkbox.checked = isChecked;
checkbox.closest('.agent-profile').classList.toggle('selected', isChecked);
});
});
function updateSelectAllCheckbox() {
const checkboxes = document.querySelectorAll('.profile-checkbox');
const selectAllCheckbox = document.getElementById('select-all');
const allChecked = Array.from(checkboxes).every(checkbox => checkbox.checked);
const someChecked = Array.from(checkboxes).some(checkbox => checkbox.checked);
selectAllCheckbox.checked = allChecked;
selectAllCheckbox.indeterminate = !allChecked && someChecked;
}
// Navigation buttons
document.getElementById('back-to-step1-btn').addEventListener('click', () => goToStep(1));
document.getElementById('to-step3-btn').addEventListener('click', () => {
// Check if at least one profile is selected
const selectedProfiles = document.querySelectorAll('.profile-checkbox:checked');
if (selectedProfiles.length === 0) {
showToast('Please select at least one agent profile', 'error');
return;
}
goToStep(3);
});
document.getElementById('back-to-step2-btn').addEventListener('click', () => goToStep(2));
// Create group button
document.getElementById('create-group-btn').addEventListener('click', function() {
// Get selected profiles
const selectedProfileIndices = Array.from(document.querySelectorAll('.profile-checkbox:checked'))
.map(checkbox => parseInt(checkbox.closest('.agent-profile').dataset.index));
if (selectedProfileIndices.length === 0) {
showToast('Please select at least one agent profile', 'error');
return;
}
const selectedProfiles = selectedProfileIndices.map(index => agentProfiles[index]);
// Process form data for common settings
const formData = new FormData(document.getElementById('group-settings-form'));
const commonSettings = AgentFormUtils.processFormData(formData);
// Process special fields
commonSettings.connectors = AgentFormUtils.processConnectors(this);
if (commonSettings.connectors === null) return; // Validation failed
commonSettings.mcp_servers = AgentFormUtils.processMCPServers();
commonSettings.actions = AgentFormUtils.processActions(this);
if (commonSettings.actions === null) return; // Validation failed
commonSettings.promptblocks = AgentFormUtils.processPromptBlocks(this);
if (commonSettings.promptblocks === null) return; // Validation failed
// Show loading state
const createButton = document.getElementById('create-group-btn');
const originalButtonText = createButton.innerHTML;
createButton.setAttribute('data-original-text', originalButtonText);
createButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Creating...';
createButton.disabled = true;
// Create payload
const payload = {
agents: selectedProfiles,
agent_config: commonSettings
};
// Send request to create agents
fetch('/api/agent/group/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
})
.then(response => response.json())
.then(data => {
const successAlert = document.getElementById('success-alert');
const errorAlert = document.getElementById('error-alert');
const errorMessage = document.getElementById('error-message');
// Hide both alerts initially
successAlert.style.display = 'none';
errorAlert.style.display = 'none';
if (data.status === "ok") {
// Show success toast
showToast(`${selectedProfiles.length} agent(s) created successfully!`, 'success');
// Show success message
successAlert.style.display = 'block';
// Redirect to agent list page after a delay
setTimeout(() => {
window.location.href = '/old/agents';
}, 2000);
} else if (data.error) {
// Show error toast
showToast('Error: ' + data.error, 'error');
// Show error message
errorMessage.textContent = data.error;
errorAlert.style.display = 'block';
// Restore button state
createButton.innerHTML = originalButtonText;
createButton.disabled = false;
} else {
// Handle unexpected response format
showToast('Unexpected response format', 'error');
errorMessage.textContent = "Unexpected response format";
errorAlert.style.display = 'block';
// Restore button state
createButton.innerHTML = originalButtonText;
createButton.disabled = false;
}
})
.catch(error => {
// Handle network or other errors
showToast('Network error: ' + error.message, 'error');
const errorAlert = document.getElementById('error-alert');
const errorMessage = document.getElementById('error-message');
errorMessage.textContent = "Network error: " + error.message;
errorAlert.style.display = 'block';
// Restore button state
createButton.innerHTML = originalButtonText;
createButton.disabled = false;
});
});
});
// Toast notification function - assuming this exists in your global scope
</script>
</body>
</html>
-224
View File
@@ -1,224 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Smart Assistant Dashboard</title>
{{template "old/views/partials/header"}}
<style>
.image-container {
display: flex;
justify-content: center;
margin: 40px 0;
position: relative;
}
.image-container img {
filter: drop-shadow(0 0 15px rgba(94, 0, 255, 0.6));
transition: all 0.5s ease;
}
.image-container:hover img {
transform: scale(1.05);
filter: drop-shadow(0 0 25px rgba(0, 255, 149, 0.8));
}
.image-container::after {
content: "";
position: absolute;
bottom: -15px;
left: 50%;
transform: translateX(-50%);
width: 40%;
height: 2px;
background: linear-gradient(90deg, transparent, var(--primary), var(--secondary), var(--tertiary), transparent);
}
.dashboard-title {
text-align: center;
margin-bottom: 40px;
font-size: 2.5rem;
letter-spacing: 2px;
position: relative;
display: inline-block;
left: 50%;
transform: translateX(-50%);
color: var(--primary);
text-shadow: var(--neon-glow);
animation: gentlePulse 3s infinite;
}
/* Gentle pulse animation for the title */
@keyframes gentlePulse {
0% { text-shadow: 0 0 7px var(--primary), 0 0 10px var(--primary); }
50% { text-shadow: 0 0 12px var(--primary), 0 0 20px var(--primary); }
100% { text-shadow: 0 0 7px var(--primary), 0 0 10px var(--primary); }
}
/* Subtle glitch effect for hover */
@keyframes subtleGlitch {
0% { transform: translateX(-50%); }
20% { transform: translateX(-50%) translate(-1px, 1px); }
40% { transform: translateX(-50%) translate(-1px, -1px); }
60% { transform: translateX(-50%) translate(1px, 1px); }
80% { transform: translateX(-50%) translate(1px, -1px); }
100% { transform: translateX(-50%); }
}
.cards-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 30px;
margin-top: 30px;
}
.user-info {
position: absolute;
top: 20px;
right: 20px;
}
.dashboard-stats {
display: flex;
justify-content: center;
gap: 20px;
margin: 20px 0 40px;
}
.stat-item {
background: rgba(17, 17, 17, 0.7);
border-radius: 8px;
padding: 15px;
min-width: 150px;
text-align: center;
position: relative;
overflow: hidden;
border: 1px solid rgba(94, 0, 255, 0.2);
}
.stat-item::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 2px;
background: linear-gradient(90deg, var(--primary), var(--secondary));
opacity: 0.7;
}
.stat-count {
font-size: 2rem;
font-weight: 700;
color: var(--primary);
text-shadow: var(--neon-glow);
}
.stat-label {
font-size: 0.9rem;
color: #cccccc;
text-transform: uppercase;
letter-spacing: 1px;
}
/* Badge positioning */
.card {
position: relative;
}
.badge {
position: absolute;
top: 15px;
right: 15px;
}
</style>
</head>
<body>
{{template "old/views/partials/menu"}}
<div class="container">
<div class="image-container">
<img src="/old/public/logo_1.png" width="250" alt="Company Logo">
</div>
<h1 class="dashboard-title">LocalAGI</h1>
<!-- Simple stats display -->
<div class="dashboard-stats">
<div class="stat-item">
<div class="stat-count">{{.Actions}}</div>
<div class="stat-label">Available Actions</div>
</div>
<div class="stat-item">
<div class="stat-count">{{.Connectors}}</div>
<div class="stat-label">Available Connectors</div>
</div>
<div class="stat-item">
<div class="stat-count">{{ .AgentCount }}</div>
<div class="stat-label">Agents</div>
</div>
</div>
<div class="cards-container">
<!-- Card for Agent List Page -->
<a href="/old/agents" class="card-link">
<div class="card">
<h2><i class="fas fa-robot"></i> Agent List</h2>
<p>View and manage your list of agents, including detailed profiles and statistics.</p>
</div>
</a>
<!-- Card for Create Agent -->
<a href="/old/create" class="card-link">
<div class="card">
<h2><i class="fas fa-plus-circle"></i> Create Agent</h2>
<p>Create a new intelligent agent with custom behaviors, connectors, and actions.</p>
</div>
</a>
<!-- Additional Cards for Future Features -->
<a href="#" class="card-link">
<div class="card">
<h2><i class="fas fa-chart-line"></i> Analytics</h2>
<p>View performance metrics and insights from your agent operations.</p>
<span class="badge badge-secondary">Coming Soon</span>
</div>
</a>
<a href="#" class="card-link">
<div class="card">
<h2><i class="fas fa-cogs"></i> Settings</h2>
<p>Configure system preferences and global settings for all agents.</p>
<span class="badge badge-secondary">Coming Soon</span>
</div>
</a>
</div>
</div>
<!-- Toast notification container -->
<div id="toast" class="toast">
<span id="toast-message"></span>
</div>
<script>
// Add the controlled glitch effect to dashboard title
document.addEventListener('DOMContentLoaded', function() {
const title = document.querySelector('.dashboard-title');
title.addEventListener('mouseover', function() {
// Use the more subtle glitch animation
this.style.animation = 'subtleGlitch 0.5s infinite';
});
title.addEventListener('mouseout', function() {
// Return to gentle pulse animation
this.style.animation = 'gentlePulse 3s infinite';
});
// Welcome toast notification
setTimeout(() => {
showToast('Welcome to Smart Assistant Dashboard', 'success');
}, 1000);
});
</script>
</body>
</html>
-215
View File
@@ -1,215 +0,0 @@
<!DOCTYPE html>
<html lang="en">
{{template "old/views/partials/header" .}}
<body class="bg-gradient-to-br from-gray-900 to-gray-950 text-gray-200">
<div class="flex flex-col min-h-screen">
{{template "old/views/partials/menu" .}}
<div class="container mx-auto px-4 py-8 flex-grow flex items-center justify-center">
<!-- Auth Card -->
<div class="max-w-md w-full bg-gray-800/90 border border-gray-700/50 rounded-xl overflow-hidden shadow-xl">
<div class="animation-container">
<div class="text-overlay">
<!-- <i class="fas fa-circle-nodes text-5xl text-blue-400 mb-2"></i> -->
</div>
</div>
<div class="p-8">
<div class="text-center mb-6">
<h2 class="text-2xl font-bold text-white">
<span class="bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-indigo-400">
Authorization Required
</span>
</h2>
<p class="text-gray-400 mt-2">Please enter your access token to continue</p>
</div>
<form id="login-form" class="space-y-6" onsubmit="login(); return false;">
<div>
<label for="token" class="block text-sm font-medium text-gray-300 mb-2">Access Token</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-key text-gray-500"></i>
</div>
<input
type="password"
id="token"
name="token"
placeholder="Enter your token"
class="bg-gray-700/50 border border-gray-600 text-white placeholder-gray-400 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full pl-10 p-2.5"
required
/>
</div>
</div>
<div>
<button
type="submit"
class="group w-full flex items-center justify-center bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white py-3 px-6 rounded-lg transition duration-300 ease-in-out transform hover:scale-[1.02] hover:shadow-lg font-medium"
>
<i class="fas fa-sign-in-alt mr-2"></i>
<span>Login</span>
<i class="fas fa-arrow-right opacity-0 group-hover:opacity-100 group-hover:translate-x-2 ml-2 transition-all duration-300"></i>
</button>
</div>
</form>
<div class="mt-8 pt-6 border-t border-gray-700/50 text-center text-sm text-gray-400">
<div class="flex items-center justify-center mb-2">
<i class="fas fa-shield-alt mr-2 text-blue-400"></i>
<span>Instance is token protected</span>
</div>
<p>Current time (UTC): <span id="current-time">{{.CurrentDate}}</span></p>
</div>
</div>
</div>
</div>
</div>
<script>
function login() {
const token = document.getElementById('token').value;
if (!token.trim()) {
// Show error with fading effect
const form = document.getElementById('login-form');
const errorMsg = document.createElement('div');
errorMsg.className = 'p-3 mt-4 bg-red-900/50 text-red-200 rounded-lg border border-red-700/50 text-sm flex items-center';
errorMsg.innerHTML = '<i class="fas fa-exclamation-circle mr-2"></i> Please enter a valid token';
// Remove any existing error message
const existingError = form.querySelector('.bg-red-900/50');
if (existingError) form.removeChild(existingError);
// Add new error message with animation
form.appendChild(errorMsg);
setTimeout(() => {
errorMsg.style.opacity = '0';
errorMsg.style.transition = 'opacity 0.5s ease';
setTimeout(() => errorMsg.remove(), 500);
}, 3000);
return;
}
var date = new Date();
date.setTime(date.getTime() + (24*60*60*1000));
document.cookie = `token=${token}; expires=${date.toGMTString()}; path=/`;
// Show loading state
const button = document.querySelector('button[type="submit"]');
const originalContent = button.innerHTML;
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i> Authenticating...';
button.classList.add('bg-gray-600');
// Reload after short delay to show loading state
setTimeout(() => {
window.location.reload();
}, 800);
}
// Update current time
function updateCurrentTime() {
const timeElement = document.getElementById('current-time');
if (timeElement) {
const now = new Date();
const year = now.getUTCFullYear();
const month = String(now.getUTCMonth() + 1).padStart(2, '0');
const day = String(now.getUTCDate()).padStart(2, '0');
const hours = String(now.getUTCHours()).padStart(2, '0');
const minutes = String(now.getUTCMinutes()).padStart(2, '0');
const seconds = String(now.getUTCSeconds()).padStart(2, '0');
timeElement.textContent = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
}
// Initialize current time and update it every second
updateCurrentTime();
setInterval(updateCurrentTime, 1000);
// Add subtle particle animation to the background
document.addEventListener('DOMContentLoaded', function() {
const animContainer = document.querySelector('.animation-container');
if (animContainer) {
const canvas = document.createElement('canvas');
animContainer.appendChild(canvas);
const ctx = canvas.getContext('2d');
canvas.width = animContainer.offsetWidth;
canvas.height = animContainer.offsetHeight;
// Create particles
const particles = [];
const particleCount = 30;
for (let i = 0; i < particleCount; i++) {
particles.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
radius: Math.random() * 3 + 1,
color: `rgba(${Math.random() * 50 + 50}, ${Math.random() * 100 + 100}, ${Math.random() * 155 + 100}, ${Math.random() * 0.4 + 0.1})`,
speedX: Math.random() * 0.5 - 0.25,
speedY: Math.random() * 0.5 - 0.25
});
}
// Animation loop
function animate() {
requestAnimationFrame(animate);
ctx.clearRect(0, 0, canvas.width, canvas.height);
particles.forEach(particle => {
particle.x += particle.speedX;
particle.y += particle.speedY;
// Bounce off edges
if (particle.x < 0 || particle.x > canvas.width) {
particle.speedX = -particle.speedX;
}
if (particle.y < 0 || particle.y > canvas.height) {
particle.speedY = -particle.speedY;
}
// Draw particle
ctx.beginPath();
ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2);
ctx.fillStyle = particle.color;
ctx.fill();
});
// Connect nearby particles with lines
for (let i = 0; i < particles.length; i++) {
for (let j = i + 1; j < particles.length; j++) {
const dx = particles[i].x - particles[j].x;
const dy = particles[i].y - particles[j].y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 100) {
ctx.beginPath();
ctx.moveTo(particles[i].x, particles[i].y);
ctx.lineTo(particles[j].x, particles[j].y);
ctx.strokeStyle = `rgba(100, 150, 255, ${0.1 * (1 - distance / 100)})`;
ctx.lineWidth = 1;
ctx.stroke();
}
}
}
}
// Start animation
animate();
// Resize handling
window.addEventListener('resize', () => {
canvas.width = animContainer.offsetWidth;
canvas.height = animContainer.offsetHeight;
});
}
});
</script>
</body>
</html>
-307
View File
@@ -1,307 +0,0 @@
<div class="agent-form-container">
<!-- Wizard Sidebar -->
<div class="wizard-sidebar">
<ul class="wizard-nav">
<li class="wizard-nav-item active" data-target="basic-section">
<i class="fas fa-info-circle"></i> Basic Information
</li>
<li class="wizard-nav-item" data-target="connectors-section">
<i class="fas fa-plug"></i> Connectors
</li>
<li class="wizard-nav-item" data-target="actions-section">
<i class="fas fa-bolt"></i> Actions
</li>
<li class="wizard-nav-item" data-target="mcp-section">
<i class="fas fa-server"></i> MCP Servers
</li>
<li class="wizard-nav-item" data-target="memory-section">
<i class="fas fa-memory"></i> Memory Settings
</li>
<li class="wizard-nav-item" data-target="model-section">
<i class="fas fa-robot"></i> Model Settings
</li>
<li class="wizard-nav-item" data-target="prompts-section">
<i class="fas fa-comment-alt"></i> Prompts & Goals
</li>
<li class="wizard-nav-item" data-target="advanced-section">
<i class="fas fa-cogs"></i> Advanced Settings
</li>
</ul>
</div>
<!-- Form Content Area -->
<div class="form-content-area">
<!-- Basic Information Section -->
<div class="form-section active" id="basic-section">
<h3 class="section-title">Basic Information</h3>
<div class="mb-4">
<label for="name">Name</label>
{{ if .Name }}
<input type="text" name="name" id="name" placeholder="Name" value="{{.Name}}" readonly >
{{ else }}
<input type="text" name="name" id="name" placeholder="Name">
{{ end }}
</div>
<div class="mb-4">
<label for="description">Description</label>
<textarea name="description" id="description" placeholder="Description"></textarea>
</div>
<div class="mb-4">
<label for="identity_guidance">Identity Guidance</label>
<textarea name="identity_guidance" id="identity_guidance" placeholder="Identity Guidance"></textarea>
</div>
<div class="mb-4">
<label for="random_identity" class="checkbox-label">
<span class="checkbox-custom">
<input type="checkbox" name="random_identity" id="random_identity">
<span class="checkmark"></span>
</span>
Random Identity
</label>
</div>
<div class="mb-4">
<label for="hud" class="checkbox-label">
<span class="checkbox-custom">
<input type="checkbox" name="hud" id="hud">
<span class="checkmark"></span>
</span>
HUD
</label>
</div>
</div>
<!-- Connectors Section -->
<div class="form-section" id="connectors-section">
<h3 class="section-title">Connectors</h3>
<div id="connectorsSection">
<!-- Connectors will be added here dynamically -->
</div>
<div class="button-container">
<button type="button" id="addConnectorButton" class="action-btn">
<i class="fas fa-plus-circle"></i> Add Connector
</button>
</div>
</div>
<!-- Actions Section -->
<div class="form-section" id="actions-section">
<h3 class="section-title">Actions</h3>
<div class="mb-4" id="action_box">
<!-- Actions will be added here dynamically -->
</div>
<div class="button-container">
<button id="action_button" type="button" class="action-btn">
<i class="fas fa-plus-circle"></i> Add Action
</button>
</div>
</div>
<!-- MCP Servers Section -->
<div class="form-section" id="mcp-section">
<h3 class="section-title">MCP Servers</h3>
<div id="mcpSection">
<!-- MCP servers will be added here dynamically -->
</div>
<div class="button-container">
<button type="button" id="addMCPButton" class="action-btn">
<i class="fas fa-plus-circle"></i> Add MCP Server
</button>
</div>
</div>
<!-- Memory Settings Section -->
<div class="form-section" id="memory-section">
<h3 class="section-title">Memory Settings</h3>
<div class="mb-4">
<label for="enable_kb" class="checkbox-label">
<span class="checkbox-custom">
<input type="checkbox" name="enable_kb" id="enable_kb">
<span class="checkmark"></span>
</span>
Enable Knowledge Base
</label>
</div>
<div class="mb-4">
<label for="kb_results">Knowledge Base Results</label>
<input type="number" name="kb_results" id="kb_results" placeholder="3">
</div>
<div class="mb-4">
<label for="long_term_memory" class="checkbox-label">
<span class="checkbox-custom">
<input type="checkbox" name="long_term_memory" id="long_term_memory">
<span class="checkmark"></span>
</span>
Long Term Memory
</label>
</div>
<div class="mb-4">
<label for="summary_long_term_memory" class="checkbox-label">
<span class="checkbox-custom">
<input type="checkbox" name="summary_long_term_memory" id="summary_long_term_memory">
<span class="checkmark"></span>
</span>
Long Term Memory (Summarize!)
</label>
</div>
</div>
<!-- Model Settings Section -->
<div class="form-section" id="model-section">
<h3 class="section-title">Model Settings</h3>
<div class="mb-4">
<label for="model">Model </label>
<input type="text" name="model" id="model" placeholder="Model name">
</div>
<div class="mb-4">
<label for="multimodal_model">Multimodal Model </label>
<input type="text" name="multimodal_model" id="multimodal_model" placeholder="Model name">
</div>
<div class="mb-4">
<label for="api_url">API URL </label>
<input type="text" name="api_url" id="api_url" placeholder="API URL">
</div>
<div class="mb-4">
<label for="api_key">API Key </label>
<input type="text" name="api_key" id="api_key" placeholder="API Key">
</div>
<div class="mb-4">
<label for="local_rag_url">LocalRAG API URL </label>
<input type="text" name="local_rag_url" id="local_rag_url" placeholder="LocalRAG API URL">
</div>
<div class="mb-4">
<label for="local_rag_api_key">LocalRAG API Key </label>
<input type="text" name="local_rag_api_key" id="local_rag_api_key" placeholder="LocalRAG API Key">
</div>
<div class="mb-4">
<label for="enable_reasoning" class="checkbox-label">
<span class="checkbox-custom">
<input type="checkbox" name="enable_reasoning" id="enable_reasoning">
<span class="checkmark"></span>
</span>
Enable Reasoning
</label>
</div>
</div>
<!-- Prompts & Goals Section -->
<div class="form-section" id="prompts-section">
<h3 class="section-title">Prompts & Goals</h3>
<div class="mb-4" id="dynamic_box">
<!-- Dynamic prompts will be added here dynamically -->
</div>
<div class="button-container">
<button id="dynamic_button" type="button" class="action-btn">
<i class="fas fa-plus-circle"></i> Add Dynamic Prompt
</button>
</div>
<div class="mb-4">
<label for="system_prompt">System Prompt</label>
<textarea name="system_prompt" id="system_prompt" placeholder="System prompt"></textarea>
</div>
<div class="mb-4">
<label for="permanent_goal">Permanent Goal</label>
<textarea name="permanent_goal" id="permanent_goal" placeholder="Permanent goal"></textarea>
</div>
</div>
<!-- Advanced Settings Section -->
<div class="form-section" id="advanced-section">
<h3 class="section-title">Advanced Settings</h3>
<div class="mb-4">
<label for="standalone_job" class="checkbox-label">
<span class="checkbox-custom">
<input type="checkbox" name="standalone_job" id="standalone_job">
<span class="checkmark"></span>
</span>
Standalone Job
</label>
</div>
<div class="mb-4">
<label for="initiate_conversations" class="checkbox-label">
<span class="checkbox-custom">
<input type="checkbox" name="initiate_conversations" id="initiate_conversations">
<span class="checkmark"></span>
</span>
Initiate Conversations
</label>
</div>
<div class="mb-4">
<label for="enable_planning" class="checkbox-label">
<span class="checkbox-custom">
<input type="checkbox" name="enable_planning" id="enable_planning">
<span class="checkmark"></span>
</span>
Enable Planning
</label>
</div>
<div class="mb-4">
<label for="can_stop_itself" class="checkbox-label">
<span class="checkbox-custom">
<input type="checkbox" name="can_stop_itself" id="can_stop_itself">
<span class="checkmark"></span>
</span>
Can Stop Itself
</label>
</div>
<div class="mb-4">
<label for="periodic_runs">Periodic Runs</label>
<input type="text" name="periodic_runs" id="periodic_runs" placeholder="Periodic Runs">
</div>
</div>
</div>
</div>
<!-- Add navigation controls at the bottom -->
<div class="wizard-controls">
<div class="wizard-controls-left">
<button type="button" id="prevSection" class="nav-btn">
<i class="fas fa-arrow-left"></i> Previous
</button>
</div>
<div class="wizard-controls-center">
<div class="progress-dots" id="progressDots">
<!-- Dots will be added dynamically via JS -->
</div>
<div class="progress-indicator">
<span id="currentStepLabel">Basic Information</span>
</div>
</div>
<div class="wizard-controls-right">
<button type="button" id="nextSection" class="nav-btn">
Next <i class="fas fa-arrow-right"></i>
</button>
</div>
</div>
-10
View File
@@ -1,10 +0,0 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&family=Permanent+Marker&display=swap" rel="stylesheet">
<script src="https://unpkg.com/htmx.org"></script>
<script src="https://unpkg.com/htmx.org/dist/ext/sse.js"></script>
<script src="https://unpkg.com/hyperscript.org@0.9.12"></script>
<link rel="stylesheet" href="/old/public/css/styles.css">
<script src="/old/public/js/common.js"></script>
-105
View File
@@ -1,105 +0,0 @@
<nav class="relative z-10 w-full" style="background-color: var(--darker-bg); border-bottom: 1px solid var(--medium-bg);">
<div class="px-6 sm:px-8 lg:px-10">
<div class="flex justify-between h-16 items-center">
<div class="flex items-center">
<!-- Logo container -->
<div class="flex-shrink-0">
<!-- Logo with glow effect -->
<a href="/old" class="flex items-center group">
<div class="relative">
<img src="/old/public/logo_2.png" alt="Logo" class="h-10 w-auto mr-4 transition-transform duration-300 group-hover:scale-105"
style="filter: drop-shadow(0 0 5px var(--primary));">
<!-- Animated scan line on hover -->
<div class="absolute inset-0 overflow-hidden opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<div class="absolute inset-0 bg-gradient-to-b from-transparent via-var(--primary) to-transparent opacity-30"
style="height: 10px; animation: scanline 1.5s linear infinite;"></div>
</div>
</div>
<span class="text-xl font-bold transition-colors duration-300"
style="color: var(--primary); text-shadow: var(--neon-glow);">LocalAGI</span>
</a>
</div>
<div class="hidden md:block ml-10">
<div class="flex space-x-4">
<a href="/old" class="px-3 py-2 rounded-md text-lg font-medium text-white hover:bg-gray-800 transition duration-300 relative overflow-hidden group">
<i class="fas fa-home mr-2"></i> Home
<!-- Underline animation -->
<span class="absolute bottom-0 left-0 w-0 h-0.5 group-hover:w-full transition-all duration-300"
style="background: linear-gradient(90deg, var(--primary), var(--secondary));"></span>
</a>
<a href="/old/agents" class="px-3 py-2 rounded-md text-lg font-medium text-gray-400 hover:bg-gray-800 transition duration-300 relative overflow-hidden group">
<i class="fas fa-users mr-2"></i> Agent List
<!-- Underline animation -->
<span class="absolute bottom-0 left-0 w-0 h-0.5 group-hover:w-full transition-all duration-300"
style="background: linear-gradient(90deg, var(--secondary), var(--tertiary));"></span>
</a>
<a href="/old/actions-playground" class="px-3 py-2 rounded-md text-lg font-medium text-gray-400 hover:bg-gray-800 transition duration-300 relative overflow-hidden group">
<i class="fas fa-bolt mr-2"></i> Actions Playground
<!-- Underline animation -->
<span class="absolute bottom-0 left-0 w-0 h-0.5 group-hover:w-full transition-all duration-300"
style="background: linear-gradient(90deg, var(--tertiary), var(--primary));"></span>
</a>
<a href="/old/group-create" class="px-3 py-2 rounded-md text-lg font-medium text-gray-400 hover:bg-gray-800 transition duration-300 relative overflow-hidden group">
<i class="fas fa-users-cog mr-2"></i> Create Agent Group
<!-- Underline animation -->
<span class="absolute bottom-0 left-0 w-0 h-0.5 group-hover:w-full transition-all duration-300"
style="background: linear-gradient(90deg, var(--secondary), var(--primary));"></span>
</a>
</div>
</div>
</div>
<div class="flex items-center space-x-4">
<!-- Status badge -->
<div class="hidden md:flex items-center">
<span class="flex items-center text-sm">
<span class="w-2 h-2 rounded-full mr-2"
style="background-color: var(--primary); box-shadow: 0 0 5px var(--primary); animation: pulse 2s infinite;"></span>
<span>State: <span style="color: var(--secondary); text-shadow: var(--pink-glow);">active</span></span>
</span>
</div>
</div>
<div class="md:hidden flex items-center">
<button class="text-gray-400 hover:text-white focus:outline-none focus:text-white transition duration-300"
style="text-shadow: var(--neon-glow);"
onclick="toggleMobileMenu()">
<i class="fas fa-bars fa-lg"></i>
</button>
</div>
</div>
</div>
<!-- Mobile menu, toggle based on menu state -->
<div id="mobile-menu" class="md:hidden hidden"
style="background-color: var(--darker-bg); border-top: 1px solid var(--medium-bg);">
<div class="px-2 pt-2 pb-3 space-y-1">
<a href="/old" class="block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-gray-800 transition duration-300"
style="border-left: 3px solid var(--primary);">
<i class="fas fa-home mr-2"></i> Home
</a>
<a href="/old/agents" class="block px-3 py-2 rounded-md text-base font-medium text-gray-400 hover:bg-gray-800 transition duration-300"
style="border-left: 3px solid var(--secondary);">
<i class="fas fa-users mr-2"></i> Agent List
</a>
<a href="/old/actions-playground" class="px-3 py-2 rounded-md text-lg font-medium text-gray-400 hover:bg-gray-800 transition duration-300 relative overflow-hidden group">
<i class="fas fa-bolt mr-2"></i> Actions Playground
<!-- Underline animation -->
<span class="absolute bottom-0 left-0 w-0 h-0.5 group-hover:w-full transition-all duration-300"
style="background: linear-gradient(90deg, var(--tertiary), var(--primary));"></span>
</a>
<a href="/old/group-create" class="px-3 py-2 rounded-md text-lg font-medium text-gray-400 hover:bg-gray-800 transition duration-300 relative overflow-hidden group">
<i class="fas fa-users-cog mr-2"></i> Create Agent Group
<!-- Underline animation -->
<span class="absolute bottom-0 left-0 w-0 h-0.5 group-hover:w-full transition-all duration-300"
style="background: linear-gradient(90deg, var(--secondary), var(--primary));"></span>
</a>
</div>
</div>
</nav>
<br>
<script>
function toggleMobileMenu() {
const mobileMenu = document.getElementById('mobile-menu');
mobileMenu.classList.toggle('hidden');
}
</script>
-462
View File
@@ -1,462 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Agent settings {{.Name}}</title>
{{template "old/views/partials/header"}}
<script src="/old/public/js/wizard.js"></script>
<link rel="stylesheet" href="/old/public/css/wizard.css">
<script src="/old/public/js/connector-templates.js"></script>
<script src="/old/public/js/agent-form.js"></script>
</head>
<body>
{{template "old/views/partials/menu"}}
<!-- Toast notification container -->
<div id="toast" class="toast">
<span id="toast-message"></span>
</div>
<div class="container">
<header class="text-center mb-8">
<h1 class="text-3xl md:text-5xl font-bold">Agent settings - {{.Name}}</h1>
</header>
<div class="max-w-4xl mx-auto">
<!-- Agent Configuration Form Section -->
<div class="section-box">
<h2>Edit Agent Configuration</h2>
<form id="edit-agent-form">
<input type="hidden" name="name" id="name" value="{{.Name}}">
{{template "old/views/partials/agent-form" .}}
<button type="submit" id="update-button" class="action-btn" data-original-text="<i class='fas fa-save'></i> Update Agent">
<i class="fas fa-save"></i> Update Agent
</button>
</form>
</div>
<div class="section-box">
<h2>Agent Control</h2>
<div class="button-container">
<button
class="action-btn toggle-btn"
data-agent="{{.Name}}"
data-active="{{.Status}}">
{{if .Status}}
<i class="fas fa-pause"></i> Pause Agent
{{else}}
<i class="fas fa-play"></i> Start Agent
{{end}}
</button>
</div>
</div>
<div class="section-box">
<h2>Export Data</h2>
<p class="mb-4">Export your agent configuration for backup or transfer.</p>
<button
class="action-btn"
onclick="window.location.href='/old/settings/export/{{.Name}}'">
<i class="fas fa-file-export"></i> Export Configuration
</button>
</div>
<div class="section-box">
<h2>Danger Zone</h2>
<p class="mb-4">Permanently delete this agent and all associated data. This action cannot be undone.</p>
<button
class="action-btn"
style="background: linear-gradient(135deg, #ff4545, var(--secondary)); color: white;"
hx-delete="/api/agent/{{.Name}}"
hx-swap="none"
data-action="delete"
data-agent="{{.Name}}">
<i class="fas fa-trash-alt"></i> Delete Agent
</button>
</div>
<div class="user-info">
<span>Agent: {{.Name}}</span>
<span class="timestamp">Last modified: <span id="current-date"></span></span>
</div>
</div>
</div>
<script>
const actions = `{{ range .Actions }}<option value="{{.}}">{{.}}</option>{{ end }}`;
const connectors = `{{ range .Connectors }}<option value="{{.}}">{{.}}</option>{{ end }}`;
const promptBlocks = `{{ range .PromptBlocks }}<option value="{{.}}">{{.}}</option>{{ end }}`;
let agentConfig = null;
document.addEventListener('DOMContentLoaded', function() {
// Initialize common form components
initAgentFormCommon({
actions: actions,
connectors: connectors,
promptBlocks: promptBlocks
});
// Load agent configuration when page loads
loadAgentConfig();
// Add event listener for delete button
document.querySelectorAll('[data-action="delete"]').forEach(button => {
button.addEventListener('htmx:afterRequest', function(event) {
handleActionResponse(event, this);
});
});
// Handle toggle button
const toggleButton = document.querySelector('.toggle-btn');
if (toggleButton) {
toggleButton.addEventListener('click', function() {
const agent = this.getAttribute('data-agent');
const isActive = this.getAttribute('data-active') === "true";
const endpoint = isActive ? `/api/agent/${agent}/pause` : `/api/agent/${agent}/start`;
// Add animation
this.style.animation = 'pulse 0.5s';
// Create a new XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.open('PUT', endpoint);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = () => {
// Clear animation
this.style.animation = '';
if (xhr.status === 200) {
try {
const response = JSON.parse(xhr.responseText);
if (response.status === "ok") {
// Toggle the button state
const newState = !isActive;
this.setAttribute('data-active', newState.toString());
// Update button text and icon
if (newState) {
this.innerHTML = '<i class="fas fa-pause"></i> Pause Agent';
} else {
this.innerHTML = '<i class="fas fa-play"></i> Start Agent';
}
// Show success toast
const action = isActive ? 'pause' : 'start';
showToast(`Agent "${agent}" ${action}ed successfully`, 'success');
} else if (response.error) {
// Show error toast
showToast(`Error: ${response.error}`, 'error');
}
} catch (e) {
// Handle parsing error
showToast("Invalid response format", 'error');
console.error("Error parsing response:", e);
}
} else {
// Handle HTTP error
showToast(`Server error: ${xhr.status}`, 'error');
}
};
xhr.onerror = () => {
// Clear animation
this.style.animation = '';
showToast("Network error occurred", 'error');
console.error("Network error occurred");
};
// Send the request
xhr.send(JSON.stringify({}));
});
}
// Set current date for timestamp
const now = new Date();
document.getElementById('current-date').textContent = now.toISOString().split('T')[0];
// Handle form submission for updating agent
const form = document.getElementById('edit-agent-form');
form.addEventListener('submit', function(e) {
e.preventDefault();
// Show a loading state
const updateButton = document.getElementById('update-button');
const originalButtonText = updateButton.innerHTML;
updateButton.setAttribute('data-original-text', originalButtonText);
updateButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Updating...';
updateButton.disabled = true;
// Build a structured data object
const formData = new FormData(form);
const jsonData = AgentFormUtils.processFormData(formData);
// Process special fields
jsonData.connectors = AgentFormUtils.processConnectors(updateButton);
if (jsonData.connectors === null) return; // Validation failed
jsonData.mcp_servers = AgentFormUtils.processMCPServers();
jsonData.actions = AgentFormUtils.processActions(updateButton);
if (jsonData.actions === null) return; // Validation failed
jsonData.promptblocks = AgentFormUtils.processPromptBlocks(updateButton);
if (jsonData.promptblocks === null) return; // Validation failed
console.log('Sending data:', jsonData);
// Send the structured data as JSON
fetch(`/api/agent/${jsonData.name}/config`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(jsonData)
})
.then(response => {
if (!response.ok) {
return response.json().then(err => {
throw new Error(err.error || `Server error: ${response.status}`);
});
}
return response.json();
})
.then(data => {
// Restore button state
updateButton.innerHTML = originalButtonText;
updateButton.disabled = false;
if (data.status === "ok") {
// Show success toast
showToast('Agent updated successfully!', 'success');
// Reload agent config to get updated values
setTimeout(() => {
loadAgentConfig();
}, 500);
} else if (data.error) {
// Show error toast
showToast('Error: ' + data.error, 'error');
} else {
// Handle unexpected response format
showToast('Unexpected response format', 'error');
}
})
.catch(error => {
// Handle network or other errors
showToast('Error: ' + error.message, 'error');
console.error('Update error:', error);
// Restore button state
updateButton.innerHTML = originalButtonText;
updateButton.disabled = false;
});
});
});
// Function to handle API responses for delete action
function handleActionResponse(event, button) {
const xhr = event.detail.xhr;
const action = button.getAttribute('data-action');
const agent = button.getAttribute('data-agent');
if (xhr.status === 200) {
try {
const response = JSON.parse(xhr.responseText);
if (response.status === "ok") {
// Action successful
let message = "";
switch(action) {
case 'delete':
message = `Agent "${agent}" deleted successfully`;
// Redirect to agent list page after short delay for delete
setTimeout(() => {
window.location.href = "/old/agents";
}, 2000);
break;
default:
message = "Operation completed successfully";
}
// Show success message
showToast(message, 'success');
} else if (response.error) {
// Show error message
showToast(`Error: ${response.error}`, 'error');
}
} catch (e) {
// Handle JSON parsing error
showToast("Invalid response format", 'error');
}
} else {
// Handle HTTP error
showToast(`Server error: ${xhr.status}`, 'error');
}
}
// Load agent configuration from server
function loadAgentConfig() {
const agentName = document.getElementById('name').value;
fetch(`/api/agent/${agentName}/config`)
.then(response => {
if (!response.ok) {
throw new Error(`Failed to load agent config: ${response.status}`);
}
return response.json();
})
.then(data => {
agentConfig = data;
populateFormWithConfig(data);
showToast('Agent configuration loaded', 'success');
})
.catch(error => {
console.error('Error loading agent config:', error);
showToast('Error loading agent configuration: ' + error.message, 'error');
});
}
// Populate form with agent configuration
function populateFormWithConfig(config) {
// Clear existing dynamic sections
document.getElementById('connectorsSection').innerHTML = '';
document.getElementById('mcpSection').innerHTML = '';
document.getElementById('action_box').innerHTML = '';
document.getElementById('dynamic_box').innerHTML = '';
// Populate simple fields
document.getElementById('hud').checked = config.hud || false;
document.getElementById('enable_kb').checked = config.enable_kb || false;
document.getElementById('enable_reasoning').checked = config.enable_reasoning || false;
document.getElementById('kb_results').value = config.kb_results || '';
document.getElementById('standalone_job').checked = config.standalone_job || false;
document.getElementById('initiate_conversations').checked = config.initiate_conversations || false;
document.getElementById('enable_planning').checked = config.enable_planning || false;
document.getElementById('can_stop_itself').checked = config.can_stop_itself || false;
document.getElementById('random_identity').checked = config.random_identity || false;
document.getElementById('long_term_memory').checked = config.long_term_memory || false;
document.getElementById('summary_long_term_memory').checked = config.summary_long_term_memory || false;
document.getElementById('identity_guidance').value = config.identity_guidance || '';
document.getElementById('description').value = config.description || '';
document.getElementById('periodic_runs').value = config.periodic_runs || '';
document.getElementById('model').value = config.model || '';
document.getElementById('multimodal_model').value = config.multimodal_model || '';
document.getElementById('api_url').value = config.api_url || '';
document.getElementById('api_key').value = config.api_key || '';
document.getElementById('local_rag_url').value = config.local_rag_url || '';
document.getElementById('local_rag_api_key').value = config.local_rag_token || '';
document.getElementById('permanent_goal').value = config.permanent_goal || '';
document.getElementById('system_prompt').value = config.system_prompt || '';
// Populate connectors
if (config.connectors && Array.isArray(config.connectors)) {
config.connectors.forEach((connector, index) => {
// Add connector section
document.getElementById('addConnectorButton').click();
// Find the added connector elements
const connectorType = document.getElementById(`connectorType${index}`);
// Set values
if (connectorType) {
// First set the connector type
AgentFormUtils.setSelectValue(connectorType, connector.type);
// Parse the config if it's a string (from backend)
let configObj = connector.config;
if (typeof connector.config === 'string') {
try {
configObj = JSON.parse(connector.config);
} catch (e) {
console.error('Error parsing connector config:', e);
configObj = {}; // Fallback to empty object if parsing fails
}
}
// Now render the appropriate form for this connector type with the config values
AgentFormUtils.renderConnectorForm(index, connector.type, configObj);
}
});
}
// Populate MCP servers
if (config.mcp_servers && Array.isArray(config.mcp_servers)) {
config.mcp_servers.forEach((server, index) => {
// Add MCP server section
document.getElementById('addMCPButton').click();
// Find the added MCP server elements
const mcpURL = document.getElementById(`mcpURL${index}`);
const mcpToken = document.getElementById(`mcpToken${index}`);
// Set values
if (mcpURL) {
// If server is a string (old format), use it as URL
if (typeof server === 'string') {
mcpURL.value = server;
}
// If server is an object (new format), extract URL
else if (typeof server === 'object' && server !== null) {
mcpURL.value = server.url || '';
if (mcpToken && server.token) {
mcpToken.value = server.token;
}
}
}
});
}
// Populate actions
if (config.actions && Array.isArray(config.actions)) {
config.actions.forEach((action, index) => {
// Add action section
document.getElementById('action_button').click();
// Find the added action elements
const actionName = document.getElementById(`actionsName${index}`);
const actionConfig = document.getElementById(`actionsConfig${index}`);
// Set values
if (actionName) {
AgentFormUtils.setSelectValue(actionName, action.name);
}
if (actionConfig) {
// Format the config value
AgentFormUtils.formatConfigValue(actionConfig, action.config);
}
});
}
// Populate prompt blocks
if (config.promptblocks && Array.isArray(config.promptblocks)) {
config.promptblocks.forEach((block, index) => {
// Add prompt block section
document.getElementById('dynamic_button').click();
// Find the added prompt block elements
const promptName = document.getElementById(`promptName${index}`);
const promptConfig = document.getElementById(`promptConfig${index}`);
// Set values
if (promptName) {
AgentFormUtils.setSelectValue(promptName, block.name);
}
if (promptConfig) {
// Format the config value
AgentFormUtils.formatConfigValue(promptConfig, block.config);
}
});
}
}
</script>
</body>
</html>
-61
View File
@@ -1,61 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Smart Agent status</title>
{{template "old/views/partials/header"}}
<style>
body { overflow: hidden; }
.chat-container { height: 90vh; display: flex; flex-direction: column; }
.chat-messages { overflow-y: auto; flex-grow: 1; }
.htmx-indicator{
opacity:0;
transition: opacity 10ms ease-in;
}
.htmx-request .htmx-indicator{
opacity:1
}
/* Loader (https://cssloaders.github.io/) */
.loader {
width: 12px;
height: 12px;
border-radius: 50%;
display: block;
margin:15px auto;
position: relative;
color: #FFF;
box-sizing: border-box;
animation: animloader 2s linear infinite;
}
@keyframes animloader {
0% { box-shadow: 14px 0 0 -2px, 38px 0 0 -2px, -14px 0 0 -2px, -38px 0 0 -2px; }
25% { box-shadow: 14px 0 0 -2px, 38px 0 0 -2px, -14px 0 0 -2px, -38px 0 0 2px; }
50% { box-shadow: 14px 0 0 -2px, 38px 0 0 -2px, -14px 0 0 2px, -38px 0 0 -2px; }
75% { box-shadow: 14px 0 0 2px, 38px 0 0 -2px, -14px 0 0 -2px, -38px 0 0 -2px; }
100% { box-shadow: 14px 0 0 -2px, 38px 0 0 2px, -14px 0 0 -2px, -38px 0 0 -2px; }
}
</style>
</head>
<body class="bg-gray-900 p-4 text-white font-sans" hx-ext="sse" sse-connect="/sse/{{.Name}}">
{{template "old/views/partials/menu"}}
<div class="chat-container bg-gray-800 shadow-lg rounded-lg" >
<!-- Chat Header -->
<div class="border-b border-gray-700 p-4">
<h1 class="text-lg font-semibold">{{.Name}}</h1>
</div>
<!-- Chat Messages -->
<div class="chat-messages p-4">
<div sse-swap="status" hx-swap="afterbegin" id="status"></div>
{{ range .History }}
<!-- Agent Status Box -->
<div class="bg-gray-700 p-4">
<h2 class="text-sm font-semibold">Agent:</h2>
<div id="agentStatus" class="text-sm text-gray-300">
Result: {{.Result}} Action: {{.Action}} Params: {{.Params}} Reasoning: {{.Reasoning}}
</div>
</div>
{{end}}
</div>
</div>
</body>
</html>
-12
View File
@@ -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 {