mirror of
https://github.com/mudler/LocalAGI.git
synced 2026-07-23 10:45:41 -04:00
feat: add inner monologue template support for scheduler and template-based skills prompt (#435)
* feat: add scheduler task template and template-based skills prompt
- Add WithSchedulerTaskTemplate option for recurring tasks run by scheduler
- Expose scheduler_task_template in core/state/config.go for user configuration
- Add WithSkillPromptTemplate option for custom skill prompt templates
- Use {{.Skills}} slice in templates to iterate over Skill.Name, Skill.Description
- Default template mimics current XML behavior with <available_skills> format
- Rename customIntro to customTemplate in services/skills for consistency
* cleanups
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* Update core/state/config.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---------
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
+20
-1
@@ -71,8 +71,10 @@ type options struct {
|
||||
|
||||
prompts []DynamicPrompt
|
||||
|
||||
systemPrompt string
|
||||
systemPrompt string
|
||||
innerMonologueTemplate string
|
||||
skillPromptTemplate string
|
||||
schedulerTaskTemplate string
|
||||
|
||||
// callbacks
|
||||
reasoningCallback func(types.ActionCurrentState) bool
|
||||
@@ -319,6 +321,14 @@ func WithInnerMonologueTemplate(template string) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithSkillPromptTemplate sets the template for rendering skills in the prompt. If empty, the default template is used.
|
||||
func WithSkillPromptTemplate(template string) Option {
|
||||
return func(o *options) error {
|
||||
o.skillPromptTemplate = template
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithMCPServers(servers ...MCPServer) Option {
|
||||
return func(o *options) error {
|
||||
o.mcpServers = servers
|
||||
@@ -569,3 +579,12 @@ func WithSchedulerStorePath(path string) Option {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithSchedulerTaskTemplate sets the prompt used for scheduled/recurring tasks run by the scheduler.
|
||||
// If empty, the default inner monologue template is used with the task injected.
|
||||
func WithSchedulerTaskTemplate(template string) Option {
|
||||
return func(o *options) error {
|
||||
o.schedulerTaskTemplate = template
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,29 @@ type agentSchedulerExecutor struct {
|
||||
|
||||
// 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
|
||||
// Render the scheduler task template - if custom template is set, it will include {{.Task}}
|
||||
// If no custom scheduler template is set, fall back to default inner monologue template
|
||||
innerMonologue := 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)
|
||||
|
||||
if e.agent.options.schedulerTaskTemplate != "" {
|
||||
tmpl, err := templateBase("taskTemplate", e.agent.options.schedulerTaskTemplate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to render scheduler task template: %w", err)
|
||||
}
|
||||
|
||||
innerMonologue, err = templateExecute(tmpl, struct {
|
||||
Task string
|
||||
}{
|
||||
Task: prompt,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to render scheduler task template: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a job for the reminder with the rendered inner monologue
|
||||
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.WithText(innerMonologue),
|
||||
types.WithReasoningCallback(e.agent.options.reasoningCallback),
|
||||
types.WithResultCallback(e.agent.options.resultCallback),
|
||||
types.WithContext(ctx),
|
||||
|
||||
@@ -85,6 +85,7 @@ type AgentConfig struct {
|
||||
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"`
|
||||
SchedulerTaskTemplate string `json:"scheduler_task_template" form:"scheduler_task_template"`
|
||||
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"`
|
||||
@@ -352,6 +353,14 @@ func NewAgentConfigMeta(
|
||||
HelpText: "Prompt used for periodic/standalone runs when the agent evaluates what to do next. If empty, the default autonomous agent instructions are used.",
|
||||
Tags: config.Tags{Section: "PromptsGoals"},
|
||||
},
|
||||
{
|
||||
Name: "scheduler_task_template",
|
||||
Label: "Scheduler Task Template",
|
||||
Type: "textarea",
|
||||
DefaultValue: "",
|
||||
HelpText: "Template for scheduled/recurring tasks. Use {{.Task}} to reference the task. Example: \"Execute: {{.Task}}\". If empty, the default inner monologue template is used with the task injected.",
|
||||
Tags: config.Tags{Section: "PromptsGoals"},
|
||||
},
|
||||
{
|
||||
Name: "standalone_job",
|
||||
Label: "Standalone Job",
|
||||
|
||||
@@ -441,6 +441,7 @@ func (a *AgentPool) startAgentWithConfig(name, pooldir string, config *AgentConf
|
||||
}),
|
||||
WithSystemPrompt(config.SystemPrompt),
|
||||
WithInnerMonologueTemplate(config.InnerMonologueTemplate),
|
||||
WithSchedulerTaskTemplate(config.SchedulerTaskTemplate),
|
||||
WithMultimodalModel(multimodalModel),
|
||||
WithLastMessageDuration(config.LastMessageDuration),
|
||||
WithAgentResultCallback(func(state types.ActionState) {
|
||||
|
||||
+64
-23
@@ -1,9 +1,12 @@
|
||||
package skills
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
"github.com/mudler/LocalAGI/core/agent"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
|
||||
@@ -12,16 +15,34 @@ import (
|
||||
|
||||
const defaultSkillsIntro = "You can use the following skills to help with the task.\nTo request the skill, you need to use the `request_skill` tool. The skill name is the name of the skill you want to use.\n"
|
||||
|
||||
// skillsPrompt implements agent.DynamicPrompt and injects the available skills XML block
|
||||
type skillsPrompt struct {
|
||||
listSkills func() ([]skilldomain.Skill, error)
|
||||
customIntro string
|
||||
// defaultSkillsTemplate is the default template that mimics the current XML behavior
|
||||
const defaultSkillsTemplate = defaultSkillsIntro + `<available_skills>
|
||||
{{range .Skills}}
|
||||
<skill>
|
||||
<name>{{escapeXML .Name}}</name>
|
||||
<description>{{escapeXML .Description}}</description>
|
||||
</skill>
|
||||
{{end}}
|
||||
</available_skills>`
|
||||
|
||||
// Skill is a local representation of a skill for template rendering
|
||||
type Skill struct {
|
||||
Name string
|
||||
Description string
|
||||
ID string
|
||||
}
|
||||
|
||||
// NewSkillsPrompt returns a DynamicPrompt that renders the list of available skills as XML.
|
||||
// If customIntro is non-empty, it is used as the intro before the skills list; otherwise the default intro is used.
|
||||
func NewSkillsPrompt(listSkills func() ([]skilldomain.Skill, error), customIntro string) agent.DynamicPrompt {
|
||||
return &skillsPrompt{listSkills: listSkills, customIntro: customIntro}
|
||||
// skillsPrompt implements agent.DynamicPrompt and injects the available skills XML block
|
||||
type skillsPrompt struct {
|
||||
listSkills func() ([]skilldomain.Skill, error)
|
||||
customTemplate string
|
||||
}
|
||||
|
||||
// NewSkillsPrompt returns a DynamicPrompt that renders the list of available skills.
|
||||
// If customTemplate is non-empty, it is used as a template with {{.Skills}} slice.
|
||||
// Otherwise, the default template is used (mimics current XML behavior).
|
||||
func NewSkillsPrompt(listSkills func() ([]skilldomain.Skill, error), customTemplate string) agent.DynamicPrompt {
|
||||
return &skillsPrompt{listSkills: listSkills, customTemplate: customTemplate}
|
||||
}
|
||||
|
||||
func (p *skillsPrompt) Render(a *agent.Agent) (types.PromptResult, error) {
|
||||
@@ -29,26 +50,46 @@ func (p *skillsPrompt) Render(a *agent.Agent) (types.PromptResult, error) {
|
||||
if err != nil {
|
||||
return types.PromptResult{}, err
|
||||
}
|
||||
var sb strings.Builder
|
||||
intro := defaultSkillsIntro
|
||||
if p.customIntro != "" {
|
||||
intro = strings.TrimSpace(p.customIntro) + "\n"
|
||||
}
|
||||
sb.WriteString(intro)
|
||||
sb.WriteString("<available_skills>\n")
|
||||
for _, s := range skills {
|
||||
name := s.ID
|
||||
|
||||
// Convert skilldomain.Skill to local Skill type for template rendering
|
||||
localSkills := make([]Skill, len(skills))
|
||||
for i, s := range skills {
|
||||
desc := ""
|
||||
if s.Metadata != nil && s.Metadata.Description != "" {
|
||||
desc = s.Metadata.Description
|
||||
}
|
||||
sb.WriteString(" <skill>\n")
|
||||
sb.WriteString(fmt.Sprintf(" <name>%s</name>\n", escapeXML(name)))
|
||||
sb.WriteString(fmt.Sprintf(" <description>%s</description>\n", escapeXML(desc)))
|
||||
sb.WriteString(" </skill>\n")
|
||||
localSkills[i] = Skill{
|
||||
Name: s.ID,
|
||||
Description: desc,
|
||||
ID: s.ID,
|
||||
}
|
||||
}
|
||||
sb.WriteString("</available_skills>")
|
||||
return types.PromptResult{Content: sb.String()}, nil
|
||||
|
||||
// Use custom template or default
|
||||
templ := p.customTemplate
|
||||
if templ == "" {
|
||||
templ = defaultSkillsTemplate
|
||||
}
|
||||
|
||||
// Parse and execute the template
|
||||
tmpl, err := template.New("skillsPrompt").Funcs(template.FuncMap{
|
||||
"escapeXML": escapeXML,
|
||||
}).Funcs(sprig.FuncMap()).Parse(templ)
|
||||
if err != nil {
|
||||
return types.PromptResult{}, fmt.Errorf("failed to parse skills template: %w", err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = tmpl.Execute(&buf, struct {
|
||||
Skills []Skill
|
||||
}{
|
||||
Skills: localSkills,
|
||||
})
|
||||
if err != nil {
|
||||
return types.PromptResult{}, fmt.Errorf("failed to execute skills template: %w", err)
|
||||
}
|
||||
|
||||
return types.PromptResult{Content: buf.String()}, nil
|
||||
}
|
||||
|
||||
func (p *skillsPrompt) Role() string {
|
||||
|
||||
@@ -135,11 +135,11 @@ func (s *Service) GetSkillsPrompt(config *state.AgentConfig) (agent.DynamicPromp
|
||||
if err != nil || mgr == nil {
|
||||
return nil, err
|
||||
}
|
||||
customIntro := ""
|
||||
customTemplate := ""
|
||||
if config != nil && config.SkillsPrompt != "" {
|
||||
customIntro = config.SkillsPrompt
|
||||
customTemplate = config.SkillsPrompt
|
||||
}
|
||||
return NewSkillsPrompt(mgr.ListSkills, customIntro), nil
|
||||
return NewSkillsPrompt(mgr.ListSkills, customTemplate), nil
|
||||
}
|
||||
|
||||
// GetMCPSession returns a shared MCP client session connected to the in-process skillserver (starts on first use)
|
||||
|
||||
Reference in New Issue
Block a user