feat(agent): clarify vague briefs before generating and harden prompts
Let the planner return a clarify/chat intent that asks 2-4 targeted questions (imageCount 0) when essentials are missing, instead of forging ahead. Tighten prompt guidance to preserve user wording and stated reference-image roles, avoid inventing copy/specs, keep durable brand constraints, and treat vague taste feedback as clarification. Also fix the generation heartbeat goroutine to stop cleanly via defer, add research-query fallbacks, and refresh image-only capability copy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1377,12 +1377,17 @@ func (s *DesignService) planAgentTask(ctx context.Context, project design.Projec
|
||||
}
|
||||
|
||||
func normalizeAgentTaskPlan(plan design.AgentTaskPlan, prompt string, mode string, messages []design.AgentChatMessage, webSearchEnabled bool, forceImage bool) design.AgentTaskPlan {
|
||||
plan.Intent = normalizeAgentIntent(plan.Intent)
|
||||
if forceImage || shouldGenerateForAgentPrompt(prompt, mode, messages) {
|
||||
rawIntent := strings.TrimSpace(plan.Intent)
|
||||
plan.Intent = normalizeAgentIntent(rawIntent)
|
||||
if forceImage || shouldForceImageMode(mode) {
|
||||
plan.Intent = "image"
|
||||
}
|
||||
if plan.Intent == "" {
|
||||
plan.Intent = "chat"
|
||||
if shouldGenerateForAgentPrompt(prompt, mode, messages) {
|
||||
plan.Intent = "image"
|
||||
} else {
|
||||
plan.Intent = "chat"
|
||||
}
|
||||
}
|
||||
if !webSearchEnabled {
|
||||
plan.ShouldSearch = false
|
||||
@@ -1434,9 +1439,9 @@ func normalizeAgentTaskPlan(plan design.AgentTaskPlan, prompt string, mode strin
|
||||
func fallbackPlanningResponse(prompt string, intent string) string {
|
||||
if normalizeAgentIntent(intent) == "image" {
|
||||
if prefersEnglishText(prompt) {
|
||||
return "I will break this into the right visual deliverables and generate them on the canvas."
|
||||
return "I will turn this into concrete image deliverables and generate them on the canvas."
|
||||
}
|
||||
return "我会先把需求拆成合适的视觉交付项,再在画布中生成。"
|
||||
return "我会先把需求拆成具体图片交付项,再在画布中生成。"
|
||||
}
|
||||
if prefersEnglishText(prompt) {
|
||||
return "I will answer based on the current canvas context."
|
||||
@@ -1573,9 +1578,9 @@ func (s *DesignService) updateAgentMessage(ctx context.Context, projectID string
|
||||
func fallbackAgentChatResponse(prompt string, project design.Project) string {
|
||||
if isCapabilityPrompt(strings.ToLower(strings.TrimSpace(prompt))) {
|
||||
if prefersEnglishText(prompt) {
|
||||
return "I mainly help with image and design-canvas work: generating visuals, editing images, shaping ecommerce pages, reading the current canvas, and suggesting the next concrete move. When you give me a clear visual task, I will enter the image-generation pipeline."
|
||||
return "I focus on image generation and image editing: brand/site visuals, ecommerce product images, posters, banners, social graphics, reference-based generation, background removal, vectorization, mockup placement, and multi-round canvas iteration. Give me a clear visual task and I will generate or edit images on the canvas."
|
||||
}
|
||||
return "我主要负责图片与设计画布:生成视觉稿、编辑图片、做电商页面方向、拆解当前画布并给下一步建议。你给我明确的作图目标时,我再进入生图链路。"
|
||||
return "我专注图片生成和图片编辑:品牌/官网视觉、电商产品图、海报 Banner、社媒图、参考图生图、去背景、矢量化、Mockup 贴图,以及在无限画布里多轮迭代。你给我明确的图片任务后,我会进入生图或修图链路。"
|
||||
}
|
||||
if strings.TrimSpace(project.Brief) != "" {
|
||||
if prefersEnglishText(prompt) {
|
||||
@@ -1594,7 +1599,9 @@ func normalizeAgentIntent(intent string) string {
|
||||
switch intent {
|
||||
case "image", "generate_image", "edit_image", "visual", "design":
|
||||
return "image"
|
||||
case "research":
|
||||
case "":
|
||||
return ""
|
||||
case "research", "clarify", "brief", "briefing", "question", "questions", "chat", "conversation":
|
||||
return "chat"
|
||||
default:
|
||||
return "chat"
|
||||
@@ -2571,7 +2578,9 @@ func (s *DesignService) completeCreateProjectGeneration(ctx context.Context, pro
|
||||
message.Type = "assistant"
|
||||
}
|
||||
if incremental {
|
||||
project.Nodes = mergeCompletedIncrementalGeneratedNodes(project.Nodes, plan.GeneratedNodes, "")
|
||||
project.Nodes = clearRemainingAgentGenerationPlaceholders(project.Nodes, "")
|
||||
project.Messages = appendMissingIncrementalArtifactMessages(project.Messages, threadID, prompt, plan.GeneratedNodes, now.Add(-time.Millisecond))
|
||||
} else {
|
||||
project.Nodes = mergeGeneratedNodesIntoTarget(nonGeneratingNodes(project.Nodes), plan.GeneratedNodes, "")
|
||||
project.Messages = append(project.Messages, toolArtifactsMessage(threadID, prompt, plan.GeneratedNodes, now.Add(-time.Millisecond)))
|
||||
@@ -2940,7 +2949,11 @@ func (s *DesignService) completeFollowupGeneration(ctx context.Context, projectI
|
||||
message.Type = "assistant"
|
||||
}
|
||||
if incremental {
|
||||
project.Nodes = mergeCompletedIncrementalGeneratedNodes(project.Nodes, plan.GeneratedNodes, targetNodeID)
|
||||
project.Nodes = clearRemainingAgentGenerationPlaceholders(project.Nodes, targetNodeID)
|
||||
if !isolated {
|
||||
project.Messages = appendMissingIncrementalArtifactMessages(project.Messages, threadID, prompt, plan.GeneratedNodes, now.Add(-time.Millisecond))
|
||||
}
|
||||
} else {
|
||||
baseNodes := nonGeneratingNodes(project.Nodes)
|
||||
if targetNodeID != "" {
|
||||
@@ -2986,7 +2999,15 @@ func (s *DesignService) planWithIncrementalImages(ctx context.Context, projectID
|
||||
|
||||
done := make(chan struct{})
|
||||
if appendArtifacts {
|
||||
go s.emitGenerationHeartbeat(ctx, projectID, threadID, req.Prompt, done)
|
||||
heartbeatStopped := make(chan struct{})
|
||||
go func() {
|
||||
defer close(heartbeatStopped)
|
||||
s.emitGenerationHeartbeat(ctx, projectID, threadID, req.Prompt, done)
|
||||
}()
|
||||
defer func() {
|
||||
close(done)
|
||||
<-heartbeatStopped
|
||||
}()
|
||||
}
|
||||
var mu sync.Mutex
|
||||
persistedNodes := make([]design.Node, 0, plannedGenerationResultCapacity(req))
|
||||
@@ -3004,9 +3025,6 @@ func (s *DesignService) planWithIncrementalImages(ctx context.Context, projectID
|
||||
persistedNodes = append(persistedNodes, node)
|
||||
return s.applyIncrementalGeneratedNode(ctx, projectID, threadID, prompt, targetNodeID, node, appendArtifacts)
|
||||
})
|
||||
if appendArtifacts {
|
||||
close(done)
|
||||
}
|
||||
if len(persistedNodes) > 0 {
|
||||
plan.GeneratedNodes = append([]design.Node(nil), persistedNodes...)
|
||||
}
|
||||
@@ -3039,6 +3057,32 @@ func completedGeneratedImageNodes(nodes []design.Node) []design.Node {
|
||||
return completed
|
||||
}
|
||||
|
||||
func mergeCompletedIncrementalGeneratedNodes(nodes []design.Node, generated []design.Node, targetNodeID string) []design.Node {
|
||||
completed := completedGeneratedImageNodes(generated)
|
||||
if len(completed) == 0 {
|
||||
return nodes
|
||||
}
|
||||
if strings.TrimSpace(targetNodeID) != "" {
|
||||
return mergeGeneratedNodesIntoTarget(nodes, completed, targetNodeID)
|
||||
}
|
||||
for _, node := range completed {
|
||||
nodes = upsertGeneratedNode(nodes, node)
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
func appendMissingIncrementalArtifactMessages(messages []design.Message, threadID string, prompt string, nodes []design.Node, createdAt time.Time) []design.Message {
|
||||
existing := generatedImageArtifactsByID(messages)
|
||||
for _, node := range completedGeneratedImageNodes(nodes) {
|
||||
if _, ok := existing[node.ID]; ok {
|
||||
continue
|
||||
}
|
||||
messages = append(messages, toolArtifactsMessage(threadID, prompt, []design.Node{node}, createdAt))
|
||||
existing[node.ID] = design.GeneratorTaskArtifact{}
|
||||
}
|
||||
return messages
|
||||
}
|
||||
|
||||
func plannedGenerationResultCapacity(req design.AgentRequest) int {
|
||||
count := req.TaskPlan.ImageCount
|
||||
if count <= 0 {
|
||||
@@ -3237,10 +3281,7 @@ func (s *DesignService) maybeAddResearchStep(ctx context.Context, projectID stri
|
||||
Messages: messages,
|
||||
Memory: buildAgentMemory(project, prompt, mode, messages),
|
||||
})
|
||||
if !decision.ShouldSearch {
|
||||
return nil
|
||||
}
|
||||
query := strings.TrimSpace(decision.Query)
|
||||
query := enabledResearchQuery(prompt, mode, decision)
|
||||
if query == "" {
|
||||
return nil
|
||||
}
|
||||
@@ -3254,16 +3295,38 @@ func (s *DesignService) maybeAddPlannedResearchStep(ctx context.Context, project
|
||||
if strings.TrimSpace(plan.Intent) == "" {
|
||||
return s.maybeAddResearchStep(ctx, projectID, threadID, project, prompt, mode, messages, enabled)
|
||||
}
|
||||
if !plan.ShouldSearch {
|
||||
return nil
|
||||
}
|
||||
query := strings.TrimSpace(plan.SearchQuery)
|
||||
if query == "" && plan.ShouldSearch {
|
||||
query = fallbackResearchQuery(prompt, mode)
|
||||
}
|
||||
if query == "" {
|
||||
return nil
|
||||
}
|
||||
return s.addResearchStep(ctx, projectID, threadID, query)
|
||||
}
|
||||
|
||||
func enabledResearchQuery(prompt string, mode string, decision design.AgentResearchDecision) string {
|
||||
if strings.TrimSpace(decision.Query) != "" {
|
||||
return strings.TrimSpace(decision.Query)
|
||||
}
|
||||
if !shouldSearchWhenEnabled(prompt, mode) {
|
||||
return ""
|
||||
}
|
||||
return fallbackResearchQuery(prompt, mode)
|
||||
}
|
||||
|
||||
func shouldSearchWhenEnabled(prompt string, mode string) bool {
|
||||
text := strings.ToLower(strings.TrimSpace(prompt))
|
||||
if text == "" || isConversationalPrompt(text) {
|
||||
return false
|
||||
}
|
||||
modeText := strings.ToLower(strings.TrimSpace(mode))
|
||||
if strings.Contains(modeText, "image-action") {
|
||||
return shouldRunResearchFallback(prompt, mode)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *DesignService) decideResearch(ctx context.Context, req design.AgentResearchDecisionRequest) design.AgentResearchDecision {
|
||||
if decider, ok := s.creativeAgent.(design.ResearchDecider); ok {
|
||||
if decision, err := decider.DecideResearch(ctx, req); err == nil {
|
||||
@@ -3366,6 +3429,11 @@ func shouldGenerateForAgentPrompt(prompt string, mode string, messages []design.
|
||||
return false
|
||||
}
|
||||
|
||||
func shouldForceImageMode(mode string) bool {
|
||||
modeText := strings.ToLower(strings.TrimSpace(mode))
|
||||
return strings.Contains(modeText, "image-action") || modeText == "image" || modeText == "pure-image"
|
||||
}
|
||||
|
||||
func hasAgentImageInput(messages []design.AgentChatMessage) bool {
|
||||
for _, message := range messages {
|
||||
for _, content := range message.Contents {
|
||||
@@ -5692,7 +5760,7 @@ func inspirationItems() []design.InspirationItem {
|
||||
return []design.InspirationItem{
|
||||
{ID: "luxury-spring", Title: "2026 春季系列设计趋势", Category: "Luxury Showcase", Description: "强对比摄影、低饱和色块与清晰购买动线。", Accent: "#111827"},
|
||||
{ID: "natural-ui", Title: "极简自然主义 UI", Category: "Mobile App", Description: "玻璃拟态信息卡与植物识别流程。", Accent: "#6b8f71"},
|
||||
{ID: "blue-geometry", Title: "蓝色 3D 几何广告", Category: "Campaign", Description: "冷感材质、漂浮图形和高亮 CTA。", Accent: "#2563eb"},
|
||||
{ID: "blue-geometry", Title: "蓝色立体几何广告", Category: "Campaign", Description: "冷感材质、漂浮图形和高亮 CTA。", Accent: "#2563eb"},
|
||||
{ID: "brand-kit", Title: "天然护肤品牌板", Category: "Brand Kit", Description: "字体、插画、包装和详情页模块统一沉淀。", Accent: "#c8a568"},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user