Skip to content

Commit 5b98cb5

Browse files
committed
feat: Add dynamic clarifying questions for incomplete creative briefs
- Updated orchestrator.parse_brief() to detect missing critical fields and return clarifying questions - Modified /api/brief/parse endpoint to return requires_clarification flag - Updated frontend types and App.tsx to display clarifying questions - Agent now asks targeted questions based on what's actually missing from the brief - Prevents hallucination by requiring user input for missing information
1 parent 5d3f053 commit 5b98cb5

6 files changed

Lines changed: 203 additions & 53 deletions

File tree

content-gen/src/app.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ async def generate():
179179
async def parse_brief():
180180
"""
181181
Parse a free-text creative brief into structured format.
182+
If critical information is missing, return clarifying questions.
182183
183184
Request body:
184185
{
@@ -188,7 +189,8 @@ async def parse_brief():
188189
}
189190
190191
Returns:
191-
Structured CreativeBrief JSON for user confirmation.
192+
Structured CreativeBrief JSON for user confirmation,
193+
or clarifying questions if info is missing.
192194
"""
193195
data = await request.get_json()
194196
brief_text = data.get("brief_text", "")
@@ -214,7 +216,34 @@ async def parse_brief():
214216
logger.warning(f"Failed to save brief message to CosmosDB: {e}")
215217

216218
orchestrator = get_orchestrator()
217-
parsed_brief = await orchestrator.parse_brief(brief_text)
219+
parsed_brief, clarifying_questions = await orchestrator.parse_brief(brief_text)
220+
221+
# Check if we need clarifying questions
222+
if clarifying_questions:
223+
# Save the clarifying questions as assistant response
224+
try:
225+
cosmos_service = await get_cosmos_service()
226+
await cosmos_service.add_message_to_conversation(
227+
conversation_id=conversation_id,
228+
user_id=user_id,
229+
message={
230+
"role": "assistant",
231+
"content": clarifying_questions,
232+
"agent": "PlanningAgent",
233+
"timestamp": datetime.now(timezone.utc).isoformat()
234+
}
235+
)
236+
except Exception as e:
237+
logger.warning(f"Failed to save clarifying questions to CosmosDB: {e}")
238+
239+
return jsonify({
240+
"brief": parsed_brief.model_dump(),
241+
"requires_clarification": True,
242+
"requires_confirmation": False,
243+
"clarifying_questions": clarifying_questions,
244+
"conversation_id": conversation_id,
245+
"message": clarifying_questions
246+
})
218247

219248
# Save the assistant's parsing response
220249
try:
@@ -234,6 +263,7 @@ async def parse_brief():
234263

