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:
LocalAI [bot]
2026-02-25 18:44:49 +01:00
committed by GitHub
parent 56ecd1921e
commit cb82322df2
6 changed files with 119 additions and 29 deletions
+20 -1
View File
@@ -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
}
}
+22 -2
View File
@@ -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),
+9
View File
@@ -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",
+1
View File
@@ -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
View File
@@ -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 {
+3 -3
View File
@@ -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)