Commit
·
9474b94
1
Parent(s):
533051b
Refactor ad generation process to improve progress tracking and niche-specific guidance
Browse files- Enhanced the GeneratePage component to provide detailed progress updates during ad generation, including estimated times for each step.
- Updated the generator service to process strategies in parallel and introduced niche-specific visual guidance for image generation.
- Improved the creative_designer method to incorporate niche requirements, ensuring generated images align with specific categories.
- Added error handling for progress intervals to maintain stability during ad generation.
- frontend/app/generate/page.tsx +127 -29
- services/generator.py +8 -1
- services/third_flow.py +50 -5
frontend/app/generate/page.tsx
CHANGED
|
@@ -242,42 +242,131 @@ export default function GeneratePage() {
|
|
| 242 |
reset();
|
| 243 |
setIsGenerating(true);
|
| 244 |
setGenerationStartTime(Date.now());
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
|
| 251 |
try {
|
| 252 |
-
//
|
| 253 |
-
|
| 254 |
-
const progressSteps = [
|
| 255 |
-
{ step: "copy" as const, progress: 20, message: "Researching psychology triggers..." },
|
| 256 |
-
{ step: "copy" as const, progress: 35, message: "Retrieving marketing knowledge..." },
|
| 257 |
-
{ step: "copy" as const, progress: 50, message: "Creating creative strategies..." },
|
| 258 |
-
{ step: "copy" as const, progress: 70, message: "Generating image prompts and copy..." },
|
| 259 |
-
{ step: "image" as const, progress: 85, message: "Generating images..." },
|
| 260 |
-
];
|
| 261 |
-
let stepIndex = 0;
|
| 262 |
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
setProgress({
|
| 270 |
-
step: "image",
|
| 271 |
-
progress: currentProgress,
|
| 272 |
-
message: "Generating images...",
|
| 273 |
-
});
|
| 274 |
-
}
|
| 275 |
-
}, 2000);
|
| 276 |
|
| 277 |
// Generate ad using extensive
|
| 278 |
const result = await generateExtensiveAd(data);
|
| 279 |
|
| 280 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
|
| 282 |
// Verify all images are included
|
| 283 |
if (result.images && result.images.length > 0) {
|
|
@@ -292,6 +381,9 @@ export default function GeneratePage() {
|
|
| 292 |
message: "Saving to database...",
|
| 293 |
});
|
| 294 |
|
|
|
|
|
|
|
|
|
|
| 295 |
setCurrentGeneration(result);
|
| 296 |
setProgress({
|
| 297 |
step: "complete",
|
|
@@ -301,6 +393,12 @@ export default function GeneratePage() {
|
|
| 301 |
|
| 302 |
toast.success("Ad generated successfully using Extensive!");
|
| 303 |
} catch (error: any) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 304 |
setError(error.message || "Failed to generate ad");
|
| 305 |
setProgress({
|
| 306 |
step: "error",
|
|
|
|
| 242 |
reset();
|
| 243 |
setIsGenerating(true);
|
| 244 |
setGenerationStartTime(Date.now());
|
| 245 |
+
|
| 246 |
+
// Calculate estimated time based on strategies and images
|
| 247 |
+
// Step 1 (Researcher): ~10-15 seconds
|
| 248 |
+
// Step 2 (Retrieve knowledge): ~15-20 seconds (parallel)
|
| 249 |
+
// Step 3 (Creative Director): ~20-30 seconds
|
| 250 |
+
// Step 4 (Process strategies): ~10-15 seconds per strategy (parallel)
|
| 251 |
+
// Step 5 (Generate images): ~15-30 seconds per image
|
| 252 |
+
const estimatedTimePerStrategy = 10; // seconds for processing strategy
|
| 253 |
+
const estimatedTimePerImage = 20; // seconds per image
|
| 254 |
+
const totalEstimatedTime =
|
| 255 |
+
15 + // Step 1: Researcher
|
| 256 |
+
20 + // Step 2: Retrieve knowledge
|
| 257 |
+
25 + // Step 3: Creative Director
|
| 258 |
+
(data.num_strategies * estimatedTimePerStrategy) + // Step 4: Process strategies
|
| 259 |
+
(data.num_strategies * data.num_images * estimatedTimePerImage); // Step 5: Generate images
|
| 260 |
+
|
| 261 |
+
let elapsedTime = 0;
|
| 262 |
+
let progressInterval: NodeJS.Timeout | null = null;
|
| 263 |
+
|
| 264 |
+
// Progress steps aligned with actual backend steps
|
| 265 |
+
const progressSteps = [
|
| 266 |
+
{
|
| 267 |
+
step: "copy" as const,
|
| 268 |
+
progress: 5,
|
| 269 |
+
message: "🔍 Step 1: Researching psychology triggers, angles, and concepts...",
|
| 270 |
+
duration: 15 // seconds
|
| 271 |
+
},
|
| 272 |
+
{
|
| 273 |
+
step: "copy" as const,
|
| 274 |
+
progress: 15,
|
| 275 |
+
message: "📚 Step 2: Retrieving marketing knowledge...",
|
| 276 |
+
duration: 20 // seconds
|
| 277 |
+
},
|
| 278 |
+
{
|
| 279 |
+
step: "copy" as const,
|
| 280 |
+
progress: 30,
|
| 281 |
+
message: `🎨 Step 3: Creating ${data.num_strategies} creative strategy/strategies...`,
|
| 282 |
+
duration: 25 // seconds
|
| 283 |
+
},
|
| 284 |
+
{
|
| 285 |
+
step: "copy" as const,
|
| 286 |
+
progress: 50,
|
| 287 |
+
message: `⚡ Step 4: Processing ${data.num_strategies} strategies in parallel...`,
|
| 288 |
+
duration: data.num_strategies * estimatedTimePerStrategy // seconds
|
| 289 |
+
},
|
| 290 |
+
{
|
| 291 |
+
step: "image" as const,
|
| 292 |
+
progress: 70,
|
| 293 |
+
message: `🖼️ Step 5: Generating ${data.num_images} image(s) per strategy...`,
|
| 294 |
+
duration: data.num_strategies * data.num_images * estimatedTimePerImage // seconds
|
| 295 |
+
},
|
| 296 |
+
];
|
| 297 |
+
|
| 298 |
+
let currentStepIndex = 0;
|
| 299 |
+
let stepStartTime = 0;
|
| 300 |
+
const progressUpdateInterval = 500; // Update every 500ms
|
| 301 |
+
|
| 302 |
+
const updateProgress = () => {
|
| 303 |
+
elapsedTime += progressUpdateInterval / 1000; // Convert to seconds
|
| 304 |
+
|
| 305 |
+
// Determine which step we should be on based on elapsed time
|
| 306 |
+
let cumulativeTime = 0;
|
| 307 |
+
let targetStepIndex = 0;
|
| 308 |
+
let stepStartCumulativeTime = 0;
|
| 309 |
+
|
| 310 |
+
for (let i = 0; i < progressSteps.length; i++) {
|
| 311 |
+
const stepStart = cumulativeTime;
|
| 312 |
+
cumulativeTime += progressSteps[i].duration;
|
| 313 |
+
if (elapsedTime <= cumulativeTime) {
|
| 314 |
+
targetStepIndex = i;
|
| 315 |
+
stepStartCumulativeTime = stepStart;
|
| 316 |
+
break;
|
| 317 |
+
}
|
| 318 |
+
targetStepIndex = i;
|
| 319 |
+
stepStartCumulativeTime = stepStart;
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
// Update to the correct step if we've moved forward
|
| 323 |
+
if (targetStepIndex > currentStepIndex) {
|
| 324 |
+
currentStepIndex = targetStepIndex;
|
| 325 |
+
stepStartTime = stepStartCumulativeTime;
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
const currentStep = progressSteps[currentStepIndex];
|
| 329 |
+
const stepElapsed = elapsedTime - stepStartTime;
|
| 330 |
+
const stepProgress = Math.min(1, Math.max(0, stepElapsed / currentStep.duration));
|
| 331 |
+
|
| 332 |
+
// Calculate overall progress
|
| 333 |
+
let overallProgress = currentStep.progress;
|
| 334 |
+
if (currentStepIndex < progressSteps.length - 1) {
|
| 335 |
+
// Within current step, interpolate to next step's progress
|
| 336 |
+
const nextStep = progressSteps[currentStepIndex + 1];
|
| 337 |
+
const progressRange = nextStep.progress - currentStep.progress;
|
| 338 |
+
overallProgress = currentStep.progress + (stepProgress * progressRange);
|
| 339 |
+
} else {
|
| 340 |
+
// Last step, interpolate from current step progress to 90%
|
| 341 |
+
overallProgress = currentStep.progress + (stepProgress * (90 - currentStep.progress));
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
setProgress({
|
| 345 |
+
step: currentStep.step,
|
| 346 |
+
progress: Math.min(90, overallProgress), // Cap at 90% until completion
|
| 347 |
+
message: currentStep.message,
|
| 348 |
+
});
|
| 349 |
+
};
|
| 350 |
|
| 351 |
try {
|
| 352 |
+
// Start progress updates
|
| 353 |
+
progressInterval = setInterval(updateProgress, progressUpdateInterval);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 354 |
|
| 355 |
+
// Set initial progress
|
| 356 |
+
setProgress({
|
| 357 |
+
step: progressSteps[0].step,
|
| 358 |
+
progress: progressSteps[0].progress,
|
| 359 |
+
message: progressSteps[0].message,
|
| 360 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 361 |
|
| 362 |
// Generate ad using extensive
|
| 363 |
const result = await generateExtensiveAd(data);
|
| 364 |
|
| 365 |
+
// Clear progress interval
|
| 366 |
+
if (progressInterval) {
|
| 367 |
+
clearInterval(progressInterval);
|
| 368 |
+
progressInterval = null;
|
| 369 |
+
}
|
| 370 |
|
| 371 |
// Verify all images are included
|
| 372 |
if (result.images && result.images.length > 0) {
|
|
|
|
| 381 |
message: "Saving to database...",
|
| 382 |
});
|
| 383 |
|
| 384 |
+
// Small delay to show saving step
|
| 385 |
+
await new Promise(resolve => setTimeout(resolve, 500));
|
| 386 |
+
|
| 387 |
setCurrentGeneration(result);
|
| 388 |
setProgress({
|
| 389 |
step: "complete",
|
|
|
|
| 393 |
|
| 394 |
toast.success("Ad generated successfully using Extensive!");
|
| 395 |
} catch (error: any) {
|
| 396 |
+
// Clear progress interval on error
|
| 397 |
+
if (progressInterval) {
|
| 398 |
+
clearInterval(progressInterval);
|
| 399 |
+
progressInterval = null;
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
setError(error.message || "Failed to generate ad");
|
| 403 |
setProgress({
|
| 404 |
step: "error",
|
services/generator.py
CHANGED
|
@@ -1967,10 +1967,17 @@ CONCEPT: {concept['name']}
|
|
| 1967 |
# Step 4: Process strategies in parallel (designer + copywriter)
|
| 1968 |
print(f"⚡ Step 4: Processing {len(creative_strategies)} strategies in parallel...")
|
| 1969 |
from concurrent.futures import ThreadPoolExecutor as TPE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1970 |
|
| 1971 |
with TPE(max_workers=8) as executor:
|
| 1972 |
strategy_results = list(executor.map(
|
| 1973 |
-
|
| 1974 |
creative_strategies
|
| 1975 |
))
|
| 1976 |
|
|
|
|
| 1967 |
# Step 4: Process strategies in parallel (designer + copywriter)
|
| 1968 |
print(f"⚡ Step 4: Processing {len(creative_strategies)} strategies in parallel...")
|
| 1969 |
from concurrent.futures import ThreadPoolExecutor as TPE
|
| 1970 |
+
from functools import partial
|
| 1971 |
+
|
| 1972 |
+
# Create a partial function that includes the niche parameter
|
| 1973 |
+
process_strategy_with_niche = partial(
|
| 1974 |
+
third_flow_service.process_strategy,
|
| 1975 |
+
niche=niche_display
|
| 1976 |
+
)
|
| 1977 |
|
| 1978 |
with TPE(max_workers=8) as executor:
|
| 1979 |
strategy_results = list(executor.map(
|
| 1980 |
+
process_strategy_with_niche,
|
| 1981 |
creative_strategies
|
| 1982 |
))
|
| 1983 |
|
services/third_flow.py
CHANGED
|
@@ -447,16 +447,57 @@ class ThirdFlowService:
|
|
| 447 |
print(f"Error in creative_director: {e}")
|
| 448 |
return []
|
| 449 |
|
| 450 |
-
def creative_designer(self, creative_strategy: CreativeStrategies) -> str:
|
| 451 |
"""
|
| 452 |
Generate image prompt from creative strategy.
|
| 453 |
|
| 454 |
Args:
|
| 455 |
creative_strategy: Creative strategy object
|
|
|
|
| 456 |
|
| 457 |
Returns:
|
| 458 |
Image generation prompt
|
| 459 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 460 |
strategy_str = f"""Psychology Trigger: {creative_strategy.phsychologyTrigger}
|
| 461 |
Angle: {creative_strategy.angle}
|
| 462 |
Concept: {creative_strategy.concept}
|
|
@@ -471,13 +512,15 @@ class ThirdFlowService:
|
|
| 471 |
"content": [
|
| 472 |
{
|
| 473 |
"type": "text",
|
| 474 |
-
"text": """You are the Creative Designer for the affiliate marketing company which makes the prompt from creative strategy given for the ad images in the affiliate marketing.
|
| 475 |
Nano Banana image model will be used to generate the images
|
| 476 |
Affiliate marketing is a performance-based model where you promote someone else's product or service and earn a commission for each qualified action (click, lead, or sale).
|
| 477 |
In affiliate marketing 'Low-production, realistic images often outperform studio creatives' runs most.
|
| 478 |
|
| 479 |
For nano banana image model here's structure for the prompt: [The Hook] + [The Subject] + [The Context/Setting] + [The Technical Polish]
|
| 480 |
|
|
|
|
|
|
|
| 481 |
CRITICAL: If the image includes people or faces, ensure they look like real, original people with:
|
| 482 |
- Photorealistic faces with natural skin texture, visible pores, and realistic skin imperfections
|
| 483 |
- Natural facial asymmetry (no perfectly symmetrical faces)
|
|
@@ -498,7 +541,7 @@ class ThirdFlowService:
|
|
| 498 |
"type": "text",
|
| 499 |
"text": f"""Following is the creative strategy:
|
| 500 |
{strategy_str}
|
| 501 |
-
Provide the image prompt for the given creative strategy."""
|
| 502 |
}
|
| 503 |
]
|
| 504 |
}
|
|
@@ -611,18 +654,20 @@ class ThirdFlowService:
|
|
| 611 |
|
| 612 |
def process_strategy(
|
| 613 |
self,
|
| 614 |
-
creative_strategy: CreativeStrategies
|
|
|
|
| 615 |
) -> tuple[str, str, str, str]:
|
| 616 |
"""
|
| 617 |
Process a single creative strategy to generate prompt and copy.
|
| 618 |
|
| 619 |
Args:
|
| 620 |
creative_strategy: Creative strategy object
|
|
|
|
| 621 |
|
| 622 |
Returns:
|
| 623 |
Tuple of (prompt, title, body, description)
|
| 624 |
"""
|
| 625 |
-
prompt = self.creative_designer(creative_strategy)
|
| 626 |
ad_copy = self.copy_writer(creative_strategy)
|
| 627 |
return (
|
| 628 |
prompt,
|
|
|
|
| 447 |
print(f"Error in creative_director: {e}")
|
| 448 |
return []
|
| 449 |
|
| 450 |
+
def creative_designer(self, creative_strategy: CreativeStrategies, niche: str = "Home Insurance") -> str:
|
| 451 |
"""
|
| 452 |
Generate image prompt from creative strategy.
|
| 453 |
|
| 454 |
Args:
|
| 455 |
creative_strategy: Creative strategy object
|
| 456 |
+
niche: Niche category (Home Insurance or GLP-1)
|
| 457 |
|
| 458 |
Returns:
|
| 459 |
Image generation prompt
|
| 460 |
"""
|
| 461 |
+
# Import niche visual guidance
|
| 462 |
+
from data.visuals import get_niche_visual_guidance
|
| 463 |
+
|
| 464 |
+
# Get niche-specific visual guidance
|
| 465 |
+
niche_lower = niche.lower().replace(" ", "_").replace("-", "_")
|
| 466 |
+
niche_guidance_data = get_niche_visual_guidance(niche_lower)
|
| 467 |
+
|
| 468 |
+
# Build niche-specific guidance text
|
| 469 |
+
if niche_guidance_data:
|
| 470 |
+
niche_guidance = f"""
|
| 471 |
+
NICHE-SPECIFIC REQUIREMENTS ({niche}):
|
| 472 |
+
SUBJECTS TO INCLUDE: {', '.join(niche_guidance_data.get('subjects', []))}
|
| 473 |
+
PROPS TO INCLUDE: {', '.join(niche_guidance_data.get('props', []))}
|
| 474 |
+
AVOID: {', '.join(niche_guidance_data.get('avoid', []))}
|
| 475 |
+
COLOR PREFERENCE: {niche_guidance_data.get('color_preference', 'balanced')}
|
| 476 |
+
|
| 477 |
+
CRITICAL: The image MUST be appropriate for {niche} niche. Ensure all visual elements match this niche category."""
|
| 478 |
+
elif niche_lower == "home_insurance":
|
| 479 |
+
niche_guidance = """
|
| 480 |
+
NICHE-SPECIFIC REQUIREMENTS (Home Insurance):
|
| 481 |
+
SUBJECTS TO INCLUDE: family in front of home, house exterior, homeowner looking confident, couple reviewing papers
|
| 482 |
+
PROPS TO INCLUDE: insurance documents, house keys, tablet showing coverage, family photos
|
| 483 |
+
AVOID: disasters, fire or floods, stressed expressions, dark settings
|
| 484 |
+
COLOR PREFERENCE: trust
|
| 485 |
+
|
| 486 |
+
CRITICAL: The image MUST show home insurance-related content. Show REAL American suburban homes, homeowners, and insurance-related elements."""
|
| 487 |
+
elif niche_lower == "glp1":
|
| 488 |
+
niche_guidance = """
|
| 489 |
+
NICHE-SPECIFIC REQUIREMENTS (GLP-1):
|
| 490 |
+
SUBJECTS TO INCLUDE: confident person smiling, active lifestyle scenes, healthy meal preparation, doctor consultation
|
| 491 |
+
PROPS TO INCLUDE: fitness equipment, healthy food, comfortable clothing
|
| 492 |
+
AVOID: before/after weight comparisons, measuring tapes, scales prominently, needle close-ups
|
| 493 |
+
COLOR PREFERENCE: health
|
| 494 |
+
|
| 495 |
+
CRITICAL: The image MUST be appropriate for GLP-1/weight loss niche. Show lifestyle, health, and confidence-related content, NOT home insurance content."""
|
| 496 |
+
else:
|
| 497 |
+
niche_guidance = f"""
|
| 498 |
+
NICHE-SPECIFIC REQUIREMENTS ({niche}):
|
| 499 |
+
CRITICAL: The image MUST be appropriate for {niche} niche."""
|
| 500 |
+
|
| 501 |
strategy_str = f"""Psychology Trigger: {creative_strategy.phsychologyTrigger}
|
| 502 |
Angle: {creative_strategy.angle}
|
| 503 |
Concept: {creative_strategy.concept}
|
|
|
|
| 512 |
"content": [
|
| 513 |
{
|
| 514 |
"type": "text",
|
| 515 |
+
"text": f"""You are the Creative Designer for the affiliate marketing company which makes the prompt from creative strategy given for the ad images in the affiliate marketing.
|
| 516 |
Nano Banana image model will be used to generate the images
|
| 517 |
Affiliate marketing is a performance-based model where you promote someone else's product or service and earn a commission for each qualified action (click, lead, or sale).
|
| 518 |
In affiliate marketing 'Low-production, realistic images often outperform studio creatives' runs most.
|
| 519 |
|
| 520 |
For nano banana image model here's structure for the prompt: [The Hook] + [The Subject] + [The Context/Setting] + [The Technical Polish]
|
| 521 |
|
| 522 |
+
{niche_guidance}
|
| 523 |
+
|
| 524 |
CRITICAL: If the image includes people or faces, ensure they look like real, original people with:
|
| 525 |
- Photorealistic faces with natural skin texture, visible pores, and realistic skin imperfections
|
| 526 |
- Natural facial asymmetry (no perfectly symmetrical faces)
|
|
|
|
| 541 |
"type": "text",
|
| 542 |
"text": f"""Following is the creative strategy:
|
| 543 |
{strategy_str}
|
| 544 |
+
Provide the image prompt for the given creative strategy. Make sure the prompt follows the NICHE-SPECIFIC REQUIREMENTS above."""
|
| 545 |
}
|
| 546 |
]
|
| 547 |
}
|
|
|
|
| 654 |
|
| 655 |
def process_strategy(
|
| 656 |
self,
|
| 657 |
+
creative_strategy: CreativeStrategies,
|
| 658 |
+
niche: str = "Home Insurance"
|
| 659 |
) -> tuple[str, str, str, str]:
|
| 660 |
"""
|
| 661 |
Process a single creative strategy to generate prompt and copy.
|
| 662 |
|
| 663 |
Args:
|
| 664 |
creative_strategy: Creative strategy object
|
| 665 |
+
niche: Niche category (Home Insurance or GLP-1)
|
| 666 |
|
| 667 |
Returns:
|
| 668 |
Tuple of (prompt, title, body, description)
|
| 669 |
"""
|
| 670 |
+
prompt = self.creative_designer(creative_strategy, niche=niche)
|
| 671 |
ad_copy = self.copy_writer(creative_strategy)
|
| 672 |
return (
|
| 673 |
prompt,
|