235264
return jsonify({
236265
"brief": parsed_brief.model_dump(),
266+
"requires_clarification": False,
237267
"requires_confirmation": True,
238268
"conversation_id": conversation_id,
239269
"message": "Please review and confirm the parsed creative brief"

content-gen/src/backend/orchestrator.py

Lines changed: 137 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -95,25 +95,82 @@
9595
- Image creation → hand off to image_content_agent
9696
- Content validation → hand off to compliance_agent
9797
98+
### Handling Planning Agent Responses:
99+
When the planning_agent returns:
100+
- If it returns CLARIFYING QUESTIONS (not a JSON brief), relay those questions to the user and WAIT for their response before proceeding
101+
- If it returns a COMPLETE parsed brief (JSON), proceed with the content generation workflow
102+
- Do NOT proceed to research or content generation until you have a complete, user-confirmed brief
103+
98104
{app_settings.brand_guidelines.get_compliance_prompt()}
99105
"""
100106

101107
PLANNING_INSTRUCTIONS = """You are a Planning Agent specializing in creative brief interpretation for MARKETING CAMPAIGNS ONLY.
102108
Your scope is limited to parsing and structuring marketing creative briefs.
103109
Do not process requests unrelated to marketing content creation.
104110
105-
When given a creative brief, extract and return a JSON object with:
106-
- overview: Campaign summary
107-
- objectives: What the campaign aims to achieve
108-
- target_audience: Who the content is for
109-
- key_message: Core message to communicate
110-
- tone_and_style: Voice and aesthetic direction
111-
- deliverable: Expected outputs (social posts, ads, etc.)
112-
- timelines: Any deadline information
113-
- visual_guidelines: Visual style requirements
114-
- cta: Call to action
115-
116-
After parsing, hand back to the triage agent with your results.
111+
When given a creative brief, extract and structure a JSON object with these REQUIRED fields:
112+
- overview: Campaign summary (what is the campaign about?)
113+
- objectives: What the campaign aims to achieve (goals, KPIs, success metrics)
114+
- target_audience: Who the content is for (demographics, psychographics, customer segments)
115+
- key_message: Core message to communicate (main value proposition)
116+
- tone_and_style: Voice and aesthetic direction (professional, playful, urgent, etc.)
117+
- deliverable: Expected outputs (social posts, ads, email, banner, etc.)
118+
- timelines: Any deadline information (launch date, review dates)
119+
- visual_guidelines: Visual style requirements (colors, imagery style, product focus)
120+
- cta: Call to action (what should the audience do?)
121+
122+
CRITICAL - NO HALLUCINATION POLICY:
123+
You MUST NOT make up, infer, assume, or hallucinate information that was not explicitly provided by the user.
124+
If the user did not mention a field, that field is MISSING - do not fill it with assumed values.
125+
Only extract information that is DIRECTLY STATED in the user's input.
126+
127+
CRITICAL FIELDS (must be explicitly provided before proceeding):
128+
- objectives
129+
- target_audience
130+
- key_message
131+
- deliverable
132+
- tone_and_style
133+
134+
CLARIFYING QUESTIONS PROCESS:
135+
Step 1: Analyze the user's input and identify what information was EXPLICITLY provided.
136+
Step 2: Determine which CRITICAL fields are missing or unclear.
137+
Step 3: Generate a DYNAMIC response that:
138+
a) Acknowledges SPECIFICALLY what the user DID provide (reference their actual words/content)
139+
b) Clearly lists ONLY the missing critical fields as bullet points
140+
c) Asks targeted questions for ONLY the missing fields (do not ask about fields already provided)
141+
142+
RESPONSE FORMAT FOR MISSING INFORMATION:
143+
---
144+
Thanks for sharing your creative brief! Here's what I understood:
145+
✓ [List each piece of information the user DID provide, referencing their specific input]
146+
147+
However, I'm missing some key details to create effective marketing content:
148+
149+
**Missing Information:**
150+
• **[Field Name]**: [Contextual question based on what they provided]
151+
[Only list fields that are actually missing]
152+
153+
Once you provide these details, I'll create a comprehensive content plan for your campaign.
154+
---
155+
156+
DYNAMIC QUESTION EXAMPLES:
157+
- If user mentions a product but no audience: "Who is the target audience for [their product name]?"
158+
- If user mentions audience but no deliverable: "What type of content would resonate best with [their audience]?"
159+
- If user mentions a goal but no tone: "What tone would best convey [their stated goal] to your audience?"
160+
161+
DO NOT:
162+
- Ask about fields the user already provided
163+
- Use generic questions - always reference the user's specific input
164+
- Invent objectives the user didn't state
165+
- Assume a target audience based on the product
166+
- Create a key message that wasn't provided
167+
- Guess at deliverable types
168+
- Fill in "reasonable defaults" for missing information
169+
- Return a JSON brief until ALL critical fields are explicitly provided
170+
171+
When you have sufficient EXPLICIT information for all critical fields, return a JSON object with all fields populated.
172+
For non-critical fields that are missing (timelines, visual_guidelines, cta), you may use "Not specified" - do NOT make up values.
173+
After parsing a complete brief, hand back to the triage agent with your results.
117174
"""
118175

119176
RESEARCH_INSTRUCTIONS = """You are a Research Agent for a retail marketing system.
@@ -497,44 +554,74 @@ async def send_user_response(
497554
async def parse_brief(
498555
self,
499556
brief_text: str
500-
) -> CreativeBrief:
557+
) -> tuple[CreativeBrief, str | None]:
501558
"""
502559
Parse a free-text creative brief into structured format.
560+
If critical information is missing, return clarifying questions.
503561
504562
Args:
505563
brief_text: Free-text creative brief from user
506564
507565
Returns:
508-
CreativeBrief: Parsed and structured creative brief
566+
tuple: (CreativeBrief, clarifying_questions_or_none)
567+
- If all critical fields are provided: (brief, None)
568+
- If critical fields are missing: (partial_brief, clarifying_questions_string)
509569
"""
510570
if not self._initialized:
511571
self.initialize()
512572

