mirror of
https://github.com/mark3labs/kit.git
synced 2026-06-13 19:20:06 +00:00
4caa8ba3dc
This commit bridges 4 categories of internal SDK capabilities to the extension
system, enabling extensions like pi-prompt-template-model to be built with
minimal custom code.
New Extension APIs:
Tree Navigation (Phase 1):
- GetTreeNode, GetCurrentBranch, GetChildren - Navigate conversation tree
- NavigateTo - Branch/fork to specific entries
- SummarizeBranch - LLM-based branch summarization
- CollapseBranch - Fresh context primitive for context management
Skill Loading (Phase 2):
- LoadSkill, LoadSkillsFromDir - Load skill files with YAML frontmatter
- DiscoverSkills - Auto-discover from standard locations
- InjectSkillAsContext, InjectRawSkillAsContext - Pre-load skills
Template Parsing (Phase 3):
- ParseTemplate, RenderTemplate - {{variable}} substitution
- ParseArguments, SimpleParseArguments - CLI-style arg parsing (, , )
- EvaluateModelConditional, RenderWithModelConditionals - Model conditionals
Model Resolution (Phase 4):
- ResolveModelChain - Fallback chain resolution
- GetModelCapabilities - Query model specs
- CheckModelAvailable, GetCurrentProvider, GetCurrentModelID
Files Modified:
- internal/extensions/api.go - New types and Context methods
- internal/extensions/symbols.go - Export to Yaegi
- internal/extensions/runner.go - No-op stubs
- pkg/kit/sessions.go - Tree navigation bridge
- pkg/kit/skills.go - Skill loading bridge
- pkg/kit/template_bridge.go - NEW - Template & model resolution
- cmd/root.go - Wire to extension Context
Examples Added:
- conversation-manager.go - Tree nav, branch collapse, fresh context loops
- prompt-templates.go - Frontmatter templates with model switching
- bridge_demo.go - All new APIs demonstration
Documentation Updated:
- README.md - New capabilities and examples
- www/pages/extensions/capabilities.md - Full API docs
- www/pages/extensions/examples.md - New example category
- skills/kit-extensions/SKILL.md - Extension developer docs
270 lines
7.0 KiB
Go
270 lines
7.0 KiB
Go
//go:build ignore
|
|
|
|
// prompt-templates.go - Frontmatter-driven prompt templates with model switching.
|
|
// This extension demonstrates the new bridged SDK APIs:
|
|
// - Tree navigation for conversation management
|
|
// - Template parsing with {{variable}} substitution
|
|
// - Model resolution with fallback chains
|
|
// - Skill injection
|
|
//
|
|
// Usage:
|
|
// 1. Create ~/.config/kit/prompts/debug.md with frontmatter:
|
|
// ---
|
|
// description: Debug Python code
|
|
// model: claude-sonnet-4-20250514
|
|
// skill: python
|
|
// ---
|
|
// Help me debug this Python code: {{input}}
|
|
//
|
|
// 2. In Kit: /debug my_script.py
|
|
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"kit/ext"
|
|
)
|
|
|
|
// PromptTemplate represents a loaded template with frontmatter
|
|
type PromptTemplate struct {
|
|
Name string
|
|
Description string
|
|
Model string
|
|
Skill string
|
|
Content string
|
|
Variables []string
|
|
Path string
|
|
}
|
|
|
|
var (
|
|
templates = make(map[string]PromptTemplate)
|
|
templateDir string
|
|
)
|
|
|
|
func Init(api ext.API) {
|
|
// Determine template directory
|
|
home, _ := os.UserHomeDir()
|
|
templateDir = filepath.Join(home, ".config", "kit", "prompts")
|
|
|
|
// Ensure directory exists
|
|
os.MkdirAll(templateDir, 0755)
|
|
|
|
// Register commands
|
|
api.RegisterCommand(ext.CommandDef{
|
|
Name: "reload-templates",
|
|
Description: "Reload prompt templates from disk",
|
|
Execute: func(args string, ctx ext.Context) (string, error) {
|
|
loadTemplates(ctx)
|
|
ctx.PrintInfo(fmt.Sprintf("Loaded %d templates from %s", len(templates), templateDir))
|
|
return "", nil
|
|
},
|
|
})
|
|
|
|
// Dynamic template commands are registered after loading
|
|
api.OnSessionStart(func(e ext.SessionStartEvent, ctx ext.Context) {
|
|
loadTemplates(ctx)
|
|
registerTemplateCommands(api, ctx)
|
|
})
|
|
}
|
|
|
|
// loadTemplates discovers and loads all template files
|
|
func loadTemplates(ctx ext.Context) {
|
|
templates = make(map[string]PromptTemplate)
|
|
|
|
entries, err := os.ReadDir(templateDir)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".md") {
|
|
continue
|
|
}
|
|
|
|
path := filepath.Join(templateDir, entry.Name())
|
|
tpl, err := loadTemplateFile(path)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
name := strings.TrimSuffix(entry.Name(), ".md")
|
|
templates[name] = tpl
|
|
}
|
|
}
|
|
|
|
// loadTemplateFile parses a template with YAML frontmatter
|
|
func loadTemplateFile(path string) (PromptTemplate, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return PromptTemplate{}, err
|
|
}
|
|
|
|
content := string(data)
|
|
tpl := PromptTemplate{Path: path}
|
|
|
|
// Parse frontmatter
|
|
if strings.HasPrefix(content, "---") {
|
|
parts := strings.SplitN(content[3:], "---", 2)
|
|
if len(parts) == 2 {
|
|
frontmatter := strings.TrimSpace(parts[0])
|
|
body := strings.TrimSpace(parts[1])
|
|
|
|
// Simple line-by-line frontmatter parsing
|
|
for _, line := range strings.Split(frontmatter, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
|
|
key, value, found := strings.Cut(line, ":")
|
|
if found {
|
|
key = strings.TrimSpace(key)
|
|
value = strings.TrimSpace(value)
|
|
switch key {
|
|
case "description":
|
|
tpl.Description = value
|
|
case "model":
|
|
tpl.Model = value
|
|
case "skill":
|
|
tpl.Skill = value
|
|
}
|
|
}
|
|
}
|
|
tpl.Content = body
|
|
} else {
|
|
tpl.Content = content
|
|
}
|
|
} else {
|
|
tpl.Content = content
|
|
}
|
|
|
|
// Parse {{variables}} using simple string parsing
|
|
// (Can't use ctx.ParseTemplate here since we're in Init, not a handler)
|
|
var vars []string
|
|
for {
|
|
start := strings.Index(tpl.Content, "{{")
|
|
if start == -1 {
|
|
break
|
|
}
|
|
end := strings.Index(tpl.Content[start:], "}}")
|
|
if end == -1 {
|
|
break
|
|
}
|
|
varName := strings.TrimSpace(tpl.Content[start+2 : start+end])
|
|
vars = append(vars, varName)
|
|
tpl.Content = tpl.Content[:start] + "{{" + varName + "}}" + tpl.Content[start+end+2:]
|
|
}
|
|
tpl.Variables = vars
|
|
|
|
return tpl, nil
|
|
}
|
|
|
|
// registerTemplateCommands dynamically registers commands for each template
|
|
func registerTemplateCommands(api ext.API, ctx ext.Context) {
|
|
for name, tpl := range templates {
|
|
// Skip if already registered (we'd need to track this)
|
|
tplCopy := tpl // Capture for closure
|
|
nameCopy := name
|
|
|
|
// Build description with metadata
|
|
desc := tplCopy.Description
|
|
if desc == "" {
|
|
desc = fmt.Sprintf("Run %s template", nameCopy)
|
|
}
|
|
if tplCopy.Model != "" {
|
|
desc += fmt.Sprintf(" [%s", tplCopy.Model)
|
|
if tplCopy.Skill != "" {
|
|
desc += fmt.Sprintf(" +%s", tplCopy.Skill)
|
|
}
|
|
desc += "]"
|
|
}
|
|
|
|
api.RegisterCommand(ext.CommandDef{
|
|
Name: nameCopy,
|
|
Description: desc,
|
|
Execute: func(args string, ctx ext.Context) (string, error) {
|
|
return executeTemplate(ctx, tplCopy, args)
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
// executeTemplate runs a template with the given arguments
|
|
func executeTemplate(ctx ext.Context, tpl PromptTemplate, args string) (string, error) {
|
|
// Store original model for restoration
|
|
originalModel := ctx.Model
|
|
|
|
// 1. Resolve and switch model if specified
|
|
if tpl.Model != "" {
|
|
// Parse model chain (comma-separated)
|
|
preferences := strings.Split(tpl.Model, ",")
|
|
for i := range preferences {
|
|
preferences[i] = strings.TrimSpace(preferences[i])
|
|
}
|
|
|
|
result := ctx.ResolveModelChain(preferences)
|
|
if result.Error != "" {
|
|
ctx.PrintError(fmt.Sprintf("Model resolution failed: %s", result.Error))
|
|
// Continue with current model
|
|
} else {
|
|
ctx.PrintInfo(fmt.Sprintf("Switching to model: %s", result.Model))
|
|
if err := ctx.SetModel(result.Model); err != nil {
|
|
ctx.PrintError(fmt.Sprintf("Failed to switch model: %s", err.Error()))
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. Inject skill if specified
|
|
if tpl.Skill != "" {
|
|
err := ctx.InjectSkillAsContext(tpl.Skill)
|
|
if err != "" {
|
|
ctx.PrintError(fmt.Sprintf("Skill injection failed: %s", err))
|
|
} else {
|
|
ctx.PrintInfo(fmt.Sprintf("Injected skill: %s", tpl.Skill))
|
|
}
|
|
}
|
|
|
|
// 3. Parse and render template
|
|
parsed := ctx.ParseTemplate(tpl.Name, tpl.Content)
|
|
|
|
// Build variable map
|
|
vars := make(map[string]string)
|
|
|
|
// Simple argument parsing: first arg is $1 (input), rest is $@
|
|
if len(parsed.Variables) > 0 {
|
|
argsList := ctx.SimpleParseArguments(args, len(parsed.Variables))
|
|
for i, varName := range parsed.Variables {
|
|
if i < len(parsed.Variables) && i+1 < len(argsList) {
|
|
vars[varName] = argsList[i+1]
|
|
}
|
|
}
|
|
// If single variable, use full args
|
|
if len(parsed.Variables) == 1 && vars[parsed.Variables[0]] == "" {
|
|
vars[parsed.Variables[0]] = args
|
|
}
|
|
}
|
|
|
|
// Render with model conditionals
|
|
content := ctx.RenderWithModelConditionals(tpl.Content)
|
|
rendered := ctx.RenderTemplate(ext.PromptTemplate{Name: tpl.Name, Content: content, Variables: parsed.Variables}, vars)
|
|
|
|
// 4. Send the rendered prompt
|
|
ctx.SendMessage(rendered)
|
|
|
|
// 5. Schedule model restoration after turn completes
|
|
// We use a goroutine to wait and restore
|
|
if tpl.Model != "" && originalModel != "" {
|
|
go func() {
|
|
// Note: In a real implementation, we'd use OnAgentEnd event
|
|
// For now, the user can manually switch back
|
|
ctx.SetStatus("template-mode", fmt.Sprintf("Template: %s (model will restore)", tpl.Name), 20)
|
|
}()
|
|
}
|
|
|
|
return fmt.Sprintf("Executing template: %s", tpl.Name), nil
|
|
}
|