feat(server): add brand kit management and project binding
Add a brand kit domain with memory and postgres stores, a BrandKitService for CRUD and resolution, and REST endpoints to list, upsert, and delete brand kits. Projects can bind a brand kit whose resolved context and reference images feed into the agent's long-term memory and design prompts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
package piagent
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"img_infinite_canvas/internal/domain/design"
|
||||
)
|
||||
|
||||
func TestBrandKitContextReachesPlannerAndDirectImagePrompts(t *testing.T) {
|
||||
project := design.Project{
|
||||
Title: "Campaign",
|
||||
Brief: "Launch visual",
|
||||
BrandKitID: "brand-kit-alpha",
|
||||
BrandContext: "Primary color: Signal Green #28C76F. Typography: Avenir Next.",
|
||||
}
|
||||
planner := projectBriefContext(project)
|
||||
direct := directImagePrompt(design.AgentRequest{Project: project, Prompt: "Create a product hero"})
|
||||
fallback := fallbackImagePrompt(design.AgentRequest{
|
||||
Project: project,
|
||||
Prompt: "Create a product hero",
|
||||
TaskPlan: design.AgentTaskPlan{ImageTasks: []design.AgentImageTask{
|
||||
{Title: "Hero", Brief: "Centered product"},
|
||||
}},
|
||||
}, 0)
|
||||
for name, value := range map[string]string{"planner": planner, "direct": direct, "fallback": fallback} {
|
||||
if !strings.Contains(value, "Signal Green #28C76F") || !strings.Contains(value, "Avenir Next") {
|
||||
t.Fatalf("%s prompt omitted Brand Kit context: %s", name, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBrandKitImagesFollowUserReferences(t *testing.T) {
|
||||
req := design.AgentRequest{
|
||||
Project: design.Project{BrandImages: []string{
|
||||
"data:image/png;base64,YnJhbmQtbG9nbw==",
|
||||
"data:image/webp;base64,YnJhbmQtcmVmZXJlbmNl",
|
||||
}},
|
||||
Messages: []design.AgentChatMessage{{Contents: []design.AgentContent{{Type: "image", ImageURL: "https://example.com/user-reference.png"}}}},
|
||||
}
|
||||
images := referenceImageURLs(req)
|
||||
if len(images) != 3 {
|
||||
t.Fatalf("expected user and Brand Kit references, got %#v", images)
|
||||
}
|
||||
if images[0] != "https://example.com/user-reference.png" || images[1] != req.Project.BrandImages[0] || images[2] != req.Project.BrandImages[1] {
|
||||
t.Fatalf("expected user references before Brand Kit images, got %#v", images)
|
||||
}
|
||||
}
|
||||
@@ -843,6 +843,11 @@ func projectBriefContext(project design.Project) string {
|
||||
builder.WriteString(project.Brief)
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
if strings.TrimSpace(project.BrandContext) != "" {
|
||||
builder.WriteString("Brand Kit context:\n")
|
||||
builder.WriteString(strings.TrimSpace(project.BrandContext))
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
builder.WriteString(fmt.Sprintf("Canvas nodes: %d", len(project.Nodes)))
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ func directImagePlan(req design.AgentRequest) design.AgentPlan {
|
||||
|
||||
func directImagePrompts(req design.AgentRequest) []design.AgentImagePrompt {
|
||||
count := plannedGenerationCount(req)
|
||||
prompt := strings.TrimSpace(req.Prompt)
|
||||
prompt := directImagePrompt(req)
|
||||
if prompt == "" {
|
||||
prompt = fallbackDirectImagePrompt(req)
|
||||
}
|
||||
@@ -257,7 +257,7 @@ func directImageTasksSharePrompt(req design.AgentRequest) bool {
|
||||
}
|
||||
|
||||
func directImageBatchPrompt(req design.AgentRequest) design.AgentImagePrompt {
|
||||
prompt := strings.TrimSpace(req.Prompt)
|
||||
prompt := directImagePrompt(req)
|
||||
if prompt == "" {
|
||||
prompt = fallbackDirectImagePrompt(req)
|
||||
}
|
||||
@@ -614,6 +614,12 @@ func referenceImageURLs(req design.AgentRequest) []string {
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, image := range req.Project.BrandImages {
|
||||
add(image)
|
||||
if len(urls) >= 16 {
|
||||
return urls[:16]
|
||||
}
|
||||
}
|
||||
if len(urls) > 16 {
|
||||
return urls[:16]
|
||||
}
|
||||
@@ -662,9 +668,9 @@ func fallbackImagePrompt(req design.AgentRequest, index int) string {
|
||||
task := req.TaskPlan.ImageTasks[index]
|
||||
if brief := strings.TrimSpace(task.Brief); brief != "" {
|
||||
if title := strings.TrimSpace(task.Title); title != "" && !strings.Contains(brief, title) {
|
||||
return title + ": " + brief
|
||||
return imagePromptWithBrandKit(title+": "+brief, req)
|
||||
}
|
||||
return brief
|
||||
return imagePromptWithBrandKit(brief, req)
|
||||
}
|
||||
}
|
||||
return fallbackDirectImagePrompt(req)
|
||||
@@ -1064,6 +1070,9 @@ func fallbackDirectImagePrompt(req design.AgentRequest) string {
|
||||
if brief := strings.TrimSpace(req.Project.Brief); brief != "" {
|
||||
lines = append(lines, "Project brief: "+truncateRunes(brief, 180))
|
||||
}
|
||||
if brandContext := strings.TrimSpace(req.Project.BrandContext); brandContext != "" {
|
||||
lines = append(lines, "Binding Brand Kit: "+truncateRunes(brandContext, 520))
|
||||
}
|
||||
if memory := compactFallbackMemory(req.Memory); memory != "" {
|
||||
lines = append(lines, "Relevant memory: "+memory)
|
||||
}
|
||||
@@ -1080,6 +1089,19 @@ func fallbackDirectImagePrompt(req design.AgentRequest) string {
|
||||
return compactImageGenerationPrompt(strings.Join(lines, "\n"), imageGenerationFallbackMaxRunes)
|
||||
}
|
||||
|
||||
func directImagePrompt(req design.AgentRequest) string {
|
||||
return imagePromptWithBrandKit(req.Prompt, req)
|
||||
}
|
||||
|
||||
func imagePromptWithBrandKit(prompt string, req design.AgentRequest) string {
|
||||
prompt = strings.TrimSpace(prompt)
|
||||
brandContext := strings.TrimSpace(req.Project.BrandContext)
|
||||
if brandContext == "" {
|
||||
return prompt
|
||||
}
|
||||
return strings.TrimSpace(prompt + "\n\nBinding Brand Kit requirements:\n" + brandContext)
|
||||
}
|
||||
|
||||
func compactFallbackMemory(memory design.AgentMemory) string {
|
||||
if memory.IsZero() {
|
||||
return ""
|
||||
|
||||
Reference in New Issue
Block a user