513573
planning_agent = self._agents["planning"]
514574

515-
parse_prompt = f"""
516-
Parse the following creative brief into the structured JSON format:
575+
# First, analyze the brief and check for missing critical fields
576+
analysis_prompt = f"""
577+
Analyze this creative brief request and determine if all critical information is provided.
517578
579+
**User's Request:**
518580
{brief_text}
519581
520-
Return ONLY a valid JSON object with these fields:
521-
- overview
522-
- objectives
523-
- target_audience
524-
- key_message
525-
- tone_and_style
526-
- deliverable
527-
- timelines
528-
- visual_guidelines
529-
- cta
582+
**Critical Fields Required:**
583+
1. objectives - What is the campaign trying to achieve?
584+
2. target_audience - Who is the intended audience?
585+
3. key_message - What is the core message or value proposition?
586+
4. deliverable - What content format is needed (e.g., email, social post, ad)?
587+
5. tone_and_style - What is the desired tone (professional, casual, playful)?
588+
589+
**Your Task:**
590+
1. Extract any information that IS explicitly provided
591+
2. Identify which critical fields are MISSING or unclear
592+
3. Return a JSON response in this EXACT format:
593+
594+
```json
595+
{{
596+
"status": "complete" or "incomplete",
597+
"extracted_fields": {{
598+
"overview": "...",
599+
"objectives": "...",
600+
"target_audience": "...",
601+
"key_message": "...",
602+
"tone_and_style": "...",
603+
"deliverable": "...",
604+
"timelines": "...",
605+
"visual_guidelines": "...",
606+
"cta": "..."
607+
}},
608+
"missing_fields": ["field1", "field2"],
609+
"clarifying_message": "If incomplete, a friendly message acknowledging what was provided and asking specific questions about what's missing. Reference the user's actual input. If complete, leave empty."
610+
}}
611+
```
612+
613+
**Rules:**
614+
- Set status to "complete" only if objectives, target_audience, key_message, deliverable, AND tone_and_style are all clearly specified
615+
- For extracted_fields, use empty string "" for any field not mentioned
616+
- Do NOT invent or assume information that wasn't explicitly stated
617+
- Make clarifying questions specific to the user's context (reference their product/campaign)
530618
"""
531619

532-
# Use the agent's run method (async in Agent Framework)
533-
response = await planning_agent.run(parse_prompt)
620+
# Use the agent's run method
621+
response = await planning_agent.run(analysis_prompt)
534622

535-
# Parse the JSON response
623+
# Parse the analysis response
536624
try:
537-
# Extract JSON from the response
538625
response_text = str(response)
539626
if "```json" in response_text:
540627
json_start = response_text.index("```json") + 7
@@ -545,26 +632,38 @@ async def parse_brief(
545632
json_end = response_text.index("```", json_start)
546633
response_text = response_text[json_start:json_end].strip()
547634

548-
brief_data = json.loads(response_text)
635+
analysis = json.loads(response_text)
636+
brief_data = analysis.get("extracted_fields", {})
549637

550-
# Ensure all fields are strings (agent might return dicts for some fields)
638+
# Ensure all fields are strings
551639
for key in brief_data:
552640
if isinstance(brief_data[key], dict):
553-
# Convert dict to formatted string
554641
brief_data[key] = " | ".join(f"{k}: {v}" for k, v in brief_data[key].items())
555642
elif isinstance(brief_data[key], list):
556-
# Convert list to comma-separated string
557643
brief_data[key] = ", ".join(str(item) for item in brief_data[key])
558644
elif brief_data[key] is None:
559645
brief_data[key] = ""
560646
elif not isinstance(brief_data[key], str):
561647
brief_data[key] = str(brief_data[key])
562648

563-
return CreativeBrief(**brief_data)
649+
# Ensure all required fields exist
650+
for field in ['overview', 'objectives', 'target_audience', 'key_message',
651+
'tone_and_style', 'deliverable', 'timelines', 'visual_guidelines', 'cta']:
652+
if field not in brief_data:
653+
brief_data[field] = ""
654+
655+
brief = CreativeBrief(**brief_data)
656+
657+
# Check if we need clarifying questions
658+
if analysis.get("status") == "incomplete" and analysis.get("clarifying_message"):
659+
return (brief, analysis["clarifying_message"])
660+
661+
return (brief, None)
662+
564663
except Exception as e:
565-
logger.error(f"Failed to parse brief response: {e}")
566-
# Try to extract fields manually from the input text
567-
return self._extract_brief_from_text(brief_text)
664+
logger.error(f"Failed to parse brief analysis response: {e}")
665+
# Fallback to basic extraction
666+
return (self._extract_brief_from_text(brief_text), None)
568667

569668
def _extract_brief_from_text(self, text: str) -> CreativeBrief:
570669
"""Extract brief fields from labeled text like 'Overview: ...'"""

content-gen/src/frontend-server/static/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
77
<title>Content Generation Accelerator</title>
8-
<script type="module" crossorigin src="/assets/index-DOcAI_0V.js"></script>
8+
<script type="module" crossorigin src="/assets/index-19loSOvH.js"></script>
99
<link rel="stylesheet" crossorigin href="/assets/index-D9ems1Py.css">
1010
</head>
1111
<body>

content-gen/src/frontend/src/App.tsx

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -288,19 +288,37 @@ function App() {
288288

289289
if (isBriefLike && !confirmedBrief) {
290290
// Parse as a creative brief
291-
setGenerationStatus('Parsing creative brief...');
291+
setGenerationStatus('Analyzing creative brief...');
292292
const parsed = await parseBrief(content, conversationId, userId, signal);
293-
setPendingBrief(parsed.brief);
294-
setGenerationStatus('');
295293

296-
const assistantMessage: ChatMessage = {
297-
id: uuidv4(),
298-
role: 'assistant',
299-
content: "I've parsed your creative brief. Please review the details below and let me know if you'd like to make any changes. You can say things like \"change the target audience to...\" or \"add a call to action...\". When everything looks good, click **Confirm Brief** to proceed.",
300-
agent: 'PlanningAgent',
301-
timestamp: new Date().toISOString(),
302-
};
303-
setMessages(prev => [...prev, assistantMessage]);
294+
// Check if clarification is needed
295+
if (parsed.requires_clarification && parsed.clarifying_questions) {
296+
// Set partial brief for display but show clarifying questions
297+
setPendingBrief(parsed.brief);
298+
setGenerationStatus('');
299+
300+
const assistantMessage: ChatMessage = {
301+
id: uuidv4(),
302+
role: 'assistant',
303+
content: parsed.clarifying_questions,
304+
agent: 'PlanningAgent',
305+
timestamp: new Date().toISOString(),
306+
};
307+
setMessages(prev => [...prev, assistantMessage]);
308+
} else {
309+
// Brief is complete, show for confirmation
310+
setPendingBrief(parsed.brief);
311+
setGenerationStatus('');
312+
313+
const assistantMessage: ChatMessage = {
314+
id: uuidv4(),
315+
role: 'assistant',
316+
content: "I've parsed your creative brief. Please review the details below and let me know if you'd like to make any changes. You can say things like \"change the target audience to...\" or \"add a call to action...\". When everything looks good, click **Confirm Brief** to proceed.",
317+
agent: 'PlanningAgent',
318+
timestamp: new Date().toISOString(),
319+
};
320+
setMessages(prev => [...prev, assistantMessage]);
321+
}
304322
} else {
305323
// Stream chat response
306324
let fullContent = '';

content-gen/src/frontend/src/types/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,10 @@ export interface BrandGuidelines {
9494
export interface ParsedBriefResponse {
9595
brief: CreativeBrief;
9696
requires_confirmation: boolean;
97+
requires_clarification?: boolean;
98+
clarifying_questions?: string;
9799
message: string;
100+
conversation_id?: string;
98101
}
99102

100103
export interface GeneratedContent {

content-gen/src/static/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
77
<title>Content Generation Accelerator</title>
8-
<script type="module" crossorigin src="/assets/index-DOcAI_0V.js"></script>
8+
<script type="module" crossorigin src="/assets/index-19loSOvH.js"></script>
99
<link rel="stylesheet" crossorigin href="/assets/index-D9ems1Py.css">
1010
</head>
1111
<body>

0 commit comments

Comments
 (0)