309 lines
9.5 KiB
Go
309 lines
9.5 KiB
Go
|
|
package application
|
||
|
|
|
||
|
|
import (
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"regexp"
|
||
|
|
"strings"
|
||
|
|
"unicode/utf8"
|
||
|
|
|
||
|
|
"img_infinite_canvas/internal/domain/design"
|
||
|
|
)
|
||
|
|
|
||
|
|
const (
|
||
|
|
shortMemoryMessageLimit = 6
|
||
|
|
shortMemoryNodeLimit = 8
|
||
|
|
longMemoryArtifactLimit = 16
|
||
|
|
longMemoryDecisionLimit = 10
|
||
|
|
shortMemoryMaxRunes = 2400
|
||
|
|
longMemoryMaxRunes = 3200
|
||
|
|
)
|
||
|
|
|
||
|
|
var agentMemoryURLPattern = regexp.MustCompile(`https?://\S+`)
|
||
|
|
|
||
|
|
func buildAgentMemory(project design.Project, prompt string, mode string, messages []design.AgentChatMessage) design.AgentMemory {
|
||
|
|
return design.AgentMemory{
|
||
|
|
ShortTerm: buildShortMemory(project, prompt, mode, messages),
|
||
|
|
LongTerm: buildLongMemory(project),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func buildShortMemory(project design.Project, prompt string, mode string, messages []design.AgentChatMessage) string {
|
||
|
|
memoryMessages := visibleAgentMemoryMessages(project.Messages)
|
||
|
|
sections := []string{
|
||
|
|
"Current turn:",
|
||
|
|
"- mode: " + strings.TrimSpace(mode),
|
||
|
|
"- user request: " + strings.TrimSpace(prompt),
|
||
|
|
}
|
||
|
|
if inline := inlineInputMemory(messages); inline != "" {
|
||
|
|
sections = append(sections, "Inline input:", inline)
|
||
|
|
}
|
||
|
|
if recent := recentMessageMemory(memoryMessages, shortMemoryMessageLimit); recent != "" {
|
||
|
|
sections = append(sections, "Recent conversation:", recent)
|
||
|
|
}
|
||
|
|
if canvas := canvasNodeMemory(project.Nodes, shortMemoryNodeLimit); canvas != "" {
|
||
|
|
sections = append(sections, "Visible canvas:", canvas)
|
||
|
|
}
|
||
|
|
return clampMemoryBlock(strings.Join(sections, "\n"), shortMemoryMaxRunes)
|
||
|
|
}
|
||
|
|
|
||
|
|
func buildLongMemory(project design.Project) string {
|
||
|
|
memoryMessages := visibleAgentMemoryMessages(project.Messages)
|
||
|
|
sections := []string{}
|
||
|
|
if strings.TrimSpace(project.Title) != "" || strings.TrimSpace(project.Brief) != "" {
|
||
|
|
sections = append(sections, "Project identity:")
|
||
|
|
if strings.TrimSpace(project.Title) != "" {
|
||
|
|
sections = append(sections, "- title: "+strings.TrimSpace(project.Title))
|
||
|
|
}
|
||
|
|
if strings.TrimSpace(project.Brief) != "" {
|
||
|
|
sections = append(sections, "- brief: "+shortenMemoryText(project.Brief, 360))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if decisions := durableDecisionMemory(memoryMessages, longMemoryDecisionLimit); decisions != "" {
|
||
|
|
sections = append(sections, "Durable user decisions and preferences:", decisions)
|
||
|
|
}
|
||
|
|
if artifacts := completedArtifactMemory(project.Nodes, longMemoryArtifactLimit); artifacts != "" {
|
||
|
|
sections = append(sections, "Completed canvas artifacts:", artifacts)
|
||
|
|
}
|
||
|
|
if research := researchMemory(memoryMessages); research != "" {
|
||
|
|
sections = append(sections, "Prior web research:", research)
|
||
|
|
}
|
||
|
|
if len(sections) == 0 {
|
||
|
|
return "none"
|
||
|
|
}
|
||
|
|
return clampMemoryBlock(strings.Join(sections, "\n"), longMemoryMaxRunes)
|
||
|
|
}
|
||
|
|
|
||
|
|
func visibleAgentMemoryMessages(messages []design.Message) []design.Message {
|
||
|
|
hiddenThreads := make(map[string]bool)
|
||
|
|
for _, message := range messages {
|
||
|
|
if message.ThreadID == "" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if isCanvasActionMemoryMessage(message) || message.Name == "text_extraction" || message.Type == textExtractionResultMessageType {
|
||
|
|
hiddenThreads[message.ThreadID] = true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
visible := make([]design.Message, 0, len(messages))
|
||
|
|
for _, message := range messages {
|
||
|
|
if message.ThreadID != "" && hiddenThreads[message.ThreadID] {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
visible = append(visible, message)
|
||
|
|
}
|
||
|
|
return visible
|
||
|
|
}
|
||
|
|
|
||
|
|
func isCanvasActionMemoryMessage(message design.Message) bool {
|
||
|
|
return message.Name == "canvas_action" || message.ToolHint == "canvas_action"
|
||
|
|
}
|
||
|
|
|
||
|
|
func inlineInputMemory(messages []design.AgentChatMessage) string {
|
||
|
|
var lines []string
|
||
|
|
for _, message := range messages {
|
||
|
|
for _, content := range message.Contents {
|
||
|
|
switch strings.ToLower(strings.TrimSpace(content.Type)) {
|
||
|
|
case "text":
|
||
|
|
if strings.EqualFold(strings.TrimSpace(content.TextSource), "settings") {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if text := strings.TrimSpace(content.Text); text != "" {
|
||
|
|
text = stripImageGeneratorDirectiveLines(text)
|
||
|
|
if text == "" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
lines = append(lines, "- text: "+shortenMemoryText(text, 280))
|
||
|
|
}
|
||
|
|
case "image":
|
||
|
|
if imageURL := strings.TrimSpace(content.ImageURL); imageURL != "" {
|
||
|
|
size := ""
|
||
|
|
if content.ImageWidth > 0 && content.ImageHeight > 0 {
|
||
|
|
size = fmt.Sprintf(" (%dx%d)", content.ImageWidth, content.ImageHeight)
|
||
|
|
}
|
||
|
|
lines = append(lines, "- reference image"+size)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return strings.Join(lines, "\n")
|
||
|
|
}
|
||
|
|
|
||
|
|
func recentMessageMemory(messages []design.Message, limit int) string {
|
||
|
|
var lines []string
|
||
|
|
start := 0
|
||
|
|
if len(messages) > limit {
|
||
|
|
start = len(messages) - limit
|
||
|
|
}
|
||
|
|
omitted := start
|
||
|
|
for _, message := range messages[start:] {
|
||
|
|
if message.Role == "tool" || message.Role == "error" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
content := strings.TrimSpace(message.Content)
|
||
|
|
if content == "" {
|
||
|
|
content = strings.TrimSpace(message.Title)
|
||
|
|
}
|
||
|
|
if content == "" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
lines = append(lines, fmt.Sprintf("- %s: %s", message.Role, shortenMemoryText(content, 260)))
|
||
|
|
}
|
||
|
|
if omitted > 0 && len(lines) > 0 {
|
||
|
|
lines = append([]string{fmt.Sprintf("- older conversation omitted: %d messages", omitted)}, lines...)
|
||
|
|
}
|
||
|
|
return strings.Join(lines, "\n")
|
||
|
|
}
|
||
|
|
|
||
|
|
func canvasNodeMemory(nodes []design.Node, limit int) string {
|
||
|
|
var candidates []design.Node
|
||
|
|
for _, node := range nodes {
|
||
|
|
if node.Status == "generating" || node.LayerRole == "pen-stroke" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
candidates = append(candidates, node)
|
||
|
|
}
|
||
|
|
if len(candidates) > limit {
|
||
|
|
candidates = candidates[len(candidates)-limit:]
|
||
|
|
}
|
||
|
|
var lines []string
|
||
|
|
for _, node := range candidates {
|
||
|
|
content := strings.TrimSpace(node.Content)
|
||
|
|
if isGeneratedImageContent(content) || strings.HasPrefix(content, "/canvas-assets/") || strings.HasPrefix(content, "http") {
|
||
|
|
content = "asset reference"
|
||
|
|
}
|
||
|
|
if content != "" {
|
||
|
|
content = " | " + shortenMemoryText(content, 160)
|
||
|
|
}
|
||
|
|
lines = append(lines, fmt.Sprintf("- %s %q at %.0f,%.0f %.0fx%.0f%s", node.Type, node.Title, node.X, node.Y, node.Width, node.Height, content))
|
||
|
|
}
|
||
|
|
return strings.Join(lines, "\n")
|
||
|
|
}
|
||
|
|
|
||
|
|
func completedArtifactMemory(nodes []design.Node, limit int) string {
|
||
|
|
var lines []string
|
||
|
|
for _, node := range nodes {
|
||
|
|
if node.Status != "success" && node.Status != "ready" && node.Status != "" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if node.Type != design.NodeTypeImage && node.Type != design.NodeTypeFrame && node.Type != design.NodeTypeText {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
content := strings.TrimSpace(node.Content)
|
||
|
|
if len(content) > 120 {
|
||
|
|
content = shortenMemoryText(content, 120)
|
||
|
|
}
|
||
|
|
line := fmt.Sprintf("- %s: %s", node.Type, strings.TrimSpace(node.Title))
|
||
|
|
if content != "" && (strings.HasPrefix(content, "http") || strings.HasPrefix(content, "/canvas-assets/")) {
|
||
|
|
line += " | asset reference"
|
||
|
|
}
|
||
|
|
lines = append(lines, line)
|
||
|
|
}
|
||
|
|
if len(lines) > limit {
|
||
|
|
lines = lines[len(lines)-limit:]
|
||
|
|
}
|
||
|
|
return strings.Join(lines, "\n")
|
||
|
|
}
|
||
|
|
|
||
|
|
func durableDecisionMemory(messages []design.Message, limit int) string {
|
||
|
|
keywords := []string{
|
||
|
|
"保留", "不要", "必须", "参考", "风格", "品牌", "颜色", "色彩", "字体", "logo", "主视觉", "电商", "官网",
|
||
|
|
"keep", "avoid", "must", "reference", "style", "brand", "color", "typography", "logo", "hero", "ecommerce",
|
||
|
|
}
|
||
|
|
var lines []string
|
||
|
|
for _, message := range messages {
|
||
|
|
if message.Role != "user" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
content := strings.TrimSpace(message.Content)
|
||
|
|
if content == "" || !containsAnyFold(content, keywords) {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
lines = append(lines, "- "+shortenMemoryText(content, 260))
|
||
|
|
}
|
||
|
|
if len(lines) > limit {
|
||
|
|
lines = lines[len(lines)-limit:]
|
||
|
|
}
|
||
|
|
return strings.Join(lines, "\n")
|
||
|
|
}
|
||
|
|
|
||
|
|
func researchMemory(messages []design.Message) string {
|
||
|
|
var lines []string
|
||
|
|
for _, message := range messages {
|
||
|
|
if message.Role != "tool" || message.Title != "Synthesize Skills" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
var report struct {
|
||
|
|
Query string `json:"query"`
|
||
|
|
Results []struct {
|
||
|
|
Title string `json:"title"`
|
||
|
|
URL string `json:"url"`
|
||
|
|
Snippet string `json:"snippet"`
|
||
|
|
} `json:"results"`
|
||
|
|
}
|
||
|
|
if err := json.Unmarshal([]byte(message.Content), &report); err != nil {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if strings.TrimSpace(report.Query) != "" {
|
||
|
|
lines = append(lines, "- query: "+shortenMemoryText(report.Query, 160))
|
||
|
|
}
|
||
|
|
for _, result := range report.Results {
|
||
|
|
title := strings.TrimSpace(result.Title)
|
||
|
|
if title == "" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
line := " result: " + shortenMemoryText(title, 140)
|
||
|
|
if snippet := strings.TrimSpace(result.Snippet); snippet != "" {
|
||
|
|
line += " | " + shortenMemoryText(snippet, 160)
|
||
|
|
}
|
||
|
|
lines = append(lines, line)
|
||
|
|
if len(lines) >= 10 {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if len(lines) > 10 {
|
||
|
|
lines = lines[len(lines)-10:]
|
||
|
|
}
|
||
|
|
return strings.Join(lines, "\n")
|
||
|
|
}
|
||
|
|
|
||
|
|
func containsAnyFold(value string, keywords []string) bool {
|
||
|
|
value = strings.ToLower(value)
|
||
|
|
for _, keyword := range keywords {
|
||
|
|
if strings.Contains(value, strings.ToLower(keyword)) {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
func shortenMemoryText(value string, limit int) string {
|
||
|
|
value = sanitizeMemoryText(value)
|
||
|
|
value = strings.Join(strings.Fields(strings.TrimSpace(value)), " ")
|
||
|
|
if limit <= 0 {
|
||
|
|
return value
|
||
|
|
}
|
||
|
|
runes := []rune(value)
|
||
|
|
if len(runes) <= limit {
|
||
|
|
return value
|
||
|
|
}
|
||
|
|
return string(runes[:limit]) + "..."
|
||
|
|
}
|
||
|
|
|
||
|
|
func sanitizeMemoryText(value string) string {
|
||
|
|
return agentMemoryURLPattern.ReplaceAllString(strings.TrimSpace(value), "[reference]")
|
||
|
|
}
|
||
|
|
|
||
|
|
func clampMemoryBlock(value string, limit int) string {
|
||
|
|
value = strings.TrimSpace(sanitizeMemoryText(value))
|
||
|
|
if limit <= 0 || utf8.RuneCountInString(value) <= limit {
|
||
|
|
return value
|
||
|
|
}
|
||
|
|
runes := []rune(value)
|
||
|
|
clamped := strings.TrimSpace(string(runes[:limit]))
|
||
|
|
if cut := strings.LastIndex(clamped, "\n"); cut > limit*3/4 {
|
||
|
|
clamped = strings.TrimSpace(clamped[:cut])
|
||
|
|
}
|
||
|
|
return clamped + "\n- memory window truncated; older low-signal context omitted"
|
||
|
|
}
|