feat: implement AIEOS v1.1 profile loading for structured agent identity (#296)
Add pkg/aieos package to load, validate and render AIEOS JSON profiles into system prompt text. When aieos.json exists in the workspace, it replaces the legacy .md bootstrap files (SOUL.md, IDENTITY.md) with structured identity, OCEAN psychology traits, linguistics, capabilities, motivations and boundaries. - pkg/aieos/profile.go: Go structs for AIEOS v1.1 specification - pkg/aieos/loader.go: LoadProfile, ProfileExists, validation - pkg/aieos/render.go: RenderToPrompt with OCEAN natural language mapping - pkg/agent/context.go: AIEOS-first loading with .md fallback + deprecation warning - pkg/agent/instance.go: pass per-agent profile path from config - pkg/config/config.go: add Profile field to AgentConfig - workspace/aieos.json: default template matching current PicoClaw identity - 17 unit tests + 3 integration tests, all passing
This commit is contained in:
parent
56a060ff61
commit
bce31a0149
10 changed files with 734 additions and 4 deletions
|
|
@ -8,6 +8,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/aieos"
|
||||
"github.com/sipeed/picoclaw/pkg/logger"
|
||||
"github.com/sipeed/picoclaw/pkg/providers"
|
||||
"github.com/sipeed/picoclaw/pkg/skills"
|
||||
|
|
@ -16,6 +17,7 @@ import (
|
|||
|
||||
type ContextBuilder struct {
|
||||
workspace string
|
||||
profilePath string // Optional override for aieos.json path
|
||||
skillsLoader *skills.SkillsLoader
|
||||
memory *MemoryStore
|
||||
tools *tools.ToolRegistry // Direct reference to tool registry
|
||||
|
|
@ -106,17 +108,27 @@ func (cb *ContextBuilder) buildToolsSection() string {
|
|||
return sb.String()
|
||||
}
|
||||
|
||||
// SetProfilePath sets a custom path for the AIEOS profile, overriding the default workspace location.
|
||||
func (cb *ContextBuilder) SetProfilePath(path string) {
|
||||
cb.profilePath = path
|
||||
}
|
||||
|
||||
func (cb *ContextBuilder) BuildSystemPrompt() string {
|
||||
parts := []string{}
|
||||
|
||||
// Core identity section
|
||||
parts = append(parts, cb.getIdentity())
|
||||
|
||||
// Bootstrap files
|
||||
// AIEOS profile (preferred) or legacy .md bootstrap files (fallback)
|
||||
if aieosContent := cb.loadAIEOSProfile(); aieosContent != "" {
|
||||
parts = append(parts, aieosContent)
|
||||
} else {
|
||||
bootstrapContent := cb.LoadBootstrapFiles()
|
||||
if bootstrapContent != "" {
|
||||
logger.WarnCF("agent", "Using deprecated .md bootstrap files. Migrate to aieos.json.", nil)
|
||||
parts = append(parts, bootstrapContent)
|
||||
}
|
||||
}
|
||||
|
||||
// Skills - show summary, AI can read full content with read_file tool
|
||||
skillsSummary := cb.skillsLoader.BuildSkillsSummary()
|
||||
|
|
@ -253,6 +265,29 @@ func (cb *ContextBuilder) loadSkills() string {
|
|||
return "# Skill Definitions\n\n" + content
|
||||
}
|
||||
|
||||
// loadAIEOSProfile attempts to load and render an AIEOS profile.
|
||||
// Returns the rendered text, or empty string if no profile is found or on error.
|
||||
func (cb *ContextBuilder) loadAIEOSProfile() string {
|
||||
path := cb.profilePath
|
||||
if path == "" {
|
||||
if !aieos.ProfileExists(cb.workspace) {
|
||||
return ""
|
||||
}
|
||||
path = aieos.DefaultProfilePath(cb.workspace)
|
||||
}
|
||||
|
||||
profile, err := aieos.LoadProfile(path)
|
||||
if err != nil {
|
||||
logger.WarnCF("agent", "Failed to load AIEOS profile, falling back to .md files", map[string]interface{}{
|
||||
"path": path,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return ""
|
||||
}
|
||||
|
||||
return aieos.RenderToPrompt(profile)
|
||||
}
|
||||
|
||||
// GetSkillsInfo returns information about loaded skills.
|
||||
func (cb *ContextBuilder) GetSkillsInfo() map[string]interface{} {
|
||||
allSkills := cb.skillsLoader.ListSkills()
|
||||
|
|
|
|||
|
|
@ -69,6 +69,9 @@ func NewAgentInstance(
|
|||
agentName = agentCfg.Name
|
||||
subagents = agentCfg.Subagents
|
||||
skillsFilter = agentCfg.Skills
|
||||
if agentCfg.Profile != "" {
|
||||
contextBuilder.SetProfilePath(agentCfg.Profile)
|
||||
}
|
||||
}
|
||||
|
||||
maxIter := defaults.MaxToolIterations
|
||||
|
|
|
|||
82
pkg/aieos/integration_test.go
Normal file
82
pkg/aieos/integration_test.go
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
package aieos_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/aieos"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestIntegration_FullPipeline tests the complete pipeline:
|
||||
// load real aieos.json → validate → render to prompt text
|
||||
func TestIntegration_FullPipeline(t *testing.T) {
|
||||
// Use the actual workspace/aieos.json template
|
||||
repoRoot := findRepoRoot(t)
|
||||
templatePath := filepath.Join(repoRoot, "workspace", "aieos.json")
|
||||
|
||||
profile, err := aieos.LoadProfile(templatePath)
|
||||
require.NoError(t, err, "should load the real workspace/aieos.json template")
|
||||
|
||||
require.Equal(t, "1.1", profile.Version)
|
||||
require.Equal(t, "PicoClaw", profile.Identity.Name)
|
||||
require.NotNil(t, profile.Psychology)
|
||||
require.NotEmpty(t, profile.Capabilities)
|
||||
|
||||
// Render to prompt
|
||||
prompt := aieos.RenderToPrompt(profile)
|
||||
|
||||
fmt.Println("=== AIEOS System Prompt Output ===")
|
||||
fmt.Println(prompt)
|
||||
fmt.Println("=== END ===")
|
||||
|
||||
require.Contains(t, prompt, "You are **PicoClaw**")
|
||||
require.Contains(t, prompt, "## Personality")
|
||||
require.Contains(t, prompt, "curious and open") // openness 0.8
|
||||
require.Contains(t, prompt, "organized, thorough") // conscientiousness 0.9
|
||||
require.Contains(t, prompt, "calm, stable") // neuroticism 0.1
|
||||
require.Contains(t, prompt, "## Capabilities")
|
||||
require.Contains(t, prompt, "web_search")
|
||||
require.Contains(t, prompt, "## Communication Style")
|
||||
require.Contains(t, prompt, "moderately detailed") // verbosity 0.3 (mid range)
|
||||
require.Contains(t, prompt, "## Values & Goals")
|
||||
require.Contains(t, prompt, "accuracy")
|
||||
require.Contains(t, prompt, "## Boundaries")
|
||||
require.Contains(t, prompt, "no_harmful_content")
|
||||
}
|
||||
|
||||
// TestIntegration_FallbackWhenNoAIEOS verifies that an empty workspace has no profile
|
||||
func TestIntegration_FallbackWhenNoAIEOS(t *testing.T) {
|
||||
emptyDir := t.TempDir()
|
||||
require.False(t, aieos.ProfileExists(emptyDir))
|
||||
}
|
||||
|
||||
// TestIntegration_FallbackOnInvalidJSON verifies graceful failure
|
||||
func TestIntegration_FallbackOnInvalidJSON(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "aieos.json")
|
||||
require.NoError(t, os.WriteFile(path, []byte(`{invalid`), 0644))
|
||||
|
||||
require.True(t, aieos.ProfileExists(dir))
|
||||
|
||||
_, err := aieos.LoadProfile(path)
|
||||
require.Error(t, err, "should fail on invalid JSON")
|
||||
}
|
||||
|
||||
func findRepoRoot(t *testing.T) string {
|
||||
t.Helper()
|
||||
dir, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
for {
|
||||
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
|
||||
return dir
|
||||
}
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
t.Fatal("could not find repo root (go.mod)")
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
}
|
||||
51
pkg/aieos/loader.go
Normal file
51
pkg/aieos/loader.go
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package aieos
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const defaultFilename = "aieos.json"
|
||||
|
||||
// DefaultProfilePath returns the default AIEOS profile path for a workspace.
|
||||
func DefaultProfilePath(workspace string) string {
|
||||
return filepath.Join(workspace, defaultFilename)
|
||||
}
|
||||
|
||||
// ProfileExists checks whether an aieos.json file exists in the workspace.
|
||||
func ProfileExists(workspace string) bool {
|
||||
_, err := os.Stat(DefaultProfilePath(workspace))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// LoadProfile reads and validates an AIEOS profile from the given path.
|
||||
func LoadProfile(path string) (*Profile, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("aieos: read profile: %w", err)
|
||||
}
|
||||
|
||||
var p Profile
|
||||
if err := json.Unmarshal(data, &p); err != nil {
|
||||
return nil, fmt.Errorf("aieos: parse profile: %w", err)
|
||||
}
|
||||
|
||||
if err := validate(&p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// validate performs minimal validation on a loaded profile.
|
||||
func validate(p *Profile) error {
|
||||
if p.Version == "" {
|
||||
return fmt.Errorf("aieos: version is required")
|
||||
}
|
||||
if p.Identity.Name == "" {
|
||||
return fmt.Errorf("aieos: identity.name is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
105
pkg/aieos/loader_test.go
Normal file
105
pkg/aieos/loader_test.go
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
package aieos
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLoadProfile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "aieos.json")
|
||||
|
||||
data := `{
|
||||
"version": "1.1",
|
||||
"identity": {
|
||||
"name": "TestAgent",
|
||||
"description": "A test agent",
|
||||
"purpose": "Testing"
|
||||
},
|
||||
"capabilities": [
|
||||
{"name": "search", "description": "Web search"}
|
||||
],
|
||||
"psychology": {
|
||||
"openness": 0.8,
|
||||
"conscientiousness": 0.9,
|
||||
"extraversion": 0.5,
|
||||
"agreeableness": 0.85,
|
||||
"neuroticism": 0.1
|
||||
}
|
||||
}`
|
||||
require.NoError(t, os.WriteFile(path, []byte(data), 0644))
|
||||
|
||||
p, err := LoadProfile(path)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "1.1", p.Version)
|
||||
assert.Equal(t, "TestAgent", p.Identity.Name)
|
||||
assert.Equal(t, "A test agent", p.Identity.Description)
|
||||
assert.Equal(t, "Testing", p.Identity.Purpose)
|
||||
assert.Len(t, p.Capabilities, 1)
|
||||
assert.Equal(t, "search", p.Capabilities[0].Name)
|
||||
require.NotNil(t, p.Psychology)
|
||||
assert.Equal(t, 0.8, p.Psychology.Openness)
|
||||
assert.Equal(t, 0.1, p.Psychology.Neuroticism)
|
||||
}
|
||||
|
||||
func TestLoadProfileInvalidJSON(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "aieos.json")
|
||||
|
||||
require.NoError(t, os.WriteFile(path, []byte(`{bad json`), 0644))
|
||||
|
||||
_, err := LoadProfile(path)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "parse profile")
|
||||
}
|
||||
|
||||
func TestLoadProfileMissingVersion(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "aieos.json")
|
||||
|
||||
data := `{"identity": {"name": "Agent"}}`
|
||||
require.NoError(t, os.WriteFile(path, []byte(data), 0644))
|
||||
|
||||
_, err := LoadProfile(path)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "version is required")
|
||||
}
|
||||
|
||||
func TestLoadProfileMissingName(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "aieos.json")
|
||||
|
||||
data := `{"version": "1.1", "identity": {}}`
|
||||
require.NoError(t, os.WriteFile(path, []byte(data), 0644))
|
||||
|
||||
_, err := LoadProfile(path)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "identity.name is required")
|
||||
}
|
||||
|
||||
func TestLoadProfileFileNotFound(t *testing.T) {
|
||||
_, err := LoadProfile("/nonexistent/aieos.json")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "read profile")
|
||||
}
|
||||
|
||||
func TestProfileExists(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
assert.False(t, ProfileExists(dir))
|
||||
|
||||
path := filepath.Join(dir, "aieos.json")
|
||||
require.NoError(t, os.WriteFile(path, []byte(`{}`), 0644))
|
||||
|
||||
assert.True(t, ProfileExists(dir))
|
||||
}
|
||||
|
||||
func TestDefaultProfilePath(t *testing.T) {
|
||||
got := DefaultProfilePath("/home/user/workspace")
|
||||
assert.Equal(t, "/home/user/workspace/aieos.json", got)
|
||||
}
|
||||
65
pkg/aieos/profile.go
Normal file
65
pkg/aieos/profile.go
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
package aieos
|
||||
|
||||
// Profile represents an AIEOS v1.1 (AI Entity Object Specification) profile.
|
||||
// It defines the identity, personality, and behavior of an AI agent through
|
||||
// structured JSON rather than free-form markdown.
|
||||
type Profile struct {
|
||||
Version string `json:"version"`
|
||||
Identity Identity `json:"identity"`
|
||||
Capabilities []Capability `json:"capabilities,omitempty"`
|
||||
Psychology *Psychology `json:"psychology,omitempty"`
|
||||
Linguistics *Linguistics `json:"linguistics,omitempty"`
|
||||
Motivations *Motivations `json:"motivations,omitempty"`
|
||||
Boundaries *Boundaries `json:"boundaries,omitempty"`
|
||||
Metadata *Metadata `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// Identity defines who the agent is.
|
||||
type Identity struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Purpose string `json:"purpose,omitempty"`
|
||||
Philosophy string `json:"philosophy,omitempty"`
|
||||
}
|
||||
|
||||
// Capability describes a single agent capability.
|
||||
type Capability struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
// Psychology holds OCEAN personality traits, each in the range 0.0-1.0.
|
||||
type Psychology struct {
|
||||
Openness float64 `json:"openness"`
|
||||
Conscientiousness float64 `json:"conscientiousness"`
|
||||
Extraversion float64 `json:"extraversion"`
|
||||
Agreeableness float64 `json:"agreeableness"`
|
||||
Neuroticism float64 `json:"neuroticism"`
|
||||
}
|
||||
|
||||
// Linguistics controls the agent's communication style.
|
||||
type Linguistics struct {
|
||||
Formality float64 `json:"formality"` // 0.0 (casual) to 1.0 (formal)
|
||||
Verbosity float64 `json:"verbosity"` // 0.0 (terse) to 1.0 (verbose)
|
||||
Idiolect []string `json:"idiolect,omitempty"`
|
||||
}
|
||||
|
||||
// Motivations captures the agent's values and goals.
|
||||
type Motivations struct {
|
||||
Values []string `json:"values,omitempty"`
|
||||
Goals []string `json:"goals,omitempty"`
|
||||
}
|
||||
|
||||
// Boundaries defines hard and soft behavioral limits.
|
||||
type Boundaries struct {
|
||||
HardLimits []string `json:"hard_limits,omitempty"`
|
||||
SoftLimits []string `json:"soft_limits,omitempty"`
|
||||
}
|
||||
|
||||
// Metadata holds authorship and licensing information.
|
||||
type Metadata struct {
|
||||
Author string `json:"author,omitempty"`
|
||||
CreatedAt string `json:"created_at,omitempty"`
|
||||
License string `json:"license,omitempty"`
|
||||
}
|
||||
160
pkg/aieos/render.go
Normal file
160
pkg/aieos/render.go
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
package aieos
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RenderToPrompt converts an AIEOS profile into natural-language text
|
||||
// suitable for injection into a system prompt.
|
||||
func RenderToPrompt(p *Profile) string {
|
||||
var sb strings.Builder
|
||||
|
||||
// Identity section
|
||||
sb.WriteString("## Identity\n\n")
|
||||
fmt.Fprintf(&sb, "You are **%s**.", p.Identity.Name)
|
||||
if p.Identity.Description != "" {
|
||||
sb.WriteString(" " + p.Identity.Description + ".")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
if p.Identity.Purpose != "" {
|
||||
fmt.Fprintf(&sb, "\n**Purpose**: %s\n", p.Identity.Purpose)
|
||||
}
|
||||
if p.Identity.Philosophy != "" {
|
||||
fmt.Fprintf(&sb, "\n**Philosophy**: %s\n", p.Identity.Philosophy)
|
||||
}
|
||||
|
||||
// Psychology (OCEAN traits)
|
||||
if p.Psychology != nil {
|
||||
sb.WriteString("\n## Personality\n\n")
|
||||
sb.WriteString(renderOCEAN(p.Psychology))
|
||||
}
|
||||
|
||||
// Capabilities
|
||||
if len(p.Capabilities) > 0 {
|
||||
sb.WriteString("\n## Capabilities\n\n")
|
||||
for _, c := range p.Capabilities {
|
||||
if c.Description != "" {
|
||||
fmt.Fprintf(&sb, "- **%s**: %s\n", c.Name, c.Description)
|
||||
} else {
|
||||
fmt.Fprintf(&sb, "- %s\n", c.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Linguistics
|
||||
if p.Linguistics != nil {
|
||||
sb.WriteString("\n## Communication Style\n\n")
|
||||
sb.WriteString(renderLinguistics(p.Linguistics))
|
||||
}
|
||||
|
||||
// Motivations
|
||||
if p.Motivations != nil {
|
||||
hasContent := len(p.Motivations.Values) > 0 || len(p.Motivations.Goals) > 0
|
||||
if hasContent {
|
||||
sb.WriteString("\n## Values & Goals\n\n")
|
||||
if len(p.Motivations.Values) > 0 {
|
||||
sb.WriteString("**Core values**: " + strings.Join(p.Motivations.Values, ", ") + "\n")
|
||||
}
|
||||
if len(p.Motivations.Goals) > 0 {
|
||||
sb.WriteString("**Goals**: " + strings.Join(p.Motivations.Goals, ", ") + "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Boundaries
|
||||
if p.Boundaries != nil {
|
||||
hasContent := len(p.Boundaries.HardLimits) > 0 || len(p.Boundaries.SoftLimits) > 0
|
||||
if hasContent {
|
||||
sb.WriteString("\n## Boundaries\n\n")
|
||||
if len(p.Boundaries.HardLimits) > 0 {
|
||||
sb.WriteString("**Hard limits** (never violate):\n")
|
||||
for _, l := range p.Boundaries.HardLimits {
|
||||
fmt.Fprintf(&sb, "- %s\n", l)
|
||||
}
|
||||
}
|
||||
if len(p.Boundaries.SoftLimits) > 0 {
|
||||
sb.WriteString("**Soft limits** (prefer to follow):\n")
|
||||
for _, l := range p.Boundaries.SoftLimits {
|
||||
fmt.Fprintf(&sb, "- %s\n", l)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// renderOCEAN converts OCEAN trait values (0.0-1.0) into natural language descriptions.
|
||||
func renderOCEAN(psy *Psychology) string {
|
||||
var lines []string
|
||||
|
||||
lines = append(lines, traitDescription(psy.Openness,
|
||||
"You are highly curious and open to new ideas.",
|
||||
"You have a balanced approach to novelty and tradition.",
|
||||
"You prefer familiar approaches and proven methods.",
|
||||
))
|
||||
lines = append(lines, traitDescription(psy.Conscientiousness,
|
||||
"You are highly organized, thorough, and detail-oriented.",
|
||||
"You balance thoroughness with flexibility.",
|
||||
"You take a relaxed approach to planning and organization.",
|
||||
))
|
||||
lines = append(lines, traitDescription(psy.Extraversion,
|
||||
"You are enthusiastic, talkative, and energetic in interactions.",
|
||||
"You balance engagement with thoughtful reflection.",
|
||||
"You are reserved and prefer concise, focused communication.",
|
||||
))
|
||||
lines = append(lines, traitDescription(psy.Agreeableness,
|
||||
"You are warm, cooperative, and prioritize the user's needs.",
|
||||
"You balance helpfulness with honest feedback.",
|
||||
"You are direct and straightforward, even when it may be uncomfortable.",
|
||||
))
|
||||
lines = append(lines, traitDescription(psy.Neuroticism,
|
||||
"You tend to be cautious and sensitive to potential issues.",
|
||||
"You have a balanced emotional response to challenges.",
|
||||
"You are calm, stable, and resilient under pressure.",
|
||||
))
|
||||
|
||||
return strings.Join(lines, "\n") + "\n"
|
||||
}
|
||||
|
||||
// traitDescription maps a 0.0-1.0 value to one of three descriptions: high (>=0.7), mid (0.3-0.7), low (<0.3).
|
||||
func traitDescription(value float64, high, mid, low string) string {
|
||||
switch {
|
||||
case value >= 0.7:
|
||||
return high
|
||||
case value >= 0.3:
|
||||
return mid
|
||||
default:
|
||||
return low
|
||||
}
|
||||
}
|
||||
|
||||
// renderLinguistics converts formality/verbosity into communication instructions.
|
||||
func renderLinguistics(ling *Linguistics) string {
|
||||
var lines []string
|
||||
|
||||
switch {
|
||||
case ling.Formality >= 0.7:
|
||||
lines = append(lines, "Use formal, professional language.")
|
||||
case ling.Formality >= 0.3:
|
||||
lines = append(lines, "Use a balanced, conversational but professional tone.")
|
||||
default:
|
||||
lines = append(lines, "Use casual, friendly language.")
|
||||
}
|
||||
|
||||
switch {
|
||||
case ling.Verbosity >= 0.7:
|
||||
lines = append(lines, "Be thorough and detailed in your responses.")
|
||||
case ling.Verbosity >= 0.3:
|
||||
lines = append(lines, "Be moderately detailed — explain when helpful, be concise when possible.")
|
||||
default:
|
||||
lines = append(lines, "Be concise and to the point. Avoid unnecessary elaboration.")
|
||||
}
|
||||
|
||||
if len(ling.Idiolect) > 0 {
|
||||
lines = append(lines, fmt.Sprintf("Use these characteristic phrases when appropriate: %s.", strings.Join(ling.Idiolect, ", ")))
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n") + "\n"
|
||||
}
|
||||
186
pkg/aieos/render_test.go
Normal file
186
pkg/aieos/render_test.go
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
package aieos
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRenderToPromptFull(t *testing.T) {
|
||||
p := &Profile{
|
||||
Version: "1.1",
|
||||
Identity: Identity{
|
||||
Name: "PicoClaw",
|
||||
Description: "Ultra-lightweight AI assistant",
|
||||
Purpose: "Provide helpful AI assistance",
|
||||
Philosophy: "Simplicity over complexity",
|
||||
},
|
||||
Capabilities: []Capability{
|
||||
{Name: "web_search", Description: "Web search and content fetching"},
|
||||
{Name: "file_ops"},
|
||||
},
|
||||
Psychology: &Psychology{
|
||||
Openness: 0.8,
|
||||
Conscientiousness: 0.9,
|
||||
Extraversion: 0.5,
|
||||
Agreeableness: 0.85,
|
||||
Neuroticism: 0.1,
|
||||
},
|
||||
Linguistics: &Linguistics{
|
||||
Formality: 0.5,
|
||||
Verbosity: 0.3,
|
||||
},
|
||||
Motivations: &Motivations{
|
||||
Values: []string{"accuracy", "transparency"},
|
||||
Goals: []string{"fast_assistant"},
|
||||
},
|
||||
Boundaries: &Boundaries{
|
||||
HardLimits: []string{"no_harmful_content"},
|
||||
SoftLimits: []string{"prefer_user_control"},
|
||||
},
|
||||
}
|
||||
|
||||
result := RenderToPrompt(p)
|
||||
|
||||
assert.Contains(t, result, "You are **PicoClaw**.")
|
||||
assert.Contains(t, result, "Ultra-lightweight AI assistant")
|
||||
assert.Contains(t, result, "**Purpose**: Provide helpful AI assistance")
|
||||
assert.Contains(t, result, "**Philosophy**: Simplicity over complexity")
|
||||
assert.Contains(t, result, "## Personality")
|
||||
assert.Contains(t, result, "## Capabilities")
|
||||
assert.Contains(t, result, "- **web_search**: Web search and content fetching")
|
||||
assert.Contains(t, result, "- file_ops")
|
||||
assert.Contains(t, result, "## Communication Style")
|
||||
assert.Contains(t, result, "## Values & Goals")
|
||||
assert.Contains(t, result, "accuracy, transparency")
|
||||
assert.Contains(t, result, "fast_assistant")
|
||||
assert.Contains(t, result, "## Boundaries")
|
||||
assert.Contains(t, result, "no_harmful_content")
|
||||
assert.Contains(t, result, "prefer_user_control")
|
||||
}
|
||||
|
||||
func TestRenderToPromptMinimal(t *testing.T) {
|
||||
p := &Profile{
|
||||
Version: "1.1",
|
||||
Identity: Identity{
|
||||
Name: "MinimalBot",
|
||||
},
|
||||
}
|
||||
|
||||
result := RenderToPrompt(p)
|
||||
|
||||
assert.Contains(t, result, "You are **MinimalBot**.")
|
||||
assert.NotContains(t, result, "## Personality")
|
||||
assert.NotContains(t, result, "## Capabilities")
|
||||
assert.NotContains(t, result, "## Communication Style")
|
||||
assert.NotContains(t, result, "## Values & Goals")
|
||||
assert.NotContains(t, result, "## Boundaries")
|
||||
}
|
||||
|
||||
func TestRenderOCEAN(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
psy *Psychology
|
||||
contains []string
|
||||
}{
|
||||
{
|
||||
name: "high values",
|
||||
psy: &Psychology{
|
||||
Openness: 0.9, Conscientiousness: 0.8,
|
||||
Extraversion: 0.7, Agreeableness: 0.75, Neuroticism: 0.8,
|
||||
},
|
||||
contains: []string{
|
||||
"curious and open",
|
||||
"organized, thorough",
|
||||
"enthusiastic, talkative",
|
||||
"warm, cooperative",
|
||||
"cautious and sensitive",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "low values",
|
||||
psy: &Psychology{
|
||||
Openness: 0.1, Conscientiousness: 0.2,
|
||||
Extraversion: 0.1, Agreeableness: 0.2, Neuroticism: 0.1,
|
||||
},
|
||||
contains: []string{
|
||||
"familiar approaches",
|
||||
"relaxed approach",
|
||||
"reserved",
|
||||
"direct and straightforward",
|
||||
"calm, stable",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mid values",
|
||||
psy: &Psychology{
|
||||
Openness: 0.5, Conscientiousness: 0.5,
|
||||
Extraversion: 0.5, Agreeableness: 0.5, Neuroticism: 0.5,
|
||||
},
|
||||
contains: []string{
|
||||
"balanced approach to novelty",
|
||||
"balance thoroughness",
|
||||
"balance engagement",
|
||||
"balance helpfulness",
|
||||
"balanced emotional response",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := renderOCEAN(tc.psy)
|
||||
for _, expected := range tc.contains {
|
||||
assert.Contains(t, result, expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderLinguistics(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ling *Linguistics
|
||||
contains []string
|
||||
}{
|
||||
{
|
||||
name: "formal and verbose",
|
||||
ling: &Linguistics{Formality: 0.9, Verbosity: 0.8},
|
||||
contains: []string{"formal, professional", "thorough and detailed"},
|
||||
},
|
||||
{
|
||||
name: "casual and concise",
|
||||
ling: &Linguistics{Formality: 0.1, Verbosity: 0.1},
|
||||
contains: []string{"casual, friendly", "concise and to the point"},
|
||||
},
|
||||
{
|
||||
name: "balanced with idiolect",
|
||||
ling: &Linguistics{Formality: 0.5, Verbosity: 0.5, Idiolect: []string{"hey there", "gotcha"}},
|
||||
contains: []string{"balanced, conversational", "moderately detailed", "hey there, gotcha"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := renderLinguistics(tc.ling)
|
||||
for _, expected := range tc.contains {
|
||||
assert.Contains(t, result, expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderEmptyMotivationsAndBoundaries(t *testing.T) {
|
||||
p := &Profile{
|
||||
Version: "1.1",
|
||||
Identity: Identity{Name: "TestBot"},
|
||||
Motivations: &Motivations{},
|
||||
Boundaries: &Boundaries{},
|
||||
}
|
||||
|
||||
result := RenderToPrompt(p)
|
||||
|
||||
assert.False(t, strings.Contains(result, "## Values & Goals"))
|
||||
assert.False(t, strings.Contains(result, "## Boundaries"))
|
||||
}
|
||||
|
|
@ -106,6 +106,7 @@ type AgentConfig struct {
|
|||
Name string `json:"name,omitempty"`
|
||||
Workspace string `json:"workspace,omitempty"`
|
||||
Model *AgentModelConfig `json:"model,omitempty"`
|
||||
Profile string `json:"profile,omitempty"` // Path to aieos.json profile
|
||||
Skills []string `json:"skills,omitempty"`
|
||||
Subagents *SubagentsConfig `json:"subagents,omitempty"`
|
||||
}
|
||||
|
|
|
|||
42
workspace/aieos.json
Normal file
42
workspace/aieos.json
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"version": "1.1",
|
||||
"identity": {
|
||||
"name": "PicoClaw",
|
||||
"description": "Ultra-lightweight personal AI assistant written in Go",
|
||||
"version": "0.1.0",
|
||||
"purpose": "Provide intelligent AI assistance with minimal resource usage",
|
||||
"philosophy": "Simplicity over complexity, performance over features"
|
||||
},
|
||||
"capabilities": [
|
||||
{"name": "web_search", "description": "Web search and content fetching"},
|
||||
{"name": "file_ops", "description": "File system operations"},
|
||||
{"name": "shell_exec", "description": "Shell command execution"},
|
||||
{"name": "messaging", "description": "Multi-channel messaging"},
|
||||
{"name": "skills", "description": "Skill-based extensibility"},
|
||||
{"name": "memory", "description": "Memory and context management"}
|
||||
],
|
||||
"psychology": {
|
||||
"openness": 0.8,
|
||||
"conscientiousness": 0.9,
|
||||
"extraversion": 0.5,
|
||||
"agreeableness": 0.85,
|
||||
"neuroticism": 0.1
|
||||
},
|
||||
"linguistics": {
|
||||
"formality": 0.5,
|
||||
"verbosity": 0.3,
|
||||
"idiolect": []
|
||||
},
|
||||
"motivations": {
|
||||
"values": ["accuracy", "user_privacy", "transparency", "continuous_improvement"],
|
||||
"goals": ["fast_lightweight_assistant", "offline_first", "easy_customization", "efficient_on_constrained_hardware"]
|
||||
},
|
||||
"boundaries": {
|
||||
"hard_limits": ["no_harmful_content", "no_illegal_activities"],
|
||||
"soft_limits": ["prefer_user_control", "transparent_operation"]
|
||||
},
|
||||
"metadata": {
|
||||
"author": "sipeed/picoclaw",
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue