Allow to specify dynamic prompts
This commit is contained in:
@@ -379,7 +379,15 @@ func (a *Agent) consumeJob(job *Job, role string) {
|
|||||||
//}
|
//}
|
||||||
// Add custom prompts
|
// Add custom prompts
|
||||||
for _, prompt := range a.options.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) {
|
if !Messages(a.currentConversation).Exist(a.options.systemPrompt) {
|
||||||
a.currentConversation = append([]openai.ChatCompletionMessage{
|
a.currentConversation = append([]openai.ChatCompletionMessage{
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ type JobResult struct {
|
|||||||
// The result of a job
|
// The result of a job
|
||||||
State []ActionState
|
State []ActionState
|
||||||
Conversation []openai.ChatCompletionMessage
|
Conversation []openai.ChatCompletionMessage
|
||||||
|
|
||||||
Response string
|
Response string
|
||||||
Error error
|
Error error
|
||||||
ready chan bool
|
ready chan bool
|
||||||
|
|||||||
@@ -42,11 +42,6 @@ type options struct {
|
|||||||
resultCallback func(ActionState)
|
resultCallback func(ActionState)
|
||||||
}
|
}
|
||||||
|
|
||||||
type PromptBlock interface {
|
|
||||||
Render(a *Agent) string
|
|
||||||
Role() string
|
|
||||||
}
|
|
||||||
|
|
||||||
func defaultOptions() *options {
|
func defaultOptions() *options {
|
||||||
return &options{
|
return &options{
|
||||||
periodicRuns: 15 * time.Minute,
|
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 {
|
func WithLLMAPIKey(key string) Option {
|
||||||
return func(o *options) error {
|
return func(o *options) error {
|
||||||
o.LLMAPI.APIKey = key
|
o.LLMAPI.APIKey = key
|
||||||
|
|||||||
101
core/agent/prompt.go
Normal file
101
core/agent/prompt.go
Normal file
@@ -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()
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package state
|
package state
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
"github.com/mudler/LocalAgent/core/agent"
|
"github.com/mudler/LocalAgent/core/agent"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,9 +16,22 @@ type ActionsConfig struct {
|
|||||||
Config string `json:"config"`
|
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 {
|
type AgentConfig struct {
|
||||||
Connector []ConnectorConfig `json:"connectors" form:"connectors" `
|
Connector []ConnectorConfig `json:"connectors" form:"connectors" `
|
||||||
Actions []ActionsConfig `json:"actions" form:"actions"`
|
Actions []ActionsConfig `json:"actions" form:"actions"`
|
||||||
|
PromptBlocks []PromptBlocksConfig `json:"promptblocks" form:"promptblocks"`
|
||||||
|
|
||||||
// This is what needs to be part of ActionsConfig
|
// This is what needs to be part of ActionsConfig
|
||||||
Model string `json:"model" form:"model"`
|
Model string `json:"model" form:"model"`
|
||||||
Name string `json:"name" form:"name"`
|
Name string `json:"name" form:"name"`
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ type AgentPool struct {
|
|||||||
apiURL, model, localRAGAPI, apiKey string
|
apiURL, model, localRAGAPI, apiKey string
|
||||||
availableActions func(*AgentConfig) func(ctx context.Context) []Action
|
availableActions func(*AgentConfig) func(ctx context.Context) []Action
|
||||||
connectors func(*AgentConfig) []Connector
|
connectors func(*AgentConfig) []Connector
|
||||||
|
promptBlocks func(*AgentConfig) []PromptBlock
|
||||||
timeout string
|
timeout string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +69,7 @@ func NewAgentPool(
|
|||||||
LocalRAGAPI string,
|
LocalRAGAPI string,
|
||||||
availableActions func(*AgentConfig) func(ctx context.Context) []agent.Action,
|
availableActions func(*AgentConfig) func(ctx context.Context) []agent.Action,
|
||||||
connectors func(*AgentConfig) []Connector,
|
connectors func(*AgentConfig) []Connector,
|
||||||
|
promptBlocks func(*AgentConfig) []PromptBlock,
|
||||||
timeout string,
|
timeout string,
|
||||||
) (*AgentPool, error) {
|
) (*AgentPool, error) {
|
||||||
// if file exists, try to load an existing pool.
|
// 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)
|
connectors := a.connectors(config)
|
||||||
|
promptBlocks := a.promptBlocks(config)
|
||||||
|
|
||||||
actions := a.availableActions(config)(ctx)
|
actions := a.availableActions(config)(ctx)
|
||||||
|
|
||||||
@@ -183,12 +186,19 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
|
|||||||
"connectors", connectorLog,
|
"connectors", connectorLog,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// dynamicPrompts := []map[string]string{}
|
||||||
|
// for _, p := range config.DynamicPrompts {
|
||||||
|
// dynamicPrompts = append(dynamicPrompts, p.ToMap())
|
||||||
|
// }
|
||||||
|
|
||||||
opts := []Option{
|
opts := []Option{
|
||||||
WithModel(model),
|
WithModel(model),
|
||||||
WithLLMAPIURL(a.apiURL),
|
WithLLMAPIURL(a.apiURL),
|
||||||
WithContext(ctx),
|
WithContext(ctx),
|
||||||
WithPeriodicRuns(config.PeriodicRuns),
|
WithPeriodicRuns(config.PeriodicRuns),
|
||||||
WithPermanentGoal(config.PermanentGoal),
|
WithPermanentGoal(config.PermanentGoal),
|
||||||
|
WithPrompts(promptBlocks...),
|
||||||
|
// WithDynamicPrompts(dynamicPrompts...),
|
||||||
WithCharacter(Character{
|
WithCharacter(Character{
|
||||||
Name: name,
|
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 {
|
for _, c := range connectors {
|
||||||
go c.Start(agent)
|
go c.Start(agent)
|
||||||
}
|
}
|
||||||
|
|||||||
12
main.go
12
main.go
@@ -41,7 +41,17 @@ func main() {
|
|||||||
os.MkdirAll(stateDir, 0755)
|
os.MkdirAll(stateDir, 0755)
|
||||||
|
|
||||||
// Create the agent pool
|
// 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 {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|||||||
41
webui/prompts.go
Normal file
41
webui/prompts.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -45,8 +45,9 @@ func (app *App) registerRoutes(pool *state.AgentPool, webapp *fiber.App) {
|
|||||||
|
|
||||||
webapp.Get("/create", func(c *fiber.Ctx) error {
|
webapp.Get("/create", func(c *fiber.Ctx) error {
|
||||||
return c.Render("views/create", fiber.Map{
|
return c.Render("views/create", fiber.Map{
|
||||||
"Actions": AvailableActions,
|
"Actions": AvailableActions,
|
||||||
"Connectors": AvailableConnectors,
|
"Connectors": AvailableConnectors,
|
||||||
|
"PromptBlocks": AvailableBlockPrompts,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,6 @@
|
|||||||
<button id="action_button" type="button" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-500 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">Add action</button>
|
<button id="action_button" type="button" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-500 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">Add action</button>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
document.getElementById('action_button').addEventListener('click', function() {
|
document.getElementById('action_button').addEventListener('click', function() {
|
||||||
const actionsSection = document.getElementById('action_box');
|
const actionsSection = document.getElementById('action_box');
|
||||||
const ii = actionsSection.getElementsByClassName('action').length;
|
const ii = actionsSection.getElementsByClassName('action').length;
|
||||||
@@ -74,8 +73,34 @@
|
|||||||
|
|
||||||
actionsSection.insertAdjacentHTML('beforeend', newActionHTML);
|
actionsSection.insertAdjacentHTML('beforeend', newActionHTML);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div class="mb-4" id="dynamic_box">
|
||||||
|
</div>
|
||||||
|
<button id="dynamic_button" type="button" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-500 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">Add Dynamic prompt</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const promptBlocks = `{{ range .PromptBlocks }}<option value="{{.}}">{{.}}</option>{{ end }}`;
|
||||||
|
|
||||||
|
document.getElementById('dynamic_button').addEventListener('click', function() {
|
||||||
|
const actionsSection = document.getElementById('dynamic_box');
|
||||||
|
const ii = actionsSection.getElementsByClassName('promptBlock').length;
|
||||||
|
|
||||||
|
const newActionHTML = `
|
||||||
|
<div class="promptBlock mb-4">
|
||||||
|
<label for="promptName${ii}" class="block text-lg font-medium text-gray-400">Block Prompt</label>
|
||||||
|
<select name="promptblocks[${ii}].name" id="promptName${ii}" class="mt-1 focus:ring-blue-500 focus:border-blue-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md bg-gray-700 text-white">
|
||||||
|
`+promptBlocks+`</select>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="promptConfig${ii}" class="block text-lg font-medium text-gray-400">Prompt Config (JSON)</label>
|
||||||
|
<textarea id="promptConfig${ii}" name="promptblocks[${ii}].config" class="mt-1 focus:ring-blue-500 focus:border-blue-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md bg-gray-700 text-white" placeholder='{"results":"5"}'>{}</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
actionsSection.insertAdjacentHTML('beforeend', newActionHTML);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="hud" class="block text-lg font-medium text-gray-400">HUD</label>
|
<label for="hud" class="block text-lg font-medium text-gray-400">HUD</label>
|
||||||
|
|||||||
Reference in New Issue
Block a user