diff --git a/core/agent/agent.go b/core/agent/agent.go index 6a11a4d..af6b2e4 100644 --- a/core/agent/agent.go +++ b/core/agent/agent.go @@ -379,7 +379,15 @@ func (a *Agent) consumeJob(job *Job, role string) { //} // Add custom prompts for _, prompt := range a.options.prompts { - message := prompt.Render(a) + message, err := prompt.Render(a) + if err != nil { + xlog.Error("Error rendering prompt", "error", err) + continue + } + if message == "" { + xlog.Debug("Prompt is empty, skipping", "agent", a.Character.Name) + continue + } if !Messages(a.currentConversation).Exist(a.options.systemPrompt) { a.currentConversation = append([]openai.ChatCompletionMessage{ { diff --git a/core/agent/jobs.go b/core/agent/jobs.go index 5d5c4df..a3b5086 100644 --- a/core/agent/jobs.go +++ b/core/agent/jobs.go @@ -25,6 +25,7 @@ type JobResult struct { // The result of a job State []ActionState Conversation []openai.ChatCompletionMessage + Response string Error error ready chan bool diff --git a/core/agent/options.go b/core/agent/options.go index ed90240..2971a97 100644 --- a/core/agent/options.go +++ b/core/agent/options.go @@ -42,11 +42,6 @@ type options struct { resultCallback func(ActionState) } -type PromptBlock interface { - Render(a *Agent) string - Role() string -} - func defaultOptions() *options { return &options{ periodicRuns: 15 * time.Minute, @@ -182,6 +177,22 @@ func WithPrompts(prompts ...PromptBlock) Option { } } +// WithDynamicPrompts is a helper function to create dynamic prompts +// Dynamic prompts contains golang code which is executed dynamically +// // to render a prompt to the LLM +// func WithDynamicPrompts(prompts ...map[string]string) Option { +// return func(o *options) error { +// for _, p := range prompts { +// prompt, err := NewDynamicPrompt(p, "") +// if err != nil { +// return err +// } +// o.prompts = append(o.prompts, prompt) +// } +// return nil +// } +// } + func WithLLMAPIKey(key string) Option { return func(o *options) error { o.LLMAPI.APIKey = key diff --git a/core/agent/prompt.go b/core/agent/prompt.go new file mode 100644 index 0000000..f1a68ed --- /dev/null +++ b/core/agent/prompt.go @@ -0,0 +1,101 @@ +package agent + +import ( + "fmt" + "strings" + + "github.com/mudler/LocalAgent/pkg/xlog" + "github.com/traefik/yaegi/interp" + "github.com/traefik/yaegi/stdlib" +) + +type PromptBlock interface { + Render(a *Agent) (string, error) + Role() string +} + +type DynamicPrompt struct { + config map[string]string + goPkgPath string + i *interp.Interpreter +} + +func NewDynamicPrompt(config map[string]string, goPkgPath string) (*DynamicPrompt, error) { + a := &DynamicPrompt{ + config: config, + goPkgPath: goPkgPath, + } + + if err := a.initializeInterpreter(); err != nil { + return nil, err + } + + if err := a.callInit(); err != nil { + xlog.Error("Error calling custom action init", "error", err) + } + + return a, nil +} + +func (a *DynamicPrompt) callInit() error { + if a.i == nil { + return nil + } + + v, err := a.i.Eval(fmt.Sprintf("%s.Init", a.config["name"])) + if err != nil { + return err + } + + run := v.Interface().(func() error) + + return run() +} + +func (a *DynamicPrompt) initializeInterpreter() error { + if _, exists := a.config["code"]; exists && a.i == nil { + unsafe := strings.ToLower(a.config["unsafe"]) == "true" + i := interp.New(interp.Options{ + GoPath: a.goPkgPath, + Unrestricted: unsafe, + }) + if err := i.Use(stdlib.Symbols); err != nil { + return err + } + + if _, exists := a.config["name"]; !exists { + a.config["name"] = "custom" + } + + _, err := i.Eval(fmt.Sprintf("package %s\n%s", a.config["name"], a.config["code"])) + if err != nil { + return err + } + + a.i = i + } + + return nil +} + +func (a *DynamicPrompt) Render(c *Agent) (string, error) { + v, err := a.i.Eval(fmt.Sprintf("%s.Render", a.config["name"])) + if err != nil { + return "", err + } + + run := v.Interface().(func() (string, error)) + + return run() +} + +func (a *DynamicPrompt) Role() string { + v, err := a.i.Eval(fmt.Sprintf("%s.Role", a.config["name"])) + if err != nil { + return "system" + } + + run := v.Interface().(func() string) + + return run() +} diff --git a/core/state/config.go b/core/state/config.go index bcb2df1..6db904b 100644 --- a/core/state/config.go +++ b/core/state/config.go @@ -1,6 +1,8 @@ package state import ( + "encoding/json" + "github.com/mudler/LocalAgent/core/agent" ) @@ -14,9 +16,22 @@ type ActionsConfig struct { Config string `json:"config"` } +type PromptBlocksConfig struct { + Type string `json:"type"` + Config string `json:"config"` +} + +func (d PromptBlocksConfig) ToMap() map[string]string { + config := map[string]string{} + json.Unmarshal([]byte(d.Config), &config) + return config +} + type AgentConfig struct { - Connector []ConnectorConfig `json:"connectors" form:"connectors" ` - Actions []ActionsConfig `json:"actions" form:"actions"` + Connector []ConnectorConfig `json:"connectors" form:"connectors" ` + Actions []ActionsConfig `json:"actions" form:"actions"` + PromptBlocks []PromptBlocksConfig `json:"promptblocks" form:"promptblocks"` + // This is what needs to be part of ActionsConfig Model string `json:"model" form:"model"` Name string `json:"name" form:"name"` diff --git a/core/state/pool.go b/core/state/pool.go index bebb941..b5ffbf9 100644 --- a/core/state/pool.go +++ b/core/state/pool.go @@ -30,6 +30,7 @@ type AgentPool struct { apiURL, model, localRAGAPI, apiKey string availableActions func(*AgentConfig) func(ctx context.Context) []Action connectors func(*AgentConfig) []Connector + promptBlocks func(*AgentConfig) []PromptBlock timeout string } @@ -68,6 +69,7 @@ func NewAgentPool( LocalRAGAPI string, availableActions func(*AgentConfig) func(ctx context.Context) []agent.Action, connectors func(*AgentConfig) []Connector, + promptBlocks func(*AgentConfig) []PromptBlock, timeout string, ) (*AgentPool, error) { // if file exists, try to load an existing pool. @@ -160,6 +162,7 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error } connectors := a.connectors(config) + promptBlocks := a.promptBlocks(config) actions := a.availableActions(config)(ctx) @@ -183,12 +186,19 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error "connectors", connectorLog, ) + // dynamicPrompts := []map[string]string{} + // for _, p := range config.DynamicPrompts { + // dynamicPrompts = append(dynamicPrompts, p.ToMap()) + // } + opts := []Option{ WithModel(model), WithLLMAPIURL(a.apiURL), WithContext(ctx), WithPeriodicRuns(config.PeriodicRuns), WithPermanentGoal(config.PermanentGoal), + WithPrompts(promptBlocks...), + // WithDynamicPrompts(dynamicPrompts...), WithCharacter(Character{ Name: name, }), @@ -313,6 +323,7 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error } }() + xlog.Info("Starting connectors", "name", name, "config", config) for _, c := range connectors { go c.Start(agent) } diff --git a/main.go b/main.go index 1bbd82e..daac218 100644 --- a/main.go +++ b/main.go @@ -41,7 +41,17 @@ func main() { os.MkdirAll(stateDir, 0755) // Create the agent pool - pool, err := state.NewAgentPool(testModel, apiURL, apiKey, stateDir, localRAG, webui.Actions, webui.Connectors, timeout) + pool, err := state.NewAgentPool( + testModel, + apiURL, + apiKey, + stateDir, + localRAG, + webui.Actions, + webui.Connectors, + webui.PromptBlocks, + timeout, + ) if err != nil { panic(err) } diff --git a/webui/prompts.go b/webui/prompts.go new file mode 100644 index 0000000..cbb9e42 --- /dev/null +++ b/webui/prompts.go @@ -0,0 +1,41 @@ +package webui + +import ( + "encoding/json" + + "github.com/mudler/LocalAgent/pkg/xlog" + + "github.com/mudler/LocalAgent/core/agent" + "github.com/mudler/LocalAgent/core/state" +) + +const ( + // Connectors + DynamicPromptCustom = "custom" +) + +var AvailableBlockPrompts = []string{ + DynamicPromptCustom, +} + +func PromptBlocks(a *state.AgentConfig) []agent.PromptBlock { + promptblocks := []agent.PromptBlock{} + + for _, c := range a.PromptBlocks { + var config map[string]string + if err := json.Unmarshal([]byte(c.Config), &config); err != nil { + xlog.Info("Error unmarshalling connector config", err) + continue + } + switch c.Type { + case DynamicPromptCustom: + prompt, err := agent.NewDynamicPrompt(config, "") + if err != nil { + xlog.Error("Error creating custom prompt", "error", err) + continue + } + promptblocks = append(promptblocks, prompt) + } + } + return promptblocks +} diff --git a/webui/routes.go b/webui/routes.go index 803d071..48075d5 100644 --- a/webui/routes.go +++ b/webui/routes.go @@ -45,8 +45,9 @@ func (app *App) registerRoutes(pool *state.AgentPool, webapp *fiber.App) { webapp.Get("/create", func(c *fiber.Ctx) error { return c.Render("views/create", fiber.Map{ - "Actions": AvailableActions, - "Connectors": AvailableConnectors, + "Actions": AvailableActions, + "Connectors": AvailableConnectors, + "PromptBlocks": AvailableBlockPrompts, }) }) diff --git a/webui/views/create.html b/webui/views/create.html index 87abf4d..a3ec522 100644 --- a/webui/views/create.html +++ b/webui/views/create.html @@ -55,7 +55,6 @@ + +
+
+ + +