mirror of
https://github.com/mudler/LocalAGI.git
synced 2026-08-04 07:56:16 -04:00
Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 419a4363c3 | |||
| 292d0c9c19 | |||
| 1147e02844 | |||
| c8e83dc4b9 | |||
| 8b5188c35b | |||
| 55b631bc51 | |||
| c1e2eeb8af | |||
| 50f168f322 | |||
| b5dacb0e4f | |||
| 0762d6fabb | |||
| 6dc515bae5 | |||
| 9110e9cfdb | |||
| 9965e3df36 | |||
| da9e8d7c20 | |||
| 7f2e52c0c8 | |||
| 6a33484fce | |||
| bdf12e4c3d | |||
| 5743d6f757 | |||
| 0845e9dc81 | |||
| ef2a6e2296 | |||
| 272c54f851 | |||
| 775b1a9ab8 | |||
| 673909918a | |||
| 807b7e756b | |||
| c75da2235d | |||
| 80a12f6662 | |||
| 1128615140 | |||
| 8847bfe0fc | |||
| 25ce5475de | |||
| 2a4bc0eb0a | |||
| d0bfa01494 | |||
| 57023d6386 | |||
| 1c3e9b36e7 | |||
| 4a3ee02081 | |||
| d804ef66e5 | |||
| cea3b7b111 | |||
| 1d574069f2 | |||
| b09da13111 | |||
| ba2eafe4b2 | |||
| cbe4cd62ca | |||
| 4f4ad5069d | |||
| 0950791be6 | |||
| f47d5d0e01 | |||
| e3d4ac3d0b | |||
| c98178d41d | |||
| 4e89c8aac0 | |||
| c3bd2bee42 | |||
| 9f358abb54 | |||
| 456c32c284 | |||
| b527cbc332 | |||
| 7cc0c3e85b | |||
| 73ce451c13 | |||
| 72a7807a81 | |||
| 16775f356a | |||
| a363682b39 | |||
| 2288e7f08d | |||
| c703495e8c | |||
| 73c1158aa2 | |||
| a1036a166b | |||
| 9a669e2552 |
@@ -11,7 +11,8 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
containerImages:
|
||||
runs-on: ubuntu-latest
|
||||
#runs-on: ubuntu-latest
|
||||
runs-on: arc-runner-localagent
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
@@ -8,3 +8,5 @@ LocalAGI
|
||||
**/.env
|
||||
.vscode
|
||||
volumes/
|
||||
example/scheduler/scheduler
|
||||
example/scheduler/example_tasks.json
|
||||
|
||||
@@ -15,7 +15,7 @@ run-nokb:
|
||||
$(MAKE) run KBDISABLEINDEX=true
|
||||
|
||||
webui/react-ui/dist:
|
||||
docker run --entrypoint /bin/bash -v $(ROOT_DIR):/app oven/bun:1 -c "cd /app/webui/react-ui && bun install && bun run build"
|
||||
docker run --entrypoint /bin/bash -v $(ROOT_DIR):/app:z oven/bun:1 -c "cd /app/webui/react-ui && bun install && bun run build"
|
||||
|
||||
.PHONY: build
|
||||
build: webui/react-ui/dist
|
||||
|
||||
@@ -18,7 +18,7 @@ Try on [ and a web browser.
|
||||
|
||||
**LocalAGI** is a powerful, self-hostable AI Agent platform that allows you to design AI automations without writing code. A complete drop-in replacement for OpenAI's Responses APIs with advanced agentic capabilities. No clouds. No data leaks. Just pure local AI that works on consumer-grade hardware (CPU and GPU).
|
||||
**LocalAGI** is a powerful, self-hostable AI Agent platform that allows you to design AI automations without writing code. Create Agents with a couple of clicks, connect via MCP and give it skills with [skillserver](https://github.com/mudler/skillserver). Every agent exposes a complete drop-in replacement for OpenAI's Responses APIs with advanced agentic capabilities. No clouds. No data leaks. Just pure local AI that works on consumer-grade hardware (CPU and GPU).
|
||||
|
||||
## 🛡️ Take Back Your Privacy
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
const ConversationActionName = "new_conversation"
|
||||
const ConversationActionName = "send_message"
|
||||
|
||||
func NewConversation() *ConversationAction {
|
||||
return &ConversationAction{}
|
||||
|
||||
+127
-52
@@ -6,19 +6,24 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/scheduler"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/robfig/cron/v3"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
const (
|
||||
ReminderActionName = "set_reminder"
|
||||
ListRemindersName = "list_reminders"
|
||||
RemoveReminderName = "remove_reminder"
|
||||
RecurringReminderActionName = "set_recurring_task"
|
||||
OneTimeReminderActionName = "set_onetime_task"
|
||||
ListRemindersName = "list_tasks"
|
||||
RemoveReminderName = "remove_task"
|
||||
)
|
||||
|
||||
func NewReminder() *ReminderAction {
|
||||
return &ReminderAction{}
|
||||
func NewRecurringReminder() *RecurringReminderAction {
|
||||
return &RecurringReminderAction{}
|
||||
}
|
||||
|
||||
func NewOneTimeReminder() *OneTimeReminderAction {
|
||||
return &OneTimeReminderAction{}
|
||||
}
|
||||
|
||||
func NewListReminders() *ListRemindersAction {
|
||||
@@ -29,7 +34,8 @@ func NewRemoveReminder() *RemoveReminderAction {
|
||||
return &RemoveReminderAction{}
|
||||
}
|
||||
|
||||
type ReminderAction struct{}
|
||||
type RecurringReminderAction struct{}
|
||||
type OneTimeReminderAction struct{}
|
||||
type ListRemindersAction struct{}
|
||||
type RemoveReminderAction struct{}
|
||||
|
||||
@@ -37,46 +43,87 @@ type RemoveReminderParams struct {
|
||||
Index int `json:"index"`
|
||||
}
|
||||
|
||||
func (a *ReminderAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := types.ReminderActionResponse{}
|
||||
func (a *RecurringReminderAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := types.RecurringReminderParams{}
|
||||
err := params.Unmarshal(&result)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
// Validate the cron expression
|
||||
parser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
|
||||
_, err = parser.Parse(result.CronExpr)
|
||||
task, err := scheduler.NewTask(
|
||||
sharedState.AgentName,
|
||||
result.Message,
|
||||
scheduler.ScheduleTypeCron,
|
||||
result.CronExpr,
|
||||
)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
// Calculate next run time
|
||||
now := time.Now()
|
||||
schedule, _ := parser.Parse(result.CronExpr) // We can ignore the error since we validated above
|
||||
nextRun := schedule.Next(now)
|
||||
task.Metadata["reminder_type"] = "user_created"
|
||||
|
||||
// Set the reminder details
|
||||
result.LastRun = now
|
||||
result.NextRun = nextRun
|
||||
// IsRecurring is set by the user through the action parameters
|
||||
|
||||
// Store the reminder in the shared state
|
||||
if sharedState.Reminders == nil {
|
||||
sharedState.Reminders = make([]types.ReminderActionResponse, 0)
|
||||
err = sharedState.Scheduler.CreateTask(task)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
sharedState.Reminders = append(sharedState.Reminders, result)
|
||||
|
||||
return types.ActionResult{
|
||||
Result: "Reminder set successfully",
|
||||
Result: fmt.Sprintf("Recurring reminder set successfully (ID: %s). Next run: %s", task.ID, task.NextRun.Format(time.RFC3339)),
|
||||
Metadata: map[string]interface{}{
|
||||
"reminder": result,
|
||||
"task_id": task.ID,
|
||||
"message": result.Message,
|
||||
"next_run": task.NextRun,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *OneTimeReminderAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := types.OneTimeReminderParams{}
|
||||
err := params.Unmarshal(&result)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
// Validate the delay parses correctly before creating the task
|
||||
_, err = scheduler.ParseDuration(result.Delay)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("invalid delay format, expected a duration like '30m', '2h', '1d', '1d12h': %w", err)
|
||||
}
|
||||
|
||||
task, err := scheduler.NewTask(
|
||||
sharedState.AgentName,
|
||||
result.Message,
|
||||
scheduler.ScheduleTypeOnce,
|
||||
result.Delay,
|
||||
)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
task.Metadata["reminder_type"] = "user_created"
|
||||
|
||||
err = sharedState.Scheduler.CreateTask(task)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
return types.ActionResult{
|
||||
Result: fmt.Sprintf("One-time reminder set in %s (at %s, ID: %s)", result.Delay, task.NextRun.Format(time.RFC3339), task.ID),
|
||||
Metadata: map[string]interface{}{
|
||||
"task_id": task.ID,
|
||||
"message": result.Message,
|
||||
"next_run": task.NextRun,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *ListRemindersAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
if sharedState.Reminders == nil || len(sharedState.Reminders) == 0 {
|
||||
tasks, err := sharedState.Scheduler.GetAllTasks()
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
if len(tasks) == 0 {
|
||||
return types.ActionResult{
|
||||
Result: "No reminders set",
|
||||
}, nil
|
||||
@@ -84,22 +131,25 @@ func (a *ListRemindersAction) Run(ctx context.Context, sharedState *types.AgentS
|
||||
|
||||
var result strings.Builder
|
||||
result.WriteString("Current reminders:\n")
|
||||
for i, reminder := range sharedState.Reminders {
|
||||
|
||||
for i, task := range tasks {
|
||||
status := "one-time"
|
||||
if reminder.IsRecurring {
|
||||
if task.ScheduleType == scheduler.ScheduleTypeCron || task.ScheduleType == scheduler.ScheduleTypeInterval {
|
||||
status = "recurring"
|
||||
}
|
||||
result.WriteString(fmt.Sprintf("%d. %s (Next run: %s, Status: %s)\n",
|
||||
|
||||
result.WriteString(fmt.Sprintf("%d. %s (Next run: %s, Status: %s, ID: %s)\n",
|
||||
i+1,
|
||||
reminder.Message,
|
||||
reminder.NextRun.Format(time.RFC3339),
|
||||
status))
|
||||
task.Prompt,
|
||||
task.NextRun.Format(time.RFC3339),
|
||||
status,
|
||||
task.ID))
|
||||
}
|
||||
|
||||
return types.ActionResult{
|
||||
Result: result.String(),
|
||||
Metadata: map[string]interface{}{
|
||||
"reminders": sharedState.Reminders,
|
||||
"tasks": tasks,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -111,7 +161,12 @@ func (a *RemoveReminderAction) Run(ctx context.Context, sharedState *types.Agent
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
if sharedState.Reminders == nil || len(sharedState.Reminders) == 0 {
|
||||
tasks, err := sharedState.Scheduler.GetAllTasks()
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
if len(tasks) == 0 {
|
||||
return types.ActionResult{
|
||||
Result: "No reminders to remove",
|
||||
}, nil
|
||||
@@ -119,23 +174,29 @@ func (a *RemoveReminderAction) Run(ctx context.Context, sharedState *types.Agent
|
||||
|
||||
// Convert from 1-based index to 0-based
|
||||
index := removeParams.Index - 1
|
||||
if index < 0 || index >= len(sharedState.Reminders) {
|
||||
if index < 0 || index >= len(tasks) {
|
||||
return types.ActionResult{}, fmt.Errorf("invalid reminder index: %d", removeParams.Index)
|
||||
}
|
||||
|
||||
// Remove the reminder
|
||||
removed := sharedState.Reminders[index]
|
||||
sharedState.Reminders = append(sharedState.Reminders[:index], sharedState.Reminders[index+1:]...)
|
||||
task := tasks[index]
|
||||
err = sharedState.Scheduler.DeleteTask(task.ID)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
return types.ActionResult{
|
||||
Result: fmt.Sprintf("Removed reminder: %s", removed.Message),
|
||||
Result: fmt.Sprintf("Removed reminder: %s", task.Prompt),
|
||||
Metadata: map[string]interface{}{
|
||||
"removed_reminder": removed,
|
||||
"removed_task_id": task.ID,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *ReminderAction) Plannable() bool {
|
||||
func (a *RecurringReminderAction) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *OneTimeReminderAction) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -147,10 +208,10 @@ func (a *RemoveReminderAction) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *ReminderAction) Definition() types.ActionDefinition {
|
||||
func (a *RecurringReminderAction) Definition() types.ActionDefinition {
|
||||
return types.ActionDefinition{
|
||||
Name: ReminderActionName,
|
||||
Description: "Set a reminder for the agent to wake up and perform a task based on a cron schedule. Examples: '0 0 * * *' (daily at midnight), '0 */2 * * *' (every 2 hours), '0 0 * * 1' (every Monday at midnight)",
|
||||
Name: RecurringReminderActionName,
|
||||
Description: "Set a recurring reminder for the agent to wake up and perform a task on a cron schedule. The reminder will keep repeating. Examples: '0 0 * * *' (daily at midnight), '0 */2 * * *' (every 2 hours), '0 0 * * 1' (every Monday at midnight)",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"message": {
|
||||
Type: jsonschema.String,
|
||||
@@ -158,14 +219,28 @@ func (a *ReminderAction) Definition() types.ActionDefinition {
|
||||
},
|
||||
"cron_expr": {
|
||||
Type: jsonschema.String,
|
||||
Description: "Cron expression for scheduling (e.g. '0 0 * * *' for daily at midnight). Format: 'second minute hour day month weekday'",
|
||||
},
|
||||
"is_recurring": {
|
||||
Type: jsonschema.Boolean,
|
||||
Description: "Whether this reminder should repeat according to the cron schedule (true) or trigger only once (false)",
|
||||
Description: "Cron expression for scheduling (e.g. '0 0 * * *' for daily at midnight). Format: 'minute hour day month weekday'",
|
||||
},
|
||||
},
|
||||
Required: []string{"message", "cron_expr", "is_recurring"},
|
||||
Required: []string{"message", "cron_expr"},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *OneTimeReminderAction) Definition() types.ActionDefinition {
|
||||
return types.ActionDefinition{
|
||||
Name: OneTimeReminderActionName,
|
||||
Description: "Set a one-time reminder for the agent to wake up and perform a task after a delay. The reminder triggers only once and is then automatically removed. Use this when asked to do something 'in X minutes/hours/days'. Examples: '30m' (30 minutes), '2h' (2 hours), '1d' (1 day), '1d12h' (1.5 days), '2h30m' (2.5 hours)",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"message": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The message or task to be reminded about",
|
||||
},
|
||||
"delay": {
|
||||
Type: jsonschema.String,
|
||||
Description: "How long to wait before triggering. Use Go duration format: '30m' (30 minutes), '2h' (2 hours), '1d' (1 day), '1d12h' (1.5 days), '2h30m' (2.5 hours)",
|
||||
},
|
||||
},
|
||||
Required: []string{"message", "delay"},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ func (m Messages) GetLatestUserMessage() *openai.ChatCompletionMessage {
|
||||
// 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
|
||||
baseActions := a.availableActions()
|
||||
baseActions := a.availableActions(job)
|
||||
|
||||
// Add user-defined actions from the job
|
||||
userTools := job.GetUserTools()
|
||||
@@ -107,12 +107,11 @@ func (a *Agent) getAvailableActionsForJob(job *types.Job) types.Actions {
|
||||
return baseActions
|
||||
}
|
||||
|
||||
func (a *Agent) availableActions() types.Actions {
|
||||
func (a *Agent) availableActions(j *types.Job) types.Actions {
|
||||
// defaultActions := append(a.options.userActions, action.NewReply())
|
||||
|
||||
defaultActions := slices.Clone(a.options.userActions)
|
||||
|
||||
if a.options.initiateConversations && a.selfEvaluationInProgress { // && self-evaluation..
|
||||
if j.Metadata["type"] == "scheduled" || (a.options.initiateConversations && a.selfEvaluationInProgress) { // && self-evaluation..
|
||||
acts := append(defaultActions, action.NewConversation())
|
||||
if a.options.enableHUD {
|
||||
acts = append(acts, action.NewState())
|
||||
|
||||
+153
-109
@@ -18,9 +18,9 @@ import (
|
||||
"github.com/mudler/xlog"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/action"
|
||||
"github.com/mudler/LocalAGI/core/scheduler"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/llm"
|
||||
"github.com/robfig/cron/v3"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
@@ -30,6 +30,20 @@ const (
|
||||
SystemRole = "system"
|
||||
)
|
||||
|
||||
// NoToolToCallArgs defines the arguments for the no_tool_to_call sink state tool
|
||||
type NoToolToCallArgs struct {
|
||||
Reasoning string `json:"reasoning" description:"The reasoning for why no tool is being called"`
|
||||
}
|
||||
|
||||
// NoToolToCallTool is a custom sink state tool that logs when no other tool is needed
|
||||
type NoToolToCallTool struct{}
|
||||
|
||||
// Run executes the no_tool_to_call tool and logs a message
|
||||
func (t NoToolToCallTool) Run(args NoToolToCallArgs) (string, any, error) {
|
||||
xlog.Info("No tool to call - agent decided no action was needed", "reasoning", args.Reasoning)
|
||||
return fmt.Sprintf("No action needed: %s", args.Reasoning), nil, nil
|
||||
}
|
||||
|
||||
type Agent struct {
|
||||
sync.Mutex
|
||||
options *options
|
||||
@@ -43,19 +57,22 @@ type Agent struct {
|
||||
selfEvaluationInProgress bool
|
||||
pause bool
|
||||
|
||||
newConversations chan openai.ChatCompletionMessage
|
||||
newConversations chan *types.ConversationMessage
|
||||
|
||||
mcpSessions []*mcp.ClientSession
|
||||
// only contains the MCP action definitions for observables
|
||||
mcpActionDefinitions types.Actions
|
||||
|
||||
subscriberMutex sync.Mutex
|
||||
newMessagesSubscribers []func(openai.ChatCompletionMessage)
|
||||
newMessagesSubscribers []func(*types.ConversationMessage)
|
||||
|
||||
observer Observer
|
||||
|
||||
llm cogito.LLM
|
||||
sharedState *types.AgentSharedState
|
||||
|
||||
// Task scheduler for managing reminders
|
||||
taskScheduler *scheduler.Scheduler
|
||||
}
|
||||
|
||||
type RAGDB interface {
|
||||
@@ -87,7 +104,7 @@ func New(opts ...Option) (*Agent, error) {
|
||||
currentState: &types.AgentInternalState{},
|
||||
llm: llmClient,
|
||||
context: types.NewActionContext(ctx, cancel),
|
||||
newConversations: make(chan openai.ChatCompletionMessage),
|
||||
newConversations: make(chan *types.ConversationMessage),
|
||||
newMessagesSubscribers: options.newConversationsSubscribers,
|
||||
sharedState: types.NewAgentSharedState(options.lastMessageDuration),
|
||||
}
|
||||
@@ -118,6 +135,28 @@ func New(opts ...Option) (*Agent, error) {
|
||||
a.initMCPActions()
|
||||
xlog.Info("Done populating actions from MCP Servers")
|
||||
|
||||
// Initialize task scheduler for reminders
|
||||
schedulerPath := options.schedulerStorePath
|
||||
if schedulerPath == "" {
|
||||
schedulerPath = "scheduled_tasks.json"
|
||||
}
|
||||
|
||||
store, err := scheduler.NewJSONStore(schedulerPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create scheduler store: %v", err)
|
||||
}
|
||||
|
||||
executor := &agentSchedulerExecutor{agent: a}
|
||||
pollInterval := options.schedulerPollInterval
|
||||
if pollInterval == 0 {
|
||||
pollInterval = 30 * time.Second
|
||||
}
|
||||
|
||||
a.taskScheduler = scheduler.NewScheduler(store, executor, pollInterval)
|
||||
a.sharedState.Scheduler = a.taskScheduler
|
||||
a.sharedState.AgentName = a.Character.Name
|
||||
xlog.Info("Task scheduler initialized", "store_path", schedulerPath, "poll_interval", pollInterval)
|
||||
|
||||
xlog.Info(
|
||||
"Agent created",
|
||||
"agent", a.Character.Name,
|
||||
@@ -142,19 +181,21 @@ func (a *Agent) startNewConversationsConsumer() {
|
||||
return
|
||||
|
||||
case msg := <-a.newConversations:
|
||||
xlog.Debug("New conversation", "agent", a.Character.Name, "message", msg.Content)
|
||||
xlog.Debug("New conversation", "agent", a.Character.Name, "message", msg.Message.Content)
|
||||
a.subscriberMutex.Lock()
|
||||
subs := a.newMessagesSubscribers
|
||||
a.subscriberMutex.Unlock()
|
||||
for _, s := range subs {
|
||||
s(msg)
|
||||
if s != nil && msg != nil {
|
||||
s(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (a *Agent) AddSubscriber(f func(openai.ChatCompletionMessage)) {
|
||||
func (a *Agent) AddSubscriber(f func(*types.ConversationMessage)) {
|
||||
a.subscriberMutex.Lock()
|
||||
defer a.subscriberMutex.Unlock()
|
||||
a.newMessagesSubscribers = append(a.newMessagesSubscribers, f)
|
||||
@@ -218,7 +259,11 @@ func (a *Agent) Execute(j *types.Job) *types.JobResult {
|
||||
}
|
||||
|
||||
a.Enqueue(j)
|
||||
return j.Result.WaitResult()
|
||||
result, err := j.Result.WaitResult(a.context.Context)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (a *Agent) Enqueue(j *types.Job) {
|
||||
@@ -267,10 +312,16 @@ func (a *Agent) TTS(ctx context.Context, text string) ([]byte, error) {
|
||||
var ErrContextCanceled = fmt.Errorf("context canceled")
|
||||
|
||||
func (a *Agent) Stop() {
|
||||
xlog.Debug("Stopping agent", "agent", a.Character.Name)
|
||||
|
||||
// Stop the scheduler
|
||||
a.taskScheduler.Stop()
|
||||
xlog.Info("Task scheduler stopped")
|
||||
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
xlog.Debug("Stopping agent", "agent", a.Character.Name)
|
||||
a.closeMCPSTDIOServers()
|
||||
|
||||
a.closeMCPServers()
|
||||
a.context.Cancel()
|
||||
}
|
||||
|
||||
@@ -637,7 +688,7 @@ func (a *Agent) validateBuiltinTools(job *types.Job) {
|
||||
}
|
||||
|
||||
// Get available actions
|
||||
availableActions := a.availableActions()
|
||||
availableActions := a.availableActions(job)
|
||||
|
||||
for _, tool := range builtinTools {
|
||||
functionName := tool.Name
|
||||
@@ -796,7 +847,7 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
|
||||
}
|
||||
|
||||
if a.options.enableHUD {
|
||||
prompt, err := renderTemplate(hudTemplate, a.prepareHUD(), a.availableActions(), "")
|
||||
prompt, err := renderTemplate(hudTemplate, a.prepareHUD(), a.availableActions(job), "")
|
||||
if err != nil {
|
||||
job.Result.Finish(fmt.Errorf("error renderTemplate: %w", err))
|
||||
return
|
||||
@@ -859,6 +910,14 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
|
||||
cogito.WithTools(
|
||||
cogitoTools...,
|
||||
),
|
||||
cogito.WithSinkState(
|
||||
cogito.NewToolDefinition(
|
||||
NoToolToCallTool{},
|
||||
NoToolToCallArgs{},
|
||||
"no_tool_to_call",
|
||||
"Called when no other tool is needed to respond to the user",
|
||||
),
|
||||
),
|
||||
cogito.WithToolCallResultCallback(func(t cogito.ToolStatus) {
|
||||
if a.observer != nil && obs != nil {
|
||||
obs := observables[t.ToolArguments.ID]
|
||||
@@ -871,6 +930,29 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
|
||||
a.observer.Update(*obs)
|
||||
}
|
||||
|
||||
// Use full ActionResult (including Metadata) from action result,
|
||||
// so connectors receive e.g. songs_paths, images_url for sending files.
|
||||
actionResult := &types.ActionResult{
|
||||
Result: t.Result,
|
||||
}
|
||||
if t.ResultData != nil {
|
||||
switch res := t.ResultData.(type) {
|
||||
case types.ActionResult:
|
||||
actionResult = &res
|
||||
}
|
||||
}
|
||||
|
||||
// Merge action metadata into job metadata so it accumulates across actions
|
||||
// and is available when ConversationAction runs
|
||||
if actionResult.Metadata != nil {
|
||||
if job.Metadata == nil {
|
||||
job.Metadata = make(map[string]interface{})
|
||||
}
|
||||
for key, value := range actionResult.Metadata {
|
||||
job.Metadata[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
aa := allActions.Find(t.Name)
|
||||
state := types.ActionState{
|
||||
ActionCurrentState: types.ActionCurrentState{
|
||||
@@ -879,14 +961,14 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
|
||||
Params: types.ActionParams(t.ToolArguments.Arguments),
|
||||
Reasoning: t.ToolArguments.Reasoning,
|
||||
},
|
||||
ActionResult: types.ActionResult{Result: t.Result},
|
||||
ActionResult: *actionResult,
|
||||
}
|
||||
job.Result.SetResult(state)
|
||||
job.CallbackWithResult(state)
|
||||
conv = a.addFunctionResultToConversation(job.GetContext(), aa, types.ActionParams(t.ToolArguments.Arguments), types.ActionResult{Result: t.Result}, conv)
|
||||
conv = a.addFunctionResultToConversation(job.GetContext(), aa, types.ActionParams(t.ToolArguments.Arguments), *actionResult, conv)
|
||||
}),
|
||||
cogito.WithToolCallBack(
|
||||
func(tc *cogito.ToolChoice) bool {
|
||||
func(tc *cogito.ToolChoice, _ *cogito.SessionState) cogito.ToolCallDecision {
|
||||
|
||||
xlog.Debug("Tool call back", "tool_call", tc)
|
||||
|
||||
@@ -899,7 +981,9 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
|
||||
xlog.Debug("User-defined action chosen, returning tool call", "action", chosenAction.Definition().Name)
|
||||
a.replyWithToolCall(job, conv, tc.Arguments, chosenAction, tc.Reasoning)
|
||||
userTool = true
|
||||
return false
|
||||
return cogito.ToolCallDecision{
|
||||
Approved: false,
|
||||
}
|
||||
}
|
||||
|
||||
if a.observer != nil && job.Obs != nil {
|
||||
@@ -922,14 +1006,18 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
|
||||
|
||||
switch tc.Name {
|
||||
case action.StopActionName:
|
||||
return false
|
||||
return cogito.ToolCallDecision{
|
||||
Approved: false,
|
||||
}
|
||||
case action.ConversationActionName:
|
||||
message := action.ConversationActionResponse{}
|
||||
toolArgs, _ := json.Marshal(tc.Arguments)
|
||||
if err := json.Unmarshal([]byte(toolArgs), &message); err != nil {
|
||||
xlog.Error("Error unmarshalling conversation response", "error", err)
|
||||
job.Result.Finish(fmt.Errorf("error unmarshalling conversation response: %w", err))
|
||||
return false
|
||||
return cogito.ToolCallDecision{
|
||||
Approved: false,
|
||||
}
|
||||
}
|
||||
|
||||
msg := openai.ChatCompletionMessage{
|
||||
@@ -937,9 +1025,15 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
|
||||
Content: message.Message,
|
||||
}
|
||||
|
||||
// Get accumulated metadata from job (e.g., images, files generated by previous actions in this job)
|
||||
// This is per-job metadata, so parallel jobs won't interfere with each other
|
||||
metadata := job.Metadata
|
||||
|
||||
go func(agent *Agent) {
|
||||
xlog.Info("Sending new conversation to channel", "agent", agent.Character.Name, "message", msg.Content)
|
||||
agent.newConversations <- msg
|
||||
xlog.Info("Sending new conversation to channel", "agent", agent.Character.Name, "message", msg.Content, "metadata_keys", len(metadata))
|
||||
// Send ConversationMessage with both the message and accumulated metadata
|
||||
agent.newConversations <- types.NewConversationMessage(msg).WithMetadata(metadata)
|
||||
// Job metadata is automatically cleared when job finishes, no need to manually clear
|
||||
}(a)
|
||||
|
||||
job.Result.Conversation = []openai.ChatCompletionMessage{
|
||||
@@ -947,7 +1041,9 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
|
||||
}
|
||||
job.Result.SetResponse("decided to initiate a new conversation")
|
||||
job.Result.Finish(nil)
|
||||
return true
|
||||
return cogito.ToolCallDecision{
|
||||
Approved: true,
|
||||
}
|
||||
case action.StateActionName:
|
||||
// We need to store the result in the state
|
||||
state := types.AgentInternalState{}
|
||||
@@ -961,7 +1057,9 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
|
||||
}
|
||||
a.observer.Update(*obs)
|
||||
}
|
||||
return false
|
||||
return cogito.ToolCallDecision{
|
||||
Approved: false,
|
||||
}
|
||||
}
|
||||
// update the current state with the one we just got from the action
|
||||
a.currentState = &state
|
||||
@@ -982,7 +1080,9 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
|
||||
a.observer.Update(*obs)
|
||||
}
|
||||
|
||||
return false
|
||||
return cogito.ToolCallDecision{
|
||||
Approved: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1010,7 +1110,9 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
|
||||
job.Result.Finish(nil)
|
||||
|
||||
}
|
||||
return cont
|
||||
return cogito.ToolCallDecision{
|
||||
Approved: cont,
|
||||
}
|
||||
},
|
||||
),
|
||||
}
|
||||
@@ -1020,12 +1122,24 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
|
||||
if a.options.enableEvaluation {
|
||||
cogitoOpts = append(cogitoOpts, cogito.EnableAutoPlanReEvaluator)
|
||||
}
|
||||
if a.options.LLMAPI.ReviewerModel != "" {
|
||||
llmClient := cogito.NewOpenAILLM(a.options.LLMAPI.ReviewerModel, a.options.LLMAPI.APIKey, a.options.LLMAPI.APIURL)
|
||||
cogitoOpts = append(cogitoOpts, cogito.WithReviewerLLM(llmClient))
|
||||
}
|
||||
}
|
||||
|
||||
if a.options.disableSinkState {
|
||||
cogitoOpts = append(cogitoOpts, cogito.DisableSinkState)
|
||||
}
|
||||
|
||||
if a.options.forceReasoning {
|
||||
cogitoOpts = append(cogitoOpts, cogito.WithForceReasoning())
|
||||
}
|
||||
|
||||
if a.options.enableGuidedTools {
|
||||
cogitoOpts = append(cogitoOpts, cogito.EnableGuidedTools)
|
||||
}
|
||||
|
||||
if a.options.maxEvaluationLoops > 0 {
|
||||
cogitoOpts = append(cogitoOpts,
|
||||
cogito.WithMaxAttempts(a.options.maxEvaluationLoops),
|
||||
@@ -1069,13 +1183,7 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
|
||||
return
|
||||
}
|
||||
|
||||
responseFragment, err := a.llm.Ask(job.GetContext(), fragment)
|
||||
if err != nil {
|
||||
job.Result.Finish(err)
|
||||
return
|
||||
}
|
||||
|
||||
result := a.cleanupLLMResponse(responseFragment.LastMessage().Content)
|
||||
result := a.cleanupLLMResponse(fragment.LastMessage().Content)
|
||||
|
||||
conv = append(fragment.Messages, openai.ChatCompletionMessage{
|
||||
Role: "assistant",
|
||||
@@ -1117,84 +1225,6 @@ func (a *Agent) periodicallyRun(timer *time.Timer) {
|
||||
|
||||
xlog.Debug("Agent is running periodically", "agent", a.Character.Name)
|
||||
|
||||
// Check for reminders that need to be triggered
|
||||
now := time.Now()
|
||||
var triggeredReminders []types.ReminderActionResponse
|
||||
var remainingReminders []types.ReminderActionResponse
|
||||
|
||||
for _, reminder := range a.sharedState.Reminders {
|
||||
xlog.Debug("Checking reminder", "reminder", reminder)
|
||||
if now.After(reminder.NextRun) {
|
||||
triggeredReminders = append(triggeredReminders, reminder)
|
||||
xlog.Debug("Reminder triggered", "reminder", reminder)
|
||||
// Calculate next run time for recurring reminders
|
||||
if reminder.IsRecurring {
|
||||
xlog.Debug("Reminder is recurring", "reminder", reminder)
|
||||
parser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
|
||||
schedule, err := parser.Parse(reminder.CronExpr)
|
||||
if err == nil {
|
||||
nextRun := schedule.Next(now)
|
||||
xlog.Debug("Next run time", "reminder", reminder, "nextRun", nextRun)
|
||||
reminder.LastRun = now
|
||||
reminder.NextRun = nextRun
|
||||
remainingReminders = append(remainingReminders, reminder)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
xlog.Debug("Reminder not triggered", "reminder", reminder)
|
||||
remainingReminders = append(remainingReminders, reminder)
|
||||
}
|
||||
}
|
||||
|
||||
// Update the reminders list
|
||||
a.sharedState.Reminders = remainingReminders
|
||||
|
||||
// Handle triggered reminders
|
||||
for _, reminder := range triggeredReminders {
|
||||
xlog.Info("Processing triggered reminder", "agent", a.Character.Name, "message", reminder.Message)
|
||||
|
||||
// Create a more natural conversation flow for the reminder
|
||||
reminderJob := types.NewJob(
|
||||
types.WithText(fmt.Sprintf("I have a reminder for you: %s", reminder.Message)),
|
||||
types.WithReasoningCallback(a.options.reasoningCallback),
|
||||
types.WithResultCallback(a.options.resultCallback),
|
||||
)
|
||||
|
||||
// Add the reminder message to the job's metadata
|
||||
reminderJob.Metadata = map[string]interface{}{
|
||||
"message": reminder.Message,
|
||||
"is_reminder": true,
|
||||
}
|
||||
|
||||
// Process the reminder as a normal conversation
|
||||
a.consumeJob(reminderJob, UserRole)
|
||||
|
||||
// After the reminder job is complete, ensure the user is notified
|
||||
if reminderJob.Result != nil && reminderJob.Result.Conversation != nil {
|
||||
// Get the last assistant message from the conversation
|
||||
var lastAssistantMsg *openai.ChatCompletionMessage
|
||||
for i := len(reminderJob.Result.Conversation) - 1; i >= 0; i-- {
|
||||
if reminderJob.Result.Conversation[i].Role == AssistantRole {
|
||||
lastAssistantMsg = &reminderJob.Result.Conversation[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if lastAssistantMsg != nil && lastAssistantMsg.Content != "" {
|
||||
// Send the reminder response to the user
|
||||
msg := openai.ChatCompletionMessage{
|
||||
Role: "assistant",
|
||||
Content: fmt.Sprintf("Reminder Update: %s\n\n%s", reminder.Message, lastAssistantMsg.Content),
|
||||
}
|
||||
|
||||
go func(agent *Agent) {
|
||||
xlog.Info("Sending reminder response to user", "agent", agent.Character.Name, "message", msg.Content)
|
||||
agent.newConversations <- msg
|
||||
}(a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !a.options.standaloneJob {
|
||||
return
|
||||
}
|
||||
@@ -1210,12 +1240,26 @@ func (a *Agent) periodicallyRun(timer *time.Timer) {
|
||||
types.WithReasoningCallback(a.options.reasoningCallback),
|
||||
types.WithResultCallback(a.options.resultCallback),
|
||||
)
|
||||
|
||||
// Attach observable so UI can show standalone job progress (decisions, actions, reasoning)
|
||||
if a.observer != nil {
|
||||
obs := a.observer.NewObservable()
|
||||
obs.Name = "standalone"
|
||||
obs.Icon = "clock"
|
||||
a.observer.Update(*obs)
|
||||
whatNext.Obs = obs
|
||||
}
|
||||
|
||||
a.consumeJob(whatNext, SystemRole)
|
||||
|
||||
xlog.Info("STOP -- Periodically run is done", "agent", a.Character.Name)
|
||||
}
|
||||
|
||||
func (a *Agent) Run() error {
|
||||
// Start the scheduler
|
||||
a.taskScheduler.Start()
|
||||
xlog.Info("Task scheduler started")
|
||||
|
||||
a.startNewConversationsConsumer()
|
||||
xlog.Debug("Agent is now running", "agent", a.Character.Name)
|
||||
// The agent run does two things:
|
||||
|
||||
@@ -296,9 +296,9 @@ var _ = Describe("Agent test", func() {
|
||||
WithModel(testModel),
|
||||
WithLLMAPIKey(apiKeyURL),
|
||||
WithTimeout("10m"),
|
||||
WithNewConversationSubscriber(func(m openai.ChatCompletionMessage) {
|
||||
WithNewConversationSubscriber(func(m *types.ConversationMessage) {
|
||||
mu.Lock()
|
||||
message = m
|
||||
message = m.Message
|
||||
mu.Unlock()
|
||||
}),
|
||||
WithActions(
|
||||
|
||||
+162
-6
@@ -1,6 +1,7 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -10,14 +11,24 @@ import (
|
||||
"github.com/mudler/cogito"
|
||||
"github.com/mudler/xlog"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
func (a *Agent) knowledgeBaseLookup(job *types.Job, conv Messages) Messages {
|
||||
if (!a.options.enableKB && !a.options.enableLongTermMemory && !a.options.enableSummaryMemory) ||
|
||||
len(conv) <= 0 {
|
||||
// Only run KB recall/lookup when KB is explicitly enabled; long-term/summary memory
|
||||
// only affect saving in saveConversation, not this lookup.
|
||||
if !a.options.enableKB || len(conv) <= 0 {
|
||||
xlog.Debug("[Knowledge Base Lookup] Disabled, skipping", "agent", a.Character.Name)
|
||||
return conv
|
||||
}
|
||||
if !a.options.kbAutoSearch {
|
||||
xlog.Debug("[Knowledge Base Lookup] Auto search disabled, skipping", "agent", a.Character.Name)
|
||||
return conv
|
||||
}
|
||||
if a.options.ragdb == nil {
|
||||
xlog.Debug("[Knowledge Base Lookup] No RAG DB configured, skipping", "agent", a.Character.Name)
|
||||
return conv
|
||||
}
|
||||
|
||||
var obs *types.Observable
|
||||
if job != nil && job.Obs != nil && a.observer != nil {
|
||||
@@ -137,12 +148,157 @@ func (a *Agent) saveCurrentConversation(conv Messages) {
|
||||
xlog.Error("Error storing into memory", "error", err)
|
||||
}
|
||||
} else {
|
||||
for _, message := range conv {
|
||||
if message.Role == "user" {
|
||||
if err := a.options.ragdb.Store(message.Content); err != nil {
|
||||
xlog.Error("Error storing into memory", "error", err)
|
||||
// Use the conversation storage mode to determine what to store
|
||||
switch a.options.conversationStorageMode {
|
||||
case StoreWholeConversation:
|
||||
// Store the entire conversation as a single block
|
||||
if len(conv) > 0 {
|
||||
convStr := Messages(conv).String()
|
||||
if err := a.options.ragdb.Store(convStr); err != nil {
|
||||
xlog.Error("Error storing whole conversation into memory", "error", err)
|
||||
}
|
||||
}
|
||||
case StoreUserAndAssistant:
|
||||
// Store user and assistant messages separately
|
||||
for _, message := range conv {
|
||||
if message.Role == "user" || message.Role == "assistant" {
|
||||
if err := a.options.ragdb.Store(message.Content); err != nil {
|
||||
xlog.Error("Error storing message into memory", "error", err, "role", message.Role)
|
||||
}
|
||||
}
|
||||
}
|
||||
case StoreUserOnly:
|
||||
fallthrough
|
||||
default:
|
||||
// Store only user messages (default behavior)
|
||||
for _, message := range conv {
|
||||
if message.Role == "user" {
|
||||
if err := a.options.ragdb.Store(message.Content); err != nil {
|
||||
xlog.Error("Error storing into memory", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// KBWrapperActions wraps RAGDB functionality as actions
|
||||
type KBWrapperActions struct {
|
||||
ragdb RAGDB
|
||||
kbResults int
|
||||
}
|
||||
|
||||
type SearchKnowledgeBaseAction struct {
|
||||
*KBWrapperActions
|
||||
}
|
||||
|
||||
type AddToKnowledgeBaseAction struct {
|
||||
*KBWrapperActions
|
||||
}
|
||||
|
||||
// NewKBWrapperActions creates factory functions for KB wrapper actions
|
||||
func NewKBWrapperActions(ragdb RAGDB, kbResults int) (*SearchKnowledgeBaseAction, *AddToKnowledgeBaseAction) {
|
||||
wrapper := &KBWrapperActions{
|
||||
ragdb: ragdb,
|
||||
kbResults: kbResults,
|
||||
}
|
||||
return &SearchKnowledgeBaseAction{wrapper}, &AddToKnowledgeBaseAction{wrapper}
|
||||
}
|
||||
|
||||
func (a *SearchKnowledgeBaseAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
if a.ragdb == nil {
|
||||
return types.ActionResult{}, fmt.Errorf("knowledge base is not configured")
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Query string `json:"query"`
|
||||
}
|
||||
if err := params.Unmarshal(&req); err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("invalid parameters: %w", err)
|
||||
}
|
||||
|
||||
if req.Query == "" {
|
||||
return types.ActionResult{}, fmt.Errorf("query cannot be empty")
|
||||
}
|
||||
|
||||
results, err := a.ragdb.Search(req.Query, a.kbResults)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to search knowledge base: %w", err)
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
return types.ActionResult{
|
||||
Result: fmt.Sprintf("No results found for query: %q", req.Query),
|
||||
}, nil
|
||||
}
|
||||
|
||||
formatResults := ""
|
||||
for i, r := range results {
|
||||
formatResults += fmt.Sprintf("%d. %s\n", i+1, r)
|
||||
}
|
||||
|
||||
return types.ActionResult{
|
||||
Result: fmt.Sprintf("Found %d result(s) for query %q:\n%s", len(results), req.Query, formatResults),
|
||||
Metadata: map[string]interface{}{
|
||||
"query": req.Query,
|
||||
"results": results,
|
||||
"count": len(results),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *SearchKnowledgeBaseAction) Definition() types.ActionDefinition {
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName("search_memory"),
|
||||
Description: "Search your memory for relevant information using a query string",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"query": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The search query to find relevant information in the knowledge base",
|
||||
},
|
||||
},
|
||||
Required: []string{"query"},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AddToKnowledgeBaseAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
if a.ragdb == nil {
|
||||
return types.ActionResult{}, fmt.Errorf("knowledge base is not configured")
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
if err := params.Unmarshal(&req); err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("invalid parameters: %w", err)
|
||||
}
|
||||
|
||||
if req.Content == "" {
|
||||
return types.ActionResult{}, fmt.Errorf("content cannot be empty")
|
||||
}
|
||||
|
||||
if err := a.ragdb.Store(req.Content); err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to store content in knowledge base: %w", err)
|
||||
}
|
||||
|
||||
return types.ActionResult{
|
||||
Result: "Successfully added content to knowledge base",
|
||||
Metadata: map[string]interface{}{
|
||||
"content": req.Content,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *AddToKnowledgeBaseAction) Definition() types.ActionDefinition {
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName("add_memory"),
|
||||
Description: "Add new content to your memory for future retrieval",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"content": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The content to store in the knowledge base",
|
||||
},
|
||||
},
|
||||
Required: []string{"content"},
|
||||
}
|
||||
}
|
||||
|
||||
+13
-11
@@ -25,6 +25,7 @@ type MCPServer struct {
|
||||
}
|
||||
|
||||
type MCPSTDIOServer struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Args []string `json:"args"`
|
||||
Env []string `json:"env"`
|
||||
Cmd string `json:"cmd"`
|
||||
@@ -136,7 +137,7 @@ 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.closeMCPServers() // Make sure we stop all previous servers if any is active
|
||||
|
||||
a.mcpActionDefinitions = nil
|
||||
var err error
|
||||
@@ -154,13 +155,17 @@ func (a *Agent) initMCPActions() error {
|
||||
Transport: newBearerTokenRoundTripper(mcpServer.Token, http.DefaultTransport),
|
||||
}
|
||||
|
||||
transport := &mcp.SSEClientTransport{HTTPClient: httpclient, Endpoint: mcpServer.URL}
|
||||
|
||||
// Create a new client
|
||||
session, err := client.Connect(a.context, transport, nil)
|
||||
streamableTransport := &mcp.StreamableClientTransport{HTTPClient: httpclient, Endpoint: mcpServer.URL}
|
||||
session, err := client.Connect(a.context, streamableTransport, nil)
|
||||
if err != nil {
|
||||
xlog.Error("Failed to connect to MCP server", "server", mcpServer, "error", err.Error())
|
||||
continue
|
||||
xlog.Error("Failed to connect to MCP server via StreamableClientTransport", "server", mcpServer, "error", err.Error())
|
||||
|
||||
sseTransport := &mcp.SSEClientTransport{HTTPClient: httpclient, Endpoint: mcpServer.URL}
|
||||
session, err = client.Connect(a.context, sseTransport, nil)
|
||||
if err != nil {
|
||||
xlog.Error("Failed to connect to MCP server via SSEClientTransport", "server", mcpServer, "error", err.Error())
|
||||
continue
|
||||
}
|
||||
}
|
||||
a.mcpSessions = append(a.mcpSessions, session)
|
||||
|
||||
@@ -173,9 +178,6 @@ func (a *Agent) initMCPActions() error {
|
||||
}
|
||||
|
||||
// MCP STDIO Servers
|
||||
|
||||
a.closeMCPSTDIOServers() // Make sure we stop all previous servers if any is active
|
||||
|
||||
if a.options.mcpPrepareScript != "" {
|
||||
xlog.Debug("Preparing MCP", "script", a.options.mcpPrepareScript)
|
||||
|
||||
@@ -214,7 +216,7 @@ func (a *Agent) initMCPActions() error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *Agent) closeMCPSTDIOServers() {
|
||||
func (a *Agent) closeMCPServers() {
|
||||
for _, s := range a.mcpSessions {
|
||||
s.Close()
|
||||
}
|
||||
|
||||
+86
-8
@@ -6,16 +6,28 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
type Option func(*options) error
|
||||
|
||||
// ConversationStorageMode defines how conversations are stored in the knowledge base
|
||||
type ConversationStorageMode string
|
||||
|
||||
const (
|
||||
// StoreUserOnly stores only user messages (default)
|
||||
StoreUserOnly ConversationStorageMode = "user_only"
|
||||
// StoreUserAndAssistant stores both user and assistant messages separately
|
||||
StoreUserAndAssistant ConversationStorageMode = "user_and_assistant"
|
||||
// StoreWholeConversation stores the entire conversation as a single block
|
||||
StoreWholeConversation ConversationStorageMode = "whole_conversation"
|
||||
)
|
||||
|
||||
type llmOptions struct {
|
||||
APIURL string
|
||||
APIKey string
|
||||
Model string
|
||||
MultimodalModel string
|
||||
ReviewerModel string
|
||||
TranscriptionModel string
|
||||
TranscriptionLanguage string
|
||||
TTSModel string
|
||||
@@ -30,17 +42,23 @@ type options struct {
|
||||
jobFilters types.JobFilters
|
||||
enableHUD, standaloneJob, showCharacter, enableKB, enableSummaryMemory, enableLongTermMemory bool
|
||||
stripThinkingTags bool
|
||||
kbAutoSearch bool
|
||||
conversationStorageMode ConversationStorageMode
|
||||
|
||||
canStopItself bool
|
||||
initiateConversations bool
|
||||
forceReasoning bool
|
||||
enableGuidedTools bool
|
||||
canPlan bool
|
||||
disableSinkState bool
|
||||
characterfile string
|
||||
statefile string
|
||||
schedulerStorePath string // Path to scheduler JSON storage file
|
||||
context context.Context
|
||||
permanentGoal string
|
||||
timeout string
|
||||
periodicRuns time.Duration
|
||||
schedulerPollInterval time.Duration
|
||||
kbResults int
|
||||
ragdb RAGDB
|
||||
|
||||
@@ -61,7 +79,7 @@ type options struct {
|
||||
mcpServers []MCPServer
|
||||
mcpStdioServers []MCPSTDIOServer
|
||||
mcpPrepareScript string
|
||||
newConversationsSubscribers []func(openai.ChatCompletionMessage)
|
||||
newConversationsSubscribers []func(*types.ConversationMessage)
|
||||
|
||||
observer Observer
|
||||
parallelJobs int
|
||||
@@ -75,10 +93,13 @@ func (o *options) SeparatedMultimodalModel() bool {
|
||||
|
||||
func defaultOptions() *options {
|
||||
return &options{
|
||||
parallelJobs: 1,
|
||||
periodicRuns: 15 * time.Minute,
|
||||
maxEvaluationLoops: 2,
|
||||
enableEvaluation: false,
|
||||
parallelJobs: 1,
|
||||
periodicRuns: 15 * time.Minute,
|
||||
schedulerPollInterval: 30 * time.Second,
|
||||
maxEvaluationLoops: 2,
|
||||
enableEvaluation: false,
|
||||
kbAutoSearch: true, // Default to true to maintain backward compatibility
|
||||
conversationStorageMode: StoreUserOnly, // Default to user-only for backward compatibility
|
||||
LLMAPI: llmOptions{
|
||||
APIURL: "http://localhost:8080",
|
||||
Model: "gpt-4",
|
||||
@@ -116,6 +137,11 @@ var EnableForceReasoning = func(o *options) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var EnableGuidedTools = func(o *options) error {
|
||||
o.enableGuidedTools = true
|
||||
return nil
|
||||
}
|
||||
|
||||
var EnableKnowledgeBase = func(o *options) error {
|
||||
o.enableKB = true
|
||||
o.kbResults = 5
|
||||
@@ -167,7 +193,7 @@ func WithParallelJobs(jobs int) Option {
|
||||
}
|
||||
}
|
||||
|
||||
func WithNewConversationSubscriber(sub func(openai.ChatCompletionMessage)) Option {
|
||||
func WithNewConversationSubscriber(sub func(*types.ConversationMessage)) Option {
|
||||
return func(o *options) error {
|
||||
o.newConversationsSubscribers = append(o.newConversationsSubscribers, sub)
|
||||
return nil
|
||||
@@ -184,6 +210,18 @@ var EnablePlanning = func(o *options) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var DisableSinkState = func(o *options) error {
|
||||
o.disableSinkState = true
|
||||
return nil
|
||||
}
|
||||
|
||||
var WithPlanReviewerLLM = func(model string) Option {
|
||||
return func(o *options) error {
|
||||
o.LLMAPI.ReviewerModel = model
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// EnableStandaloneJob is an option to enable the agent
|
||||
// to run jobs in the background automatically
|
||||
var EnableStandaloneJob = func(o *options) error {
|
||||
@@ -213,6 +251,19 @@ func WithRAGDB(db RAGDB) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithConversationStorageMode sets how conversations are stored in the knowledge base
|
||||
func WithConversationStorageMode(mode ConversationStorageMode) Option {
|
||||
return func(o *options) error {
|
||||
switch mode {
|
||||
case StoreUserOnly, StoreUserAndAssistant, StoreWholeConversation:
|
||||
o.conversationStorageMode = mode
|
||||
default:
|
||||
o.conversationStorageMode = StoreUserOnly
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithSystemPrompt(prompt string) Option {
|
||||
return func(o *options) error {
|
||||
o.systemPrompt = prompt
|
||||
@@ -320,6 +371,18 @@ func WithPeriodicRuns(duration string) Option {
|
||||
}
|
||||
}
|
||||
|
||||
func WithSchedulerPollInterval(duration string) Option {
|
||||
return func(o *options) error {
|
||||
t, err := time.ParseDuration(duration)
|
||||
if err != nil {
|
||||
o.schedulerPollInterval = 30 * time.Second
|
||||
return nil
|
||||
}
|
||||
o.schedulerPollInterval = t
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithContext(ctx context.Context) Option {
|
||||
return func(o *options) error {
|
||||
o.context = ctx
|
||||
@@ -377,7 +440,7 @@ func WithRandomIdentity(guidance ...string) Option {
|
||||
|
||||
func WithActions(actions ...types.Action) Option {
|
||||
return func(o *options) error {
|
||||
o.userActions = actions
|
||||
o.userActions = append(o.userActions, actions...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -435,3 +498,18 @@ func WithTTSModel(model string) Option {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithKBAutoSearch(enabled bool) Option {
|
||||
return func(o *options) error {
|
||||
o.kbAutoSearch = enabled
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithSchedulerStorePath sets the path for the scheduler's JSON storage file
|
||||
func WithSchedulerStorePath(path string) Option {
|
||||
return func(o *options) error {
|
||||
o.schedulerStorePath = path
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/scheduler"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
)
|
||||
|
||||
// agentSchedulerExecutor implements scheduler.AgentExecutor for executing scheduled tasks through the agent
|
||||
type agentSchedulerExecutor struct {
|
||||
agent *Agent
|
||||
}
|
||||
|
||||
// Execute processes a scheduled task by creating a job for the agent
|
||||
func (e *agentSchedulerExecutor) Execute(ctx context.Context, agentName string, prompt string) (*scheduler.JobResult, error) {
|
||||
// Create a job for the reminder
|
||||
reminderJob := types.NewJob(
|
||||
types.WithText(fmt.Sprintf("You need to execute the following task, by using the tools available to you. When the task is completed, you need to send a message to the user with send_message tool to inform them that the task is completed: %s", prompt)),
|
||||
types.WithReasoningCallback(e.agent.options.reasoningCallback),
|
||||
types.WithResultCallback(e.agent.options.resultCallback),
|
||||
types.WithContext(ctx),
|
||||
types.WithMetadata(map[string]any{
|
||||
"message": prompt,
|
||||
"is_reminder": true,
|
||||
"type": "scheduled",
|
||||
}),
|
||||
)
|
||||
|
||||
// Attach observable so UI can show reminder processing state
|
||||
if e.agent.observer != nil {
|
||||
obs := e.agent.observer.NewObservable()
|
||||
obs.Name = "reminder"
|
||||
obs.Icon = "bell"
|
||||
e.agent.observer.Update(*obs)
|
||||
reminderJob.Obs = obs
|
||||
}
|
||||
|
||||
// Send the job to be processed
|
||||
e.agent.jobQueue <- reminderJob
|
||||
|
||||
// Wait for the job to complete or context to be cancelled
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
result, err := reminderJob.Result.WaitResult(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.Error != nil {
|
||||
return &scheduler.JobResult{
|
||||
Response: "",
|
||||
Error: result.Error,
|
||||
}, result.Error
|
||||
}
|
||||
return &scheduler.JobResult{
|
||||
Response: result.Response,
|
||||
Error: nil,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package scheduler
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// TaskStore defines the interface for task persistence
|
||||
type TaskStore interface {
|
||||
// Create adds a new task
|
||||
Create(task *Task) error
|
||||
|
||||
// Get retrieves a task by ID
|
||||
Get(id string) (*Task, error)
|
||||
|
||||
// GetAll retrieves all tasks
|
||||
GetAll() ([]*Task, error)
|
||||
|
||||
// GetDue retrieves tasks that are due for execution
|
||||
GetDue() ([]*Task, error)
|
||||
|
||||
// GetByAgent retrieves all tasks for a specific agent
|
||||
GetByAgent(agentName string) ([]*Task, error)
|
||||
|
||||
// Update updates an existing task
|
||||
Update(task *Task) error
|
||||
|
||||
// Delete removes a task
|
||||
Delete(id string) error
|
||||
|
||||
// LogRun records a task execution
|
||||
LogRun(run *TaskRun) error
|
||||
|
||||
// GetRuns retrieves execution history for a task
|
||||
GetRuns(taskID string, limit int) ([]*TaskRun, error)
|
||||
|
||||
// Close releases resources
|
||||
Close() error
|
||||
}
|
||||
|
||||
// AgentExecutor defines the interface for executing agent tasks
|
||||
type AgentExecutor interface {
|
||||
Execute(ctx context.Context, agentName string, prompt string) (*JobResult, error)
|
||||
}
|
||||
|
||||
// JobResult represents the result of an agent execution
|
||||
type JobResult struct {
|
||||
Response string
|
||||
Error error
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package scheduler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// JSONStore implements TaskStore using JSON file storage
|
||||
type JSONStore struct {
|
||||
filePath string
|
||||
mu sync.RWMutex
|
||||
data *storeData
|
||||
}
|
||||
|
||||
type storeData struct {
|
||||
Tasks []*Task `json:"tasks"`
|
||||
TaskRuns []*TaskRun `json:"task_runs"`
|
||||
}
|
||||
|
||||
// NewJSONStore creates a new JSON-based task store
|
||||
func NewJSONStore(filePath string) (*JSONStore, error) {
|
||||
store := &JSONStore{
|
||||
filePath: filePath,
|
||||
data: &storeData{
|
||||
Tasks: make([]*Task, 0),
|
||||
TaskRuns: make([]*TaskRun, 0),
|
||||
},
|
||||
}
|
||||
|
||||
if err := store.load(); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("failed to load store: %w", err)
|
||||
}
|
||||
// File doesn't exist, create it
|
||||
if err := store.save(); err != nil {
|
||||
return nil, fmt.Errorf("failed to create store file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return store, nil
|
||||
}
|
||||
|
||||
// Create adds a new task
|
||||
func (s *JSONStore) Create(task *Task) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// Check for duplicate ID
|
||||
for _, t := range s.data.Tasks {
|
||||
if t.ID == task.ID {
|
||||
return fmt.Errorf("task with ID %s already exists", task.ID)
|
||||
}
|
||||
}
|
||||
|
||||
s.data.Tasks = append(s.data.Tasks, task)
|
||||
return s.save()
|
||||
}
|
||||
|
||||
// Get retrieves a task by ID
|
||||
func (s *JSONStore) Get(id string) (*Task, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
for _, task := range s.data.Tasks {
|
||||
if task.ID == id {
|
||||
return task, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("task not found: %s", id)
|
||||
}
|
||||
|
||||
// GetAll retrieves all tasks
|
||||
func (s *JSONStore) GetAll() ([]*Task, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
// Return a copy to prevent external modification
|
||||
tasks := make([]*Task, len(s.data.Tasks))
|
||||
copy(tasks, s.data.Tasks)
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
// GetDue retrieves tasks that are due for execution
|
||||
func (s *JSONStore) GetDue() ([]*Task, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
now := time.Now()
|
||||
dueTasks := make([]*Task, 0)
|
||||
|
||||
for _, task := range s.data.Tasks {
|
||||
if task.Status == TaskStatusActive && now.After(task.NextRun) {
|
||||
dueTasks = append(dueTasks, task)
|
||||
}
|
||||
}
|
||||
|
||||
return dueTasks, nil
|
||||
}
|
||||
|
||||
// GetByAgent retrieves all tasks for a specific agent
|
||||
func (s *JSONStore) GetByAgent(agentName string) ([]*Task, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
agentTasks := make([]*Task, 0)
|
||||
for _, task := range s.data.Tasks {
|
||||
if task.AgentName == agentName {
|
||||
agentTasks = append(agentTasks, task)
|
||||
}
|
||||
}
|
||||
|
||||
return agentTasks, nil
|
||||
}
|
||||
|
||||
// Update updates an existing task
|
||||
func (s *JSONStore) Update(task *Task) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
for i, t := range s.data.Tasks {
|
||||
if t.ID == task.ID {
|
||||
task.UpdatedAt = time.Now()
|
||||
s.data.Tasks[i] = task
|
||||
return s.save()
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("task not found: %s", task.ID)
|
||||
}
|
||||
|
||||
// Delete removes a task
|
||||
func (s *JSONStore) Delete(id string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
for i, task := range s.data.Tasks {
|
||||
if task.ID == id {
|
||||
// Remove task from slice
|
||||
s.data.Tasks = append(s.data.Tasks[:i], s.data.Tasks[i+1:]...)
|
||||
return s.save()
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("task not found: %s", id)
|
||||
}
|
||||
|
||||
// LogRun records a task execution
|
||||
func (s *JSONStore) LogRun(run *TaskRun) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.data.TaskRuns = append(s.data.TaskRuns, run)
|
||||
return s.save()
|
||||
}
|
||||
|
||||
// GetRuns retrieves execution history for a task
|
||||
func (s *JSONStore) GetRuns(taskID string, limit int) ([]*TaskRun, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
runs := make([]*TaskRun, 0)
|
||||
for i := len(s.data.TaskRuns) - 1; i >= 0 && len(runs) < limit; i-- {
|
||||
if s.data.TaskRuns[i].TaskID == taskID {
|
||||
runs = append(runs, s.data.TaskRuns[i])
|
||||
}
|
||||
}
|
||||
|
||||
return runs, nil
|
||||
}
|
||||
|
||||
// Close releases resources (no-op for JSON store)
|
||||
func (s *JSONStore) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// load reads data from the JSON file
|
||||
func (s *JSONStore) load() error {
|
||||
file, err := os.ReadFile(s.filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Handle empty file
|
||||
if len(file) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(file, s.data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure slices are not nil after unmarshaling
|
||||
if s.data.Tasks == nil {
|
||||
s.data.Tasks = make([]*Task, 0)
|
||||
}
|
||||
if s.data.TaskRuns == nil {
|
||||
s.data.TaskRuns = make([]*TaskRun, 0)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// save writes data to the JSON file
|
||||
func (s *JSONStore) save() error {
|
||||
data, err := json.MarshalIndent(s.data, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal data: %w", err)
|
||||
}
|
||||
|
||||
basePath := filepath.Dir(s.filePath)
|
||||
os.MkdirAll(basePath, 0755)
|
||||
|
||||
return os.WriteFile(s.filePath, data, 0644)
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
package scheduler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mudler/xlog"
|
||||
)
|
||||
|
||||
// Scheduler manages scheduled tasks
|
||||
type Scheduler struct {
|
||||
store TaskStore
|
||||
executor AgentExecutor
|
||||
pollInterval time.Duration
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
mu sync.RWMutex
|
||||
runningTasks map[string]context.CancelFunc
|
||||
}
|
||||
|
||||
// NewScheduler creates a new scheduler with the given store and executor
|
||||
func NewScheduler(store TaskStore, executor AgentExecutor, pollInterval time.Duration) *Scheduler {
|
||||
|
||||
return &Scheduler{
|
||||
store: store,
|
||||
executor: executor,
|
||||
pollInterval: pollInterval,
|
||||
runningTasks: make(map[string]context.CancelFunc),
|
||||
}
|
||||
}
|
||||
|
||||
// Start begins the scheduler's polling loop
|
||||
func (s *Scheduler) Start() {
|
||||
if s.ctx != nil {
|
||||
xlog.Warn("Scheduler already started")
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
s.ctx = ctx
|
||||
s.cancel = cancel
|
||||
s.wg.Add(1)
|
||||
go s.run()
|
||||
xlog.Info("Task scheduler started", "poll_interval", s.pollInterval)
|
||||
}
|
||||
|
||||
// Stop gracefully stops the scheduler
|
||||
func (s *Scheduler) Stop() {
|
||||
if s.cancel != nil {
|
||||
s.cancel()
|
||||
}
|
||||
s.wg.Wait()
|
||||
s.store.Close()
|
||||
xlog.Info("Task scheduler stopped")
|
||||
s.cancel = nil
|
||||
s.ctx = nil
|
||||
}
|
||||
|
||||
// run is the main polling loop
|
||||
func (s *Scheduler) run() {
|
||||
defer s.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(s.pollInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
s.processDueTasks()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processDueTasks checks for and executes due tasks
|
||||
func (s *Scheduler) processDueTasks() {
|
||||
tasks, err := s.store.GetDue()
|
||||
if err != nil {
|
||||
xlog.Error("Failed to get due tasks", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(tasks) > 0 {
|
||||
xlog.Debug("Processing due tasks", "count", len(tasks))
|
||||
}
|
||||
|
||||
for _, task := range tasks {
|
||||
// Check if task is already running
|
||||
s.mu.RLock()
|
||||
_, running := s.runningTasks[task.ID]
|
||||
s.mu.RUnlock()
|
||||
|
||||
if running {
|
||||
xlog.Warn("Task already running, skipping", "task_id", task.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
// Execute task in goroutine
|
||||
s.wg.Add(1)
|
||||
go s.executeTask(task)
|
||||
}
|
||||
}
|
||||
|
||||
// executeTask runs a single task
|
||||
func (s *Scheduler) executeTask(task *Task) {
|
||||
defer s.wg.Done()
|
||||
|
||||
taskCtx, cancel := context.WithCancel(s.ctx)
|
||||
defer cancel()
|
||||
|
||||
// Register running task
|
||||
s.mu.Lock()
|
||||
s.runningTasks[task.ID] = cancel
|
||||
s.mu.Unlock()
|
||||
|
||||
defer func() {
|
||||
s.mu.Lock()
|
||||
delete(s.runningTasks, task.ID)
|
||||
s.mu.Unlock()
|
||||
}()
|
||||
|
||||
xlog.Info("Executing task", "task_id", task.ID, "agent", task.AgentName, "prompt", task.Prompt)
|
||||
|
||||
startTime := time.Now()
|
||||
run := NewTaskRun(task.ID)
|
||||
|
||||
// Execute the task
|
||||
result, err := s.executor.Execute(taskCtx, task.AgentName, task.Prompt)
|
||||
|
||||
run.DurationMs = time.Since(startTime).Milliseconds()
|
||||
|
||||
if err != nil {
|
||||
run.Status = "error"
|
||||
run.Error = err.Error()
|
||||
xlog.Error("Task execution failed", "task_id", task.ID, "error", err)
|
||||
} else {
|
||||
run.Status = "success"
|
||||
if result != nil {
|
||||
run.Result = result.Response
|
||||
}
|
||||
xlog.Info("Task executed successfully", "task_id", task.ID, "duration_ms", run.DurationMs)
|
||||
}
|
||||
|
||||
// Log the run
|
||||
if err := s.store.LogRun(run); err != nil {
|
||||
xlog.Error("Failed to log task run", "task_id", task.ID, "error", err)
|
||||
}
|
||||
|
||||
// Update task for next run
|
||||
now := time.Now()
|
||||
task.LastRun = &now
|
||||
|
||||
// For one-time tasks, mark as deleted
|
||||
if task.ScheduleType == ScheduleTypeOnce {
|
||||
if err := s.store.Delete(task.ID); err != nil {
|
||||
xlog.Error("Failed to delete task", "task_id", task.ID, "error", err)
|
||||
}
|
||||
} else {
|
||||
// Calculate next run
|
||||
if err := task.CalculateNextRun(); err != nil {
|
||||
xlog.Error("Failed to calculate next run", "task_id", task.ID, "error", err)
|
||||
task.Status = TaskStatusPaused
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.store.Update(task); err != nil {
|
||||
xlog.Error("Failed to update task", "task_id", task.ID, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// CRUD operations
|
||||
|
||||
// CreateTask adds a new task
|
||||
func (s *Scheduler) CreateTask(task *Task) error {
|
||||
return s.store.Create(task)
|
||||
}
|
||||
|
||||
// GetTask retrieves a task by ID
|
||||
func (s *Scheduler) GetTask(id string) (*Task, error) {
|
||||
return s.store.Get(id)
|
||||
}
|
||||
|
||||
// GetAllTasks retrieves all tasks
|
||||
func (s *Scheduler) GetAllTasks() ([]*Task, error) {
|
||||
return s.store.GetAll()
|
||||
}
|
||||
|
||||
// GetTasksByAgent retrieves all tasks for a specific agent
|
||||
func (s *Scheduler) GetTasksByAgent(agentName string) ([]*Task, error) {
|
||||
return s.store.GetByAgent(agentName)
|
||||
}
|
||||
|
||||
// UpdateTask updates an existing task
|
||||
func (s *Scheduler) UpdateTask(task *Task) error {
|
||||
return s.store.Update(task)
|
||||
}
|
||||
|
||||
// DeleteTask removes a task
|
||||
func (s *Scheduler) DeleteTask(id string) error {
|
||||
return s.store.Delete(id)
|
||||
}
|
||||
|
||||
// GetTaskRuns retrieves execution history for a task
|
||||
func (s *Scheduler) GetTaskRuns(taskID string, limit int) ([]*TaskRun, error) {
|
||||
return s.store.GetRuns(taskID, limit)
|
||||
}
|
||||
|
||||
// PauseTask pauses a task
|
||||
func (s *Scheduler) PauseTask(id string) error {
|
||||
task, err := s.store.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
task.Status = TaskStatusPaused
|
||||
return s.store.Update(task)
|
||||
}
|
||||
|
||||
// ResumeTask resumes a paused task
|
||||
func (s *Scheduler) ResumeTask(id string) error {
|
||||
task, err := s.store.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
task.Status = TaskStatusActive
|
||||
if err := task.CalculateNextRun(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.store.Update(task)
|
||||
}
|
||||
|
||||
// CancelRunningTask cancels a currently running task
|
||||
func (s *Scheduler) CancelRunningTask(id string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
cancel, exists := s.runningTasks[id]
|
||||
if !exists {
|
||||
return fmt.Errorf("task not running: %s", id)
|
||||
}
|
||||
|
||||
cancel()
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package scheduler_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestScheduler(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Scheduler Suite")
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
package scheduler_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/scheduler"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
// MockExecutor for testing
|
||||
type MockExecutor struct {
|
||||
executedTasks []string
|
||||
shouldError bool
|
||||
}
|
||||
|
||||
func (m *MockExecutor) Execute(ctx context.Context, agentName string, prompt string) (*scheduler.JobResult, error) {
|
||||
m.executedTasks = append(m.executedTasks, agentName+":"+prompt)
|
||||
if m.shouldError {
|
||||
return nil, errors.New("mock execution error")
|
||||
}
|
||||
return &scheduler.JobResult{Response: "test response"}, nil
|
||||
}
|
||||
|
||||
var _ = Describe("Scheduler", func() {
|
||||
var (
|
||||
tempFile string
|
||||
store scheduler.TaskStore
|
||||
executor *MockExecutor
|
||||
sched *scheduler.Scheduler
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
// Create temporary file for JSON store
|
||||
f, err := os.CreateTemp("", "scheduler_test_*.json")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
tempFile = f.Name()
|
||||
f.Close()
|
||||
|
||||
store, err = scheduler.NewJSONStore(tempFile)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
executor = &MockExecutor{}
|
||||
sched = scheduler.NewScheduler(store, executor, 100*time.Millisecond)
|
||||
sched.Start()
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
if sched != nil {
|
||||
sched.Stop()
|
||||
}
|
||||
os.Remove(tempFile)
|
||||
})
|
||||
|
||||
Describe("Task Creation", func() {
|
||||
It("should create a valid task with cron schedule", func() {
|
||||
task, err := scheduler.NewTask("test-agent", "test prompt", scheduler.ScheduleTypeCron, "0 0 * * *")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(task.ID).NotTo(BeEmpty())
|
||||
Expect(task.AgentName).To(Equal("test-agent"))
|
||||
Expect(task.Prompt).To(Equal("test prompt"))
|
||||
Expect(task.ScheduleType).To(Equal(scheduler.ScheduleTypeCron))
|
||||
Expect(task.Status).To(Equal(scheduler.TaskStatusActive))
|
||||
Expect(task.NextRun).NotTo(BeZero())
|
||||
})
|
||||
|
||||
It("should return error for invalid cron expression", func() {
|
||||
_, err := scheduler.NewTask("test-agent", "test prompt", scheduler.ScheduleTypeCron, "invalid cron")
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("should create a valid task with interval schedule", func() {
|
||||
task, err := scheduler.NewTask("test-agent", "test prompt", scheduler.ScheduleTypeInterval, "3600000")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(task.NextRun).To(BeTemporally("~", time.Now().Add(time.Hour), 5*time.Second))
|
||||
})
|
||||
|
||||
It("should create a valid task with once schedule using duration", func() {
|
||||
task, err := scheduler.NewTask("test-agent", "test prompt", scheduler.ScheduleTypeOnce, "24h")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(task.NextRun).To(BeTemporally("~", time.Now().Add(24*time.Hour), 5*time.Second))
|
||||
})
|
||||
|
||||
It("should create a valid task with once schedule using day syntax", func() {
|
||||
task, err := scheduler.NewTask("test-agent", "test prompt", scheduler.ScheduleTypeOnce, "1d")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(task.NextRun).To(BeTemporally("~", time.Now().Add(24*time.Hour), 5*time.Second))
|
||||
})
|
||||
|
||||
It("should create a valid task with once schedule using combined day+time", func() {
|
||||
task, err := scheduler.NewTask("test-agent", "test prompt", scheduler.ScheduleTypeOnce, "2d12h30m")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(task.NextRun).To(BeTemporally("~", time.Now().Add(2*24*time.Hour+12*time.Hour+30*time.Minute), 5*time.Second))
|
||||
})
|
||||
|
||||
It("should return error for invalid once duration", func() {
|
||||
_, err := scheduler.NewTask("test-agent", "test prompt", scheduler.ScheduleTypeOnce, "invalid")
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Task IsDue", func() {
|
||||
It("should return true for active task past due time", func() {
|
||||
task := &scheduler.Task{
|
||||
Status: scheduler.TaskStatusActive,
|
||||
NextRun: time.Now().Add(-1 * time.Hour),
|
||||
}
|
||||
Expect(task.IsDue()).To(BeTrue())
|
||||
})
|
||||
|
||||
It("should return false for active task not yet due", func() {
|
||||
task := &scheduler.Task{
|
||||
Status: scheduler.TaskStatusActive,
|
||||
NextRun: time.Now().Add(1 * time.Hour),
|
||||
}
|
||||
Expect(task.IsDue()).To(BeFalse())
|
||||
})
|
||||
|
||||
It("should return false for paused task even if past due", func() {
|
||||
task := &scheduler.Task{
|
||||
Status: scheduler.TaskStatusPaused,
|
||||
NextRun: time.Now().Add(-1 * time.Hour),
|
||||
}
|
||||
Expect(task.IsDue()).To(BeFalse())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("JSON Store", func() {
|
||||
Context("CRUD operations", func() {
|
||||
It("should create and retrieve a task", func() {
|
||||
task, err := scheduler.NewTask("test-agent", "test prompt", scheduler.ScheduleTypeCron, "0 0 * * *")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
err = store.Create(task)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
retrieved, err := store.Get(task.ID)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(retrieved.ID).To(Equal(task.ID))
|
||||
Expect(retrieved.AgentName).To(Equal(task.AgentName))
|
||||
Expect(retrieved.Prompt).To(Equal(task.Prompt))
|
||||
})
|
||||
|
||||
It("should update a task", func() {
|
||||
task, _ := scheduler.NewTask("test-agent", "test prompt", scheduler.ScheduleTypeCron, "0 0 * * *")
|
||||
store.Create(task)
|
||||
|
||||
task.Prompt = "updated prompt"
|
||||
err := store.Update(task)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
updated, err := store.Get(task.ID)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(updated.Prompt).To(Equal("updated prompt"))
|
||||
})
|
||||
|
||||
It("should delete a task", func() {
|
||||
task, _ := scheduler.NewTask("test-agent", "test prompt", scheduler.ScheduleTypeCron, "0 0 * * *")
|
||||
store.Create(task)
|
||||
|
||||
err := store.Delete(task.ID)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
_, err = store.Get(task.ID)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("should return error when getting non-existent task", func() {
|
||||
_, err := store.Get("non-existent-id")
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Context("Querying tasks", func() {
|
||||
BeforeEach(func() {
|
||||
// Create test tasks
|
||||
// task1: once schedule with 0s delay => immediately due
|
||||
task1, err := scheduler.NewTask("agent1", "prompt1", scheduler.ScheduleTypeOnce, "0s")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
task1.NextRun = time.Now().Add(-1 * time.Hour) // force into the past
|
||||
// task2: cron schedule => next run in the future, not due
|
||||
task2, err := scheduler.NewTask("agent2", "prompt2", scheduler.ScheduleTypeCron, "0 0 1 1 *")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
// task3: once schedule but paused => not due
|
||||
task3, err := scheduler.NewTask("agent1", "prompt3", scheduler.ScheduleTypeOnce, "0s")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
task3.Status = scheduler.TaskStatusPaused
|
||||
|
||||
Expect(store.Create(task1)).To(Succeed())
|
||||
Expect(store.Create(task2)).To(Succeed())
|
||||
Expect(store.Create(task3)).To(Succeed())
|
||||
})
|
||||
|
||||
It("should get all tasks", func() {
|
||||
tasks, err := store.GetAll()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(tasks).To(HaveLen(3))
|
||||
})
|
||||
|
||||
It("should get only due tasks", func() {
|
||||
dueTasks, err := store.GetDue()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(dueTasks).To(HaveLen(1))
|
||||
Expect(dueTasks[0].AgentName).To(Equal("agent1"))
|
||||
Expect(dueTasks[0].Prompt).To(Equal("prompt1"))
|
||||
})
|
||||
|
||||
It("should get tasks by agent", func() {
|
||||
agentTasks, err := store.GetByAgent("agent1")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(agentTasks).To(HaveLen(2))
|
||||
})
|
||||
})
|
||||
|
||||
Context("Task runs", func() {
|
||||
It("should log and retrieve task runs", func() {
|
||||
task, _ := scheduler.NewTask("test-agent", "test prompt", scheduler.ScheduleTypeCron, "0 0 * * *")
|
||||
store.Create(task)
|
||||
|
||||
run := scheduler.NewTaskRun(task.ID)
|
||||
run.Status = "success"
|
||||
run.Result = "test result"
|
||||
run.DurationMs = 1000
|
||||
|
||||
err := store.LogRun(run)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
runs, err := store.GetRuns(task.ID, 10)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(runs).To(HaveLen(1))
|
||||
Expect(runs[0].Status).To(Equal("success"))
|
||||
Expect(runs[0].Result).To(Equal("test result"))
|
||||
})
|
||||
|
||||
It("should limit returned runs", func() {
|
||||
task, _ := scheduler.NewTask("test-agent", "test prompt", scheduler.ScheduleTypeCron, "0 0 * * *")
|
||||
store.Create(task)
|
||||
|
||||
// Create 5 runs
|
||||
for i := 0; i < 5; i++ {
|
||||
run := scheduler.NewTaskRun(task.ID)
|
||||
store.LogRun(run)
|
||||
}
|
||||
|
||||
runs, err := store.GetRuns(task.ID, 3)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(runs).To(HaveLen(3))
|
||||
})
|
||||
})
|
||||
|
||||
Context("Persistence", func() {
|
||||
It("should persist data across store instances", func() {
|
||||
task, _ := scheduler.NewTask("test-agent", "test prompt", scheduler.ScheduleTypeCron, "0 0 * * *")
|
||||
store.Create(task)
|
||||
store.Close()
|
||||
|
||||
// Create new store instance with same file
|
||||
newStore, err := scheduler.NewJSONStore(tempFile)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer newStore.Close()
|
||||
|
||||
retrieved, err := newStore.Get(task.ID)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(retrieved.ID).To(Equal(task.ID))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Scheduler Execution", func() {
|
||||
It("should execute a due task", func() {
|
||||
task, _ := scheduler.NewTask("test-agent", "test prompt", scheduler.ScheduleTypeOnce, "0s")
|
||||
task.NextRun = time.Now().Add(-1 * time.Second) // force into the past
|
||||
err := sched.CreateTask(task)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Scheduler is already started in BeforeEach
|
||||
|
||||
Eventually(func() int {
|
||||
return len(executor.executedTasks)
|
||||
}, "2s", "100ms").Should(Equal(1))
|
||||
|
||||
Expect(executor.executedTasks[0]).To(Equal("test-agent:test prompt"))
|
||||
|
||||
// Verify task run was logged
|
||||
runs, err := sched.GetTaskRuns(task.ID, 10)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(runs).To(HaveLen(1))
|
||||
Expect(runs[0].Status).To(Equal("success"))
|
||||
|
||||
// Verify one-time task was deleted
|
||||
_, err = sched.GetTask(task.ID)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("should execute recurring tasks multiple times", func() {
|
||||
task, _ := scheduler.NewTask("test-agent", "recurring", scheduler.ScheduleTypeInterval, "500")
|
||||
task.NextRun = time.Now().Add(-1 * time.Second)
|
||||
err := sched.CreateTask(task)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Scheduler is already started in BeforeEach
|
||||
|
||||
Eventually(func() int {
|
||||
return len(executor.executedTasks)
|
||||
}, "3s", "100ms").Should(BeNumerically(">=", 2))
|
||||
|
||||
// Verify task is still active
|
||||
updatedTask, err := sched.GetTask(task.ID)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(updatedTask.Status).To(Equal(scheduler.TaskStatusActive))
|
||||
})
|
||||
|
||||
It("should handle task execution errors", func() {
|
||||
executor.shouldError = true
|
||||
task, _ := scheduler.NewTask("test-agent", "error task", scheduler.ScheduleTypeOnce, "0s")
|
||||
task.NextRun = time.Now().Add(-1 * time.Second) // force into the past
|
||||
sched.CreateTask(task)
|
||||
|
||||
// Scheduler is already started in BeforeEach
|
||||
|
||||
Eventually(func() int {
|
||||
runs, _ := sched.GetTaskRuns(task.ID, 10)
|
||||
return len(runs)
|
||||
}, "2s", "100ms").Should(Equal(1))
|
||||
|
||||
runs, _ := sched.GetTaskRuns(task.ID, 10)
|
||||
Expect(runs[0].Status).To(Equal("error"))
|
||||
Expect(runs[0].Error).NotTo(BeEmpty())
|
||||
})
|
||||
|
||||
It("should not execute paused tasks", func() {
|
||||
task, _ := scheduler.NewTask("test-agent", "paused", scheduler.ScheduleTypeOnce, "0s")
|
||||
task.NextRun = time.Now().Add(-1 * time.Second) // force into the past
|
||||
task.Status = scheduler.TaskStatusPaused
|
||||
sched.CreateTask(task)
|
||||
|
||||
// Scheduler is already started in BeforeEach
|
||||
|
||||
Consistently(func() int {
|
||||
return len(executor.executedTasks)
|
||||
}, "1s", "100ms").Should(Equal(0))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Task Management", func() {
|
||||
It("should pause and resume a task", func() {
|
||||
task, _ := scheduler.NewTask("test-agent", "test", scheduler.ScheduleTypeCron, "0 0 * * *")
|
||||
sched.CreateTask(task)
|
||||
|
||||
err := sched.PauseTask(task.ID)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
paused, _ := sched.GetTask(task.ID)
|
||||
Expect(paused.Status).To(Equal(scheduler.TaskStatusPaused))
|
||||
|
||||
err = sched.ResumeTask(task.ID)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
resumed, _ := sched.GetTask(task.ID)
|
||||
Expect(resumed.Status).To(Equal(scheduler.TaskStatusActive))
|
||||
Expect(resumed.NextRun).NotTo(BeZero())
|
||||
})
|
||||
|
||||
It("should get tasks by agent", func() {
|
||||
task1, err := scheduler.NewTask("agent1", "prompt1", scheduler.ScheduleTypeCron, "0 0 * * *")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
task2, err := scheduler.NewTask("agent2", "prompt2", scheduler.ScheduleTypeCron, "0 0 * * *")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
task3, err := scheduler.NewTask("agent1", "prompt3", scheduler.ScheduleTypeCron, "0 0 * * *")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(sched.CreateTask(task1)).To(Succeed())
|
||||
Expect(sched.CreateTask(task2)).To(Succeed())
|
||||
Expect(sched.CreateTask(task3)).To(Succeed())
|
||||
|
||||
agent1Tasks, err := sched.GetTasksByAgent("agent1")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(agent1Tasks).To(HaveLen(2))
|
||||
})
|
||||
|
||||
It("should delete a task", func() {
|
||||
task, _ := scheduler.NewTask("test-agent", "test", scheduler.ScheduleTypeCron, "0 0 * * *")
|
||||
sched.CreateTask(task)
|
||||
|
||||
err := sched.DeleteTask(task.ID)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
_, err = sched.GetTask(task.ID)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,156 @@
|
||||
package scheduler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
var dayPattern = regexp.MustCompile(`^(\d+)d(.*)$`)
|
||||
|
||||
// ParseDuration extends time.ParseDuration with support for days ("d").
|
||||
// Examples: "1d" = 24h, "2d12h" = 60h, "30m", "2h30m".
|
||||
func ParseDuration(s string) (time.Duration, error) {
|
||||
if m := dayPattern.FindStringSubmatch(s); m != nil {
|
||||
days, err := strconv.Atoi(m[1])
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid duration: %s", s)
|
||||
}
|
||||
d := time.Duration(days) * 24 * time.Hour
|
||||
if m[2] != "" {
|
||||
rest, err := time.ParseDuration(m[2])
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid duration: %w", err)
|
||||
}
|
||||
d += rest
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
return time.ParseDuration(s)
|
||||
}
|
||||
|
||||
type TaskStatus string
|
||||
|
||||
const (
|
||||
TaskStatusActive TaskStatus = "active"
|
||||
TaskStatusPaused TaskStatus = "paused"
|
||||
)
|
||||
|
||||
type ScheduleType string
|
||||
|
||||
const (
|
||||
ScheduleTypeCron ScheduleType = "cron"
|
||||
ScheduleTypeInterval ScheduleType = "interval"
|
||||
ScheduleTypeOnce ScheduleType = "once"
|
||||
)
|
||||
|
||||
// Task represents a scheduled task
|
||||
type Task struct {
|
||||
ID string `json:"id"`
|
||||
AgentName string `json:"agent_name"`
|
||||
Prompt string `json:"prompt"`
|
||||
ScheduleType ScheduleType `json:"schedule_type"`
|
||||
ScheduleValue string `json:"schedule_value"`
|
||||
Status TaskStatus `json:"status"`
|
||||
NextRun time.Time `json:"next_run"`
|
||||
LastRun *time.Time `json:"last_run,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ContextMode string `json:"context_mode"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// TaskRun represents a single execution of a task
|
||||
type TaskRun struct {
|
||||
ID string `json:"id"`
|
||||
TaskID string `json:"task_id"`
|
||||
RunAt time.Time `json:"run_at"`
|
||||
DurationMs int64 `json:"duration_ms"`
|
||||
Status string `json:"status"` // "success", "error", "timeout"
|
||||
Result string `json:"result,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// NewTask creates a new task with the given parameters
|
||||
func NewTask(agentName, prompt string, scheduleType ScheduleType, scheduleValue string) (*Task, error) {
|
||||
task := &Task{
|
||||
ID: uuid.New().String(),
|
||||
AgentName: agentName,
|
||||
Prompt: prompt,
|
||||
ScheduleType: scheduleType,
|
||||
ScheduleValue: scheduleValue,
|
||||
Status: TaskStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
ContextMode: "agent",
|
||||
Metadata: make(map[string]interface{}),
|
||||
}
|
||||
|
||||
if err := task.CalculateNextRun(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return task, nil
|
||||
}
|
||||
|
||||
// CalculateNextRun calculates the next run time based on schedule type
|
||||
func (t *Task) CalculateNextRun() error {
|
||||
now := time.Now()
|
||||
|
||||
switch t.ScheduleType {
|
||||
case ScheduleTypeCron:
|
||||
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
|
||||
schedule, err := parser.Parse(t.ScheduleValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid cron expression: %w", err)
|
||||
}
|
||||
t.NextRun = schedule.Next(now)
|
||||
|
||||
case ScheduleTypeInterval:
|
||||
intervalMs, err := strconv.ParseInt(t.ScheduleValue, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid interval: %w", err)
|
||||
}
|
||||
if intervalMs <= 0 {
|
||||
return fmt.Errorf("invalid interval: %d", intervalMs)
|
||||
}
|
||||
if t.LastRun != nil {
|
||||
t.NextRun = t.LastRun.Add(time.Duration(intervalMs) * time.Millisecond)
|
||||
} else {
|
||||
t.NextRun = now.Add(time.Duration(intervalMs) * time.Millisecond)
|
||||
}
|
||||
|
||||
case ScheduleTypeOnce:
|
||||
duration, err := ParseDuration(t.ScheduleValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid duration: %w", err)
|
||||
}
|
||||
if duration < 0 {
|
||||
return fmt.Errorf("duration must be positive: %s", t.ScheduleValue)
|
||||
}
|
||||
t.NextRun = now.Add(duration)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unknown schedule type: %s", t.ScheduleType)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsDue checks if the task should be executed now
|
||||
func (t *Task) IsDue() bool {
|
||||
return t.Status == TaskStatusActive && time.Now().After(t.NextRun)
|
||||
}
|
||||
|
||||
// NewTaskRun creates a new task run record
|
||||
func NewTaskRun(taskID string) *TaskRun {
|
||||
return &TaskRun{
|
||||
ID: uuid.New().String(),
|
||||
TaskID: taskID,
|
||||
RunAt: time.Now(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mudler/LocalAGI/pkg/llm"
|
||||
"github.com/mudler/LocalAGI/pkg/localrag"
|
||||
"github.com/mudler/xlog"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
// datePrefixRegex matches YYYY-MM-DD at the start of a filename (e.g. 2006-01-02-15-04-05-hash.txt).
|
||||
var datePrefixRegex = regexp.MustCompile(`^(\d{4}-\d{2}-\d{2})`)
|
||||
|
||||
// summaryPrefix is the filename prefix for compaction summary entries; skip re-compacting these.
|
||||
const summaryPrefix = "summary-"
|
||||
|
||||
// bucketKey returns the period bucket key for a date string (YYYY-MM-DD).
|
||||
func bucketKey(dateStr, period string) (string, error) {
|
||||
t, err := time.Parse("2006-01-02", dateStr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
switch period {
|
||||
case "daily":
|
||||
return dateStr, nil
|
||||
case "weekly":
|
||||
year, week := t.ISOWeek()
|
||||
return fmt.Sprintf("%04d-W%02d", year, week), nil
|
||||
case "monthly":
|
||||
return t.Format("2006-01"), nil
|
||||
default:
|
||||
return dateStr, nil
|
||||
}
|
||||
}
|
||||
|
||||
// dateFromFilename extracts YYYY-MM-DD from the start of a filename if present.
|
||||
func dateFromFilename(filename string) (string, bool) {
|
||||
base := filepath.Base(filename)
|
||||
matches := datePrefixRegex.FindStringSubmatch(base)
|
||||
if len(matches) < 2 {
|
||||
return "", false
|
||||
}
|
||||
return matches[1], true
|
||||
}
|
||||
|
||||
// groupEntriesByPeriod groups entry names by period bucket (daily/weekly/monthly). Skips summary-* and entries without a parseable date.
|
||||
func groupEntriesByPeriod(entries []string, period string) map[string][]string {
|
||||
groups := make(map[string][]string)
|
||||
for _, entry := range entries {
|
||||
if strings.HasPrefix(filepath.Base(entry), summaryPrefix) {
|
||||
continue
|
||||
}
|
||||
dateStr, ok := dateFromFilename(entry)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
key, err := bucketKey(dateStr, period)
|
||||
if err != nil {
|
||||
xlog.Debug("compaction: skip entry, invalid date", "entry", entry, "error", err)
|
||||
continue
|
||||
}
|
||||
groups[key] = append(groups[key], entry)
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
// summarizer summarizes text via the LLM.
|
||||
type summarizer interface {
|
||||
Summarize(ctx context.Context, content string) (string, error)
|
||||
}
|
||||
|
||||
type openAISummarizer struct {
|
||||
client *openai.Client
|
||||
model string
|
||||
}
|
||||
|
||||
func (s *openAISummarizer) Summarize(ctx context.Context, content string) (string, error) {
|
||||
if content == "" {
|
||||
return "", nil
|
||||
}
|
||||
resp, err := s.client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
|
||||
Model: s.model,
|
||||
Messages: []openai.ChatCompletionMessage{
|
||||
{Role: openai.ChatMessageRoleSystem, Content: "Summarize the following knowledge base entries into a concise summary. Preserve important facts and key points."},
|
||||
{Role: openai.ChatMessageRoleUser, Content: content},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(resp.Choices) == 0 {
|
||||
return "", fmt.Errorf("no completion choices")
|
||||
}
|
||||
return strings.TrimSpace(resp.Choices[0].Message.Content), nil
|
||||
}
|
||||
|
||||
// RunCompaction runs one compaction pass: list entries, group by period, for each group fetch content, optionally summarize, store result, delete originals.
|
||||
func RunCompaction(ctx context.Context, client *localrag.WrappedClient, period string, summarize bool, apiURL, apiKey, model string) error {
|
||||
collection := client.Collection()
|
||||
entries, err := client.Client.ListEntries(collection)
|
||||
if err != nil {
|
||||
return fmt.Errorf("list entries: %w", err)
|
||||
}
|
||||
groups := groupEntriesByPeriod(entries, period)
|
||||
if len(groups) == 0 {
|
||||
xlog.Debug("compaction: no groups to compact", "collection", collection, "period", period)
|
||||
return nil
|
||||
}
|
||||
|
||||
var sum summarizer
|
||||
if summarize && apiURL != "" && model != "" {
|
||||
openAIClient := llm.NewClient(apiKey, apiURL, "120s")
|
||||
sum = &openAISummarizer{client: openAIClient, model: model}
|
||||
}
|
||||
|
||||
for key, groupEntries := range groups {
|
||||
if len(groupEntries) == 0 {
|
||||
continue
|
||||
}
|
||||
var combined strings.Builder
|
||||
for _, entry := range groupEntries {
|
||||
entryContent, _, err := client.GetEntryContent(entry)
|
||||
if err != nil {
|
||||
xlog.Warn("compaction: get entry content failed", "entry", entry, "error", err)
|
||||
continue
|
||||
}
|
||||
if entryContent != "" {
|
||||
combined.WriteString(entryContent)
|
||||
combined.WriteString("\n\n")
|
||||
}
|
||||
}
|
||||
content := strings.TrimSpace(combined.String())
|
||||
if content == "" {
|
||||
xlog.Debug("compaction: empty content for group", "key", key)
|
||||
continue
|
||||
}
|
||||
|
||||
if sum != nil {
|
||||
summary, err := sum.Summarize(ctx, content)
|
||||
if err != nil {
|
||||
xlog.Warn("compaction: summarize failed", "key", key, "error", err)
|
||||
continue
|
||||
}
|
||||
content = summary
|
||||
}
|
||||
|
||||
// Store result as summary-<key>.txt
|
||||
resultFilename := fmt.Sprintf("%s%s.txt", summaryPrefix, key)
|
||||
tmpDir, err := os.MkdirTemp("", "localagi-compact")
|
||||
if err != nil {
|
||||
xlog.Warn("compaction: mkdir temp failed", "error", err)
|
||||
continue
|
||||
}
|
||||
tmpPath := filepath.Join(tmpDir, resultFilename)
|
||||
if err := os.WriteFile(tmpPath, []byte(content), 0644); err != nil {
|
||||
os.RemoveAll(tmpDir)
|
||||
xlog.Warn("compaction: write temp file failed", "error", err)
|
||||
continue
|
||||
}
|
||||
if err := client.Client.Store(collection, tmpPath); err != nil {
|
||||
os.RemoveAll(tmpDir)
|
||||
xlog.Warn("compaction: store failed", "key", key, "error", err)
|
||||
continue
|
||||
}
|
||||
os.RemoveAll(tmpDir)
|
||||
|
||||
for _, entry := range groupEntries {
|
||||
if _, err := client.Client.DeleteEntry(collection, entry); err != nil {
|
||||
xlog.Warn("compaction: delete entry failed", "entry", entry, "error", err)
|
||||
}
|
||||
}
|
||||
xlog.Info("compaction: compacted group", "collection", collection, "period", period, "key", key, "entries", len(groupEntries))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// runCompactionTicker runs compaction on a schedule (daily/weekly/monthly). It stops when ctx is done.
|
||||
func runCompactionTicker(ctx context.Context, client *localrag.WrappedClient, config *AgentConfig, apiURL, apiKey, model string) {
|
||||
// Run first compaction immediately on startup
|
||||
if err := RunCompaction(ctx, client, config.KBCompactionInterval, config.KBCompactionSummarize, apiURL, apiKey, model); err != nil {
|
||||
xlog.Warn("compaction ticker initial run failed", "collection", client.Collection(), "error", err)
|
||||
}
|
||||
|
||||
interval := 24 * time.Hour
|
||||
switch config.KBCompactionInterval {
|
||||
case "weekly":
|
||||
interval = 7 * 24 * time.Hour
|
||||
case "monthly":
|
||||
interval = 30 * 24 * time.Hour
|
||||
default:
|
||||
interval = 24 * time.Hour
|
||||
}
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
xlog.Debug("compaction ticker stopped", "collection", client.Collection())
|
||||
return
|
||||
case <-ticker.C:
|
||||
if err := RunCompaction(ctx, client, config.KBCompactionInterval, config.KBCompactionSummarize, apiURL, apiKey, model); err != nil {
|
||||
xlog.Warn("compaction ticker failed", "collection", client.Collection(), "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+134
-7
@@ -3,6 +3,7 @@ package state
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/agent"
|
||||
@@ -10,6 +11,21 @@ import (
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
)
|
||||
|
||||
// parseIntField parses an integer field that may be received as either a number or a string
|
||||
func parseIntField(value interface{}) int {
|
||||
switch v := value.(type) {
|
||||
case int:
|
||||
return v
|
||||
case float64:
|
||||
return int(v)
|
||||
case string:
|
||||
if i, err := strconv.Atoi(v); err == nil {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type ConnectorConfig struct {
|
||||
Type string `json:"type"` // e.g. Slack
|
||||
Config string `json:"config"`
|
||||
@@ -64,17 +80,27 @@ type AgentConfig struct {
|
||||
RandomIdentity bool `json:"random_identity" form:"random_identity"`
|
||||
InitiateConversations bool `json:"initiate_conversations" form:"initiate_conversations"`
|
||||
CanPlan bool `json:"enable_planning" form:"enable_planning"`
|
||||
PlanReviewerModel string `json:"plan_reviewer_model" form:"plan_reviewer_model"`
|
||||
DisableSinkState bool `json:"disable_sink_state" form:"disable_sink_state"`
|
||||
IdentityGuidance string `json:"identity_guidance" form:"identity_guidance"`
|
||||
PeriodicRuns string `json:"periodic_runs" form:"periodic_runs"`
|
||||
SchedulerPollInterval string `json:"scheduler_poll_interval" form:"scheduler_poll_interval"`
|
||||
PermanentGoal string `json:"permanent_goal" form:"permanent_goal"`
|
||||
EnableKnowledgeBase bool `json:"enable_kb" form:"enable_kb"`
|
||||
EnableKBCompaction bool `json:"enable_kb_compaction" form:"enable_kb_compaction"`
|
||||
KBCompactionInterval string `json:"kb_compaction_interval" form:"kb_compaction_interval"`
|
||||
KBCompactionSummarize bool `json:"kb_compaction_summarize" form:"kb_compaction_summarize"`
|
||||
KBAutoSearch bool `json:"kb_auto_search" form:"kb_auto_search"`
|
||||
KBAsTools bool `json:"kb_as_tools" form:"kb_as_tools"`
|
||||
EnableReasoning bool `json:"enable_reasoning" form:"enable_reasoning"`
|
||||
EnableGuidedTools bool `json:"enable_guided_tools" form:"enable_guided_tools"`
|
||||
KnowledgeBaseResults int `json:"kb_results" form:"kb_results"`
|
||||
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"`
|
||||
SummaryLongTermMemory bool `json:"summary_long_term_memory" form:"summary_long_term_memory"`
|
||||
ParallelJobs int `json:"parallel_jobs" form:"parallel_jobs"`
|
||||
LongTermMemory bool `json:"long_term_memory" form:"long_term_memory"`
|
||||
SummaryLongTermMemory bool `json:"summary_long_term_memory" form:"summary_long_term_memory"`
|
||||
ConversationStorageMode string `json:"conversation_storage_mode" form:"conversation_storage_mode"`
|
||||
ParallelJobs int `json:"parallel_jobs" form:"parallel_jobs"`
|
||||
StripThinkingTags bool `json:"strip_thinking_tags" form:"strip_thinking_tags"`
|
||||
EnableEvaluation bool `json:"enable_evaluation" form:"enable_evaluation"`
|
||||
MaxEvaluationLoops int `json:"max_evaluation_loops" form:"max_evaluation_loops"`
|
||||
@@ -168,6 +194,13 @@ func NewAgentConfigMeta(
|
||||
DefaultValue: "",
|
||||
Tags: config.Tags{Section: "ModelSettings"},
|
||||
},
|
||||
{
|
||||
Name: "plan_reviewer_model",
|
||||
Label: "Plan Reviewer Model",
|
||||
Type: "text",
|
||||
DefaultValue: "",
|
||||
Tags: config.Tags{Section: "ModelSettings"},
|
||||
},
|
||||
{
|
||||
Name: "api_url",
|
||||
Label: "API URL",
|
||||
@@ -212,6 +245,31 @@ func NewAgentConfigMeta(
|
||||
Step: 1,
|
||||
Tags: config.Tags{Section: "MemorySettings"},
|
||||
},
|
||||
{
|
||||
Name: "enable_kb_compaction",
|
||||
Label: "Enable KB Compaction",
|
||||
Type: "checkbox",
|
||||
DefaultValue: false,
|
||||
HelpText: "Periodically group collection entries by date (daily/weekly/monthly), optionally summarize or concatenate, then store and remove originals",
|
||||
Tags: config.Tags{Section: "MemorySettings"},
|
||||
},
|
||||
{
|
||||
Name: "kb_compaction_interval",
|
||||
Label: "KB Compaction Interval",
|
||||
Type: "text",
|
||||
DefaultValue: "daily",
|
||||
Placeholder: "daily, weekly, monthly",
|
||||
HelpText: "Compaction window: daily, weekly, or monthly",
|
||||
Tags: config.Tags{Section: "MemorySettings"},
|
||||
},
|
||||
{
|
||||
Name: "kb_compaction_summarize",
|
||||
Label: "KB Compaction Summarize",
|
||||
Type: "checkbox",
|
||||
DefaultValue: true,
|
||||
HelpText: "When enabled, summarize grouped content via LLM; when disabled, store concatenated content only (no LLM call)",
|
||||
Tags: config.Tags{Section: "MemorySettings"},
|
||||
},
|
||||
{
|
||||
Name: "long_term_memory",
|
||||
Label: "Long Term Memory",
|
||||
@@ -226,6 +284,35 @@ func NewAgentConfigMeta(
|
||||
DefaultValue: false,
|
||||
Tags: config.Tags{Section: "MemorySettings"},
|
||||
},
|
||||
{
|
||||
Name: "kb_auto_search",
|
||||
Label: "KB Auto Search",
|
||||
Type: "checkbox",
|
||||
DefaultValue: true,
|
||||
HelpText: "Automatically search knowledge base when a user message is received",
|
||||
Tags: config.Tags{Section: "MemorySettings"},
|
||||
},
|
||||
{
|
||||
Name: "kb_as_tools",
|
||||
Label: "KB As Tools",
|
||||
Type: "checkbox",
|
||||
DefaultValue: false,
|
||||
HelpText: "Inject knowledge base search and add actions as tools, allowing the agent to access its memory without manual configuration",
|
||||
Tags: config.Tags{Section: "MemorySettings"},
|
||||
},
|
||||
{
|
||||
Name: "conversation_storage_mode",
|
||||
Label: "Conversation Storage Mode",
|
||||
Type: "select",
|
||||
DefaultValue: "user_only",
|
||||
Options: []config.FieldOption{
|
||||
{Value: "user_only", Label: "User Messages Only"},
|
||||
{Value: "user_and_assistant", Label: "User and Assistant Messages"},
|
||||
{Value: "whole_conversation", Label: "Whole Conversation as Block"},
|
||||
},
|
||||
HelpText: "Controls what gets stored in the knowledge base: only user messages, user and assistant messages separately, or the entire conversation as a single block",
|
||||
Tags: config.Tags{Section: "MemorySettings"},
|
||||
},
|
||||
{
|
||||
Name: "system_prompt",
|
||||
Label: "System Prompt",
|
||||
@@ -283,6 +370,15 @@ func NewAgentConfigMeta(
|
||||
HelpText: "Duration for scheduling periodic agent runs",
|
||||
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||
},
|
||||
{
|
||||
Name: "scheduler_poll_interval",
|
||||
Label: "Scheduler Poll Interval",
|
||||
Type: "text",
|
||||
DefaultValue: "30s",
|
||||
Placeholder: "30s",
|
||||
HelpText: "Duration for polling the scheduler for planned tasks",
|
||||
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||
},
|
||||
{
|
||||
Name: "enable_reasoning",
|
||||
Label: "Enable Reasoning",
|
||||
@@ -291,6 +387,14 @@ func NewAgentConfigMeta(
|
||||
HelpText: "Enable agent to explain its reasoning process",
|
||||
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||
},
|
||||
{
|
||||
Name: "enable_guided_tools",
|
||||
Label: "Enable Guided Tools",
|
||||
Type: "checkbox",
|
||||
DefaultValue: false,
|
||||
HelpText: "Filter tools through guidance using their descriptions; creates virtual guidelines when none exist",
|
||||
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||
},
|
||||
{
|
||||
Name: "parallel_jobs",
|
||||
Label: "Parallel Jobs",
|
||||
@@ -301,13 +405,21 @@ func NewAgentConfigMeta(
|
||||
HelpText: "Number of concurrent tasks that can run in parallel",
|
||||
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||
},
|
||||
{
|
||||
Name: "disable_sink_state",
|
||||
Label: "Disable Sink State",
|
||||
Type: "checkbox",
|
||||
DefaultValue: false,
|
||||
HelpText: "Disable the sink state of the agent",
|
||||
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||
},
|
||||
{
|
||||
Name: "mcp_stdio_servers",
|
||||
Label: "MCP STDIO Servers",
|
||||
Type: "textarea",
|
||||
DefaultValue: "",
|
||||
HelpText: "JSON configuration for MCP STDIO servers",
|
||||
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||
Tags: config.Tags{Section: "MCP"},
|
||||
},
|
||||
{
|
||||
Name: "mcp_prepare_script",
|
||||
@@ -315,7 +427,7 @@ func NewAgentConfigMeta(
|
||||
Type: "textarea",
|
||||
DefaultValue: "",
|
||||
HelpText: "Script to prepare for running MCP servers",
|
||||
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||
Tags: config.Tags{Section: "MCP"},
|
||||
},
|
||||
{
|
||||
Name: "strip_thinking_tags",
|
||||
@@ -386,6 +498,9 @@ func (a *AgentConfig) UnmarshalJSON(data []byte) error {
|
||||
aux := &struct {
|
||||
*Alias
|
||||
MCPSTDIOServersConfig interface{} `json:"mcp_stdio_servers"`
|
||||
MaxEvaluationLoops interface{} `json:"max_evaluation_loops"`
|
||||
ParallelJobs interface{} `json:"parallel_jobs"`
|
||||
KnowledgeBaseResults interface{} `json:"kb_results"`
|
||||
}{
|
||||
Alias: (*Alias)(a),
|
||||
}
|
||||
@@ -394,6 +509,11 @@ func (a *AgentConfig) UnmarshalJSON(data []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Parse integer fields that may come as strings
|
||||
a.MaxEvaluationLoops = parseIntField(aux.MaxEvaluationLoops)
|
||||
a.ParallelJobs = parseIntField(aux.ParallelJobs)
|
||||
a.KnowledgeBaseResults = parseIntField(aux.KnowledgeBaseResults)
|
||||
|
||||
// Handle MCP STDIO servers configuration
|
||||
if aux.MCPSTDIOServersConfig != nil {
|
||||
switch v := aux.MCPSTDIOServersConfig.(type) {
|
||||
@@ -412,7 +532,7 @@ func (a *AgentConfig) UnmarshalJSON(data []byte) error {
|
||||
}
|
||||
|
||||
a.MCPSTDIOServers = make([]agent.MCPSTDIOServer, 0, len(mcpConfig.MCPServers))
|
||||
for _, server := range mcpConfig.MCPServers {
|
||||
for name, server := range mcpConfig.MCPServers {
|
||||
// Convert env map to slice of "KEY=VALUE" strings
|
||||
envSlice := make([]string, 0, len(server.Env))
|
||||
for k, v := range server.Env {
|
||||
@@ -420,6 +540,7 @@ func (a *AgentConfig) UnmarshalJSON(data []byte) error {
|
||||
}
|
||||
|
||||
a.MCPSTDIOServers = append(a.MCPSTDIOServers, agent.MCPSTDIOServer{
|
||||
Name: name,
|
||||
Cmd: server.Command,
|
||||
Args: server.Args,
|
||||
Env: envSlice,
|
||||
@@ -434,6 +555,7 @@ func (a *AgentConfig) UnmarshalJSON(data []byte) error {
|
||||
return fmt.Errorf("invalid server configuration format")
|
||||
}
|
||||
|
||||
name, _ := serverMap["name"].(string)
|
||||
cmd, _ := serverMap["cmd"].(string)
|
||||
args := make([]string, 0)
|
||||
if argsInterface, ok := serverMap["args"].([]interface{}); ok {
|
||||
@@ -454,6 +576,7 @@ func (a *AgentConfig) UnmarshalJSON(data []byte) error {
|
||||
}
|
||||
|
||||
a.MCPSTDIOServers = append(a.MCPSTDIOServers, agent.MCPSTDIOServer{
|
||||
Name: name,
|
||||
Cmd: cmd,
|
||||
Args: args,
|
||||
Env: env,
|
||||
@@ -502,7 +625,11 @@ func (a *AgentConfig) MarshalJSON() ([]byte, error) {
|
||||
}
|
||||
}
|
||||
|
||||
mcpConfig.MCPServers[fmt.Sprintf("server%d", i)] = struct {
|
||||
key := server.Name
|
||||
if key == "" {
|
||||
key = fmt.Sprintf("server%d", i)
|
||||
}
|
||||
mcpConfig.MCPServers[key] = struct {
|
||||
Command string `json:"command"`
|
||||
Args []string `json:"args"`
|
||||
Env map[string]string `json:"env"`
|
||||
|
||||
+54
-104
@@ -2,7 +2,6 @@ package state
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -15,11 +14,8 @@ import (
|
||||
. "github.com/mudler/LocalAGI/core/agent"
|
||||
"github.com/mudler/LocalAGI/core/sse"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/llm"
|
||||
"github.com/mudler/LocalAGI/pkg/localrag"
|
||||
"github.com/mudler/LocalAGI/pkg/utils"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
|
||||
"github.com/mudler/xlog"
|
||||
)
|
||||
@@ -34,7 +30,7 @@ type AgentPool struct {
|
||||
agentStatus map[string]*Status
|
||||
apiURL, defaultModel, defaultMultimodalModel, defaultTTSModel string
|
||||
defaultTranscriptionModel, defaultTranscriptionLanguage string
|
||||
imageModel, localRAGAPI, localRAGKey, apiKey string
|
||||
localRAGAPI, localRAGKey, apiKey string
|
||||
availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []types.Action
|
||||
connectors func(*AgentConfig) []Connector
|
||||
dynamicPrompt func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []DynamicPrompt
|
||||
@@ -74,7 +70,7 @@ func loadPoolFromFile(path string) (*AgentPoolData, error) {
|
||||
}
|
||||
|
||||
func NewAgentPool(
|
||||
defaultModel, defaultMultimodalModel, defaultTranscriptionModel, defaultTranscriptionLanguage, defaultTTSModel, imageModel, apiURL, apiKey, directory string,
|
||||
defaultModel, defaultMultimodalModel, defaultTranscriptionModel, defaultTranscriptionLanguage, defaultTTSModel, apiURL, apiKey, directory string,
|
||||
LocalRAGAPI string,
|
||||
availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []types.Action,
|
||||
connectors func(*AgentConfig) []Connector,
|
||||
@@ -92,7 +88,6 @@ func NewAgentPool(
|
||||
if withLogs {
|
||||
conversationPath = filepath.Join(directory, "conversations")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(poolfile); err != nil {
|
||||
// file does not exist, create a new pool
|
||||
return &AgentPool{
|
||||
@@ -104,7 +99,6 @@ func NewAgentPool(
|
||||
defaultTranscriptionModel: defaultTranscriptionModel,
|
||||
defaultTranscriptionLanguage: defaultTranscriptionLanguage,
|
||||
defaultTTSModel: defaultTTSModel,
|
||||
imageModel: imageModel,
|
||||
localRAGAPI: LocalRAGAPI,
|
||||
apiKey: apiKey,
|
||||
agents: make(map[string]*Agent),
|
||||
@@ -133,7 +127,6 @@ func NewAgentPool(
|
||||
defaultTranscriptionModel: defaultTranscriptionModel,
|
||||
defaultTranscriptionLanguage: defaultTranscriptionLanguage,
|
||||
defaultTTSModel: defaultTTSModel,
|
||||
imageModel: imageModel,
|
||||
apiKey: apiKey,
|
||||
agents: make(map[string]*Agent),
|
||||
managers: make(map[string]sse.Manager),
|
||||
@@ -170,14 +163,7 @@ func (a *AgentPool) CreateAgent(name string, agentConfig *AgentConfig) error {
|
||||
return err
|
||||
}
|
||||
|
||||
go func(ac AgentConfig) {
|
||||
// Create the agent avatar
|
||||
if err := createAgentAvatar(a.apiURL, a.apiKey, a.defaultModel, a.imageModel, a.pooldir, ac); err != nil {
|
||||
xlog.Error("Failed to create agent avatar", "error", err)
|
||||
}
|
||||
}(a.pool[name])
|
||||
|
||||
return a.startAgentWithConfig(name, agentConfig, nil)
|
||||
return a.startAgentWithConfig(name, a.pooldir, agentConfig, nil)
|
||||
}
|
||||
|
||||
func (a *AgentPool) RecreateAgent(name string, agentConfig *AgentConfig) error {
|
||||
@@ -213,7 +199,7 @@ func (a *AgentPool) RecreateAgent(name string, agentConfig *AgentConfig) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := a.startAgentWithConfig(name, agentConfig, obs); err != nil {
|
||||
if err := a.startAgentWithConfig(name, a.pooldir, agentConfig, obs); err != nil {
|
||||
if obs != nil {
|
||||
o.Completion = &types.Completion{Error: err.Error()}
|
||||
obs.Update(*o)
|
||||
@@ -229,84 +215,6 @@ func (a *AgentPool) RecreateAgent(name string, agentConfig *AgentConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func createAgentAvatar(APIURL, APIKey, model, imageModel, avatarDir string, agent AgentConfig) error {
|
||||
client := llm.NewClient(APIKey, APIURL+"/v1", "10m")
|
||||
|
||||
if imageModel == "" {
|
||||
return fmt.Errorf("image model not set")
|
||||
}
|
||||
|
||||
if model == "" {
|
||||
return fmt.Errorf("default model not set")
|
||||
}
|
||||
|
||||
imagePath := filepath.Join(avatarDir, "avatars", fmt.Sprintf("%s.png", agent.Name))
|
||||
if _, err := os.Stat(imagePath); err == nil {
|
||||
// Image already exists
|
||||
xlog.Debug("Avatar already exists", "path", imagePath)
|
||||
return nil
|
||||
}
|
||||
|
||||
var results struct {
|
||||
ImagePrompt string `json:"image_prompt"`
|
||||
}
|
||||
|
||||
err := llm.GenerateTypedJSONWithGuidance(
|
||||
context.Background(),
|
||||
llm.NewClient(APIKey, APIURL, "10m"),
|
||||
"Generate a prompt that I can use to create a random avatar for the bot '"+agent.Name+"', the description of the bot is: "+agent.Description,
|
||||
model,
|
||||
jsonschema.Definition{
|
||||
Type: jsonschema.Object,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"image_prompt": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The prompt to generate the image",
|
||||
},
|
||||
},
|
||||
Required: []string{"image_prompt"},
|
||||
}, &results)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate image prompt: %w", err)
|
||||
}
|
||||
|
||||
if results.ImagePrompt == "" {
|
||||
xlog.Error("Failed to generate image prompt")
|
||||
return fmt.Errorf("failed to generate image prompt")
|
||||
}
|
||||
|
||||
req := openai.ImageRequest{
|
||||
Prompt: results.ImagePrompt,
|
||||
Model: imageModel,
|
||||
Size: openai.CreateImageSize256x256,
|
||||
ResponseFormat: openai.CreateImageResponseFormatB64JSON,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resp, err := client.CreateImage(ctx, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate image: %w", err)
|
||||
}
|
||||
|
||||
if len(resp.Data) == 0 {
|
||||
return fmt.Errorf("failed to generate image")
|
||||
}
|
||||
|
||||
imageJson := resp.Data[0].B64JSON
|
||||
|
||||
os.MkdirAll(filepath.Join(avatarDir, "avatars"), 0755)
|
||||
|
||||
// Save the image to the agent directory
|
||||
imageData, err := base64.StdEncoding.DecodeString(imageJson)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(imagePath, imageData, 0644)
|
||||
}
|
||||
|
||||
func (a *AgentPool) List() []string {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
@@ -328,7 +236,7 @@ func (a *AgentPool) GetStatusHistory(name string) *Status {
|
||||
return a.agentStatus[name]
|
||||
}
|
||||
|
||||
func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs Observer) error {
|
||||
func (a *AgentPool) startAgentWithConfig(name, pooldir string, config *AgentConfig, obs Observer) error {
|
||||
var manager sse.Manager
|
||||
if m, ok := a.managers[name]; ok {
|
||||
manager = m
|
||||
@@ -367,6 +275,10 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
|
||||
config.PeriodicRuns = "10m"
|
||||
}
|
||||
|
||||
if config.SchedulerPollInterval == "" {
|
||||
config.SchedulerPollInterval = "30s"
|
||||
}
|
||||
|
||||
// XXX: Why do we update the pool config from an Agent's config?
|
||||
if config.APIURL != "" {
|
||||
a.apiURL = config.APIURL
|
||||
@@ -429,6 +341,7 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
|
||||
}
|
||||
|
||||
opts := []Option{
|
||||
WithSchedulerStorePath(filepath.Join(pooldir, fmt.Sprintf("scheduler-%s.json", name))),
|
||||
WithModel(model),
|
||||
WithLLMAPIURL(a.apiURL),
|
||||
WithContext(ctx),
|
||||
@@ -437,6 +350,7 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
|
||||
WithTranscriptionLanguage(transcriptionLanguage),
|
||||
WithTTSModel(ttsModel),
|
||||
WithPeriodicRuns(config.PeriodicRuns),
|
||||
WithSchedulerPollInterval(config.SchedulerPollInterval),
|
||||
WithPermanentGoal(config.PermanentGoal),
|
||||
WithMCPSTDIOServers(config.MCPSTDIOServers...),
|
||||
WithPrompts(promptBlocks...),
|
||||
@@ -453,7 +367,6 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
|
||||
WithCharacterFile(characterFile),
|
||||
WithLLMAPIKey(a.apiKey),
|
||||
WithTimeout(a.timeout),
|
||||
WithRAGDB(localrag.NewWrappedClient(a.localRAGAPI, a.localRAGKey, name)),
|
||||
WithAgentReasoningCallback(func(state types.ActionCurrentState) bool {
|
||||
xlog.Info(
|
||||
"Agent is thinking",
|
||||
@@ -534,6 +447,10 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
|
||||
opts = append(opts, EnableSummaryMemory)
|
||||
}
|
||||
|
||||
if config.ConversationStorageMode != "" {
|
||||
opts = append(opts, WithConversationStorageMode(ConversationStorageMode(config.ConversationStorageMode)))
|
||||
}
|
||||
|
||||
if config.CanStopItself {
|
||||
opts = append(opts, CanStopItself)
|
||||
}
|
||||
@@ -542,6 +459,14 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
|
||||
opts = append(opts, EnablePlanning)
|
||||
}
|
||||
|
||||
if config.PlanReviewerModel != "" {
|
||||
opts = append(opts, WithPlanReviewerLLM(config.PlanReviewerModel))
|
||||
}
|
||||
|
||||
if config.DisableSinkState {
|
||||
opts = append(opts, DisableSinkState)
|
||||
}
|
||||
|
||||
if config.InitiateConversations {
|
||||
opts = append(opts, EnableInitiateConversations)
|
||||
}
|
||||
@@ -554,14 +479,38 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
|
||||
}
|
||||
}
|
||||
|
||||
var ragClient *localrag.WrappedClient
|
||||
if config.EnableKnowledgeBase {
|
||||
opts = append(opts, EnableKnowledgeBase)
|
||||
ragClient = localrag.NewWrappedClient(a.localRAGAPI, a.localRAGKey, name)
|
||||
opts = append(opts, WithRAGDB(ragClient), EnableKnowledgeBase)
|
||||
// Set KB auto search option (defaults to true for backward compatibility)
|
||||
// For backward compatibility: if both new KB fields are false (zero values),
|
||||
// assume this is an old config and default KBAutoSearch to true
|
||||
kbAutoSearch := config.KBAutoSearch
|
||||
if !config.KBAutoSearch && !config.KBAsTools {
|
||||
// Both new fields are false, likely an old config - default to true for backward compatibility
|
||||
kbAutoSearch = true
|
||||
}
|
||||
opts = append(opts, WithKBAutoSearch(kbAutoSearch))
|
||||
// Inject KB wrapper actions if enabled
|
||||
if config.KBAsTools && ragClient != nil {
|
||||
kbResults := config.KnowledgeBaseResults
|
||||
if kbResults <= 0 {
|
||||
kbResults = 5 // Default
|
||||
}
|
||||
searchAction, addAction := NewKBWrapperActions(ragClient, kbResults)
|
||||
opts = append(opts, WithActions(searchAction, addAction))
|
||||
}
|
||||
}
|
||||
|
||||
if config.EnableReasoning {
|
||||
opts = append(opts, EnableForceReasoning)
|
||||
}
|
||||
|
||||
if config.EnableGuidedTools {
|
||||
opts = append(opts, EnableGuidedTools)
|
||||
}
|
||||
|
||||
if config.StripThinkingTags {
|
||||
opts = append(opts, EnableStripThinkingTags)
|
||||
}
|
||||
@@ -598,6 +547,10 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
|
||||
}
|
||||
}()
|
||||
|
||||
if config.EnableKnowledgeBase && config.EnableKBCompaction && ragClient != nil {
|
||||
go runCompactionTicker(ctx, ragClient, config, a.apiURL, a.apiKey, model)
|
||||
}
|
||||
|
||||
xlog.Info("Starting connectors", "name", name, "config", config)
|
||||
|
||||
for _, c := range connectors {
|
||||
@@ -626,7 +579,7 @@ func (a *AgentPool) StartAll() error {
|
||||
if a.agents[name] != nil { // Agent already started
|
||||
continue
|
||||
}
|
||||
if err := a.startAgentWithConfig(name, &config, nil); err != nil {
|
||||
if err := a.startAgentWithConfig(name, a.pooldir, &config, nil); err != nil {
|
||||
xlog.Error("Failed to start agent", "name", name, "error", err)
|
||||
}
|
||||
}
|
||||
@@ -664,7 +617,7 @@ func (a *AgentPool) Start(name string) error {
|
||||
return nil
|
||||
}
|
||||
if config, ok := a.pool[name]; ok {
|
||||
return a.startAgentWithConfig(name, &config, nil)
|
||||
return a.startAgentWithConfig(name, a.pooldir, &config, nil)
|
||||
}
|
||||
|
||||
return fmt.Errorf("agent %s not found", name)
|
||||
@@ -690,9 +643,6 @@ func (a *AgentPool) Remove(name string) error {
|
||||
delete(a.agents, name)
|
||||
delete(a.pool, name)
|
||||
|
||||
// remove avatar
|
||||
os.Remove(filepath.Join(a.pooldir, "avatars", fmt.Sprintf("%s.png", name)))
|
||||
|
||||
if err := a.save(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -102,16 +102,17 @@ func (c *cogitoWrapper) Tool() openai.Tool {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cogitoWrapper) Run(args map[string]any) (string, error) {
|
||||
func (c *cogitoWrapper) Execute(args map[string]any) (string, any, 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 "", nil, err
|
||||
}
|
||||
return result.Result, nil
|
||||
|
||||
return result.Result, result, nil
|
||||
}
|
||||
|
||||
// Actions is something the agent can do
|
||||
@@ -187,8 +188,8 @@ func (a Actions) ToTools() []openai.Tool {
|
||||
return tools
|
||||
}
|
||||
|
||||
func (a Actions) ToCogitoTools(ctx context.Context, sharedState *AgentSharedState) []cogito.Tool {
|
||||
tools := []cogito.Tool{}
|
||||
func (a Actions) ToCogitoTools(ctx context.Context, sharedState *AgentSharedState) []cogito.ToolDefinitionInterface {
|
||||
tools := []cogito.ToolDefinitionInterface{}
|
||||
for _, action := range a {
|
||||
tools = append(tools, &cogitoWrapper{action: action, ctx: ctx, sharedState: sharedState})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
// ConversationMessage represents a message with associated metadata
|
||||
// Used when the agent initiates new conversations to preserve context
|
||||
// such as generated images, files, or URLs
|
||||
type ConversationMessage struct {
|
||||
Message openai.ChatCompletionMessage
|
||||
Metadata map[string]interface{}
|
||||
}
|
||||
|
||||
// NewConversationMessage creates a new ConversationMessage with the given message
|
||||
func NewConversationMessage(msg openai.ChatCompletionMessage) *ConversationMessage {
|
||||
return &ConversationMessage{
|
||||
Message: msg,
|
||||
Metadata: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
// WithMetadata adds metadata to the conversation message
|
||||
func (c *ConversationMessage) WithMetadata(metadata map[string]interface{}) *ConversationMessage {
|
||||
c.Metadata = metadata
|
||||
return c
|
||||
}
|
||||
+1
-1
@@ -77,7 +77,7 @@ func WithResultCallback(f func(ActionState)) JobOption {
|
||||
}
|
||||
}
|
||||
|
||||
func WithMetadata(metadata map[string]interface{}) JobOption {
|
||||
func WithMetadata(metadata map[string]any) JobOption {
|
||||
return func(j *Job) {
|
||||
j.Metadata = metadata
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/mudler/cogito"
|
||||
@@ -61,9 +62,13 @@ func (j *JobResult) SetResponse(response string) {
|
||||
}
|
||||
|
||||
// WaitResult waits for the result of a job
|
||||
func (j *JobResult) WaitResult() *JobResult {
|
||||
<-j.ready
|
||||
func (j *JobResult) WaitResult(ctx context.Context) (*JobResult, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-j.ready:
|
||||
}
|
||||
j.Lock()
|
||||
defer j.Unlock()
|
||||
return j
|
||||
return j, nil
|
||||
}
|
||||
|
||||
+27
-8
@@ -5,8 +5,19 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/conversations"
|
||||
"github.com/mudler/LocalAGI/core/scheduler"
|
||||
)
|
||||
|
||||
// Forward declaration to avoid circular import
|
||||
type TaskScheduler interface {
|
||||
CreateTask(task *scheduler.Task) error
|
||||
GetAllTasks() ([]*scheduler.Task, error)
|
||||
GetTask(id string) (*scheduler.Task, error)
|
||||
DeleteTask(id string) error
|
||||
PauseTask(id string) error
|
||||
ResumeTask(id string) error
|
||||
}
|
||||
|
||||
// State is the structure
|
||||
// that is used to keep track of the current state
|
||||
// and the Agent's short memory that it can update
|
||||
@@ -29,17 +40,26 @@ const (
|
||||
DefaultLastMessageDuration = 5 * time.Minute
|
||||
)
|
||||
|
||||
type ReminderActionResponse struct {
|
||||
Message string `json:"message"`
|
||||
CronExpr string `json:"cron_expr"` // Cron expression for scheduling
|
||||
LastRun time.Time `json:"last_run"` // Last time this reminder was triggered
|
||||
NextRun time.Time `json:"next_run"` // Next scheduled run time
|
||||
IsRecurring bool `json:"is_recurring"` // Whether this is a recurring reminder
|
||||
// RecurringReminderParams are the parameters the LLM provides for set_recurring_reminder.
|
||||
type RecurringReminderParams struct {
|
||||
Message string `json:"message"`
|
||||
CronExpr string `json:"cron_expr"`
|
||||
}
|
||||
|
||||
// OneTimeReminderParams are the parameters the LLM provides for set_onetime_reminder.
|
||||
type OneTimeReminderParams struct {
|
||||
Message string `json:"message"`
|
||||
Delay string `json:"delay"` // Go duration format with day support: "30m", "2h", "1d", "1d12h"
|
||||
}
|
||||
|
||||
// ReminderActionResponse is kept for backward compatibility.
|
||||
// Deprecated: use RecurringReminderParams or OneTimeReminderParams.
|
||||
type ReminderActionResponse = RecurringReminderParams
|
||||
|
||||
type AgentSharedState struct {
|
||||
ConversationTracker *conversations.ConversationTracker[string] `json:"conversation_tracker"`
|
||||
Reminders []ReminderActionResponse `json:"reminders"`
|
||||
Scheduler TaskScheduler `json:"-"` // Not serialized, set at runtime
|
||||
AgentName string `json:"agent_name"`
|
||||
}
|
||||
|
||||
func NewAgentSharedState(lastMessageDuration time.Duration) *AgentSharedState {
|
||||
@@ -48,7 +68,6 @@ func NewAgentSharedState(lastMessageDuration time.Duration) *AgentSharedState {
|
||||
}
|
||||
return &AgentSharedState{
|
||||
ConversationTracker: conversations.NewConversationTracker[string](lastMessageDuration),
|
||||
Reminders: make([]ReminderActionResponse, 0),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,11 @@ services:
|
||||
file: docker-compose.yaml
|
||||
service: dind
|
||||
|
||||
localrecall-postgres:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: localrecall-postgres
|
||||
|
||||
localrecall:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
|
||||
@@ -17,6 +17,11 @@ services:
|
||||
file: docker-compose.yaml
|
||||
service: dind
|
||||
|
||||
localrecall-postgres:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: localrecall-postgres
|
||||
|
||||
localrecall:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
|
||||
@@ -22,6 +22,11 @@ services:
|
||||
file: docker-compose.yaml
|
||||
service: dind
|
||||
|
||||
localrecall-postgres:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: localrecall-postgres
|
||||
|
||||
localrecall:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
|
||||
+43
-11
@@ -8,8 +8,7 @@ services:
|
||||
image: localai/localai:master
|
||||
command:
|
||||
- ${MODEL_NAME:-gemma-3-4b-it-qat}
|
||||
- ${MULTIMODAL_MODEL:-moondream2-20250414}
|
||||
- ${IMAGE_MODEL:-sd-1.5-ggml}
|
||||
- ${MULTIMODAL_MODEL:-gemma-3-4b-it-qat}
|
||||
- granite-embedding-107m-multilingual
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/readyz"]
|
||||
@@ -22,23 +21,46 @@ services:
|
||||
- DEBUG=true
|
||||
#- LOCALAI_API_KEY=sk-1234567890
|
||||
volumes:
|
||||
- ./volumes/models:/models
|
||||
- ./volumes/backends:/backends
|
||||
- ./volumes/images:/tmp/generated/images
|
||||
- models:/models
|
||||
- backends:/backends
|
||||
- images:/tmp/generated/images
|
||||
|
||||
localrecall-postgres:
|
||||
image: quay.io/mudler/localrecall:${LOCALRECALL_VERSION:-v0.5.2}-postgresql
|
||||
environment:
|
||||
- POSTGRES_DB=localrecall
|
||||
- POSTGRES_USER=localrecall
|
||||
- POSTGRES_PASSWORD=localrecall
|
||||
ports:
|
||||
- 5432:5432
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U localrecall"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
localrecall:
|
||||
image: quay.io/mudler/localrecall:main
|
||||
image: quay.io/mudler/localrecall:${LOCALRECALL_VERSION:-v0.5.4}
|
||||
depends_on:
|
||||
localrecall-postgres:
|
||||
condition: service_healthy
|
||||
localai:
|
||||
condition: service_started
|
||||
ports:
|
||||
- 8080
|
||||
environment:
|
||||
- COLLECTION_DB_PATH=/db
|
||||
- DATABASE_URL=postgresql://localrecall:localrecall@localrecall-postgres:5432/localrecall?sslmode=disable
|
||||
- VECTOR_ENGINE=postgres
|
||||
- EMBEDDING_MODEL=granite-embedding-107m-multilingual
|
||||
- FILE_ASSETS=/assets
|
||||
- OPENAI_API_KEY=sk-1234567890
|
||||
- OPENAI_BASE_URL=http://localai:8080
|
||||
- HYBRID_SEARCH_BM25_WEIGHT=0.5
|
||||
- HYBRID_SEARCH_VECTOR_WEIGHT=0.5
|
||||
volumes:
|
||||
- ./volumes/localrag/db:/db
|
||||
- ./volumes/localrag/assets/:/assets
|
||||
- localrag_assets:/assets
|
||||
|
||||
localrecall-healthcheck:
|
||||
depends_on:
|
||||
@@ -64,8 +86,11 @@ services:
|
||||
dind:
|
||||
image: docker:dind
|
||||
privileged: true
|
||||
command: ["dockerd", "-H", "tcp://0.0.0.0:2375", "-H", "unix:///var/run/docker.sock"]
|
||||
environment:
|
||||
- DOCKER_TLS_CERTDIR=""
|
||||
expose:
|
||||
- 2375
|
||||
healthcheck:
|
||||
test: ["CMD", "docker", "info"]
|
||||
interval: 10s
|
||||
@@ -89,7 +114,6 @@ services:
|
||||
environment:
|
||||
- LOCALAGI_MODEL=${MODEL_NAME:-gemma-3-4b-it-qat}
|
||||
- LOCALAGI_MULTIMODAL_MODEL=${MULTIMODAL_MODEL:-moondream2-20250414}
|
||||
- LOCALAGI_IMAGE_MODEL=${IMAGE_MODEL:-sd-1.5-ggml}
|
||||
- LOCALAGI_LLM_API_URL=http://localai:8080
|
||||
#- LOCALAGI_LLM_API_KEY=sk-1234567890
|
||||
- LOCALAGI_LOCALRAG_URL=http://localrecall:8080
|
||||
@@ -101,4 +125,12 @@ services:
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
volumes:
|
||||
- ./volumes/localagi/:/pool
|
||||
- localagi_pool:/pool
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
models:
|
||||
backends:
|
||||
images:
|
||||
localrag_assets:
|
||||
localagi_pool:
|
||||
|
||||
@@ -4,16 +4,20 @@ go 1.24.4
|
||||
|
||||
require (
|
||||
github.com/Masterminds/sprig/v3 v3.3.0
|
||||
github.com/blevesearch/bleve/v2 v2.5.7
|
||||
github.com/bwmarrin/discordgo v0.29.0
|
||||
github.com/chasefleming/elem-go v0.30.0
|
||||
github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2
|
||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
|
||||
github.com/eritikass/githubmarkdownconvertergo v0.1.10
|
||||
github.com/go-telegram/bot v1.17.0
|
||||
github.com/gofiber/fiber/v2 v2.52.9
|
||||
github.com/google/go-github/v69 v69.2.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/jung-kurt/gofpdf v1.16.2
|
||||
github.com/modelcontextprotocol/go-sdk v1.1.0
|
||||
github.com/mudler/cogito v0.4.2
|
||||
github.com/mudler/cogito v0.9.1-0.20260216182842-e9820e6bf7b9
|
||||
github.com/mudler/xlog v0.0.1
|
||||
github.com/onsi/ginkgo/v2 v2.25.3
|
||||
github.com/onsi/gomega v1.38.2
|
||||
github.com/philippgille/chromem-go v0.7.0
|
||||
@@ -35,15 +39,39 @@ require (
|
||||
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/RoaringBitmap/roaring/v2 v2.4.5 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.22.0 // indirect
|
||||
github.com/blevesearch/bleve_index_api v1.2.11 // indirect
|
||||
github.com/blevesearch/geo v0.2.4 // indirect
|
||||
github.com/blevesearch/go-faiss v1.0.26 // indirect
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
|
||||
github.com/blevesearch/gtreap v0.1.1 // indirect
|
||||
github.com/blevesearch/mmap-go v1.0.4 // indirect
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.3.13 // indirect
|
||||
github.com/blevesearch/segment v0.9.1 // indirect
|
||||
github.com/blevesearch/snowballstem v0.9.0 // indirect
|
||||
github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
|
||||
github.com/blevesearch/vellum v1.1.0 // indirect
|
||||
github.com/blevesearch/zapx/v11 v11.4.2 // indirect
|
||||
github.com/blevesearch/zapx/v12 v12.4.2 // indirect
|
||||
github.com/blevesearch/zapx/v13 v13.4.2 // indirect
|
||||
github.com/blevesearch/zapx/v14 v14.4.2 // indirect
|
||||
github.com/blevesearch/zapx/v15 v15.4.2 // indirect
|
||||
github.com/blevesearch/zapx/v16 v16.2.8 // indirect
|
||||
github.com/golang/snappy v0.0.4 // 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/json-iterator/go v1.1.12 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/mudler/xlog v0.0.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mschoch/smat v0.2.0 // 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.etcd.io/bbolt v1.4.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
)
|
||||
|
||||
@@ -16,6 +16,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
|
||||
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/RoaringBitmap/roaring/v2 v2.4.5 h1:uGrrMreGjvAtTBobc0g5IrW1D5ldxDQYe2JW2gggRdg=
|
||||
github.com/RoaringBitmap/roaring/v2 v2.4.5/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
@@ -27,6 +29,46 @@ github.com/antchfx/xmlquery v1.4.4/go.mod h1:AEPEEPYE9GnA2mj5Ur2L5Q5/2PycJ0N9Fus
|
||||
github.com/antchfx/xpath v1.3.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||
github.com/antchfx/xpath v1.3.4 h1:1ixrW1VnXd4HurCj7qnqnR0jo14g8JMe20Fshg1Vgz4=
|
||||
github.com/antchfx/xpath v1.3.4/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||
github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4=
|
||||
github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/blevesearch/bleve/v2 v2.5.7 h1:2d9YrL5zrX5EBBW++GOaEKjE+NPWeZGaX77IM26m1Z8=
|
||||
github.com/blevesearch/bleve/v2 v2.5.7/go.mod h1:yj0NlS7ocGC4VOSAedqDDMktdh2935v2CSWOCDMHdSA=
|
||||
github.com/blevesearch/bleve_index_api v1.2.11 h1:bXQ54kVuwP8hdrXUSOnvTQfgK0KI1+f9A0ITJT8tX1s=
|
||||
github.com/blevesearch/bleve_index_api v1.2.11/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0=
|
||||
github.com/blevesearch/geo v0.2.4 h1:ECIGQhw+QALCZaDcogRTNSJYQXRtC8/m8IKiA706cqk=
|
||||
github.com/blevesearch/geo v0.2.4/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8=
|
||||
github.com/blevesearch/go-faiss v1.0.26 h1:4dRLolFgjPyjkaXwff4NfbZFdE/dfywbzDqporeQvXI=
|
||||
github.com/blevesearch/go-faiss v1.0.26/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk=
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
|
||||
github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y=
|
||||
github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk=
|
||||
github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc=
|
||||
github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.3.13 h1:ZPjv/4VwWvHJZKeMSgScCapOy8+DdmsmRyLmSB88UoY=
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.3.13/go.mod h1:ENk2LClTehOuMS8XzN3UxBEErYmtwkE7MAArFTXs9Vc=
|
||||
github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU=
|
||||
github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw=
|
||||
github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s=
|
||||
github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs=
|
||||
github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMGZzVrdmaozG2MfoB+A=
|
||||
github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ=
|
||||
github.com/blevesearch/vellum v1.1.0 h1:CinkGyIsgVlYf8Y2LUQHvdelgXr6PYuvoDIajq6yR9w=
|
||||
github.com/blevesearch/vellum v1.1.0/go.mod h1:QgwWryE8ThtNPxtgWJof5ndPfx0/YMBh+W2weHKPw8Y=
|
||||
github.com/blevesearch/zapx/v11 v11.4.2 h1:l46SV+b0gFN+Rw3wUI1YdMWdSAVhskYuvxlcgpQFljs=
|
||||
github.com/blevesearch/zapx/v11 v11.4.2/go.mod h1:4gdeyy9oGa/lLa6D34R9daXNUvfMPZqUYjPwiLmekwc=
|
||||
github.com/blevesearch/zapx/v12 v12.4.2 h1:fzRbhllQmEMUuAQ7zBuMvKRlcPA5ESTgWlDEoB9uQNE=
|
||||
github.com/blevesearch/zapx/v12 v12.4.2/go.mod h1:TdFmr7afSz1hFh/SIBCCZvcLfzYvievIH6aEISCte58=
|
||||
github.com/blevesearch/zapx/v13 v13.4.2 h1:46PIZCO/ZuKZYgxI8Y7lOJqX3Irkc3N8W82QTK3MVks=
|
||||
github.com/blevesearch/zapx/v13 v13.4.2/go.mod h1:knK8z2NdQHlb5ot/uj8wuvOq5PhDGjNYQQy0QDnopZk=
|
||||
github.com/blevesearch/zapx/v14 v14.4.2 h1:2SGHakVKd+TrtEqpfeq8X+So5PShQ5nW6GNxT7fWYz0=
|
||||
github.com/blevesearch/zapx/v14 v14.4.2/go.mod h1:rz0XNb/OZSMjNorufDGSpFpjoFKhXmppH9Hi7a877D8=
|
||||
github.com/blevesearch/zapx/v15 v15.4.2 h1:sWxpDE0QQOTjyxYbAVjt3+0ieu8NCE0fDRaFxEsp31k=
|
||||
github.com/blevesearch/zapx/v15 v15.4.2/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw=
|
||||
github.com/blevesearch/zapx/v16 v16.2.8 h1:SlnzF0YGtSlrsOE3oE7EgEX6BIepGpeqxs1IjMbHLQI=
|
||||
github.com/blevesearch/zapx/v16 v16.2.8/go.mod h1:murSoCJPCk25MqURrcJaBQ1RekuqSCSfMjXH4rHyA14=
|
||||
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
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=
|
||||
@@ -48,8 +90,11 @@ github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHf
|
||||
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 h1:OtSeLS5y0Uy01jaKK4mA/WVIYtpzVm63vLVAPzJXigg=
|
||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8/go.mod h1:apkPC/CR3s48O2D7Y++n1XWEpgPNNCjXYga3PPbJe2E=
|
||||
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=
|
||||
@@ -104,6 +149,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b h1:EY/KpStFl60qA17CptGXhwfZ+k1sFNJIUNR8DdbcuUk=
|
||||
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
@@ -115,6 +162,7 @@ github.com/google/go-github/v69 v69.2.0 h1:wR+Wi/fN2zdUx9YxSmYE0ktiX9IAR/BeePzea
|
||||
github.com/google/go-github/v69 v69.2.0/go.mod h1:xne4jymxLR6Uj9b7J7PyTpkMYstEMMwGZa0Aehh1azM=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
|
||||
github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
|
||||
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 h1:gD0vax+4I+mAj+jEChEf25Ia07Jq7kYOFO5PPhAxFl4=
|
||||
@@ -126,6 +174,11 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
|
||||
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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
||||
github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc=
|
||||
github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0=
|
||||
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.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||
@@ -168,10 +221,29 @@ github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/modelcontextprotocol/go-sdk v1.1.0 h1:Qjayg53dnKC4UZ+792W21e4BpwEZBzwgRW6LrjLWSwA=
|
||||
github.com/modelcontextprotocol/go-sdk v1.1.0/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
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.4.2 h1:1PypFvUq3A+iBxMCR0v+sGXlhOsRnvyFZhEE93tsuDU=
|
||||
github.com/mudler/cogito v0.4.2/go.mod h1:2uhEElCTq8eXSsqJ1JF01oA5h9niXSELVKqCF1PqjEw=
|
||||
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
|
||||
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
|
||||
github.com/mudler/cogito v0.8.2-0.20260206153401-a5346975d42b h1:LXHovZzNgP0n/oYEoO4zDt4k4CRvG0Owhu8x/OVGhYc=
|
||||
github.com/mudler/cogito v0.8.2-0.20260206153401-a5346975d42b/go.mod h1:6sfja3lcu2nWRzEc0wwqGNu/eCG3EWgij+8s7xyUeQ4=
|
||||
github.com/mudler/cogito v0.8.2-0.20260214201734-da0d4ceb2b44 h1:joGszpItINnZdoL/0p2077Wz2xnxMGRSRgYN5mS7I4c=
|
||||
github.com/mudler/cogito v0.8.2-0.20260214201734-da0d4ceb2b44/go.mod h1:6sfja3lcu2nWRzEc0wwqGNu/eCG3EWgij+8s7xyUeQ4=
|
||||
github.com/mudler/cogito v0.8.2-0.20260215213413-af34921ff561 h1:qA7dGJhF5GjgGKHh0lOITZjl9q2jehjKqxxCnaUR1yg=
|
||||
github.com/mudler/cogito v0.8.2-0.20260215213413-af34921ff561/go.mod h1:6sfja3lcu2nWRzEc0wwqGNu/eCG3EWgij+8s7xyUeQ4=
|
||||
github.com/mudler/cogito v0.8.2-0.20260215230740-d1c0dc9bd9dc h1:tBAGwQq5kOSIh+vfLffVr5Th2ajFwrTj0usLgyGM2CQ=
|
||||
github.com/mudler/cogito v0.8.2-0.20260215230740-d1c0dc9bd9dc/go.mod h1:6sfja3lcu2nWRzEc0wwqGNu/eCG3EWgij+8s7xyUeQ4=
|
||||
github.com/mudler/cogito v0.9.1-0.20260216144443-c96e8ddc1157 h1:M2Yx84QtKQrhMWlB+K99RKJ+x5s8HoRumegObaLuQlI=
|
||||
github.com/mudler/cogito v0.9.1-0.20260216144443-c96e8ddc1157/go.mod h1:6sfja3lcu2nWRzEc0wwqGNu/eCG3EWgij+8s7xyUeQ4=
|
||||
github.com/mudler/cogito v0.9.1-0.20260216163119-07b624cc772f h1:LkCJD11Mlx8ZyHoNRbpxbrw1Znl9GiFKjZbcXYwAKwk=
|
||||
github.com/mudler/cogito v0.9.1-0.20260216163119-07b624cc772f/go.mod h1:6sfja3lcu2nWRzEc0wwqGNu/eCG3EWgij+8s7xyUeQ4=
|
||||
github.com/mudler/cogito v0.9.1-0.20260216182842-e9820e6bf7b9 h1:Ek+fAIUt9v5tqBgPZ89QTgGXwOSZcCiElD37NkbdmSk=
|
||||
github.com/mudler/cogito v0.9.1-0.20260216182842-e9820e6bf7b9/go.mod h1:6sfja3lcu2nWRzEc0wwqGNu/eCG3EWgij+8s7xyUeQ4=
|
||||
github.com/mudler/xlog v0.0.1 h1:yR3/wszd3ZM6u1n96YITJZ4yUcDgqHSwvQmzUJa+8vg=
|
||||
github.com/mudler/xlog v0.0.1/go.mod h1:39f5vcd05Qd6GWKM8IjyHNQ7AmOx3ZM0YfhfIGhC18U=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
@@ -186,6 +258,8 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw
|
||||
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/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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=
|
||||
@@ -207,6 +281,7 @@ github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a/go.mod h1:
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
|
||||
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
|
||||
github.com/sashabaranov/go-openai v1.41.2 h1:vfPRBZNMpnqu8ELsclWcAvF19lDNgh1t6TVfFFOPiSM=
|
||||
@@ -230,7 +305,9 @@ github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02n
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
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=
|
||||
@@ -270,6 +347,8 @@ 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.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
|
||||
go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
|
||||
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=
|
||||
@@ -299,6 +378,7 @@ golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
@@ -325,6 +405,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -386,6 +468,8 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056 h1:6YFJoB+0fUH6X3xU/G2tQqCYg+PkGtnZ5nMR5rpw72g=
|
||||
|
||||
@@ -23,9 +23,7 @@ var stateDir = os.Getenv("LOCALAGI_STATE_DIR")
|
||||
var localRAG = os.Getenv("LOCALAGI_LOCALRAG_URL")
|
||||
var withLogs = os.Getenv("LOCALAGI_ENABLE_CONVERSATIONS_LOGGING") == "true"
|
||||
var apiKeysEnv = os.Getenv("LOCALAGI_API_KEYS")
|
||||
var imageModel = os.Getenv("LOCALAGI_IMAGE_MODEL")
|
||||
var conversationDuration = os.Getenv("LOCALAGI_CONVERSATION_DURATION")
|
||||
var localOperatorBaseURL = os.Getenv("LOCALOPERATOR_BASE_URL")
|
||||
var customActionsDir = os.Getenv("LOCALAGI_CUSTOM_ACTIONS_DIR")
|
||||
var sshBoxURL = os.Getenv("LOCALAGI_SSHBOX_URL")
|
||||
|
||||
@@ -65,17 +63,14 @@ func main() {
|
||||
transcriptionModel,
|
||||
transcriptionLanguage,
|
||||
ttsModel,
|
||||
imageModel,
|
||||
apiURL,
|
||||
apiKey,
|
||||
stateDir,
|
||||
localRAG,
|
||||
services.Actions(map[string]string{
|
||||
services.ActionConfigBrowserAgentRunner: localOperatorBaseURL,
|
||||
services.ActionConfigDeepResearchRunner: localOperatorBaseURL,
|
||||
services.ActionConfigSSHBoxURL: sshBoxURL,
|
||||
services.ConfigStateDir: stateDir,
|
||||
services.CustomActionsDir: customActionsDir,
|
||||
services.ActionConfigSSHBoxURL: sshBoxURL,
|
||||
services.ConfigStateDir: stateDir,
|
||||
services.CustomActionsDir: customActionsDir,
|
||||
}),
|
||||
services.Connectors,
|
||||
services.DynamicPrompts(map[string]string{
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
package localoperator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
baseURL string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewClient(baseURL string, timeout ...time.Duration) *Client {
|
||||
defaultTimeout := 30 * time.Second
|
||||
if len(timeout) > 0 {
|
||||
defaultTimeout = timeout[0]
|
||||
}
|
||||
|
||||
return &Client{
|
||||
baseURL: baseURL,
|
||||
httpClient: &http.Client{
|
||||
Timeout: defaultTimeout,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type AgentRequest struct {
|
||||
Goal string `json:"goal"`
|
||||
MaxAttempts int `json:"max_attempts,omitempty"`
|
||||
MaxNoActionAttempts int `json:"max_no_action_attempts,omitempty"`
|
||||
}
|
||||
|
||||
type DesktopAgentRequest struct {
|
||||
AgentRequest
|
||||
DesktopURL string `json:"desktop_url"`
|
||||
}
|
||||
|
||||
type DeepResearchRequest struct {
|
||||
Topic string `json:"topic"`
|
||||
MaxCycles int `json:"max_cycles,omitempty"`
|
||||
MaxNoActionAttempts int `json:"max_no_action_attempts,omitempty"`
|
||||
MaxResults int `json:"max_results,omitempty"`
|
||||
}
|
||||
|
||||
// Response types
|
||||
type StateDescription struct {
|
||||
CurrentURL string `json:"current_url"`
|
||||
PageTitle string `json:"page_title"`
|
||||
PageContentDescription string `json:"page_content_description"`
|
||||
Screenshot string `json:"screenshot"`
|
||||
ScreenshotMimeType string `json:"screenshot_mime_type"`
|
||||
}
|
||||
|
||||
type StateHistory struct {
|
||||
States []StateDescription `json:"states"`
|
||||
}
|
||||
|
||||
type DesktopStateDescription struct {
|
||||
ScreenContent string `json:"screen_content"`
|
||||
ScreenshotPath string `json:"screenshot_path"`
|
||||
}
|
||||
|
||||
type DesktopStateHistory struct {
|
||||
States []DesktopStateDescription `json:"states"`
|
||||
}
|
||||
|
||||
type SearchResult struct {
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type ResearchResult struct {
|
||||
Topic string `json:"topic"`
|
||||
Summary string `json:"summary"`
|
||||
Sources []SearchResult `json:"sources"`
|
||||
KnowledgeGaps []string `json:"knowledge_gaps"`
|
||||
SearchQueries []string `json:"search_queries"`
|
||||
ResearchCycles int `json:"research_cycles"`
|
||||
CompletionTime time.Duration `json:"completion_time"`
|
||||
}
|
||||
|
||||
func (c *Client) RunBrowserAgent(req AgentRequest) (*StateHistory, error) {
|
||||
return post[*StateHistory](c.httpClient, c.baseURL+"/api/browser/run", req)
|
||||
}
|
||||
|
||||
func (c *Client) RunDesktopAgent(req DesktopAgentRequest) (*DesktopStateHistory, error) {
|
||||
return post[*DesktopStateHistory](c.httpClient, c.baseURL+"/api/desktop/run", req)
|
||||
}
|
||||
|
||||
func (c *Client) RunDeepResearch(req DeepResearchRequest) (*ResearchResult, error) {
|
||||
return post[*ResearchResult](c.httpClient, c.baseURL+"/api/deep-research/run", req)
|
||||
}
|
||||
|
||||
func (c *Client) Readyz() (string, error) {
|
||||
return c.get("/readyz")
|
||||
}
|
||||
|
||||
func (c *Client) Healthz() (string, error) {
|
||||
return c.get("/healthz")
|
||||
}
|
||||
|
||||
func (c *Client) get(path string) (string, error) {
|
||||
resp, err := c.httpClient.Get(c.baseURL + path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to make request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return "", fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
return resp.Status, nil
|
||||
}
|
||||
|
||||
func post[T any](client *http.Client, url string, body interface{}) (T, error) {
|
||||
var result T
|
||||
jsonBody, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("failed to marshal request body: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Sending request", "url", url, "body", string(jsonBody))
|
||||
|
||||
resp, err := client.Post(url, "application/json", bytes.NewBuffer(jsonBody))
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("failed to make request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
fmt.Println("Response", "status", resp.StatusCode)
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return result, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return result, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
+175
-48
@@ -11,6 +11,7 @@ import (
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -39,6 +40,11 @@ func NewWrappedClient(baseURL, apiKey, c string) *WrappedClient {
|
||||
return wc
|
||||
}
|
||||
|
||||
// Collection returns the collection name for this client.
|
||||
func (c *WrappedClient) Collection() string {
|
||||
return c.collection
|
||||
}
|
||||
|
||||
func (c *WrappedClient) Count() int {
|
||||
entries, err := c.ListEntries(c.collection)
|
||||
if err != nil {
|
||||
@@ -90,6 +96,37 @@ func (c *WrappedClient) Store(s string) error {
|
||||
return c.Client.Store(c.collection, f)
|
||||
}
|
||||
|
||||
// GetEntryContent returns the full file content (no chunk overlap) and the number of chunks for the entry.
|
||||
func (c *WrappedClient) GetEntryContent(entry string) (content string, chunkCount int, err error) {
|
||||
return c.Client.GetEntryContent(c.collection, entry)
|
||||
}
|
||||
|
||||
// apiResponse is the standardized LocalRecall API response wrapper (since 3f73ff3a).
|
||||
type apiResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Data json.RawMessage `json:"data,omitempty"`
|
||||
Error *apiError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type apiError struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Details string `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
// parseAPIError reads the response body and returns an error from the API response or a generic message.
|
||||
func parseAPIError(resp *http.Response, body []byte, fallback string) error {
|
||||
var wrap apiResponse
|
||||
if err := json.Unmarshal(body, &wrap); err == nil && wrap.Error != nil {
|
||||
if wrap.Error.Details != "" {
|
||||
return fmt.Errorf("%s: %s", wrap.Error.Message, wrap.Error.Details)
|
||||
}
|
||||
return errors.New(wrap.Error.Message)
|
||||
}
|
||||
return fmt.Errorf("%s: %s", fallback, string(body))
|
||||
}
|
||||
|
||||
// Result represents a single result from a query.
|
||||
type Result struct {
|
||||
ID string
|
||||
@@ -103,6 +140,13 @@ type Result struct {
|
||||
Similarity float32
|
||||
}
|
||||
|
||||
// EntryChunk represents a single chunk (legacy; GetEntryContent now returns full file content).
|
||||
type EntryChunk struct {
|
||||
ID string `json:"id"`
|
||||
Content string `json:"content"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
}
|
||||
|
||||
// Client is a client for the RAG API
|
||||
type Client struct {
|
||||
BaseURL string
|
||||
@@ -153,7 +197,8 @@ func (c *Client) CreateCollection(name string) error {
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
return errors.New("failed to create collection")
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return parseAPIError(resp, body, "failed to create collection")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -176,17 +221,30 @@ func (c *Client) ListCollections() ([]string, error) {
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.New("failed to list collections")
|
||||
}
|
||||
|
||||
var collections []string
|
||||
err = json.NewDecoder(resp.Body).Decode(&collections)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return collections, nil
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, parseAPIError(resp, body, "failed to list collections")
|
||||
}
|
||||
|
||||
var wrap apiResponse
|
||||
if err := json.Unmarshal(body, &wrap); err != nil || !wrap.Success {
|
||||
if wrap.Error != nil {
|
||||
return nil, errors.New(wrap.Error.Message)
|
||||
}
|
||||
return nil, fmt.Errorf("invalid response: %w", err)
|
||||
}
|
||||
|
||||
var data struct {
|
||||
Collections []string `json:"collections"`
|
||||
}
|
||||
if err := json.Unmarshal(wrap.Data, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data.Collections, nil
|
||||
}
|
||||
|
||||
// ListEntries lists all entries in a collection
|
||||
@@ -206,17 +264,75 @@ func (c *Client) ListEntries(collection string) ([]string, error) {
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.New("failed to list entries")
|
||||
}
|
||||
|
||||
var entries []string
|
||||
err = json.NewDecoder(resp.Body).Decode(&entries)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return entries, nil
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, parseAPIError(resp, body, "failed to list entries")
|
||||
}
|
||||
|
||||
var wrap apiResponse
|
||||
if err := json.Unmarshal(body, &wrap); err != nil || !wrap.Success {
|
||||
if wrap.Error != nil {
|
||||
return nil, errors.New(wrap.Error.Message)
|
||||
}
|
||||
return nil, fmt.Errorf("invalid response: %w", err)
|
||||
}
|
||||
|
||||
var data struct {
|
||||
Entries []string `json:"entries"`
|
||||
}
|
||||
if err := json.Unmarshal(wrap.Data, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data.Entries, nil
|
||||
}
|
||||
|
||||
// GetEntryContent returns the full file content (no chunk overlap) and the number of chunks for the entry.
|
||||
func (c *Client) GetEntryContent(collection, entry string) (content string, chunkCount int, err error) {
|
||||
entryEscaped := url.PathEscape(entry)
|
||||
reqURL := fmt.Sprintf("%s/api/collections/%s/entries/%s", c.BaseURL, collection, entryEscaped)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, reqURL, nil)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
c.addAuthHeader(req)
|
||||
|
||||
httpClient := &http.Client{}
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", 0, parseAPIError(resp, body, "failed to get entry content")
|
||||
}
|
||||
|
||||
var wrap apiResponse
|
||||
if err := json.Unmarshal(body, &wrap); err != nil || !wrap.Success {
|
||||
if wrap.Error != nil {
|
||||
return "", 0, errors.New(wrap.Error.Message)
|
||||
}
|
||||
return "", 0, fmt.Errorf("invalid response: %w", err)
|
||||
}
|
||||
|
||||
var data struct {
|
||||
Content string `json:"content"`
|
||||
ChunkCount int `json:"chunk_count"`
|
||||
}
|
||||
if err := json.Unmarshal(wrap.Data, &data); err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
return data.Content, data.ChunkCount, nil
|
||||
}
|
||||
|
||||
// DeleteEntry deletes an entry in a collection
|
||||
@@ -246,19 +362,30 @@ func (c *Client) DeleteEntry(collection, entry string) ([]string, error) {
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
bodyResult := new(bytes.Buffer)
|
||||
bodyResult.ReadFrom(resp.Body)
|
||||
return nil, errors.New("failed to delete entry: " + bodyResult.String())
|
||||
}
|
||||
|
||||
var results []string
|
||||
err = json.NewDecoder(resp.Body).Decode(&results)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, parseAPIError(resp, body, "failed to delete entry")
|
||||
}
|
||||
|
||||
var wrap apiResponse
|
||||
if err := json.Unmarshal(body, &wrap); err != nil || !wrap.Success {
|
||||
if wrap.Error != nil {
|
||||
return nil, errors.New(wrap.Error.Message)
|
||||
}
|
||||
return nil, fmt.Errorf("invalid response: %w", err)
|
||||
}
|
||||
|
||||
var data struct {
|
||||
RemainingEntries []string `json:"remaining_entries"`
|
||||
}
|
||||
if err := json.Unmarshal(wrap.Data, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data.RemainingEntries, nil
|
||||
}
|
||||
|
||||
// Search searches a collection
|
||||
@@ -289,17 +416,30 @@ func (c *Client) Search(collection, query string, maxResults int) ([]Result, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.New("failed to search collection")
|
||||
}
|
||||
|
||||
var results []Result
|
||||
err = json.NewDecoder(resp.Body).Decode(&results)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, parseAPIError(resp, body, "failed to search collection")
|
||||
}
|
||||
|
||||
var wrap apiResponse
|
||||
if err := json.Unmarshal(body, &wrap); err != nil || !wrap.Success {
|
||||
if wrap.Error != nil {
|
||||
return nil, errors.New(wrap.Error.Message)
|
||||
}
|
||||
return nil, fmt.Errorf("invalid response: %w", err)
|
||||
}
|
||||
|
||||
var data struct {
|
||||
Results []Result `json:"results"`
|
||||
}
|
||||
if err := json.Unmarshal(wrap.Data, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data.Results, nil
|
||||
}
|
||||
|
||||
// Reset resets a collection
|
||||
@@ -320,9 +460,8 @@ func (c *Client) Reset(collection string) error {
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
b := new(bytes.Buffer)
|
||||
b.ReadFrom(resp.Body)
|
||||
return errors.New("failed to reset collection: " + b.String())
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return parseAPIError(resp, body, "failed to reset collection")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -371,20 +510,8 @@ func (c *Client) Store(collection, filePath string) error {
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
b := new(bytes.Buffer)
|
||||
b.ReadFrom(resp.Body)
|
||||
|
||||
type response struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
var r response
|
||||
err = json.Unmarshal(b.Bytes(), &r)
|
||||
if err == nil {
|
||||
return errors.New("failed to upload file: " + r.Error)
|
||||
}
|
||||
|
||||
return errors.New("failed to upload file")
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return parseAPIError(resp, body, "failed to upload file")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
+47
-31
@@ -22,8 +22,6 @@ const (
|
||||
// Actions
|
||||
ActionSearch = "search"
|
||||
ActionCustom = "custom"
|
||||
ActionBrowserAgentRunner = "browser-agent-runner"
|
||||
ActionDeepResearchRunner = "deep-research-runner"
|
||||
ActionGithubIssueLabeler = "github-issue-labeler"
|
||||
ActionGithubIssueOpener = "github-issue-opener"
|
||||
ActionGithubIssueEditor = "github-issue-editor"
|
||||
@@ -47,16 +45,21 @@ const (
|
||||
ActionTwitterPost = "twitter-post"
|
||||
ActionSendMail = "send-mail"
|
||||
ActionGenerateImage = "generate_image"
|
||||
ActionGenerateSong = "generate_song"
|
||||
ActionGeneratePDF = "generate_pdf"
|
||||
ActionCounter = "counter"
|
||||
ActionCallAgents = "call_agents"
|
||||
ActionShellcommand = "shell-command"
|
||||
ActionSendTelegramMessage = "send-telegram-message"
|
||||
ActionSetReminder = "set_reminder"
|
||||
ActionSetRecurringReminder = "set_recurring_reminder"
|
||||
ActionSetOneTimeReminder = "set_onetime_reminder"
|
||||
ActionListReminders = "list_reminders"
|
||||
ActionRemoveReminder = "remove_reminder"
|
||||
ActionAddToMemory = "add_to_memory"
|
||||
ActionListMemory = "list_memory"
|
||||
ActionRemoveFromMemory = "remove_from_memory"
|
||||
ActionSearchMemory = "search_memory"
|
||||
ActionPiKVMPowerControl = "pikvm_power_control"
|
||||
ActionWebhook = "webhook"
|
||||
)
|
||||
@@ -79,8 +82,6 @@ var AvailableActions = []string{
|
||||
ActionGithubGetAllContent,
|
||||
ActionGithubRepositorySearchFiles,
|
||||
ActionGithubRepositoryListFiles,
|
||||
ActionBrowserAgentRunner,
|
||||
ActionDeepResearchRunner,
|
||||
ActionGithubRepositoryCreateOrUpdate,
|
||||
ActionGithubIssueReader,
|
||||
ActionGithubIssueCommenter,
|
||||
@@ -94,6 +95,8 @@ var AvailableActions = []string{
|
||||
ActionWikipedia,
|
||||
ActionSendMail,
|
||||
ActionGenerateImage,
|
||||
ActionGenerateSong,
|
||||
ActionGeneratePDF,
|
||||
ActionTwitterPost,
|
||||
ActionCounter,
|
||||
ActionCallAgents,
|
||||
@@ -105,6 +108,7 @@ var AvailableActions = []string{
|
||||
ActionAddToMemory,
|
||||
ActionListMemory,
|
||||
ActionRemoveFromMemory,
|
||||
ActionSearchMemory,
|
||||
ActionPiKVMPowerControl,
|
||||
ActionWebhook,
|
||||
}
|
||||
@@ -115,21 +119,21 @@ var DefaultActions = []config.FieldGroup{
|
||||
Label: "Search",
|
||||
Fields: actions.SearchConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "browser-agent-runner",
|
||||
Label: "Browser Agent Runner",
|
||||
Fields: actions.BrowserAgentRunnerConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "deep-research-runner",
|
||||
Label: "Deep Research Runner",
|
||||
Fields: actions.DeepResearchRunnerConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "generate_image",
|
||||
Label: "Generate Image",
|
||||
Fields: actions.GenImageConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "generate_song",
|
||||
Label: "Generate Song",
|
||||
Fields: actions.GenSongConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "generate_pdf",
|
||||
Label: "Generate PDF",
|
||||
Fields: actions.GenPDFConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "add_to_memory",
|
||||
Label: "Add to Memory",
|
||||
@@ -145,6 +149,11 @@ var DefaultActions = []config.FieldGroup{
|
||||
Label: "Remove from Memory",
|
||||
Fields: actions.RemoveFromMemoryConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "search_memory",
|
||||
Label: "Search Memory",
|
||||
Fields: actions.SearchMemoryConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-labeler",
|
||||
Label: "GitHub Issue Labeler",
|
||||
@@ -281,8 +290,13 @@ var DefaultActions = []config.FieldGroup{
|
||||
Fields: actions.SendTelegramMessageConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "set_reminder",
|
||||
Label: "Set Reminder",
|
||||
Name: "set_recurring_reminder",
|
||||
Label: "Set Recurring Reminder",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
{
|
||||
Name: "set_onetime_reminder",
|
||||
Label: "Set One-Time Reminder",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
{
|
||||
@@ -308,11 +322,9 @@ var DefaultActions = []config.FieldGroup{
|
||||
}
|
||||
|
||||
const (
|
||||
ActionConfigBrowserAgentRunner = "browser-agent-runner-base-url"
|
||||
ActionConfigDeepResearchRunner = "deep-research-runner-base-url"
|
||||
ActionConfigSSHBoxURL = "sshbox-url"
|
||||
ConfigStateDir = "state-dir"
|
||||
CustomActionsDir = "custom-actions-dir"
|
||||
ActionConfigSSHBoxURL = "sshbox-url"
|
||||
ConfigStateDir = "state-dir"
|
||||
CustomActionsDir = "custom-actions-dir"
|
||||
)
|
||||
|
||||
func customActions(customActionsDir string, existingActionConfigs map[string]map[string]string) (allActions []types.Action) {
|
||||
@@ -400,13 +412,17 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
|
||||
config = map[string]string{}
|
||||
}
|
||||
|
||||
memoryFilePath := memoryPath(agentName, actionsConfigs)
|
||||
memoryIdxPath := memoryIndexPath(agentName, actionsConfigs)
|
||||
|
||||
switch name {
|
||||
case ActionCustom:
|
||||
a, err = action.NewCustom(config, "")
|
||||
case ActionGenerateImage:
|
||||
a = actions.NewGenImage(config)
|
||||
case ActionGenerateSong:
|
||||
a = actions.NewGenSong(config)
|
||||
case ActionGeneratePDF:
|
||||
a = actions.NewGenPDF(config)
|
||||
case ActionSearch:
|
||||
a = actions.NewSearch(config)
|
||||
case ActionGithubIssueLabeler:
|
||||
@@ -419,10 +435,6 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
|
||||
a = actions.NewGithubIssueCloser(config)
|
||||
case ActionGithubIssueSearcher:
|
||||
a = actions.NewGithubIssueSearch(config)
|
||||
case ActionBrowserAgentRunner:
|
||||
a = actions.NewBrowserAgentRunner(config, actionsConfigs[ActionConfigBrowserAgentRunner])
|
||||
case ActionDeepResearchRunner:
|
||||
a = actions.NewDeepResearchRunner(config, actionsConfigs[ActionConfigDeepResearchRunner])
|
||||
case ActionGithubIssueReader:
|
||||
a = actions.NewGithubIssueReader(config)
|
||||
case ActionGithubPRReader:
|
||||
@@ -467,18 +479,22 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
|
||||
a = actions.NewShell(config, actionsConfigs[ActionConfigSSHBoxURL])
|
||||
case ActionSendTelegramMessage:
|
||||
a = actions.NewSendTelegramMessageRunner(config)
|
||||
case ActionSetReminder:
|
||||
a = action.NewReminder()
|
||||
case ActionSetRecurringReminder:
|
||||
a = action.NewRecurringReminder()
|
||||
case ActionSetOneTimeReminder:
|
||||
a = action.NewOneTimeReminder()
|
||||
case ActionListReminders:
|
||||
a = action.NewListReminders()
|
||||
case ActionRemoveReminder:
|
||||
a = action.NewRemoveReminder()
|
||||
case ActionAddToMemory:
|
||||
a, _, _ = actions.NewMemoryActions(memoryFilePath, config)
|
||||
a, _, _, _ = actions.NewMemoryActions(memoryIdxPath, config)
|
||||
case ActionListMemory:
|
||||
_, a, _ = actions.NewMemoryActions(memoryFilePath, config)
|
||||
_, a, _, _ = actions.NewMemoryActions(memoryIdxPath, config)
|
||||
case ActionRemoveFromMemory:
|
||||
_, _, a = actions.NewMemoryActions(memoryFilePath, config)
|
||||
_, _, a, _ = actions.NewMemoryActions(memoryIdxPath, config)
|
||||
case ActionSearchMemory:
|
||||
_, _, _, a = actions.NewMemoryActions(memoryIdxPath, config)
|
||||
case ActionPiKVMPowerControl:
|
||||
a = actions.NewPiKVMAction(config)
|
||||
default:
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
api "github.com/mudler/LocalAGI/pkg/localoperator"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
const (
|
||||
MetadataBrowserAgentHistory = "browser_agent_history"
|
||||
)
|
||||
|
||||
type BrowserAgentRunner struct {
|
||||
baseURL, customActionName string
|
||||
client *api.Client
|
||||
}
|
||||
|
||||
func NewBrowserAgentRunner(config map[string]string, defaultURL string) *BrowserAgentRunner {
|
||||
if config["baseURL"] == "" {
|
||||
config["baseURL"] = defaultURL
|
||||
}
|
||||
|
||||
timeout := "15m"
|
||||
if config["timeout"] != "" {
|
||||
timeout = config["timeout"]
|
||||
}
|
||||
|
||||
duration, err := time.ParseDuration(timeout)
|
||||
if err != nil {
|
||||
// If parsing fails, use default 15 minutes
|
||||
duration = 15 * time.Minute
|
||||
}
|
||||
|
||||
client := api.NewClient(config["baseURL"], duration)
|
||||
|
||||
return &BrowserAgentRunner{
|
||||
client: client,
|
||||
baseURL: config["baseURL"],
|
||||
customActionName: config["customActionName"],
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BrowserAgentRunner) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := api.AgentRequest{}
|
||||
err := params.Unmarshal(&result)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to unmarshal params: %w", err)
|
||||
}
|
||||
|
||||
req := api.AgentRequest{
|
||||
Goal: result.Goal,
|
||||
MaxAttempts: result.MaxAttempts,
|
||||
MaxNoActionAttempts: result.MaxNoActionAttempts,
|
||||
}
|
||||
|
||||
stateHistory, err := b.client.RunBrowserAgent(req)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to run browser agent: %w", err)
|
||||
}
|
||||
|
||||
// Format the state history into a readable string
|
||||
var historyStr string
|
||||
// for i, state := range stateHistory.States {
|
||||
// historyStr += fmt.Sprintf("State %d:\n", i+1)
|
||||
// historyStr += fmt.Sprintf(" URL: %s\n", state.CurrentURL)
|
||||
// historyStr += fmt.Sprintf(" Title: %s\n", state.PageTitle)
|
||||
// historyStr += fmt.Sprintf(" Description: %s\n\n", state.PageContentDescription)
|
||||
// }
|
||||
|
||||
historyStr += fmt.Sprintf(" URL: %s\n", stateHistory.States[len(stateHistory.States)-1].CurrentURL)
|
||||
historyStr += fmt.Sprintf(" Title: %s\n", stateHistory.States[len(stateHistory.States)-1].PageTitle)
|
||||
historyStr += fmt.Sprintf(" Description: %s\n\n", stateHistory.States[len(stateHistory.States)-1].PageContentDescription)
|
||||
|
||||
return types.ActionResult{
|
||||
Result: fmt.Sprintf("Browser agent completed successfully. History:\n%s", historyStr),
|
||||
Metadata: map[string]interface{}{MetadataBrowserAgentHistory: stateHistory},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *BrowserAgentRunner) Definition() types.ActionDefinition {
|
||||
actionName := "run_browser_agent"
|
||||
if b.customActionName != "" {
|
||||
actionName = b.customActionName
|
||||
}
|
||||
description := "Run a browser agent to achieve a specific goal, for example: 'Go to https://www.google.com and search for 'LocalAI', and tell me what's on the first page'"
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(actionName),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"goal": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The goal for the browser agent to achieve",
|
||||
},
|
||||
"max_attempts": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "Maximum number of attempts the agent can make (optional)",
|
||||
},
|
||||
"max_no_action_attempts": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "Maximum number of attempts without taking an action (optional)",
|
||||
},
|
||||
},
|
||||
Required: []string{"goal"},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *BrowserAgentRunner) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// BrowserAgentRunnerConfigMeta returns the metadata for Browser Agent Runner action configuration fields
|
||||
func BrowserAgentRunnerConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "baseURL",
|
||||
Label: "Base URL",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Base URL of the LocalOperator API",
|
||||
},
|
||||
{
|
||||
Name: "customActionName",
|
||||
Label: "Custom Action Name",
|
||||
Type: config.FieldTypeText,
|
||||
HelpText: "Custom name for this action",
|
||||
},
|
||||
{
|
||||
Name: "timeout",
|
||||
Label: "Client Timeout",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Client timeout duration (e.g. '15m', '1h'). Defaults to '15m' if not specified.",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
api "github.com/mudler/LocalAGI/pkg/localoperator"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
const (
|
||||
MetadataDeepResearchResult = "deep_research_result"
|
||||
)
|
||||
|
||||
type DeepResearchRunner struct {
|
||||
baseURL, customActionName string
|
||||
client *api.Client
|
||||
}
|
||||
|
||||
func NewDeepResearchRunner(config map[string]string, defaultURL string) *DeepResearchRunner {
|
||||
if config["baseURL"] == "" {
|
||||
config["baseURL"] = defaultURL
|
||||
}
|
||||
|
||||
timeout := "15m"
|
||||
if config["timeout"] != "" {
|
||||
timeout = config["timeout"]
|
||||
}
|
||||
|
||||
duration, err := time.ParseDuration(timeout)
|
||||
if err != nil {
|
||||
// If parsing fails, use default 15 minutes
|
||||
duration = 15 * time.Minute
|
||||
}
|
||||
|
||||
client := api.NewClient(config["baseURL"], duration)
|
||||
|
||||
return &DeepResearchRunner{
|
||||
client: client,
|
||||
baseURL: config["baseURL"],
|
||||
customActionName: config["customActionName"],
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DeepResearchRunner) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := api.DeepResearchRequest{}
|
||||
err := params.Unmarshal(&result)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to unmarshal params: %w", err)
|
||||
}
|
||||
|
||||
req := api.DeepResearchRequest{
|
||||
Topic: result.Topic,
|
||||
MaxCycles: result.MaxCycles,
|
||||
MaxNoActionAttempts: result.MaxNoActionAttempts,
|
||||
MaxResults: result.MaxResults,
|
||||
}
|
||||
|
||||
researchResult, err := d.client.RunDeepResearch(req)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to run deep research: %w", err)
|
||||
}
|
||||
|
||||
// Format the research result into a readable string
|
||||
var resultStr string
|
||||
|
||||
resultStr += "Deep research result\n"
|
||||
resultStr += fmt.Sprintf("Topic: %s\n", researchResult.Topic)
|
||||
resultStr += fmt.Sprintf("Summary: %s\n", researchResult.Summary)
|
||||
resultStr += fmt.Sprintf("Research Cycles: %d\n", researchResult.ResearchCycles)
|
||||
resultStr += fmt.Sprintf("Completion Time: %s\n\n", researchResult.CompletionTime)
|
||||
|
||||
if len(researchResult.Sources) > 0 {
|
||||
resultStr += "Sources:\n"
|
||||
for _, source := range researchResult.Sources {
|
||||
resultStr += fmt.Sprintf("- %s (%s)\n %s\n", source.Title, source.URL, source.Description)
|
||||
}
|
||||
}
|
||||
|
||||
return types.ActionResult{
|
||||
Result: fmt.Sprintf("Deep research completed successfully.\n%s", resultStr),
|
||||
Metadata: map[string]interface{}{MetadataDeepResearchResult: researchResult},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *DeepResearchRunner) Definition() types.ActionDefinition {
|
||||
actionName := "run_deep_research"
|
||||
if d.customActionName != "" {
|
||||
actionName = d.customActionName
|
||||
}
|
||||
description := "Run a deep research on a specific topic, gathering information from multiple sources and providing a comprehensive summary"
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(actionName),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"topic": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The topic to research",
|
||||
},
|
||||
"max_cycles": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "Maximum number of research cycles to perform (optional)",
|
||||
},
|
||||
"max_no_action_attempts": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "Maximum number of attempts without taking an action (optional)",
|
||||
},
|
||||
"max_results": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "Maximum number of results to collect (optional)",
|
||||
},
|
||||
},
|
||||
Required: []string{"topic"},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DeepResearchRunner) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// DeepResearchRunnerConfigMeta returns the metadata for Deep Research Runner action configuration fields
|
||||
func DeepResearchRunnerConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "baseURL",
|
||||
Label: "Base URL",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Base URL of the LocalOperator API",
|
||||
},
|
||||
{
|
||||
Name: "customActionName",
|
||||
Label: "Custom Action Name",
|
||||
Type: config.FieldTypeText,
|
||||
HelpText: "Custom name for this action",
|
||||
},
|
||||
{
|
||||
Name: "timeout",
|
||||
Label: "Client Timeout",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Client timeout duration (e.g. '15m', '1h'). Defaults to '15m' if not specified.",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jung-kurt/gofpdf"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
"github.com/mudler/xlog"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
const (
|
||||
MetadataPDFs = "pdf_paths"
|
||||
)
|
||||
|
||||
// NewGenPDF creates a new PDF generation action
|
||||
func NewGenPDF(config map[string]string) *GenPDFAction {
|
||||
a := &GenPDFAction{
|
||||
outputDir: config["outputDir"],
|
||||
cleanOnStart: config["cleanOnStart"] == "true" || config["cleanOnStart"] == "1",
|
||||
}
|
||||
|
||||
if a.outputDir != "" {
|
||||
if err := os.MkdirAll(a.outputDir, 0755); err != nil {
|
||||
xlog.Error("Failed to create output directory", "path", a.outputDir, "error", err)
|
||||
}
|
||||
if a.cleanOnStart {
|
||||
entries, err := os.ReadDir(a.outputDir)
|
||||
if err == nil {
|
||||
for _, e := range entries {
|
||||
_ = os.Remove(filepath.Join(a.outputDir, e.Name()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
type GenPDFAction struct {
|
||||
outputDir string
|
||||
cleanOnStart bool
|
||||
}
|
||||
|
||||
func (a *GenPDFAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Filename string `json:"filename"`
|
||||
}{}
|
||||
if err := params.Unmarshal(&result); err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
if result.Content == "" {
|
||||
return types.ActionResult{}, fmt.Errorf("content is required")
|
||||
}
|
||||
|
||||
if a.outputDir == "" {
|
||||
return types.ActionResult{}, fmt.Errorf("outputDir is required for generate_pdf (configure the action with an output directory)")
|
||||
}
|
||||
|
||||
// Generate filename if not provided
|
||||
filename := result.Filename
|
||||
if filename == "" {
|
||||
filename = fmt.Sprintf("document_%d", time.Now().UnixNano())
|
||||
}
|
||||
|
||||
// Clean filename to prevent path traversal
|
||||
filename = filepath.Base(filename)
|
||||
|
||||
// Ensure filename has .pdf extension
|
||||
if !strings.HasSuffix(strings.ToLower(filename), ".pdf") {
|
||||
filename = filename + ".pdf"
|
||||
}
|
||||
|
||||
// Create PDF
|
||||
pdf := gofpdf.New("P", "mm", "A4", "")
|
||||
pdf.AddPage()
|
||||
|
||||
// Add title if provided
|
||||
if result.Title != "" {
|
||||
pdf.SetFont("Arial", "B", 16)
|
||||
pdf.MultiCell(0, 10, result.Title, "", "", false)
|
||||
pdf.Ln(5)
|
||||
}
|
||||
|
||||
// Add content
|
||||
pdf.SetFont("Arial", "", 12)
|
||||
pdf.MultiCell(0, 10, result.Content, "", "", false)
|
||||
|
||||
// Save PDF
|
||||
savedPath := filepath.Join(a.outputDir, filename)
|
||||
if err := pdf.OutputFileAndClose(savedPath); err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to save PDF: %w", err)
|
||||
}
|
||||
|
||||
return types.ActionResult{
|
||||
Result: fmt.Sprintf("PDF generated and saved to: %s", savedPath),
|
||||
Metadata: map[string]interface{}{
|
||||
MetadataPDFs: []string{savedPath},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *GenPDFAction) Definition() types.ActionDefinition {
|
||||
return types.ActionDefinition{
|
||||
Name: "generate_pdf",
|
||||
Description: "Generate a PDF document from text content. The PDF is saved locally and can be sent to the user by connectors.",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"title": {
|
||||
Type: jsonschema.String,
|
||||
Description: "Title of the PDF document",
|
||||
},
|
||||
"content": {
|
||||
Type: jsonschema.String,
|
||||
Description: "Text content to include in the PDF document",
|
||||
},
|
||||
"filename": {
|
||||
Type: jsonschema.String,
|
||||
Description: "Optional custom filename (extension is optional - .pdf will be automatically added if missing)",
|
||||
},
|
||||
},
|
||||
Required: []string{"content"},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *GenPDFAction) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// GenPDFConfigMeta returns the metadata for GenPDF action configuration fields.
|
||||
func GenPDFConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "outputDir",
|
||||
Label: "Output directory",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
HelpText: "Directory where generated PDF files are saved",
|
||||
},
|
||||
{
|
||||
Name: "cleanOnStart",
|
||||
Label: "Clean output directory on start",
|
||||
Type: config.FieldTypeCheckbox,
|
||||
DefaultValue: false,
|
||||
HelpText: "If enabled, clear the output directory when the action is loaded",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package actions_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/services/actions"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("GenPDFAction", func() {
|
||||
var (
|
||||
tmpDir string
|
||||
action *actions.GenPDFAction
|
||||
ctx context.Context
|
||||
sharedState *types.AgentSharedState
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
tmpDir, err = os.MkdirTemp("", "genpdf_test_*")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
action = actions.NewGenPDF(map[string]string{
|
||||
"outputDir": tmpDir,
|
||||
})
|
||||
|
||||
ctx = context.Background()
|
||||
sharedState = &types.AgentSharedState{}
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
os.RemoveAll(tmpDir)
|
||||
})
|
||||
|
||||
It("generates PDF with title and content", func() {
|
||||
result, err := action.Run(ctx, sharedState, types.ActionParams{
|
||||
"title": "Test Document",
|
||||
"content": "This is test content for the PDF.",
|
||||
})
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(result.Result).To(ContainSubstring("PDF generated and saved to:"))
|
||||
Expect(result.Metadata).To(HaveKey(actions.MetadataPDFs))
|
||||
|
||||
paths := result.Metadata[actions.MetadataPDFs].([]string)
|
||||
Expect(paths).To(HaveLen(1))
|
||||
Expect(paths[0]).To(BeAnExistingFile())
|
||||
})
|
||||
|
||||
It("requires content parameter", func() {
|
||||
_, err := action.Run(ctx, sharedState, types.ActionParams{
|
||||
"title": "Test",
|
||||
})
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("content is required"))
|
||||
})
|
||||
|
||||
It("uses custom filename when provided", func() {
|
||||
result, err := action.Run(ctx, sharedState, types.ActionParams{
|
||||
"content": "Test content",
|
||||
"filename": "custom_name",
|
||||
})
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
paths := result.Metadata[actions.MetadataPDFs].([]string)
|
||||
Expect(filepath.Base(paths[0])).To(Equal("custom_name.pdf"))
|
||||
})
|
||||
|
||||
It("generates PDF with content only (no title)", func() {
|
||||
result, err := action.Run(ctx, sharedState, types.ActionParams{
|
||||
"content": "Just some content without a title.",
|
||||
})
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(result.Result).To(ContainSubstring("PDF generated and saved to:"))
|
||||
paths := result.Metadata[actions.MetadataPDFs].([]string)
|
||||
Expect(paths).To(HaveLen(1))
|
||||
Expect(paths[0]).To(BeAnExistingFile())
|
||||
})
|
||||
|
||||
It("automatically adds .pdf extension if missing", func() {
|
||||
result, err := action.Run(ctx, sharedState, types.ActionParams{
|
||||
"content": "Test content",
|
||||
"filename": "my_document",
|
||||
})
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
paths := result.Metadata[actions.MetadataPDFs].([]string)
|
||||
Expect(filepath.Base(paths[0])).To(Equal("my_document.pdf"))
|
||||
})
|
||||
|
||||
It("does not double-add .pdf extension", func() {
|
||||
result, err := action.Run(ctx, sharedState, types.ActionParams{
|
||||
"content": "Test content",
|
||||
"filename": "document.pdf",
|
||||
})
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
paths := result.Metadata[actions.MetadataPDFs].([]string)
|
||||
Expect(filepath.Base(paths[0])).To(Equal("document.pdf"))
|
||||
})
|
||||
|
||||
It("requires outputDir to be configured", func() {
|
||||
actionNoDir := actions.NewGenPDF(map[string]string{})
|
||||
_, err := actionNoDir.Run(ctx, sharedState, types.ActionParams{
|
||||
"content": "Test content",
|
||||
})
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("outputDir is required"))
|
||||
})
|
||||
|
||||
It("cleans output directory on start if cleanOnStart is enabled", func() {
|
||||
// Create a test file in the directory
|
||||
testFile := filepath.Join(tmpDir, "test.txt")
|
||||
err := os.WriteFile(testFile, []byte("test"), 0644)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(testFile).To(BeAnExistingFile())
|
||||
|
||||
// Create a new action with cleanOnStart enabled
|
||||
_ = actions.NewGenPDF(map[string]string{
|
||||
"outputDir": tmpDir,
|
||||
"cleanOnStart": "true",
|
||||
})
|
||||
|
||||
// The test file should be deleted
|
||||
Expect(testFile).ToNot(BeAnExistingFile())
|
||||
})
|
||||
|
||||
It("does not clean output directory if cleanOnStart is disabled", func() {
|
||||
// Create a test file in the directory
|
||||
testFile := filepath.Join(tmpDir, "test.txt")
|
||||
err := os.WriteFile(testFile, []byte("test"), 0644)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(testFile).To(BeAnExistingFile())
|
||||
|
||||
// Create a new action with cleanOnStart disabled (default)
|
||||
_ = actions.NewGenPDF(map[string]string{
|
||||
"outputDir": tmpDir,
|
||||
})
|
||||
|
||||
// The test file should still exist
|
||||
Expect(testFile).To(BeAnExistingFile())
|
||||
})
|
||||
|
||||
It("prevents path traversal in filename", func() {
|
||||
result, err := action.Run(ctx, sharedState, types.ActionParams{
|
||||
"content": "Test content",
|
||||
"filename": "../../../etc/passwd",
|
||||
})
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
paths := result.Metadata[actions.MetadataPDFs].([]string)
|
||||
// Should only use the base filename, not the path
|
||||
Expect(filepath.Base(paths[0])).To(Equal("passwd.pdf"))
|
||||
// Should be in the tmpDir, not in /etc
|
||||
Expect(filepath.Dir(paths[0])).To(Equal(tmpDir))
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,302 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dhowden/tag"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
const (
|
||||
MetadataSongs = "songs_paths"
|
||||
)
|
||||
|
||||
// audioExtensionFromContentType returns a file extension for common audio MIME types.
|
||||
// It strips parameters (e.g. "audio/flac; rate=44100" -> "flac").
|
||||
func audioExtensionFromContentType(contentType string) string {
|
||||
mediaType, _, _ := strings.Cut(strings.TrimSpace(contentType), ";")
|
||||
mediaType = strings.TrimSpace(strings.ToLower(mediaType))
|
||||
switch mediaType {
|
||||
case "audio/flac":
|
||||
return "flac"
|
||||
case "audio/mpeg", "audio/mp3":
|
||||
return "mp3"
|
||||
case "audio/wav", "audio/wave", "audio/x-wav":
|
||||
return "wav"
|
||||
case "audio/ogg":
|
||||
return "ogg"
|
||||
case "audio/webm":
|
||||
return "webm"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// audioExtensionFromTag uses github.com/dhowden/tag to identify format from audio bytes.
|
||||
// Identify works on raw audio (e.g. FLAC without vorbis comments) and returns FileType.
|
||||
func audioExtensionFromTag(data []byte) string {
|
||||
if len(data) < 11 {
|
||||
return ""
|
||||
}
|
||||
r := bytes.NewReader(data)
|
||||
_, fileType, err := tag.Identify(r)
|
||||
if err != nil || fileType == tag.UnknownFileType {
|
||||
return ""
|
||||
}
|
||||
switch fileType {
|
||||
case tag.FLAC:
|
||||
return "flac"
|
||||
case tag.MP3:
|
||||
return "mp3"
|
||||
case tag.OGG:
|
||||
return "ogg"
|
||||
case tag.M4A:
|
||||
return "m4a"
|
||||
case tag.M4B:
|
||||
return "m4b"
|
||||
case tag.M4P:
|
||||
return "m4p"
|
||||
case tag.ALAC:
|
||||
return "m4a"
|
||||
case tag.DSF:
|
||||
return "dsf"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// soundRequest matches LocalAI /sound endpoint (ACE-Step advanced mode) request body.
|
||||
// See: https://localai.io/features/text-to-audio/
|
||||
type soundRequest struct {
|
||||
Model string `json:"model"`
|
||||
Caption string `json:"caption"`
|
||||
Lyrics string `json:"lyrics,omitempty"`
|
||||
BPM *int `json:"bpm,omitempty"`
|
||||
Keyscale string `json:"keyscale,omitempty"`
|
||||
Language string `json:"language,omitempty"`
|
||||
DurationSeconds *float64 `json:"duration_seconds,omitempty"`
|
||||
}
|
||||
|
||||
func NewGenSong(config map[string]string) *GenSongAction {
|
||||
model := config["model"]
|
||||
if model == "" {
|
||||
model = "ace-step-turbo"
|
||||
}
|
||||
a := &GenSongAction{
|
||||
apiURL: strings.TrimSuffix(config["apiURL"], "/"),
|
||||
apiKey: config["apiKey"],
|
||||
outputDir: config["outputDir"],
|
||||
model: model,
|
||||
cleanOnStart: config["cleanOnStart"] == "true" || config["cleanOnStart"] == "1",
|
||||
}
|
||||
|
||||
if a.outputDir != "" {
|
||||
if err := os.MkdirAll(a.outputDir, 0755); err != nil {
|
||||
// log but continue; Run will fail with a clear error when saving
|
||||
_ = err
|
||||
}
|
||||
if a.cleanOnStart {
|
||||
entries, err := os.ReadDir(a.outputDir)
|
||||
if err == nil {
|
||||
for _, e := range entries {
|
||||
_ = os.Remove(filepath.Join(a.outputDir, e.Name()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
type GenSongAction struct {
|
||||
apiURL string
|
||||
apiKey string
|
||||
outputDir string
|
||||
model string
|
||||
cleanOnStart bool
|
||||
}
|
||||
|
||||
func (a *GenSongAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
Caption string `json:"caption"`
|
||||
Lyrics string `json:"lyrics"`
|
||||
BPM *int `json:"bpm"`
|
||||
Keyscale string `json:"keyscale"`
|
||||
Language string `json:"language"`
|
||||
Duration *float64 `json:"duration_seconds"`
|
||||
Model string `json:"model"`
|
||||
}{}
|
||||
if err := params.Unmarshal(&result); err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
if result.Caption == "" {
|
||||
return types.ActionResult{}, fmt.Errorf("caption is required")
|
||||
}
|
||||
|
||||
if a.outputDir == "" {
|
||||
return types.ActionResult{}, fmt.Errorf("outputDir is required for generate_song (configure the action with an output directory)")
|
||||
}
|
||||
|
||||
reqBody := soundRequest{
|
||||
Model: a.model,
|
||||
Caption: result.Caption,
|
||||
Lyrics: result.Lyrics,
|
||||
Keyscale: result.Keyscale,
|
||||
Language: result.Language,
|
||||
DurationSeconds: result.Duration,
|
||||
BPM: result.BPM,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
url := a.apiURL + "/v1/sound-generation"
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if a.apiKey != "" {
|
||||
req.Header.Set("xi-api-key", a.apiKey)
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return types.ActionResult{Result: "Failed to generate song: " + err.Error()}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
msg, _ := io.ReadAll(resp.Body)
|
||||
return types.ActionResult{}, fmt.Errorf("sound endpoint failed: %s: %s", resp.Status, string(msg))
|
||||
}
|
||||
|
||||
audioBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
if len(audioBytes) == 0 {
|
||||
return types.ActionResult{}, fmt.Errorf("no audio data returned")
|
||||
}
|
||||
|
||||
ext := audioExtensionFromContentType(resp.Header.Get("Content-Type"))
|
||||
if ext == "" {
|
||||
ext = audioExtensionFromTag(audioBytes)
|
||||
}
|
||||
if ext == "" {
|
||||
ext = "flac" // default when unknown (e.g. ACE-Step)
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf("song_%d.%s", time.Now().UnixNano(), ext)
|
||||
savedPath := filepath.Join(a.outputDir, filename)
|
||||
if err := os.WriteFile(savedPath, audioBytes, 0644); err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to save song: %w", err)
|
||||
}
|
||||
|
||||
return types.ActionResult{
|
||||
Result: fmt.Sprintf("The song was generated and saved to: %s", savedPath),
|
||||
Metadata: map[string]interface{}{
|
||||
MetadataSongs: []string{savedPath},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *GenSongAction) Definition() types.ActionDefinition {
|
||||
return types.ActionDefinition{
|
||||
Name: "generate_song",
|
||||
Description: "Generate a song or music track using LocalAI /sound endpoint (ACE-Step advanced mode). Uses caption, optional lyrics, BPM, key scale, language and duration. The file is saved locally and can be sent to the user by connectors.",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"caption": {
|
||||
Type: jsonschema.String,
|
||||
Description: "Description of the song or music to generate (e.g. 'A funky Japanese disco track').",
|
||||
},
|
||||
"lyrics": {
|
||||
Type: jsonschema.String,
|
||||
Description: "Lyrics or structure (e.g. '[Verse 1]\\n...').",
|
||||
},
|
||||
"bpm": {
|
||||
Type: jsonschema.Integer,
|
||||
Description: "Beats per minute (e.g. 120). Optional.",
|
||||
},
|
||||
"keyscale": {
|
||||
Type: jsonschema.String,
|
||||
Description: "Key and scale (e.g. 'Ab major'). Optional.",
|
||||
},
|
||||
"language": {
|
||||
Type: jsonschema.String,
|
||||
Description: "Language code for vocals (e.g. 'ja', 'en'). Optional.",
|
||||
},
|
||||
"duration_seconds": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "Duration of the generated audio in seconds (e.g. 225). Optional.",
|
||||
},
|
||||
"model": {
|
||||
Type: jsonschema.String,
|
||||
Description: "Model name (e.g. ace-step-turbo). Optional; uses action config default if omitted.",
|
||||
},
|
||||
},
|
||||
Required: []string{"caption"},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *GenSongAction) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// GenSongConfigMeta returns the metadata for GenSong action configuration fields.
|
||||
func GenSongConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "apiURL",
|
||||
Label: "API URL",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
DefaultValue: "http://localhost:8080",
|
||||
HelpText: "LocalAI base URL (e.g. http://localhost:8080) for /sound endpoint",
|
||||
},
|
||||
{
|
||||
Name: "model",
|
||||
Label: "Model",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
DefaultValue: "ace-step-turbo",
|
||||
HelpText: "Default model for sound generation (e.g. ace-step-turbo)",
|
||||
},
|
||||
{
|
||||
Name: "apiKey",
|
||||
Label: "API Key",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Optional API key if the endpoint requires authentication",
|
||||
},
|
||||
{
|
||||
Name: "outputDir",
|
||||
Label: "Output directory",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
HelpText: "Directory where generated song files are saved (required for connectors to send files)",
|
||||
},
|
||||
{
|
||||
Name: "cleanOnStart",
|
||||
Label: "Clean output directory on start",
|
||||
Type: config.FieldTypeCheckbox,
|
||||
DefaultValue: false,
|
||||
HelpText: "If enabled, clear the output directory when the action is loaded",
|
||||
},
|
||||
}
|
||||
}
|
||||
+284
-103
@@ -2,22 +2,29 @@ package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"io"
|
||||
|
||||
"github.com/blevesearch/bleve/v2"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
// Remove global const and mutex, and add them as fields to a struct
|
||||
// indexCache avoids opening the same Bleve index path multiple times, which would
|
||||
// deadlock (Bleve uses file locks; a second Open() on the same path blocks).
|
||||
var (
|
||||
indexCache = map[string]bleve.Index{}
|
||||
indexCacheMu sync.Mutex
|
||||
)
|
||||
|
||||
type MemoryActions struct {
|
||||
filePath string
|
||||
index bleve.Index
|
||||
indexPath string
|
||||
customName string
|
||||
customDescription string
|
||||
}
|
||||
@@ -25,138 +32,269 @@ type MemoryActions struct {
|
||||
type AddToMemoryAction struct{ *MemoryActions }
|
||||
type ListMemoryAction struct{ *MemoryActions }
|
||||
type RemoveFromMemoryAction struct{ *MemoryActions }
|
||||
type SearchMemoryAction struct{ *MemoryActions }
|
||||
|
||||
// NewMemoryActions returns the three actions, using the provided filePath and config
|
||||
func NewMemoryActions(filePath string, config map[string]string) (*AddToMemoryAction, *ListMemoryAction, *RemoveFromMemoryAction) {
|
||||
ma := &MemoryActions{filePath: filePath}
|
||||
// MemoryEntry matches the MCP memory structure (Bleve-backed).
|
||||
type MemoryEntry struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// NewMemoryActions returns the four memory actions (Add, List, Remove, Search) using a Bleve index at indexPath.
|
||||
func NewMemoryActions(indexPath string, config map[string]string) (*AddToMemoryAction, *ListMemoryAction, *RemoveFromMemoryAction, *SearchMemoryAction) {
|
||||
ma := &MemoryActions{indexPath: indexPath}
|
||||
if config != nil {
|
||||
ma.customName = config["custom_name"]
|
||||
ma.customDescription = config["custom_description"]
|
||||
}
|
||||
return &AddToMemoryAction{ma}, &ListMemoryAction{ma}, &RemoveFromMemoryAction{ma}
|
||||
}
|
||||
|
||||
type addToMemoryParams struct {
|
||||
Item string `json:"item"`
|
||||
}
|
||||
|
||||
type removeFromMemoryParams struct {
|
||||
Index *int `json:"index,omitempty"`
|
||||
Value string `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
func (m *MemoryActions) readMemory() ([]string, error) {
|
||||
f, err := os.Open(m.filePath)
|
||||
idx, err := openOrCreateBleveIndex(indexPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return []string{}, nil
|
||||
}
|
||||
return nil, err
|
||||
// Allow lazy init: index will be nil and operations will return this error
|
||||
ma.index = nil
|
||||
} else {
|
||||
ma.index = idx
|
||||
}
|
||||
defer f.Close()
|
||||
var items []string
|
||||
if err := json.NewDecoder(f).Decode(&items); err != nil {
|
||||
if err == io.EOF {
|
||||
return []string{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
return &AddToMemoryAction{ma}, &ListMemoryAction{ma}, &RemoveFromMemoryAction{ma}, &SearchMemoryAction{ma}
|
||||
}
|
||||
|
||||
func (m *MemoryActions) writeMemory(items []string) error {
|
||||
f, err := os.Create(m.filePath)
|
||||
func openOrCreateBleveIndex(indexPath string) (bleve.Index, error) {
|
||||
indexCacheMu.Lock()
|
||||
if idx, ok := indexCache[indexPath]; ok {
|
||||
indexCacheMu.Unlock()
|
||||
return idx, nil
|
||||
}
|
||||
indexCacheMu.Unlock()
|
||||
|
||||
var idx bleve.Index
|
||||
var err error
|
||||
if _, statErr := os.Stat(indexPath); statErr == nil {
|
||||
idx, err = bleve.Open(indexPath)
|
||||
} else {
|
||||
os.MkdirAll(filepath.Dir(indexPath), 0755)
|
||||
mapping := bleve.NewIndexMapping()
|
||||
entryMapping := bleve.NewDocumentMapping()
|
||||
|
||||
nameFieldMapping := bleve.NewTextFieldMapping()
|
||||
nameFieldMapping.Analyzer = "standard"
|
||||
nameFieldMapping.Store = true
|
||||
entryMapping.AddFieldMappingsAt("name", nameFieldMapping)
|
||||
|
||||
contentFieldMapping := bleve.NewTextFieldMapping()
|
||||
contentFieldMapping.Analyzer = "standard"
|
||||
contentFieldMapping.Store = true
|
||||
entryMapping.AddFieldMappingsAt("content", contentFieldMapping)
|
||||
|
||||
dateFieldMapping := bleve.NewDateTimeFieldMapping()
|
||||
dateFieldMapping.Store = true
|
||||
entryMapping.AddFieldMappingsAt("created_at", dateFieldMapping)
|
||||
|
||||
mapping.AddDocumentMapping("_default", entryMapping)
|
||||
idx, err = bleve.New(indexPath, mapping)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
indexCacheMu.Lock()
|
||||
indexCache[indexPath] = idx
|
||||
indexCacheMu.Unlock()
|
||||
return idx, nil
|
||||
}
|
||||
|
||||
func (m *MemoryActions) ensureIndex() error {
|
||||
if m.index != nil {
|
||||
return nil
|
||||
}
|
||||
idx, err := openOrCreateBleveIndex(m.indexPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
return json.NewEncoder(f).Encode(items)
|
||||
m.index = idx
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateID() string {
|
||||
return fmt.Sprintf("%d", time.Now().UnixNano())
|
||||
}
|
||||
|
||||
type addToMemoryParams struct {
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type removeFromMemoryParams struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type searchMemoryParams struct {
|
||||
Query string `json:"query"`
|
||||
}
|
||||
|
||||
func (a *AddToMemoryAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
if err := a.ensureIndex(); err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
var req addToMemoryParams
|
||||
if err := params.Unmarshal(&req); err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("invalid parameters: %w", err)
|
||||
}
|
||||
if req.Item == "" {
|
||||
return types.ActionResult{}, fmt.Errorf("item cannot be empty")
|
||||
if req.Name == "" && req.Content == "" {
|
||||
return types.ActionResult{}, fmt.Errorf("name or content cannot both be empty")
|
||||
}
|
||||
items, err := a.readMemory()
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
entry := MemoryEntry{
|
||||
ID: generateID(),
|
||||
Name: req.Name,
|
||||
Content: req.Content,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
items = append(items, req.Item)
|
||||
if err := a.writeMemory(items); err != nil {
|
||||
return types.ActionResult{}, err
|
||||
if err := a.index.Index(entry.ID, entry); err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to index memory entry: %w", err)
|
||||
}
|
||||
return types.ActionResult{
|
||||
Result: fmt.Sprintf("Added item to memory: %s", req.Item),
|
||||
Metadata: map[string]any{"item": req.Item, "count": len(items)},
|
||||
Result: fmt.Sprintf("Added memory entry: id=%s name=%q", entry.ID, entry.Name),
|
||||
Metadata: map[string]any{"id": entry.ID, "name": entry.Name, "content": entry.Content, "created_at": entry.CreatedAt},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *ListMemoryAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
items, err := a.readMemory()
|
||||
if err != nil {
|
||||
if err := a.ensureIndex(); err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
query := bleve.NewMatchAllQuery()
|
||||
searchRequest := bleve.NewSearchRequest(query)
|
||||
searchRequest.Size = 10000
|
||||
searchRequest.Fields = []string{"name", "created_at"}
|
||||
searchRequest.SortBy([]string{"-created_at"})
|
||||
|
||||
outputResult := "Number of items in memory: " + strconv.Itoa(len(items)) + "\n"
|
||||
for i, item := range items {
|
||||
outputResult += fmt.Sprintf("%d) %s\n", i, item)
|
||||
searchResult, err := a.index.Search(searchRequest)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to search index: %w", err)
|
||||
}
|
||||
|
||||
type listEntry struct {
|
||||
Name string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
entries := make([]listEntry, 0, len(searchResult.Hits))
|
||||
for _, hit := range searchResult.Hits {
|
||||
e := listEntry{}
|
||||
if v, ok := hit.Fields["name"].(string); ok {
|
||||
e.Name = v
|
||||
}
|
||||
if v, ok := hit.Fields["created_at"].(string); ok {
|
||||
if t, err := time.Parse(time.RFC3339, v); err == nil {
|
||||
e.CreatedAt = t
|
||||
}
|
||||
} else if v, ok := hit.Fields["created_at"].(time.Time); ok {
|
||||
e.CreatedAt = v
|
||||
}
|
||||
entries = append(entries, e)
|
||||
}
|
||||
|
||||
outputResult := "Number of items in memory: " + strconv.Itoa(len(entries)) + "\n"
|
||||
for i, e := range entries {
|
||||
createdStr := e.CreatedAt.Format(time.RFC3339)
|
||||
outputResult += fmt.Sprintf("%d) %s (created_at: %s)\n", i, e.Name, createdStr)
|
||||
}
|
||||
|
||||
names := make([]string, len(entries))
|
||||
for i, e := range entries {
|
||||
names[i] = e.Name
|
||||
}
|
||||
return types.ActionResult{
|
||||
Result: outputResult,
|
||||
Metadata: map[string]any{"items": items},
|
||||
Metadata: map[string]any{"names": names, "entries": entries, "count": len(entries)},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *RemoveFromMemoryAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
if err := a.ensureIndex(); err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
var req removeFromMemoryParams
|
||||
if err := params.Unmarshal(&req); err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("invalid parameters: %w", err)
|
||||
}
|
||||
items, err := a.readMemory()
|
||||
if req.ID == "" {
|
||||
return types.ActionResult{}, fmt.Errorf("id is required to remove a memory entry")
|
||||
}
|
||||
doc, err := a.index.Document(req.ID)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
return types.ActionResult{}, fmt.Errorf("failed to check document: %w", err)
|
||||
}
|
||||
var removed string
|
||||
if req.Index != nil {
|
||||
idx := *req.Index
|
||||
if idx < 0 || idx >= len(items) {
|
||||
return types.ActionResult{}, fmt.Errorf("index out of range")
|
||||
}
|
||||
removed = items[idx]
|
||||
items = append(items[:idx], items[idx+1:]...)
|
||||
} else if req.Value != "" {
|
||||
found := false
|
||||
for i, v := range items {
|
||||
if v == req.Value {
|
||||
removed = v
|
||||
items = append(items[:i], items[i+1:]...)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return types.ActionResult{}, fmt.Errorf("value not found in memory")
|
||||
}
|
||||
} else {
|
||||
return types.ActionResult{}, fmt.Errorf("must provide index or value to remove")
|
||||
if doc == nil {
|
||||
return types.ActionResult{}, fmt.Errorf("memory entry with ID %q not found", req.ID)
|
||||
}
|
||||
if err := a.writeMemory(items); err != nil {
|
||||
return types.ActionResult{}, err
|
||||
if err := a.index.Delete(req.ID); err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to delete memory entry: %w", err)
|
||||
}
|
||||
return types.ActionResult{
|
||||
Result: fmt.Sprintf("Removed item from memory: %s", removed),
|
||||
Metadata: map[string]any{"removed": removed, "count": len(items)},
|
||||
Result: fmt.Sprintf("Removed memory entry with ID %q", req.ID),
|
||||
Metadata: map[string]any{"removed_id": req.ID},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *SearchMemoryAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
if err := a.ensureIndex(); err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
var req searchMemoryParams
|
||||
if err := params.Unmarshal(&req); err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("invalid parameters: %w", err)
|
||||
}
|
||||
if req.Query == "" {
|
||||
return types.ActionResult{}, fmt.Errorf("query cannot be empty")
|
||||
}
|
||||
nameQuery := bleve.NewMatchQuery(req.Query)
|
||||
nameQuery.SetField("name")
|
||||
contentQuery := bleve.NewMatchQuery(req.Query)
|
||||
contentQuery.SetField("content")
|
||||
disjunctionQuery := bleve.NewDisjunctionQuery(nameQuery, contentQuery)
|
||||
|
||||
searchRequest := bleve.NewSearchRequest(disjunctionQuery)
|
||||
searchRequest.Size = 100
|
||||
searchRequest.Fields = []string{"name", "content", "created_at"}
|
||||
|
||||
searchResult, err := a.index.Search(searchRequest)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to search index: %w", err)
|
||||
}
|
||||
|
||||
results := make([]MemoryEntry, 0, len(searchResult.Hits))
|
||||
for _, hit := range searchResult.Hits {
|
||||
e := MemoryEntry{ID: hit.ID}
|
||||
if v, ok := hit.Fields["name"].(string); ok {
|
||||
e.Name = v
|
||||
}
|
||||
if v, ok := hit.Fields["content"].(string); ok {
|
||||
e.Content = v
|
||||
}
|
||||
if v, ok := hit.Fields["created_at"].(string); ok {
|
||||
if t, err := time.Parse(time.RFC3339, v); err == nil {
|
||||
e.CreatedAt = t
|
||||
}
|
||||
} else if v, ok := hit.Fields["created_at"].(time.Time); ok {
|
||||
e.CreatedAt = v
|
||||
}
|
||||
results = append(results, e)
|
||||
}
|
||||
|
||||
outputResult := fmt.Sprintf("Query: %q — %d result(s)\n", req.Query, len(results))
|
||||
for i, e := range results {
|
||||
outputResult += fmt.Sprintf("%d) [%s] %s — %s\n", i, e.ID, e.Name, e.Content)
|
||||
}
|
||||
|
||||
return types.ActionResult{
|
||||
Result: outputResult,
|
||||
Metadata: map[string]any{"query": req.Query, "results": results, "count": len(results)},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *AddToMemoryAction) Definition() types.ActionDefinition {
|
||||
name := "add_to_memory"
|
||||
description := "Add a string item to memory (stored in a JSON file)."
|
||||
description := "Add a new entry to memory storage (name and/or content). Stored in a Bleve index."
|
||||
if a.customName != "" {
|
||||
name = a.customName
|
||||
}
|
||||
@@ -167,18 +305,22 @@ func (a *AddToMemoryAction) Definition() types.ActionDefinition {
|
||||
Name: types.ActionDefinitionName(name),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"item": {
|
||||
"name": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The string item to add to memory.",
|
||||
Description: "The name/title of the memory entry.",
|
||||
},
|
||||
"content": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The content to store in memory.",
|
||||
},
|
||||
},
|
||||
Required: []string{"item"},
|
||||
Required: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ListMemoryAction) Definition() types.ActionDefinition {
|
||||
name := "list_memory"
|
||||
description := "List all items currently stored in memory."
|
||||
description := "List all memory entry names."
|
||||
if a.customName != "" {
|
||||
name = a.customName
|
||||
}
|
||||
@@ -195,7 +337,7 @@ func (a *ListMemoryAction) Definition() types.ActionDefinition {
|
||||
|
||||
func (a *RemoveFromMemoryAction) Definition() types.ActionDefinition {
|
||||
name := "remove_from_memory"
|
||||
description := "Remove an item from memory by index or value."
|
||||
description := "Remove a memory entry by ID."
|
||||
if a.customName != "" {
|
||||
name = a.customName
|
||||
}
|
||||
@@ -206,22 +348,41 @@ func (a *RemoveFromMemoryAction) Definition() types.ActionDefinition {
|
||||
Name: types.ActionDefinitionName(name),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"index": {
|
||||
Type: jsonschema.Integer,
|
||||
Description: "The index of the item to remove (optional, 0-based)",
|
||||
},
|
||||
"value": {
|
||||
"id": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The value of the item to remove (optional)",
|
||||
Description: "The ID of the memory entry to remove.",
|
||||
},
|
||||
},
|
||||
Required: []string{},
|
||||
Required: []string{"id"},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AddToMemoryAction) Plannable() bool { return true }
|
||||
func (a *ListMemoryAction) Plannable() bool { return true }
|
||||
func (a *RemoveFromMemoryAction) Plannable() bool { return true }
|
||||
func (a *SearchMemoryAction) Definition() types.ActionDefinition {
|
||||
name := "search_memory"
|
||||
description := "Search memory entries by name and content using full-text search."
|
||||
if a.customName != "" {
|
||||
name = a.customName
|
||||
}
|
||||
if a.customDescription != "" {
|
||||
description = a.customDescription
|
||||
}
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(name),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"query": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The search query to find matching memory entries.",
|
||||
},
|
||||
},
|
||||
Required: []string{"query"},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AddToMemoryAction) Plannable() bool { return true }
|
||||
func (a *ListMemoryAction) Plannable() bool { return true }
|
||||
func (a *RemoveFromMemoryAction) Plannable() bool { return true }
|
||||
func (a *SearchMemoryAction) Plannable() bool { return true }
|
||||
|
||||
// AddToMemoryConfigMeta returns the metadata for AddToMemory action configuration fields
|
||||
func AddToMemoryConfigMeta() []config.Field {
|
||||
@@ -238,7 +399,7 @@ func AddToMemoryConfigMeta() []config.Field {
|
||||
Label: "Custom Description",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Custom description for the action (optional, defaults to 'Add a string item to memory (stored in a JSON file).')",
|
||||
HelpText: "Custom description for the action (optional)",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -258,7 +419,7 @@ func ListMemoryConfigMeta() []config.Field {
|
||||
Label: "Custom Description",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Custom description for the action (optional, defaults to 'List all items currently stored in memory.')",
|
||||
HelpText: "Custom description for the action (optional)",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -278,7 +439,27 @@ func RemoveFromMemoryConfigMeta() []config.Field {
|
||||
Label: "Custom Description",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Custom description for the action (optional, defaults to 'Remove an item from memory by index or value.')",
|
||||
HelpText: "Custom description for the action (optional)",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// SearchMemoryConfigMeta returns the metadata for SearchMemory action configuration fields
|
||||
func SearchMemoryConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "custom_name",
|
||||
Label: "Custom Name",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Custom name for the action (optional, defaults to 'search_memory')",
|
||||
},
|
||||
{
|
||||
Name: "custom_description",
|
||||
Label: "Custom Description",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Custom description for the action (optional)",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package actions_test
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/services/actions"
|
||||
@@ -12,72 +13,79 @@ import (
|
||||
|
||||
var _ = Describe("MemoryActions", func() {
|
||||
var (
|
||||
tmpFile string
|
||||
tmpDir string
|
||||
indexPath string
|
||||
aAdd *actions.AddToMemoryAction
|
||||
aList *actions.ListMemoryAction
|
||||
aRemove *actions.RemoveFromMemoryAction
|
||||
aSearch *actions.SearchMemoryAction
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
f, err := os.CreateTemp("", "memory_test_*.json")
|
||||
var err error
|
||||
tmpDir, err = os.MkdirTemp("", "memory_test_*")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
tmpFile = f.Name()
|
||||
f.Close()
|
||||
aAdd, aList, aRemove = actions.NewMemoryActions(tmpFile, map[string]string{})
|
||||
indexPath = filepath.Join(tmpDir, "memory.bleve")
|
||||
aAdd, aList, aRemove, aSearch = actions.NewMemoryActions(indexPath, map[string]string{})
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
os.Remove(tmpFile)
|
||||
os.RemoveAll(tmpDir)
|
||||
})
|
||||
|
||||
It("adds and lists items", func() {
|
||||
_, err := aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "foo"})
|
||||
It("adds and lists entries by name", func() {
|
||||
_, err := aAdd.Run(context.TODO(), nil, types.ActionParams{"name": "foo", "content": "bar"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
_, err = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "bar"})
|
||||
_, err = aAdd.Run(context.TODO(), nil, types.ActionParams{"name": "baz", "content": "qux"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
res, err := aList.Run(context.TODO(), nil, types.ActionParams{})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(res.Metadata["items"]).To(ContainElements("foo", "bar"))
|
||||
Expect(res.Metadata["names"]).To(ContainElements("foo", "baz"))
|
||||
Expect(res.Metadata["count"]).To(Equal(2))
|
||||
})
|
||||
|
||||
It("removes by index", func() {
|
||||
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "foo"})
|
||||
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "bar"})
|
||||
_, err := aRemove.Run(context.TODO(), nil, types.ActionParams{"index": 0})
|
||||
It("removes by id", func() {
|
||||
addRes, _ := aAdd.Run(context.TODO(), nil, types.ActionParams{"name": "foo", "content": "bar"})
|
||||
id, ok := addRes.Metadata["id"].(string)
|
||||
Expect(ok).To(BeTrue())
|
||||
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"name": "baz", "content": "qux"})
|
||||
_, err := aRemove.Run(context.TODO(), nil, types.ActionParams{"id": id})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
res, _ := aList.Run(context.TODO(), nil, types.ActionParams{})
|
||||
Expect(res.Metadata["items"]).To(ConsistOf("bar"))
|
||||
Expect(res.Metadata["names"]).To(ConsistOf("baz"))
|
||||
})
|
||||
|
||||
It("removes by value", func() {
|
||||
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "foo"})
|
||||
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "bar"})
|
||||
_, err := aRemove.Run(context.TODO(), nil, types.ActionParams{"value": "bar"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
res, _ := aList.Run(context.TODO(), nil, types.ActionParams{})
|
||||
Expect(res.Metadata["items"]).To(ConsistOf("foo"))
|
||||
})
|
||||
|
||||
It("returns error for out of range index", func() {
|
||||
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "foo"})
|
||||
_, err := aRemove.Run(context.TODO(), nil, types.ActionParams{"index": 2})
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns error for value not found", func() {
|
||||
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "foo"})
|
||||
_, err := aRemove.Run(context.TODO(), nil, types.ActionParams{"value": "bar"})
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns error for empty item", func() {
|
||||
_, err := aAdd.Run(context.TODO(), nil, types.ActionParams{"item": ""})
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns error if neither index nor value provided", func() {
|
||||
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "foo"})
|
||||
It("returns error for missing id on remove", func() {
|
||||
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"name": "foo", "content": "bar"})
|
||||
_, err := aRemove.Run(context.TODO(), nil, types.ActionParams{})
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns error for unknown id on remove", func() {
|
||||
_, err := aRemove.Run(context.TODO(), nil, types.ActionParams{"id": "nonexistent"})
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns error for empty name and content on add", func() {
|
||||
_, err := aAdd.Run(context.TODO(), nil, types.ActionParams{"name": "", "content": ""})
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("search returns matching entries", func() {
|
||||
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"name": "meeting", "content": "discussed project X"})
|
||||
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"name": "lunch", "content": "ate pizza"})
|
||||
res, err := aSearch.Run(context.TODO(), nil, types.ActionParams{"query": "project"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(res.Metadata["count"]).To(Equal(1))
|
||||
results, ok := res.Metadata["results"].([]actions.MemoryEntry)
|
||||
Expect(ok).To(BeTrue())
|
||||
Expect(results).To(HaveLen(1))
|
||||
Expect(results[0].Name).To(Equal("meeting"))
|
||||
Expect(results[0].Content).To(Equal("discussed project X"))
|
||||
})
|
||||
|
||||
It("search returns error for empty query", func() {
|
||||
_, err := aSearch.Run(context.TODO(), nil, types.ActionParams{"query": ""})
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ package services
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mudler/xlog"
|
||||
)
|
||||
@@ -26,3 +27,24 @@ func memoryPath(agentName string, actionsConfigs map[string]string) string {
|
||||
|
||||
return memoryFilePath
|
||||
}
|
||||
|
||||
// memoryIndexPath returns the directory path for the Bleve index (used by memory actions).
|
||||
func memoryIndexPath(agentName string, actionsConfigs map[string]string) string {
|
||||
indexPath := "memory.bleve"
|
||||
if actionsConfigs != nil {
|
||||
if stateDir, ok := actionsConfigs[ConfigStateDir]; ok && stateDir != "" {
|
||||
memoryDir := fmt.Sprintf("%s/memory", stateDir)
|
||||
if err := os.MkdirAll(memoryDir, 0755); err != nil {
|
||||
xlog.Error("Error creating memory directory", "error", err)
|
||||
return indexPath
|
||||
}
|
||||
indexPath = filepath.Join(memoryDir, agentName+".bleve")
|
||||
} else {
|
||||
indexPath = agentName + ".memory.bleve"
|
||||
}
|
||||
}
|
||||
if dir := filepath.Dir(indexPath); dir != "." {
|
||||
os.MkdirAll(dir, 0755)
|
||||
}
|
||||
return indexPath
|
||||
}
|
||||
|
||||
@@ -85,11 +85,11 @@ func (d *Discord) Start(a *agent.Agent) {
|
||||
|
||||
if d.defaultChannel != "" {
|
||||
// handle new conversations
|
||||
a.AddSubscriber(func(ccm openai.ChatCompletionMessage) {
|
||||
xlog.Debug("Subscriber(discord)", "message", ccm.Content)
|
||||
a.AddSubscriber(func(ccm *types.ConversationMessage) {
|
||||
xlog.Debug("Subscriber(discord)", "message", ccm.Message.Content)
|
||||
|
||||
// Send the message to the default channel
|
||||
_, err := dg.ChannelMessageSend(d.defaultChannel, ccm.Content)
|
||||
_, err := dg.ChannelMessageSend(d.defaultChannel, ccm.Message.Content)
|
||||
if err != nil {
|
||||
xlog.Error(fmt.Sprintf("Error sending message: %v", err))
|
||||
}
|
||||
@@ -97,7 +97,7 @@ func (d *Discord) Start(a *agent.Agent) {
|
||||
a.SharedState().ConversationTracker.AddMessage(
|
||||
fmt.Sprintf("discord:%s", d.defaultChannel),
|
||||
openai.ChatCompletionMessage{
|
||||
Content: ccm.Content,
|
||||
Content: ccm.Message.Content,
|
||||
Role: "assistant",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -377,14 +377,14 @@ func (e *Email) Start(a *agent.Agent) {
|
||||
go func() {
|
||||
if e.defaultEmail != "" {
|
||||
// handle new conversations
|
||||
a.AddSubscriber(func(ccm openai.ChatCompletionMessage) {
|
||||
xlog.Debug("Subscriber(email)", "message", ccm.Content)
|
||||
a.AddSubscriber(func(ccm *types.ConversationMessage) {
|
||||
xlog.Debug("Subscriber(email)", "message", ccm.Message.Content)
|
||||
|
||||
// Send the message to the default email
|
||||
e.sendMail(
|
||||
e.defaultEmail,
|
||||
"Message from LocalAGI",
|
||||
ccm.Content,
|
||||
ccm.Message.Content,
|
||||
"",
|
||||
"",
|
||||
[]string{e.defaultEmail},
|
||||
@@ -394,7 +394,7 @@ func (e *Email) Start(a *agent.Agent) {
|
||||
a.SharedState().ConversationTracker.AddMessage(
|
||||
fmt.Sprintf("email:%s", e.defaultEmail),
|
||||
openai.ChatCompletionMessage{
|
||||
Content: ccm.Content,
|
||||
Content: ccm.Message.Content,
|
||||
Role: "assistant",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -73,12 +73,12 @@ func (i *IRC) Start(a *agent.Agent) {
|
||||
|
||||
if i.channel != "" {
|
||||
// handle new conversations
|
||||
a.AddSubscriber(func(ccm openai.ChatCompletionMessage) {
|
||||
xlog.Debug("Subscriber(irc)", "message", ccm.Content)
|
||||
a.AddSubscriber(func(ccm *types.ConversationMessage) {
|
||||
xlog.Debug("Subscriber(irc)", "message", ccm.Message.Content)
|
||||
|
||||
// Split the response into multiple messages if it's too long
|
||||
maxLength := 400 // Safe limit for most IRC servers
|
||||
response := ccm.Content
|
||||
response := ccm.Message.Content
|
||||
|
||||
// Handle multiline responses
|
||||
lines := strings.Split(response, "\n")
|
||||
@@ -109,7 +109,7 @@ func (i *IRC) Start(a *agent.Agent) {
|
||||
a.SharedState().ConversationTracker.AddMessage(
|
||||
fmt.Sprintf("irc:%s", i.channel),
|
||||
openai.ChatCompletionMessage{
|
||||
Content: ccm.Content,
|
||||
Content: ccm.Message.Content,
|
||||
Role: "assistant",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -225,16 +225,16 @@ func (m *Matrix) Start(a *agent.Agent) {
|
||||
|
||||
if m.roomID != "" {
|
||||
// handle new conversations
|
||||
a.AddSubscriber(func(ccm openai.ChatCompletionMessage) {
|
||||
xlog.Debug("Subscriber(matrix)", "message", ccm.Content)
|
||||
_, err := m.client.SendText(context.Background(), id.RoomID(m.roomID), ccm.Content)
|
||||
a.AddSubscriber(func(ccm *types.ConversationMessage) {
|
||||
xlog.Debug("Subscriber(matrix)", "message", ccm.Message.Content)
|
||||
_, err := m.client.SendText(context.Background(), id.RoomID(m.roomID), ccm.Message.Content)
|
||||
if err != nil {
|
||||
xlog.Error(fmt.Sprintf("Error posting message: %v", err))
|
||||
}
|
||||
a.SharedState().ConversationTracker.AddMessage(
|
||||
fmt.Sprintf("matrix:%s", m.roomID),
|
||||
openai.ChatCompletionMessage{
|
||||
Content: ccm.Content,
|
||||
Content: ccm.Message.Content,
|
||||
Role: "assistant",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -6,11 +6,11 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
"github.com/mudler/LocalAGI/pkg/localoperator"
|
||||
"github.com/mudler/LocalAGI/pkg/xstrings"
|
||||
"github.com/mudler/LocalAGI/services/actions"
|
||||
"github.com/mudler/xlog"
|
||||
@@ -152,34 +152,34 @@ func replaceUserIDsWithNamesInMessage(api *slack.Client, message string) string
|
||||
func generateAttachmentsFromJobResponse(j *types.JobResult, api *slack.Client, channelID, ts string) (attachments []slack.Attachment) {
|
||||
for _, state := range j.State {
|
||||
// coming from the browser agent
|
||||
if history, exists := state.Metadata[actions.MetadataBrowserAgentHistory]; exists {
|
||||
if historyStruct, ok := history.(*localoperator.StateHistory); ok {
|
||||
state := historyStruct.States[len(historyStruct.States)-1]
|
||||
// Decode base64 screenshot and upload to Slack
|
||||
if state.Screenshot != "" {
|
||||
screenshotData, err := base64.StdEncoding.DecodeString(state.Screenshot)
|
||||
if err != nil {
|
||||
xlog.Error(fmt.Sprintf("Error decoding screenshot: %v", err))
|
||||
continue
|
||||
}
|
||||
// if history, exists := state.Metadata[actions.MetadataBrowserAgentHistory]; exists {
|
||||
// if historyStruct, ok := history.(*localoperator.StateHistory); ok {
|
||||
// state := historyStruct.States[len(historyStruct.States)-1]
|
||||
// // Decode base64 screenshot and upload to Slack
|
||||
// if state.Screenshot != "" {
|
||||
// screenshotData, err := base64.StdEncoding.DecodeString(state.Screenshot)
|
||||
// if err != nil {
|
||||
// xlog.Error(fmt.Sprintf("Error decoding screenshot: %v", err))
|
||||
// continue
|
||||
// }
|
||||
|
||||
data := string(screenshotData)
|
||||
// Upload the file to Slack
|
||||
_, err = api.UploadFileV2(slack.UploadFileV2Parameters{
|
||||
Reader: bytes.NewReader(screenshotData),
|
||||
FileSize: len(data),
|
||||
ThreadTimestamp: ts,
|
||||
Channel: channelID,
|
||||
Filename: "screenshot.png",
|
||||
InitialComment: "Browser Agent Screenshot",
|
||||
})
|
||||
if err != nil {
|
||||
xlog.Error(fmt.Sprintf("Error uploading screenshot: %v", err))
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// data := string(screenshotData)
|
||||
// // Upload the file to Slack
|
||||
// _, err = api.UploadFileV2(slack.UploadFileV2Parameters{
|
||||
// Reader: bytes.NewReader(screenshotData),
|
||||
// FileSize: len(data),
|
||||
// ThreadTimestamp: ts,
|
||||
// Channel: channelID,
|
||||
// Filename: "screenshot.png",
|
||||
// InitialComment: "Browser Agent Screenshot",
|
||||
// })
|
||||
// if err != nil {
|
||||
// xlog.Error(fmt.Sprintf("Error uploading screenshot: %v", err))
|
||||
// continue
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// coming from the search action
|
||||
if urls, exists := state.Metadata[actions.MetadataUrls]; exists {
|
||||
@@ -204,6 +204,59 @@ func generateAttachmentsFromJobResponse(j *types.JobResult, api *slack.Client, c
|
||||
attachments = append(attachments, attachment)
|
||||
}
|
||||
}
|
||||
|
||||
// coming from the generate_song action (local file paths)
|
||||
if songPaths, exists := state.Metadata[actions.MetadataSongs]; exists {
|
||||
for _, path := range xstrings.UniqueSlice(songPaths.([]string)) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
xlog.Error(fmt.Sprintf("Error reading song file %s: %v", path, err))
|
||||
continue
|
||||
}
|
||||
filename := filepath.Base(path)
|
||||
if filename == "" || filename == "." {
|
||||
filename = "audio"
|
||||
}
|
||||
_, err = api.UploadFileV2(slack.UploadFileV2Parameters{
|
||||
Reader: bytes.NewReader(data),
|
||||
FileSize: len(data),
|
||||
ThreadTimestamp: ts,
|
||||
Channel: channelID,
|
||||
Filename: filename,
|
||||
InitialComment: "Generated song",
|
||||
})
|
||||
if err != nil {
|
||||
xlog.Error(fmt.Sprintf("Error uploading song to Slack: %v", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// coming from the generate_pdf action (local file paths)
|
||||
if pdfPaths, exists := state.Metadata[actions.MetadataPDFs]; exists {
|
||||
for _, path := range xstrings.UniqueSlice(pdfPaths.([]string)) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
xlog.Error(fmt.Sprintf("Error reading PDF file %s: %v", path, err))
|
||||
continue
|
||||
}
|
||||
filename := filepath.Base(path)
|
||||
if filename == "" || filename == "." {
|
||||
filename = "document.pdf"
|
||||
}
|
||||
|
||||
_, err = api.UploadFileV2(slack.UploadFileV2Parameters{
|
||||
Reader: bytes.NewReader(data),
|
||||
FileSize: len(data),
|
||||
ThreadTimestamp: ts,
|
||||
Channel: channelID,
|
||||
Filename: filename,
|
||||
InitialComment: "Generated PDF document",
|
||||
})
|
||||
if err != nil {
|
||||
xlog.Error(fmt.Sprintf("Error uploading PDF to Slack: %v", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -735,12 +788,12 @@ func (t *Slack) Start(a *agent.Agent) {
|
||||
if t.channelID != "" {
|
||||
xlog.Debug(fmt.Sprintf("Listening for messages in channel %s", t.channelID))
|
||||
// handle new conversations
|
||||
a.AddSubscriber(func(ccm openai.ChatCompletionMessage) {
|
||||
xlog.Debug("Subscriber(slack)", "message", ccm.Content)
|
||||
a.AddSubscriber(func(ccm *types.ConversationMessage) {
|
||||
xlog.Debug("Subscriber(slack)", "message", ccm.Message.Content)
|
||||
_, _, err := api.PostMessage(t.channelID,
|
||||
slack.MsgOptionLinkNames(true),
|
||||
slack.MsgOptionEnableLinkUnfurl(),
|
||||
slack.MsgOptionText(ccm.Content, true),
|
||||
slack.MsgOptionText(ccm.Message.Content, true),
|
||||
slack.MsgOptionPostMessageParameters(postMessageParams),
|
||||
)
|
||||
if err != nil {
|
||||
@@ -749,7 +802,7 @@ func (t *Slack) Start(a *agent.Agent) {
|
||||
a.SharedState().ConversationTracker.AddMessage(
|
||||
fmt.Sprintf("slack:%s", t.channelID),
|
||||
openai.ChatCompletionMessage{
|
||||
Content: ccm.Content,
|
||||
Content: ccm.Message.Content,
|
||||
Role: "assistant",
|
||||
},
|
||||
)
|
||||
|
||||
+150
-29
@@ -10,7 +10,9 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -20,7 +22,6 @@ import (
|
||||
"github.com/mudler/LocalAGI/core/agent"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
"github.com/mudler/LocalAGI/pkg/localoperator"
|
||||
"github.com/mudler/LocalAGI/pkg/xstrings"
|
||||
"github.com/mudler/LocalAGI/services/actions"
|
||||
"github.com/mudler/xlog"
|
||||
@@ -535,6 +536,30 @@ func sendAudioToTelegram(ctx context.Context, b *bot.Bot, chatID int64, audioDat
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendSongToTelegram reads a song file from path and sends it to Telegram as audio.
|
||||
func sendSongToTelegram(ctx context.Context, b *bot.Bot, chatID int64, path string) error {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading song file: %w", err)
|
||||
}
|
||||
filename := filepath.Base(path)
|
||||
if filename == "" || filename == "." {
|
||||
filename = "audio"
|
||||
}
|
||||
_, err = b.SendAudio(ctx, &bot.SendAudioParams{
|
||||
ChatID: chatID,
|
||||
Audio: &models.InputFileUpload{
|
||||
Filename: filename,
|
||||
Data: bytes.NewReader(data),
|
||||
},
|
||||
Caption: "Generated song",
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error sending audio: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleMultimediaContent processes and sends multimedia content from the agent's response
|
||||
func (t *Telegram) handleMultimediaContent(ctx context.Context, chatID int64, res *types.JobResult) ([]string, error) {
|
||||
var urls []string
|
||||
@@ -555,33 +580,72 @@ func (t *Telegram) handleMultimediaContent(ctx context.Context, chatID int64, re
|
||||
}
|
||||
}
|
||||
|
||||
// Handle browser agent screenshots
|
||||
if history, exists := state.Metadata[actions.MetadataBrowserAgentHistory]; exists {
|
||||
if historyStruct, ok := history.(*localoperator.StateHistory); ok {
|
||||
state := historyStruct.States[len(historyStruct.States)-1]
|
||||
if state.Screenshot != "" {
|
||||
// Decode base64 screenshot
|
||||
screenshotData, err := base64.StdEncoding.DecodeString(state.Screenshot)
|
||||
if err != nil {
|
||||
xlog.Error("Error decoding screenshot", "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Send screenshot with caption
|
||||
_, err = t.bot.SendPhoto(ctx, &bot.SendPhotoParams{
|
||||
ChatID: chatID,
|
||||
Photo: &models.InputFileUpload{
|
||||
Filename: "screenshot.png",
|
||||
Data: bytes.NewReader(screenshotData),
|
||||
},
|
||||
Caption: "Browser Agent Screenshot",
|
||||
})
|
||||
if err != nil {
|
||||
xlog.Error("Error sending screenshot", "error", err)
|
||||
}
|
||||
// Handle songs from generate_song action (local file paths)
|
||||
if songPaths, exists := state.Metadata[actions.MetadataSongs]; exists {
|
||||
for _, path := range xstrings.UniqueSlice(songPaths.([]string)) {
|
||||
xlog.Debug("Sending song", "path", path)
|
||||
if err := sendSongToTelegram(ctx, t.bot, chatID, path); err != nil {
|
||||
xlog.Error("Error sending song", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle PDFs from generate_pdf action (local file paths)
|
||||
if pdfPaths, exists := state.Metadata[actions.MetadataPDFs]; exists {
|
||||
for _, path := range xstrings.UniqueSlice(pdfPaths.([]string)) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
xlog.Error("Error reading PDF file", "path", path, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
filename := filepath.Base(path)
|
||||
if filename == "" || filename == "." {
|
||||
filename = "document.pdf"
|
||||
}
|
||||
|
||||
xlog.Debug("Sending PDF document", "filename", filename, "size", len(data))
|
||||
_, err = t.bot.SendDocument(ctx, &bot.SendDocumentParams{
|
||||
ChatID: chatID,
|
||||
Document: &models.InputFileUpload{
|
||||
Filename: filename,
|
||||
Data: bytes.NewReader(data),
|
||||
},
|
||||
Caption: "Generated PDF",
|
||||
})
|
||||
if err != nil {
|
||||
xlog.Error("Error sending PDF", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle browser agent screenshots
|
||||
// if history, exists := state.Metadata[actions.MetadataBrowserAgentHistory]; exists {
|
||||
// if historyStruct, ok := history.(*localoperator.StateHistory); ok {
|
||||
// state := historyStruct.States[len(historyStruct.States)-1]
|
||||
// if state.Screenshot != "" {
|
||||
// // Decode base64 screenshot
|
||||
// screenshotData, err := base64.StdEncoding.DecodeString(state.Screenshot)
|
||||
// if err != nil {
|
||||
// xlog.Error("Error decoding screenshot", "error", err)
|
||||
// continue
|
||||
// }
|
||||
|
||||
// // Send screenshot with caption
|
||||
// _, err = t.bot.SendPhoto(ctx, &bot.SendPhotoParams{
|
||||
// ChatID: chatID,
|
||||
// Photo: &models.InputFileUpload{
|
||||
// Filename: "screenshot.png",
|
||||
// Data: bytes.NewReader(screenshotData),
|
||||
// },
|
||||
// Caption: "Browser Agent Screenshot",
|
||||
// })
|
||||
// if err != nil {
|
||||
// xlog.Error("Error sending screenshot", "error", err)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
return urls, nil
|
||||
@@ -855,11 +919,68 @@ func (t *Telegram) Start(a *agent.Agent) {
|
||||
|
||||
if t.channelID != "" {
|
||||
// handle new conversations
|
||||
a.AddSubscriber(func(ccm openai.ChatCompletionMessage) {
|
||||
xlog.Debug("Subscriber(telegram)", "message", ccm.Content)
|
||||
a.AddSubscriber(func(ccm *types.ConversationMessage) {
|
||||
xlog.Debug("Subscriber(telegram)", "message", ccm.Message.Content)
|
||||
|
||||
// First, handle any multimedia content from metadata
|
||||
if ccm.Metadata != nil {
|
||||
// Handle images from gen image actions
|
||||
if imagesUrls, exists := ccm.Metadata[actions.MetadataImages]; exists {
|
||||
for _, url := range xstrings.UniqueSlice(imagesUrls.([]string)) {
|
||||
xlog.Debug("Sending photo from new conversation", "url", url)
|
||||
chatID, _ := strconv.ParseInt(t.channelID, 10, 64)
|
||||
if err := sendImageToTelegram(ctx, t.bot, chatID, url); err != nil {
|
||||
xlog.Error("Error handling image", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle songs from generate_song action (local file paths)
|
||||
if songPaths, exists := ccm.Metadata[actions.MetadataSongs]; exists {
|
||||
for _, path := range xstrings.UniqueSlice(songPaths.([]string)) {
|
||||
xlog.Debug("Sending song from new conversation", "path", path)
|
||||
chatID, _ := strconv.ParseInt(t.channelID, 10, 64)
|
||||
if err := sendSongToTelegram(ctx, t.bot, chatID, path); err != nil {
|
||||
xlog.Error("Error sending song", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle PDFs from generate_pdf action (local file paths)
|
||||
if pdfPaths, exists := ccm.Metadata[actions.MetadataPDFs]; exists {
|
||||
for _, path := range xstrings.UniqueSlice(pdfPaths.([]string)) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
xlog.Error("Error reading PDF file", "path", path, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
filename := filepath.Base(path)
|
||||
if filename == "" || filename == "." {
|
||||
filename = "document.pdf"
|
||||
}
|
||||
|
||||
xlog.Debug("Sending PDF document from new conversation", "filename", filename, "size", len(data))
|
||||
chatID, _ := strconv.ParseInt(t.channelID, 10, 64)
|
||||
_, err = t.bot.SendDocument(ctx, &bot.SendDocumentParams{
|
||||
ChatID: chatID,
|
||||
Document: &models.InputFileUpload{
|
||||
Filename: filename,
|
||||
Data: bytes.NewReader(data),
|
||||
},
|
||||
Caption: "Generated PDF",
|
||||
})
|
||||
if err != nil {
|
||||
xlog.Error("Error sending PDF", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then send the text message
|
||||
_, err := b.SendMessage(ctx, &bot.SendMessageParams{
|
||||
ChatID: t.channelID,
|
||||
Text: ccm.Content,
|
||||
Text: ccm.Message.Content,
|
||||
})
|
||||
if err != nil {
|
||||
xlog.Error("Error sending message", "error", err)
|
||||
@@ -869,7 +990,7 @@ func (t *Telegram) Start(a *agent.Agent) {
|
||||
t.agent.SharedState().ConversationTracker.AddMessage(
|
||||
fmt.Sprintf("telegram:%s", t.channelID),
|
||||
openai.ChatCompletionMessage{
|
||||
Content: ccm.Content,
|
||||
Content: ccm.Message.Content,
|
||||
Role: "assistant",
|
||||
},
|
||||
)
|
||||
|
||||
+2
-2
@@ -122,7 +122,7 @@ func DynamicPrompts(dynamicConfig map[string]string) func(*state.AgentConfig) fu
|
||||
|
||||
dynamicPromptsFound := dynamicPrompts(customDirectory, existingDynamicPromptsConfigs)
|
||||
|
||||
memoryFilePath := memoryPath(a.Name, dynamicConfig)
|
||||
memoryIdxPath := memoryIndexPath(a.Name, dynamicConfig)
|
||||
promptblocks := []agent.DynamicPrompt{}
|
||||
|
||||
for _, c := range a.DynamicPrompts {
|
||||
@@ -137,7 +137,7 @@ func DynamicPrompts(dynamicConfig map[string]string) func(*state.AgentConfig) fu
|
||||
}
|
||||
promptblocks = append(promptblocks, prompt)
|
||||
case DynamicPromptMemory:
|
||||
_, memory, _ := actions.NewMemoryActions(memoryFilePath, dynamicConfig)
|
||||
_, memory, _, _ := actions.NewMemoryActions(memoryIdxPath, dynamicConfig)
|
||||
|
||||
promptblocks = append(promptblocks,
|
||||
prompts.NewMemoryPrompt(config, memory),
|
||||
|
||||
+94
-116
@@ -6,49 +6,49 @@
|
||||
"name": "react-ui",
|
||||
"dependencies": {
|
||||
"highlight.js": "^11.11.1",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@types/react": "^19.2.7",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"eslint": "^9.39.2",
|
||||
"@vitejs/plugin-react": "^5.1.4",
|
||||
"eslint": "^10.0.0",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.26",
|
||||
"globals": "^16.5.0",
|
||||
"react-router-dom": "^7.11.0",
|
||||
"vite": "^7.3.0",
|
||||
"eslint-plugin-react-refresh": "^0.5.0",
|
||||
"globals": "^17.3.0",
|
||||
"react-router-dom": "^7.13.0",
|
||||
"vite": "^7.3.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
|
||||
|
||||
"@babel/compat-data": ["@babel/compat-data@7.27.5", "", {}, "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg=="],
|
||||
"@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
|
||||
|
||||
"@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="],
|
||||
"@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
|
||||
|
||||
"@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||
"@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
|
||||
|
||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
|
||||
|
||||
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
||||
|
||||
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
||||
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
|
||||
|
||||
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
|
||||
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
|
||||
|
||||
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="],
|
||||
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||
|
||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
||||
|
||||
"@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="],
|
||||
"@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="],
|
||||
|
||||
@@ -56,11 +56,11 @@
|
||||
|
||||
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
|
||||
|
||||
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
|
||||
|
||||
"@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||
"@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA=="],
|
||||
|
||||
@@ -116,21 +116,19 @@
|
||||
|
||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="],
|
||||
|
||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
|
||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
||||
|
||||
"@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="],
|
||||
"@eslint/config-array": ["@eslint/config-array@0.23.1", "", { "dependencies": { "@eslint/object-schema": "^3.0.1", "debug": "^4.3.1", "minimatch": "^10.1.1" } }, "sha512-uVSdg/V4dfQmTjJzR0szNczjOH/J+FyUMMjYtr07xFRXR7EDf9i1qdxrD0VusZH9knj1/ecxzCQQxyic5NzAiA=="],
|
||||
|
||||
"@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
|
||||
"@eslint/config-helpers": ["@eslint/config-helpers@0.5.2", "", { "dependencies": { "@eslint/core": "^1.1.0" } }, "sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ=="],
|
||||
|
||||
"@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
|
||||
"@eslint/core": ["@eslint/core@1.1.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw=="],
|
||||
|
||||
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
|
||||
"@eslint/js": ["@eslint/js@10.0.1", "", { "peerDependencies": { "eslint": "^10.0.0" }, "optionalPeers": ["eslint"] }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="],
|
||||
|
||||
"@eslint/js": ["@eslint/js@9.39.2", "", {}, "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA=="],
|
||||
"@eslint/object-schema": ["@eslint/object-schema@3.0.1", "", {}, "sha512-P9cq2dpr+LU8j3qbLygLcSZrl2/ds/pUpfnHNNuk5HW7mnngHs+6WSq5C9mO3rqRX8A1poxqLTC9cu0KOyJlBg=="],
|
||||
|
||||
"@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
|
||||
|
||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
|
||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.6.0", "", { "dependencies": { "@eslint/core": "^1.1.0", "levn": "^0.4.1" } }, "sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ=="],
|
||||
|
||||
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
||||
|
||||
@@ -140,6 +138,10 @@
|
||||
|
||||
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.2", "", {}, "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ=="],
|
||||
|
||||
"@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="],
|
||||
|
||||
"@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.1", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg=="],
|
||||
|
||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||
@@ -152,7 +154,7 @@
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="],
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.50.0", "", { "os": "android", "cpu": "arm" }, "sha512-lVgpeQyy4fWN5QYebtW4buT/4kn4p4IJ+kDNB4uYNT5b8c8DLJDg6titg20NIg7E8RWwdWZORW6vUFfrLyG3KQ=="],
|
||||
|
||||
@@ -204,15 +206,17 @@
|
||||
|
||||
"@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="],
|
||||
|
||||
"@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="],
|
||||
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.2", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.53", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ=="],
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.4", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA=="],
|
||||
|
||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
|
||||
@@ -220,28 +224,10 @@
|
||||
|
||||
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
|
||||
|
||||
"browserslist": ["browserslist@4.24.4", "", { "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" } }, "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A=="],
|
||||
|
||||
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001714", "", {}, "sha512-mtgapdwDLSSBnCI3JokHM7oEQBLxiJKVRtg10AxM1AyeiKcM96f0Mkbqeq+1AbiCtvMcHRulAAEMu693JrSWqg=="],
|
||||
|
||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
|
||||
@@ -262,19 +248,19 @@
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
||||
"eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="],
|
||||
"eslint": ["eslint@10.0.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.0", "@eslint/config-helpers": "^0.5.2", "@eslint/core": "^1.1.0", "@eslint/plugin-kit": "^0.6.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.0", "eslint-visitor-keys": "^5.0.0", "espree": "^11.1.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.1.1", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-O0piBKY36YSJhlFSG8p9VUdPV/SxxS4FYDWVpr/9GJuMaepzwlf4J8I4ov1b+ySQfDTPhc3DtLaxcT1fN0yqCg=="],
|
||||
|
||||
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="],
|
||||
|
||||
"eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.26", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ=="],
|
||||
"eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.5.0", "", { "peerDependencies": { "eslint": ">=9" } }, "sha512-ZYvmh7VfVgqR/7wR71I3Zl6hK/C5CcxdWYKZSpHawS5JCNgE4efhQWg/+/WPpgGAp9Ngp/rRZYyaIwmPQBq/lA=="],
|
||||
|
||||
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
||||
"eslint-scope": ["eslint-scope@9.1.0", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-CkWE42hOJsNj9FJRaoMX9waUFYhqY4jmyLFdAdzZr6VaCg3ynLYx4WnOdkaIifGfH4gsUcBTn4OZbHXkpLD0FQ=="],
|
||||
|
||||
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
|
||||
"eslint-visitor-keys": ["eslint-visitor-keys@5.0.0", "", {}, "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q=="],
|
||||
|
||||
"espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
|
||||
"espree": ["espree@11.1.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-WFWYhO1fV4iYkqOOvq8FbqIhr2pYfoDY0kCotMkDeNtGpiGGkZ1iov2u8ydjtgM8yF8rzK7oaTbw2NAzbAbehw=="],
|
||||
|
||||
"esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
|
||||
"esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
|
||||
|
||||
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
|
||||
|
||||
@@ -304,9 +290,7 @@
|
||||
|
||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
|
||||
"globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="],
|
||||
|
||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
"globals": ["globals@17.3.0", "", {}, "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw=="],
|
||||
|
||||
"hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="],
|
||||
|
||||
@@ -316,8 +300,6 @@
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||
|
||||
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
@@ -328,8 +310,6 @@
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
|
||||
|
||||
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||
|
||||
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
||||
@@ -346,11 +326,9 @@
|
||||
|
||||
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||
|
||||
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
||||
|
||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
"minimatch": ["minimatch@10.1.2", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.1" } }, "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
@@ -366,8 +344,6 @@
|
||||
|
||||
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||
|
||||
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
||||
|
||||
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
@@ -382,17 +358,15 @@
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="],
|
||||
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
|
||||
|
||||
"react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="],
|
||||
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
|
||||
|
||||
"react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
|
||||
|
||||
"react-router": ["react-router@7.11.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-uI4JkMmjbWCZc01WVP2cH7ZfSzH91JAZUDd7/nIprDgWxBV1TkkmLToFh7EbMTcMak8URFRa2YoBL/W8GWnCTQ=="],
|
||||
"react-router": ["react-router@7.13.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw=="],
|
||||
|
||||
"react-router-dom": ["react-router-dom@7.11.0", "", { "dependencies": { "react-router": "7.11.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-e49Ir/kMGRzFOOrYQBdoitq3ULigw4lKbAyKusnvtDu2t4dBX4AGYPrzNvorXmVuOyeakai6FUPW5MmibvVG8g=="],
|
||||
|
||||
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||
"react-router-dom": ["react-router-dom@7.13.0", "", { "dependencies": { "react-router": "7.13.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g=="],
|
||||
|
||||
"rollup": ["rollup@4.50.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.50.0", "@rollup/rollup-android-arm64": "4.50.0", "@rollup/rollup-darwin-arm64": "4.50.0", "@rollup/rollup-darwin-x64": "4.50.0", "@rollup/rollup-freebsd-arm64": "4.50.0", "@rollup/rollup-freebsd-x64": "4.50.0", "@rollup/rollup-linux-arm-gnueabihf": "4.50.0", "@rollup/rollup-linux-arm-musleabihf": "4.50.0", "@rollup/rollup-linux-arm64-gnu": "4.50.0", "@rollup/rollup-linux-arm64-musl": "4.50.0", "@rollup/rollup-linux-loongarch64-gnu": "4.50.0", "@rollup/rollup-linux-ppc64-gnu": "4.50.0", "@rollup/rollup-linux-riscv64-gnu": "4.50.0", "@rollup/rollup-linux-riscv64-musl": "4.50.0", "@rollup/rollup-linux-s390x-gnu": "4.50.0", "@rollup/rollup-linux-x64-gnu": "4.50.0", "@rollup/rollup-linux-x64-musl": "4.50.0", "@rollup/rollup-openharmony-arm64": "4.50.0", "@rollup/rollup-win32-arm64-msvc": "4.50.0", "@rollup/rollup-win32-ia32-msvc": "4.50.0", "@rollup/rollup-win32-x64-msvc": "4.50.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-/Zl4D8zPifNmyGzJS+3kVoyXeDeT/GrsJM94sACNg9RtUE0hrHa1bNPtRSrfHTMH5HjRzce6K7rlTh3Khiw+pw=="],
|
||||
|
||||
@@ -408,10 +382,6 @@
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
||||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
|
||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||
@@ -420,7 +390,7 @@
|
||||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||
|
||||
"vite": ["vite@7.3.0", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="],
|
||||
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
@@ -434,34 +404,18 @@
|
||||
|
||||
"zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="],
|
||||
|
||||
"@babel/core/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||
"@babel/core/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
|
||||
|
||||
"@babel/generator/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||
|
||||
"@babel/helper-module-imports/@babel/traverse": ["@babel/traverse@7.27.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.3", "@babel/parser": "^7.27.4", "@babel/template": "^7.27.2", "@babel/types": "^7.27.3", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA=="],
|
||||
|
||||
"@babel/helper-module-imports/@babel/types": ["@babel/types@7.27.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q=="],
|
||||
|
||||
"@babel/helper-module-transforms/@babel/traverse": ["@babel/traverse@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/types": "^7.28.4", "debug": "^4.3.1" } }, "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ=="],
|
||||
|
||||
"@babel/helpers/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
|
||||
"@babel/generator/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
|
||||
|
||||
"@babel/parser/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
|
||||
|
||||
"@babel/template/@babel/parser": ["@babel/parser@7.27.5", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg=="],
|
||||
"@babel/template/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
|
||||
|
||||
"@babel/template/@babel/types": ["@babel/types@7.27.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q=="],
|
||||
|
||||
"@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||
|
||||
"@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||
"@babel/traverse/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
|
||||
|
||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"@eslint/eslintrc/espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="],
|
||||
|
||||
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
||||
|
||||
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
|
||||
|
||||
"@types/babel__core/@babel/parser": ["@babel/parser@7.27.0", "", { "dependencies": { "@babel/types": "^7.27.0" }, "bin": "./bin/babel-parser.js" }, "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg=="],
|
||||
@@ -478,19 +432,7 @@
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="],
|
||||
|
||||
"@babel/helper-module-imports/@babel/traverse/@babel/generator": ["@babel/generator@7.27.5", "", { "dependencies": { "@babel/parser": "^7.27.5", "@babel/types": "^7.27.3", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw=="],
|
||||
|
||||
"@babel/helper-module-imports/@babel/traverse/@babel/parser": ["@babel/parser@7.27.5", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg=="],
|
||||
|
||||
"@babel/helper-module-imports/@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
|
||||
|
||||
"@babel/helper-module-transforms/@babel/traverse/@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
|
||||
|
||||
"@babel/helper-module-transforms/@babel/traverse/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
|
||||
|
||||
"@eslint/eslintrc/espree/acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],
|
||||
|
||||
"@eslint/eslintrc/espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="],
|
||||
"@babel/parser/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||
|
||||
"@types/babel__core/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="],
|
||||
|
||||
@@ -508,14 +450,50 @@
|
||||
|
||||
"@types/babel__traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/traverse": ["@babel/traverse@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/types": "^7.28.4", "debug": "^4.3.1" } }, "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
|
||||
|
||||
"@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||
|
||||
"@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.27.5", "", {}, "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/template/@babel/parser": ["@babel/parser@7.27.5", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/template/@babel/types": ["@babel/types@7.27.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/traverse": ["@babel/traverse@7.27.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.3", "@babel/parser": "^7.27.4", "@babel/template": "^7.27.2", "@babel/types": "^7.27.3", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/types": ["@babel/types@7.27.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/template/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/traverse/@babel/generator": ["@babel/generator@7.27.5", "", { "dependencies": { "@babel/parser": "^7.27.5", "@babel/types": "^7.27.3", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/traverse/@babel/parser": ["@babel/parser@7.27.5", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||
}
|
||||
}
|
||||
|
||||
+10
-10
@@ -10,20 +10,20 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"highlight.js": "^11.11.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@types/react": "^19.2.7",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"eslint": "^9.39.2",
|
||||
"@vitejs/plugin-react": "^5.1.4",
|
||||
"eslint": "^10.0.0",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.26",
|
||||
"globals": "^16.5.0",
|
||||
"react-router-dom": "^7.11.0",
|
||||
"vite": "^7.3.0"
|
||||
"eslint-plugin-react-refresh": "^0.5.0",
|
||||
"globals": "^17.3.0",
|
||||
"react-router-dom": "^7.13.0",
|
||||
"vite": "^7.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
+885
-1425
File diff suppressed because it is too large
Load Diff
+11
-11
@@ -22,19 +22,19 @@ function App() {
|
||||
return (
|
||||
<div className="app-container">
|
||||
{/* Navigation Menu */}
|
||||
<nav className="main-nav" style={{ backgroundColor: 'var(--darker-bg)', borderBottom: '1px solid var(--medium-bg)', zIndex: 10 }}>
|
||||
<nav className="main-nav">
|
||||
<div className="container">
|
||||
<div className="nav-content">
|
||||
<div className="logo-container">
|
||||
{/* Logo with glow effect */}
|
||||
{/* Logo */}
|
||||
<Link to="/" className="logo-link">
|
||||
<div className="logo-image-container">
|
||||
<img src="/app/logo_2.png" alt="Logo" className="logo-image" />
|
||||
</div>
|
||||
{/* <span className="logo-text">LocalAGI</span> */}
|
||||
{/* <span className="logo-text">LocalAGI</span> */}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="desktop-menu">
|
||||
<ul className="nav-links">
|
||||
<li>
|
||||
@@ -59,22 +59,22 @@ function App() {
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="">
|
||||
<span className="status-indicator"></span>
|
||||
<span className="status-text">State: <span className="status-value">active</span></span>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="mobile-menu-toggle" onClick={toggleMobileMenu}>
|
||||
<i className="fas fa-bars"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
{/* Mobile Menu */}
|
||||
{mobileMenuOpen && (
|
||||
<div className="mobile-menu" style={{ backgroundColor: 'var(--darker-bg)', borderTop: '1px solid var(--medium-bg)' }}>
|
||||
<div className="mobile-menu">
|
||||
<ul className="mobile-nav-links">
|
||||
<li>
|
||||
<Link to="/" className="mobile-nav-link" onClick={() => setMobileMenuOpen(false)}>
|
||||
@@ -99,21 +99,21 @@ function App() {
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* Toast Notification */}
|
||||
{toast.visible && (
|
||||
<div className={`toast ${toast.type}`}>
|
||||
<span>{toast.message}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* Main Content Area */}
|
||||
<main className="main-content">
|
||||
<div className="container">
|
||||
<Outlet context={{ showToast }} />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,10 @@ const AgentForm = ({
|
||||
|
||||
// Handle input changes
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value, type, checked } = e.target.name.target;
|
||||
const { value, type, checked } = e.target;
|
||||
const name = e.target.name;
|
||||
// Guard: name must be a string (some sections pass synthetic events where name can be wrong)
|
||||
if (typeof name !== 'string') return;
|
||||
|
||||
// Convert value to number if it's a number input
|
||||
const processedValue = type === 'number' ? Number(value) : value;
|
||||
@@ -272,7 +275,7 @@ const AgentForm = ({
|
||||
</div>
|
||||
|
||||
<div style={{ display: activeSection === 'mcp-section' ? 'block' : 'none' }}>
|
||||
<MCPServersSection formData={formData} handleAddMCPServer={handleAddMCPServer} handleRemoveMCPServer={handleRemoveMCPServer} handleMCPServerChange={handleMCPServerChange} />
|
||||
<MCPServersSection formData={formData} handleAddMCPServer={handleAddMCPServer} handleRemoveMCPServer={handleRemoveMCPServer} handleMCPServerChange={handleMCPServerChange} handleInputChange={handleInputChange} metadata={metadata} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: activeSection === 'memory-section' ? 'block' : 'none' }}>
|
||||
@@ -327,7 +330,7 @@ const AgentForm = ({
|
||||
</div>
|
||||
|
||||
<div style={{ display: activeSection === 'mcp-section' ? 'block' : 'none' }}>
|
||||
<MCPServersSection formData={formData} handleAddMCPServer={handleAddMCPServer} handleRemoveMCPServer={handleRemoveMCPServer} handleMCPServerChange={handleMCPServerChange} />
|
||||
<MCPServersSection formData={formData} handleAddMCPServer={handleAddMCPServer} handleRemoveMCPServer={handleRemoveMCPServer} handleMCPServerChange={handleMCPServerChange} handleInputChange={handleInputChange} metadata={metadata} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: activeSection === 'memory-section' ? 'block' : 'none' }}>
|
||||
|
||||
@@ -13,21 +13,23 @@ const AdvancedSettingsSection = ({ formData, handleInputChange, metadata }) => {
|
||||
// Get fields from metadata
|
||||
const fields = metadata?.AdvancedSettingsSection || [];
|
||||
|
||||
// Handle field value changes
|
||||
const handleFieldChange = (name, value) => {
|
||||
// Handle field value changes (FormField passes the event)
|
||||
const handleFieldChange = (e) => {
|
||||
const { name, value, type, checked } = e.target;
|
||||
const field = fields.find(f => f.name === name);
|
||||
if (field && field.type === 'checkbox') {
|
||||
handleInputChange({
|
||||
target: {
|
||||
name,
|
||||
type: 'checkbox',
|
||||
checked: value === 'true'
|
||||
checked
|
||||
}
|
||||
});
|
||||
} else {
|
||||
handleInputChange({
|
||||
target: {
|
||||
name,
|
||||
type,
|
||||
value
|
||||
}
|
||||
});
|
||||
|
||||
@@ -30,21 +30,23 @@ const BasicInfoSection = ({ formData, handleInputChange, isEdit, isGroupForm, me
|
||||
return field;
|
||||
}) || [];
|
||||
|
||||
// Handle field value changes
|
||||
const handleFieldChange = (name, value) => {
|
||||
// Handle field value changes (FormField passes the event)
|
||||
const handleFieldChange = (e) => {
|
||||
const { name, value, type, checked } = e.target;
|
||||
const field = fields.find(f => f.name === name);
|
||||
if (field && field.type === 'checkbox') {
|
||||
handleInputChange({
|
||||
target: {
|
||||
name,
|
||||
type: 'checkbox',
|
||||
checked: value === 'true'
|
||||
checked
|
||||
}
|
||||
});
|
||||
} else {
|
||||
handleInputChange({
|
||||
target: {
|
||||
name,
|
||||
type,
|
||||
value
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,42 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import FormFieldDefinition from '../common/FormFieldDefinition';
|
||||
|
||||
// Parse mcp_stdio_servers JSON string to array of { name, command, args, env }
|
||||
function parseStdioJson(str) {
|
||||
if (!str || typeof str !== 'string') return [];
|
||||
try {
|
||||
const parsed = JSON.parse(str);
|
||||
const mcpServers = parsed?.mcpServers || {};
|
||||
return Object.entries(mcpServers).map(([name, s]) => ({
|
||||
name: name || '',
|
||||
command: s?.command ?? '',
|
||||
args: Array.isArray(s?.args) ? [...s.args] : [],
|
||||
env: s?.env && typeof s.env === 'object' && !Array.isArray(s.env) ? { ...s.env } : {},
|
||||
}));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Build JSON string from array of STDIO servers (unique keys: name or server0, server1, etc.)
|
||||
function buildStdioJson(list) {
|
||||
const mcpServers = {};
|
||||
const usedKeys = new Set();
|
||||
list.forEach((item, index) => {
|
||||
let key = (item.name && item.name.trim()) ? item.name.trim() : `server${index}`;
|
||||
while (usedKeys.has(key)) {
|
||||
key = `${key}_${index}`;
|
||||
}
|
||||
usedKeys.add(key);
|
||||
mcpServers[key] = {
|
||||
command: item.command || '',
|
||||
args: item.args || [],
|
||||
env: item.env && typeof item.env === 'object' ? { ...item.env } : {},
|
||||
};
|
||||
});
|
||||
return JSON.stringify({ mcpServers }, null, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* MCP Servers section of the agent form
|
||||
*/
|
||||
@@ -8,8 +44,118 @@ const MCPServersSection = ({
|
||||
formData,
|
||||
handleAddMCPServer,
|
||||
handleRemoveMCPServer,
|
||||
handleMCPServerChange
|
||||
handleMCPServerChange,
|
||||
handleInputChange,
|
||||
metadata
|
||||
}) => {
|
||||
// MCP configuration fields excluding mcp_stdio_servers (handled by dynamic STDIO block below)
|
||||
const mcpFields = useMemo(
|
||||
() => (metadata?.MCPSection || []).filter((f) => f.name !== 'mcp_stdio_servers'),
|
||||
[metadata?.MCPSection]
|
||||
);
|
||||
|
||||
// Parsed STDIO servers list derived from formData.mcp_stdio_servers
|
||||
const stdioList = useMemo(
|
||||
() => parseStdioJson(formData.mcp_stdio_servers),
|
||||
[formData.mcp_stdio_servers]
|
||||
);
|
||||
|
||||
const setStdioJson = (newList) => {
|
||||
handleInputChange({
|
||||
target: { name: 'mcp_stdio_servers', value: buildStdioJson(newList) },
|
||||
});
|
||||
};
|
||||
|
||||
const addStdioServer = () => {
|
||||
setStdioJson([...stdioList, { name: '', command: '', args: [], env: {} }]);
|
||||
};
|
||||
|
||||
const removeStdioServer = (index) => {
|
||||
setStdioJson(stdioList.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const updateStdioServer = (index, field, value) => {
|
||||
const next = stdioList.map((s, i) =>
|
||||
i === index ? { ...s, [field]: value } : s
|
||||
);
|
||||
setStdioJson(next);
|
||||
};
|
||||
|
||||
const addArg = (serverIndex, argValue = '') => {
|
||||
const server = stdioList[serverIndex];
|
||||
if (!server) return;
|
||||
updateStdioServer(serverIndex, 'args', [...(server.args || []), argValue]);
|
||||
};
|
||||
|
||||
const removeArg = (serverIndex, argIndex) => {
|
||||
const server = stdioList[serverIndex];
|
||||
if (!server) return;
|
||||
const args = (server.args || []).filter((_, i) => i !== argIndex);
|
||||
updateStdioServer(serverIndex, 'args', args);
|
||||
};
|
||||
|
||||
const updateArg = (serverIndex, argIndex, value) => {
|
||||
const server = stdioList[serverIndex];
|
||||
if (!server) return;
|
||||
const args = [...(server.args || [])];
|
||||
args[argIndex] = value;
|
||||
updateStdioServer(serverIndex, 'args', args);
|
||||
};
|
||||
|
||||
const addEnv = (serverIndex, key = '', value = '') => {
|
||||
const server = stdioList[serverIndex];
|
||||
if (!server) return;
|
||||
const env = { ...(server.env || {}), [key || `key_${Date.now()}`]: value };
|
||||
updateStdioServer(serverIndex, 'env', env);
|
||||
};
|
||||
|
||||
const removeEnv = (serverIndex, envKey) => {
|
||||
const server = stdioList[serverIndex];
|
||||
if (!server) return;
|
||||
const env = { ...(server.env || {}) };
|
||||
delete env[envKey];
|
||||
updateStdioServer(serverIndex, 'env', env);
|
||||
};
|
||||
|
||||
const updateEnvKey = (serverIndex, oldKey, newKey) => {
|
||||
const server = stdioList[serverIndex];
|
||||
if (!server) return;
|
||||
const env = { ...(server.env || {}) };
|
||||
const val = env[oldKey];
|
||||
delete env[oldKey];
|
||||
env[newKey || oldKey] = val;
|
||||
updateStdioServer(serverIndex, 'env', env);
|
||||
};
|
||||
|
||||
const updateEnvValue = (serverIndex, envKey, value) => {
|
||||
const server = stdioList[serverIndex];
|
||||
if (!server) return;
|
||||
const env = { ...(server.env || {}), [envKey]: value };
|
||||
updateStdioServer(serverIndex, 'env', env);
|
||||
};
|
||||
|
||||
// Handle MCP configuration field value changes (FormField passes the event)
|
||||
const handleMCPFieldChange = (e) => {
|
||||
const { name, value, type, checked } = e.target;
|
||||
const field = mcpFields.find(f => f.name === name);
|
||||
if (field && field.type === 'checkbox') {
|
||||
handleInputChange({
|
||||
target: {
|
||||
name,
|
||||
type: 'checkbox',
|
||||
checked
|
||||
}
|
||||
});
|
||||
} else {
|
||||
handleInputChange({
|
||||
target: {
|
||||
name,
|
||||
value
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Define field definitions for each MCP server
|
||||
const getServerFields = () => [
|
||||
{
|
||||
@@ -43,12 +189,149 @@ const MCPServersSection = ({
|
||||
<p className="section-description">
|
||||
Configure MCP servers for this agent.
|
||||
</p>
|
||||
|
||||
<div className="mcp-servers-container">
|
||||
|
||||
{mcpFields.length > 0 && (
|
||||
<div className="mcp-config-fields mb-4">
|
||||
<h4 className="subsection-title">MCP configuration</h4>
|
||||
<FormFieldDefinition
|
||||
fields={mcpFields}
|
||||
values={formData}
|
||||
onChange={handleMCPFieldChange}
|
||||
idPrefix="mcp_"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mcp-block mcp-stdio-block mb-4">
|
||||
<h4 className="subsection-title">MCP STDIO Servers</h4>
|
||||
<p className="section-description">
|
||||
Configure MCP servers that run as local commands (e.g. docker run). Each server has a name, command, args, and env.
|
||||
</p>
|
||||
{stdioList.map((server, index) => (
|
||||
<div key={index} className="mcp-server-item mb-4">
|
||||
<div className="mcp-server-header">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control stdio-server-name-input"
|
||||
placeholder="Server name (e.g. memory)"
|
||||
value={server.name || ''}
|
||||
onChange={(e) => updateStdioServer(index, 'name', e.target.value)}
|
||||
aria-label="STDIO server name"
|
||||
style={{ flex: 1, marginRight: '12px' }}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="action-btn delete-btn"
|
||||
onClick={() => removeStdioServer(index)}
|
||||
aria-label="Remove STDIO server"
|
||||
>
|
||||
<i className="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`stdio_cmd_${index}`}>Command</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`stdio_cmd_${index}`}
|
||||
className="form-control"
|
||||
placeholder="e.g. docker"
|
||||
value={server.command || ''}
|
||||
onChange={(e) => updateStdioServer(index, 'command', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group mb-3">
|
||||
<label>Args</label>
|
||||
{(server.args || []).map((arg, argIndex) => (
|
||||
<div key={argIndex} className="input-group mb-2" style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Argument"
|
||||
value={arg}
|
||||
onChange={(e) => updateArg(index, argIndex, e.target.value)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="action-btn delete-btn"
|
||||
onClick={() => removeArg(index, argIndex)}
|
||||
aria-label="Remove arg"
|
||||
>
|
||||
<i className="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className="action-btn"
|
||||
onClick={() => addArg(index)}
|
||||
>
|
||||
<i className="fas fa-plus"></i> Add Arg
|
||||
</button>
|
||||
</div>
|
||||
<div className="form-group mb-3">
|
||||
<label>Environment</label>
|
||||
{Object.entries(server.env || {}).map(([envKey, envVal]) => (
|
||||
<div key={envKey} className="input-group mb-2" style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Key"
|
||||
value={envKey}
|
||||
onChange={(e) => updateEnvKey(index, envKey, e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Value"
|
||||
value={envVal}
|
||||
onChange={(e) => updateEnvValue(index, envKey, e.target.value)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="action-btn delete-btn"
|
||||
onClick={() => removeEnv(index, envKey)}
|
||||
aria-label="Remove env"
|
||||
>
|
||||
<i className="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className="action-btn"
|
||||
onClick={() => addEnv(index)}
|
||||
>
|
||||
<i className="fas fa-plus"></i> Add Env
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<button type="button" className="action-btn" onClick={addStdioServer}>
|
||||
<i className="fas fa-plus"></i> Add MCP STDIO Server
|
||||
</button>
|
||||
<details className="mt-3">
|
||||
<summary className="subsection-title" style={{ cursor: 'pointer' }}>Edit as JSON</summary>
|
||||
<textarea
|
||||
className="form-control mt-2"
|
||||
rows={8}
|
||||
placeholder='{"mcpServers":{"memory":{"command":"docker","args":["run","-i","--rm",...],"env":{...}}}}'
|
||||
value={formData.mcp_stdio_servers || ''}
|
||||
onChange={(e) => handleInputChange({ target: { name: 'mcp_stdio_servers', value: e.target.value } })}
|
||||
spellCheck={false}
|
||||
style={{ fontFamily: 'monospace', fontSize: '12px' }}
|
||||
/>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div className="mcp-block mcp-http-block mcp-servers-container">
|
||||
<h4 className="subsection-title">MCP HTTP Servers</h4>
|
||||
<p className="section-description">
|
||||
Configure MCP servers that connect over HTTP (URL and optional API key).
|
||||
</p>
|
||||
{formData.mcp_servers && formData.mcp_servers.map((server, index) => (
|
||||
<div key={index} className="mcp-server-item mb-4">
|
||||
<div className="mcp-server-header">
|
||||
<h4>MCP Server #{index + 1}</h4>
|
||||
<h4>MCP HTTP Server #{index + 1}</h4>
|
||||
<button
|
||||
type="button"
|
||||
className="action-btn delete-btn"
|
||||
@@ -72,7 +355,7 @@ const MCPServersSection = ({
|
||||
className="action-btn"
|
||||
onClick={handleAddMCPServer}
|
||||
>
|
||||
<i className="fas fa-plus"></i> Add MCP Server
|
||||
<i className="fas fa-plus"></i> Add MCP HTTP Server
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,21 +13,23 @@ const MemorySettingsSection = ({ formData, handleInputChange, metadata }) => {
|
||||
// Get fields from metadata
|
||||
const fields = metadata?.MemorySettingsSection || [];
|
||||
|
||||
// Handle field value changes
|
||||
const handleFieldChange = (name, value) => {
|
||||
// Handle field value changes (FormField passes the event)
|
||||
const handleFieldChange = (e) => {
|
||||
const { name, value, type, checked } = e.target;
|
||||
const field = fields.find(f => f.name === name);
|
||||
if (field && field.type === 'checkbox') {
|
||||
handleInputChange({
|
||||
target: {
|
||||
name,
|
||||
type: 'checkbox',
|
||||
checked: value === 'true'
|
||||
checked
|
||||
}
|
||||
});
|
||||
} else {
|
||||
handleInputChange({
|
||||
target: {
|
||||
name,
|
||||
type,
|
||||
value
|
||||
}
|
||||
});
|
||||
|
||||
@@ -13,21 +13,23 @@ const ModelSettingsSection = ({ formData, handleInputChange, metadata }) => {
|
||||
// Get fields from metadata
|
||||
const fields = metadata?.ModelSettingsSection || [];
|
||||
|
||||
// Handle field value changes
|
||||
const handleFieldChange = (name, value) => {
|
||||
// Handle field value changes (FormField passes the event)
|
||||
const handleFieldChange = (e) => {
|
||||
const { name, value, type, checked } = e.target;
|
||||
const field = fields.find(f => f.name === name);
|
||||
if (field && field.type === 'checkbox') {
|
||||
handleInputChange({
|
||||
target: {
|
||||
name,
|
||||
type: 'checkbox',
|
||||
checked: value === 'true'
|
||||
checked
|
||||
}
|
||||
});
|
||||
} else {
|
||||
handleInputChange({
|
||||
target: {
|
||||
name,
|
||||
type,
|
||||
value
|
||||
}
|
||||
});
|
||||
|
||||
@@ -34,21 +34,23 @@ const PromptsGoalsSection = ({
|
||||
return metadata.PromptsGoalsSection;
|
||||
};
|
||||
|
||||
// Handle field value changes
|
||||
const handleFieldChange = (name, value) => {
|
||||
// Handle field value changes (FormField passes the event)
|
||||
const handleFieldChange = (e) => {
|
||||
const { name, value, type, checked } = e.target;
|
||||
const field = getFields().find(f => f.name === name);
|
||||
if (field && field.type === 'checkbox') {
|
||||
handleInputChange({
|
||||
target: {
|
||||
name,
|
||||
type: 'checkbox',
|
||||
checked: value === 'true'
|
||||
checked
|
||||
}
|
||||
});
|
||||
} else {
|
||||
handleInputChange({
|
||||
target: {
|
||||
name,
|
||||
type,
|
||||
value
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
/* LocalAGI Base Styles */
|
||||
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
color-scheme: dark;
|
||||
color: var(--color-text-primary);
|
||||
background-color: var(--color-bg-primary);
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
@@ -13,56 +15,99 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
transition: color var(--duration-fast) var(--ease-default);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
color: var(--color-primary-hover);
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
background-color: var(--color-bg-primary);
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-border);
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
background-color: var(--color-bg-secondary);
|
||||
color: var(--color-text-primary);
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
transition: all var(--duration-fast) var(--ease-default);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
button:hover {
|
||||
border-color: var(--color-primary);
|
||||
background-color: var(--color-primary-light);
|
||||
}
|
||||
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Selection styling */
|
||||
::selection {
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-text-inverse);
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--color-border);
|
||||
border-radius: var(--radius-full);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-text-muted);
|
||||
}
|
||||
@@ -2,13 +2,14 @@ import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { RouterProvider } from 'react-router-dom'
|
||||
import { router } from './router'
|
||||
import './theme.css'
|
||||
import './index.css'
|
||||
import './App.css'
|
||||
|
||||
// Add the Google Fonts for the cyberpunk styling
|
||||
// Add professional Google Fonts
|
||||
const fontLink = document.createElement('link');
|
||||
fontLink.rel = 'stylesheet';
|
||||
fontLink.href = 'https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;700&family=Permanent+Marker&display=swap';
|
||||
fontLink.href = 'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap';
|
||||
document.head.appendChild(fontLink);
|
||||
|
||||
// Add Font Awesome for icons
|
||||
|
||||
@@ -17,7 +17,7 @@ function AgentsList() {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server responded with status: ${response.status}`);
|
||||
}
|
||||
|
||||
|
||||
const data = await response.json();
|
||||
setAgents(data.agents || []);
|
||||
setStatuses(data.statuses || {});
|
||||
@@ -38,18 +38,18 @@ function AgentsList() {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
|
||||
|
||||
if (response.ok) {
|
||||
// Update local state
|
||||
setStatuses(prev => ({
|
||||
...prev,
|
||||
[name]: !isActive
|
||||
}));
|
||||
|
||||
|
||||
// Show success toast
|
||||
const action = isActive ? 'paused' : 'started';
|
||||
showToast(`Agent "${name}" ${action} successfully`, 'success');
|
||||
|
||||
|
||||
// Refresh the agents list to ensure we have the latest data
|
||||
fetchAgents();
|
||||
} else {
|
||||
@@ -67,13 +67,13 @@ function AgentsList() {
|
||||
if (!confirm(`Are you sure you want to delete agent "${name}"? This action cannot be undone.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/agent/${name}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
|
||||
if (response.ok) {
|
||||
// Remove from local state
|
||||
setAgents(prev => prev.filter(agent => agent !== name));
|
||||
@@ -82,7 +82,7 @@ function AgentsList() {
|
||||
delete newStatuses[name];
|
||||
return newStatuses;
|
||||
});
|
||||
|
||||
|
||||
// Show success toast
|
||||
showToast(`Agent "${name}" deleted successfully`, 'success');
|
||||
} else {
|
||||
@@ -130,76 +130,69 @@ function AgentsList() {
|
||||
</header>
|
||||
|
||||
{agents.length > 0 ? (
|
||||
<div className="agents-grid">
|
||||
{agents.map(name => (
|
||||
<div key={name} className="agent-card" data-agent={name} data-active={statuses[name]}>
|
||||
<div className="agent-content text-center">
|
||||
<div className="avatar-container mb-4">
|
||||
<img
|
||||
src={`/avatars/${name}.png`}
|
||||
alt={name}
|
||||
className="w-24 h-24 rounded-full"
|
||||
style={{
|
||||
border: '2px solid var(--primary)',
|
||||
boxShadow: 'var(--neon-glow)',
|
||||
display: 'none',
|
||||
margin: '0 auto'
|
||||
}}
|
||||
onLoad={(e) => {
|
||||
e.target.style.display = 'block';
|
||||
e.target.nextElementSibling.style.display = 'none';
|
||||
}}
|
||||
onError={(e) => {
|
||||
e.target.style.display = 'none';
|
||||
e.target.nextElementSibling.style.display = 'flex';
|
||||
}}
|
||||
/>
|
||||
<div className="avatar-placeholder" style={{margin: '0 auto'}}>
|
||||
<span className="placeholder-text"><i className="fas fa-sync fa-spin"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="agent-header">
|
||||
<h2>{name}</h2>
|
||||
<span className={`status-badge ${statuses[name] ? 'active' : 'inactive'}`}>
|
||||
{statuses[name] ? 'Active' : 'Paused'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="agent-actions">
|
||||
<Link to={`/talk/${name}`} className="action-btn chat-btn">
|
||||
<i className="fas fa-comment"></i> Chat
|
||||
</Link>
|
||||
<Link to={`/status/${name}`} className="action-btn status-btn">
|
||||
<i className="fas fa-chart-line"></i> Status
|
||||
</Link>
|
||||
<Link to={`/settings/${name}`} className="action-btn settings-btn">
|
||||
<i className="fas fa-cog"></i> Settings
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="agent-actions mt-2">
|
||||
<button
|
||||
className="action-btn toggle-btn"
|
||||
onClick={() => toggleAgentStatus(name, statuses[name])}
|
||||
>
|
||||
{statuses[name] ? (
|
||||
<><i className="fas fa-pause"></i> Pause</>
|
||||
) : (
|
||||
<><i className="fas fa-play"></i> Start</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="action-btn delete-btn"
|
||||
onClick={() => deleteAgent(name)}
|
||||
>
|
||||
<i className="fas fa-trash-alt"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="agents-table-container">
|
||||
<table className="agents-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Agent Name</th>
|
||||
<th>Status</th>
|
||||
<th>Quick Actions</th>
|
||||
<th>Management</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{agents.map(name => (
|
||||
<tr key={name} data-agent={name} data-active={statuses[name]}>
|
||||
<td>
|
||||
<div className="agent-info">
|
||||
<span className="agent-name-main">{name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span className={`status-badge ${statuses[name] ? 'active' : 'inactive'}`}>
|
||||
{statuses[name] ? 'Active' : 'Paused'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div className="agent-table-actions">
|
||||
<Link to={`/talk/${name}`} className="action-btn chat-btn" title="Chat">
|
||||
<i className="fas fa-comment"></i> Chat
|
||||
</Link>
|
||||
<Link to={`/status/${name}`} className="action-btn status-btn" title="Status">
|
||||
<i className="fas fa-chart-line"></i> Status
|
||||
</Link>
|
||||
<Link to={`/settings/${name}`} className="action-btn settings-btn" title="Settings">
|
||||
<i className="fas fa-cog"></i> Settings
|
||||
</Link>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="agent-table-actions">
|
||||
<button
|
||||
className="action-btn toggle-btn"
|
||||
onClick={() => toggleAgentStatus(name, statuses[name])}
|
||||
title={statuses[name] ? "Pause Agent" : "Start Agent"}
|
||||
>
|
||||
{statuses[name] ? (
|
||||
<><i className="fas fa-pause"></i> Pause</>
|
||||
) : (
|
||||
<><i className="fas fa-play"></i> Start</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="action-btn delete-btn"
|
||||
onClick={() => deleteAgent(name)}
|
||||
title="Delete Agent"
|
||||
>
|
||||
<i className="fas fa-trash-alt"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<div className="no-agents">
|
||||
|
||||
@@ -59,9 +59,9 @@ function Home() {
|
||||
<div className="image-container">
|
||||
<img src="/app/logo_1.png" width="250" alt="LocalAGI Logo" />
|
||||
</div>
|
||||
|
||||
|
||||
{/*<h1 className="dashboard-title">LocalAGI</h1>*/}
|
||||
|
||||
|
||||
{/* Dashboard Stats */}
|
||||
<div className="dashboard-stats">
|
||||
<div className="stat-item">
|
||||
@@ -87,7 +87,7 @@ function Home() {
|
||||
<p>View and manage your list of agents, including detailed profiles and statistics.</p>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
|
||||
{/* Card for Create Agent */}
|
||||
<Link to="/create" className="card-link">
|
||||
<div className="card">
|
||||
@@ -95,7 +95,7 @@ function Home() {
|
||||
<p>Create a new intelligent agent with custom behaviors, connectors, and actions.</p>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
|
||||
{/* Card for Actions Playground */}
|
||||
<Link to="/actions-playground" className="card-link">
|
||||
<div className="card">
|
||||
@@ -103,7 +103,7 @@ function Home() {
|
||||
<p>Explore and test available actions for your agents.</p>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
|
||||
{/* Card for Group Create */}
|
||||
<Link to="/group-create" className="card-link">
|
||||
<div className="card">
|
||||
@@ -122,32 +122,6 @@ function Home() {
|
||||
|
||||
</div>
|
||||
|
||||
{stats.agents.length > 0 && (
|
||||
<div className="recent-agents">
|
||||
<h2>Your Agents</h2>
|
||||
<div className="cards-container">
|
||||
{stats.agents.map((agent) => (
|
||||
<div key={agent} className="card">
|
||||
<div className={`status-badge ${stats.status[agent] ? 'status-active' : 'status-paused'}`}>
|
||||
{stats.status[agent] ? 'Active' : 'Paused'}
|
||||
</div>
|
||||
<h2><i className="fas fa-robot"></i> {agent}</h2>
|
||||
<div className="agent-actions">
|
||||
<Link to={`/talk/${agent}`} className="action-btn chat-btn">
|
||||
<i className="fas fa-comment"></i> Chat
|
||||
</Link>
|
||||
<Link to={`/settings/${agent}`} className="action-btn settings-btn">
|
||||
<i className="fas fa-cog"></i> Settings
|
||||
</Link>
|
||||
<Link to={`/status/${agent}`} className="action-btn status-btn">
|
||||
<i className="fas fa-chart-line"></i> Status
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/* LocalAGI Theme - CSS Variables System */
|
||||
/* Inspired by LocalAI's elegant professional design */
|
||||
|
||||
:root {
|
||||
/* Background Colors */
|
||||
--color-bg-primary: #0F172A; /* Deep navy background */
|
||||
--color-bg-secondary: #1E293B; /* Elevated surfaces */
|
||||
--color-bg-tertiary: #1E293B; /* Cards, panels */
|
||||
--color-bg-overlay: rgba(15, 23, 42, 0.8); /* Modals, overlays */
|
||||
|
||||
/* Brand Colors - Primary Palette */
|
||||
--color-primary: #38BDF8; /* Cyan - primary actions */
|
||||
--color-primary-hover: #0EA5E9; /* Darker cyan on hover */
|
||||
--color-primary-active: #0284C7; /* Active state */
|
||||
--color-primary-text: #FFFFFF; /* Text on primary background */
|
||||
--color-primary-light: rgba(56, 189, 248, 0.08);
|
||||
--color-primary-border: rgba(56, 189, 248, 0.15);
|
||||
|
||||
/* Secondary Colors */
|
||||
--color-secondary: #14B8A6; /* Teal - secondary actions */
|
||||
--color-secondary-hover: #0D9488;
|
||||
--color-secondary-light: rgba(20, 184, 166, 0.1);
|
||||
|
||||
/* Accent Colors */
|
||||
--color-accent: #8B5CF6; /* Purple - special states */
|
||||
--color-accent-hover: #7C3AED;
|
||||
--color-accent-light: rgba(139, 92, 246, 0.1);
|
||||
--color-accent-purple: #A78BFA; /* Light purple for gradients */
|
||||
--color-accent-teal: #2DD4BF; /* Light teal for gradients */
|
||||
|
||||
/* Text Colors */
|
||||
--color-text-primary: #E5E7EB; /* Primary text */
|
||||
--color-text-secondary: #94A3B8; /* Secondary text */
|
||||
--color-text-muted: #64748B; /* Tertiary/muted text */
|
||||
--color-text-disabled: #475569; /* Disabled text */
|
||||
--color-text-inverse: #0F172A; /* Text on light backgrounds */
|
||||
|
||||
/* Border Colors */
|
||||
--color-border: rgba(148, 163, 184, 0.12);
|
||||
--color-border-subtle: rgba(148, 163, 184, 0.08);
|
||||
--color-border-strong: rgba(56, 189, 248, 0.2);
|
||||
--color-border-focus: rgba(56, 189, 248, 0.3);
|
||||
|
||||
/* Status Colors */
|
||||
--color-success: #14B8A6;
|
||||
--color-success-light: rgba(20, 184, 166, 0.1);
|
||||
--color-warning: #F59E0B;
|
||||
--color-warning-light: rgba(245, 158, 11, 0.1);
|
||||
--color-error: #EF4444;
|
||||
--color-error-light: rgba(239, 68, 68, 0.1);
|
||||
--color-info: #38BDF8;
|
||||
--color-info-light: rgba(56, 189, 248, 0.1);
|
||||
|
||||
/* Gradient Definitions */
|
||||
--gradient-primary: linear-gradient(135deg, #38BDF8 0%, #8B5CF6 50%, #14B8A6 100%);
|
||||
--gradient-subtle: linear-gradient(135deg, rgba(56, 189, 248, 0.05) 0%, rgba(139, 92, 246, 0.05) 100%);
|
||||
--gradient-text: linear-gradient(135deg, #38BDF8 0%, #8B5CF6 50%, #14B8A6 100%);
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.12);
|
||||
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
|
||||
--shadow-glow: 0 0 0 1px rgba(56, 189, 248, 0.1), 0 0 8px rgba(56, 189, 248, 0.15);
|
||||
|
||||
/* Animation Timing */
|
||||
--duration-fast: 150ms;
|
||||
--duration-normal: 200ms;
|
||||
--duration-slow: 300ms;
|
||||
--ease-default: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
/* Border Radius */
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 6px;
|
||||
--radius-lg: 8px;
|
||||
--radius-xl: 12px;
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* Spacing Scale */
|
||||
--spacing-xs: 0.25rem;
|
||||
--spacing-sm: 0.5rem;
|
||||
--spacing-md: 1rem;
|
||||
--spacing-lg: 1.5rem;
|
||||
--spacing-xl: 2rem;
|
||||
--spacing-2xl: 3rem;
|
||||
|
||||
/* Legacy Variable Mappings (for backward compatibility) */
|
||||
--primary: var(--color-primary);
|
||||
--secondary: var(--color-secondary);
|
||||
--tertiary: var(--color-accent);
|
||||
--dark-bg: var(--color-bg-primary);
|
||||
--darker-bg: var(--color-bg-primary);
|
||||
--medium-bg: var(--color-bg-secondary);
|
||||
--light-bg: var(--color-bg-tertiary);
|
||||
--text: var(--color-text-primary);
|
||||
--border: var(--color-border);
|
||||
--success: var(--color-success);
|
||||
--danger: var(--color-error);
|
||||
--warning: var(--color-warning);
|
||||
--info: var(--color-info);
|
||||
|
||||
/* Remove old glow effects - use subtle shadows instead */
|
||||
--neon-glow: none;
|
||||
--pink-glow: none;
|
||||
--purple-glow: none;
|
||||
}
|
||||
@@ -29,8 +29,7 @@ export default defineConfig(({ mode }) => {
|
||||
'/chat': backendUrl,
|
||||
'/status': backendUrl,
|
||||
'/action': backendUrl,
|
||||
'/actions': backendUrl,
|
||||
'/avatars': backendUrl
|
||||
'/actions': backendUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/dave-gray101/v2keyauth"
|
||||
fiber "github.com/gofiber/fiber/v2"
|
||||
@@ -27,13 +26,6 @@ var reactUI embed.FS
|
||||
|
||||
func (app *App) registerRoutes(pool *state.AgentPool, webapp *fiber.App) {
|
||||
|
||||
// Static avatars in a.pooldir/avatars
|
||||
webapp.Use("/avatars", filesystem.New(filesystem.Config{
|
||||
Root: http.Dir(filepath.Join(app.config.StateDir, "avatars")),
|
||||
// PathPrefix: "avatars",
|
||||
Browse: true,
|
||||
}))
|
||||
|
||||
if len(app.config.ApiKeys) > 0 {
|
||||
kaConfig, err := GetKeyAuthConfig(app.config.ApiKeys)
|
||||
if err != nil || kaConfig == nil {
|
||||
|
||||
Reference in New Issue
Block a user