feat(ai-core): add LLM orchestration engine
- model: shared data types (Message, Intent, StreamToken, LLMMessage) - persona: YAML/Raw loading + system prompt injection - llm: OpenAI-compatible adapter with streaming + semantic cache - context: sliding window context store with token budgeting - subsession: parallel provider dispatch + review message parser - orchestrator: intent analysis pipeline (IoT/greeting/chat/memory) - background: autonomous thinking engine - 20+ tests, build + vet clean, ~130 total tests
This commit is contained in:
1 parent
99806fefad
commit
3b3e5a0697
9 files changed
+1529
No files matched your search
@@ -0,0 +1,185 @@
|
||||
// Package persona manages AI character loading and system prompt injection.
|
||||
package persona
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Persona defines an AI character's configuration.
|
||||
type Persona struct {
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Description string `yaml:"description" json:"description"`
|
||||
Greeting string `yaml:"greeting" json:"greeting"`
|
||||
Traits []string `yaml:"traits" json:"traits"`
|
||||
Style string `yaml:"style" json:"style"`
|
||||
Constraints []string `yaml:"constraints" json:"constraints"`
|
||||
|
||||
// IoT-specific personality
|
||||
IoTResponseStyle string `yaml:"iot_response_style" json:"iot_response_style,omitempty"`
|
||||
}
|
||||
|
||||
// Loader loads persona configurations from a directory.
|
||||
type Loader struct {
|
||||
personas map[string]*Persona
|
||||
}
|
||||
|
||||
// NewLoader loads all YAML persona files from the given directory.
|
||||
func NewLoader(dir string) (*Loader, error) {
|
||||
l := &Loader{personas: make(map[string]*Persona)}
|
||||
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
// Return empty loader if directory doesn't exist
|
||||
if os.IsNotExist(err) {
|
||||
return l, nil
|
||||
}
|
||||
return nil, fmt.Errorf("read persona dir %s: %w", dir, err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".yaml") && !strings.HasSuffix(entry.Name(), ".yml") {
|
||||
continue
|
||||
}
|
||||
path := filepath.Join(dir, entry.Name())
|
||||
p, err := loadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load persona %s: %w", entry.Name(), err)
|
||||
}
|
||||
name := strings.TrimSuffix(entry.Name(), filepath.Ext(entry.Name()))
|
||||
p.Name = name
|
||||
l.personas[name] = p
|
||||
}
|
||||
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// Get returns a persona by name, or nil if not found.
|
||||
func (l *Loader) Get(name string) *Persona {
|
||||
return l.personas[name]
|
||||
}
|
||||
|
||||
// List returns all loaded persona names.
|
||||
func (l *Loader) List() []string {
|
||||
names := make([]string, 0, len(l.personas))
|
||||
for name := range l.personas {
|
||||
names = append(names, name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// Count returns the number of loaded personas.
|
||||
func (l *Loader) Count() int {
|
||||
return len(l.personas)
|
||||
}
|
||||
|
||||
// Default returns the first persona, or a built-in default.
|
||||
func (l *Loader) Default() *Persona {
|
||||
for _, p := range l.personas {
|
||||
return p
|
||||
}
|
||||
return BuiltinDefault()
|
||||
}
|
||||
|
||||
// loadFile reads a single persona YAML file.
|
||||
func loadFile(path string) (*Persona, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parseYAML(data)
|
||||
}
|
||||
|
||||
// parseYAML parses persona YAML content (simple key-value parser).
|
||||
func parseYAML(data []byte) (*Persona, error) {
|
||||
p := &Persona{}
|
||||
lines := strings.Split(string(data), "\n")
|
||||
var currentList *[]string
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "- ") && currentList != nil {
|
||||
*currentList = append(*currentList, strings.TrimPrefix(line, "- "))
|
||||
continue
|
||||
}
|
||||
currentList = nil
|
||||
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSpace(parts[0])
|
||||
val := strings.TrimSpace(parts[1])
|
||||
|
||||
switch key {
|
||||
case "name":
|
||||
p.Name = val
|
||||
case "description":
|
||||
p.Description = val
|
||||
case "greeting":
|
||||
p.Greeting = val
|
||||
case "style":
|
||||
p.Style = val
|
||||
case "iot_response_style":
|
||||
p.IoTResponseStyle = val
|
||||
case "traits":
|
||||
currentList = &p.Traits
|
||||
case "constraints":
|
||||
currentList = &p.Constraints
|
||||
}
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// BuiltinDefault returns a hardcoded default persona.
|
||||
func BuiltinDefault() *Persona {
|
||||
return &Persona{
|
||||
Name: "default",
|
||||
Description: "一个友好的 AI 助手",
|
||||
Greeting: "你好呀~ 有什么可以帮你的吗?",
|
||||
Traits: []string{"友善", "细心", "幽默"},
|
||||
Style: "用轻松自然的中文交流,偶尔加一点俏皮表情",
|
||||
Constraints: []string{"不编造事实", "不提供医疗法律建议", "保持礼貌"},
|
||||
}
|
||||
}
|
||||
|
||||
// Injector builds the system prompt from a persona.
|
||||
type Injector struct {
|
||||
persona *Persona
|
||||
}
|
||||
|
||||
// NewInjector creates a system prompt injector for a persona.
|
||||
func NewInjector(p *Persona) *Injector {
|
||||
if p == nil {
|
||||
p = BuiltinDefault()
|
||||
}
|
||||
return &Injector{persona: p}
|
||||
}
|
||||
|
||||
// BuildSystemPrompt constructs the full system prompt string.
|
||||
func (inj *Injector) BuildSystemPrompt() string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(fmt.Sprintf("你是%s,%s。\n", inj.persona.Name, inj.persona.Description))
|
||||
|
||||
if len(inj.persona.Traits) > 0 {
|
||||
sb.WriteString(fmt.Sprintf("性格特点: %s\n", strings.Join(inj.persona.Traits, "、")))
|
||||
}
|
||||
if inj.persona.Style != "" {
|
||||
sb.WriteString(fmt.Sprintf("说话风格: %s\n", inj.persona.Style))
|
||||
}
|
||||
if len(inj.persona.Constraints) > 0 {
|
||||
sb.WriteString("遵守规则:\n")
|
||||
for _, c := range inj.persona.Constraints {
|
||||
sb.WriteString(fmt.Sprintf("- %s\n", c))
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
Reference in new issue
Block a user