deckbuilder 1.0.0b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,595 @@
1
+ """
2
+ Layout Recommendation Engine for Content-First Presentation Intelligence
3
+
4
+ This module implements Tool #2: recommend_slide_approach() which analyzes specific
5
+ content pieces and recommends optimal PowerPoint layouts based on communication intent.
6
+
7
+ Design Philosophy: Match content structure and message intent to most effective layout,
8
+ not just "what layouts exist?"
9
+ """
10
+
11
+ import re
12
+ from dataclasses import dataclass
13
+ from typing import Any, Dict, List, Optional
14
+
15
+
16
+ @dataclass
17
+ class LayoutRecommendation:
18
+ """Single layout recommendation with confidence scoring"""
19
+
20
+ layout: str
21
+ confidence: float
22
+ reason: str
23
+ best_for: str
24
+
25
+
26
+ class LayoutRecommendationEngine:
27
+ """
28
+ Analyzes content pieces and recommends optimal slide layouts.
29
+
30
+ Focuses on content structure, message intent, and communication effectiveness
31
+ rather than just available template options.
32
+ """
33
+
34
+ def __init__(self):
35
+ """Initialize with layout intelligence mapping"""
36
+ self.layout_intelligence = self._build_layout_intelligence()
37
+ self.available_layouts = self._get_available_layouts()
38
+
39
+ def recommend_slide_approach(
40
+ self, content_piece: str, message_intent: str, presentation_context: Optional[Dict] = None
41
+ ) -> Dict[str, Any]:
42
+ """
43
+ Analyze content and recommend optimal slide layouts.
44
+
45
+ Args:
46
+ content_piece: Specific content to present
47
+ message_intent: What they want this content to communicate
48
+ presentation_context: Optional context from analyze_presentation_needs()
49
+
50
+ Returns:
51
+ Dictionary with layout recommendations, content analysis, and suggestions
52
+ """
53
+ # Analyze the content structure and type
54
+ content_analysis = self._analyze_content_structure(content_piece)
55
+
56
+ # Determine communication intent
57
+ intent_analysis = self._analyze_message_intent(message_intent, content_piece)
58
+
59
+ # Generate layout recommendations with confidence scoring
60
+ layout_recommendations = self._generate_layout_recommendations(
61
+ content_analysis, intent_analysis, presentation_context
62
+ )
63
+
64
+ # Provide content structuring suggestions
65
+ content_suggestions = self._generate_content_suggestions(
66
+ content_analysis, intent_analysis, layout_recommendations
67
+ )
68
+
69
+ # Generate structured frontmatter preview
70
+ structured_frontmatter = self._generate_structured_frontmatter_preview(
71
+ layout_recommendations, content_piece, message_intent
72
+ )
73
+
74
+ return {
75
+ "recommended_layouts": [
76
+ {
77
+ "layout": rec.layout,
78
+ "confidence": rec.confidence,
79
+ "reason": rec.reason,
80
+ "best_for": rec.best_for,
81
+ }
82
+ for rec in layout_recommendations
83
+ ],
84
+ "content_analysis": content_analysis,
85
+ "content_suggestions": content_suggestions,
86
+ "structured_frontmatter": structured_frontmatter,
87
+ }
88
+
89
+ def _analyze_content_structure(self, content_piece: str) -> Dict[str, Any]:
90
+ """Analyze the structure and characteristics of the content."""
91
+ content_lower = content_piece.lower()
92
+
93
+ # Count various content elements
94
+ numbers_count = len(re.findall(r"\d+%|\$[\d,]+|\d+", content_piece))
95
+ bullet_indicators = len(re.findall(r"[-*•]|\d+\.|\w\)", content_piece))
96
+
97
+ # Detect content patterns
98
+ has_comparison = any(
99
+ word in content_lower for word in ["vs", "versus", "compared to", "versus", "against"]
100
+ )
101
+ has_list_structure = (
102
+ bullet_indicators > 2
103
+ or content_piece.count("\n") > 2
104
+ or content_piece.count(",") >= 3
105
+ or content_piece.count(":") >= 2
106
+ )
107
+ has_metrics = numbers_count >= 2
108
+ has_process = any(
109
+ word in content_lower for word in ["first", "then", "next", "finally", "step"]
110
+ )
111
+
112
+ # Determine content type
113
+ if has_comparison:
114
+ content_type = "comparison"
115
+ elif has_metrics:
116
+ content_type = "metrics_data"
117
+ elif has_process:
118
+ content_type = "process_timeline"
119
+ elif has_list_structure:
120
+ content_type = "structured_list"
121
+ else:
122
+ content_type = "narrative_text"
123
+
124
+ # Count distinct elements for layout sizing
125
+ distinct_elements = self._count_distinct_elements(content_piece)
126
+
127
+ return {
128
+ "content_type": content_type,
129
+ "data_elements": numbers_count,
130
+ "distinct_elements": distinct_elements,
131
+ "has_comparison": has_comparison,
132
+ "has_metrics": has_metrics,
133
+ "has_list_structure": has_list_structure,
134
+ "has_process": has_process,
135
+ "structure_pattern": self._determine_structure_pattern(content_piece),
136
+ "visual_emphasis": self._determine_visual_emphasis(content_piece, content_type),
137
+ }
138
+
139
+ def _analyze_message_intent(self, message_intent: str, content_piece: str) -> Dict[str, Any]:
140
+ """Analyze what the user wants to communicate with this content."""
141
+ intent_lower = message_intent.lower()
142
+
143
+ # Categorize communication intent
144
+ if any(word in intent_lower for word in ["compare", "contrast", "versus", "vs"]):
145
+ intent_category = "comparison"
146
+ elif any(
147
+ word in intent_lower for word in ["highlight", "emphasize", "showcase", "feature"]
148
+ ):
149
+ intent_category = "emphasis"
150
+ elif any(
151
+ word in intent_lower
152
+ for word in ["explain", "show process", "demonstrate", "walk through"]
153
+ ):
154
+ intent_category = "explanation"
155
+ elif any(word in intent_lower for word in ["data", "metrics", "numbers", "performance"]):
156
+ intent_category = "data_presentation"
157
+ elif any(word in intent_lower for word in ["overview", "summary", "introduction"]):
158
+ intent_category = "overview"
159
+ else:
160
+ intent_category = "general_information"
161
+
162
+ return {
163
+ "intent_category": intent_category,
164
+ "emphasis_level": self._determine_emphasis_level(message_intent),
165
+ "audience_focus": self._determine_audience_focus(message_intent),
166
+ "communication_goal": self._determine_communication_goal(message_intent),
167
+ }
168
+
169
+ def _generate_layout_recommendations(
170
+ self,
171
+ content_analysis: Dict[str, Any],
172
+ intent_analysis: Dict[str, Any],
173
+ presentation_context: Optional[Dict] = None,
174
+ ) -> List[LayoutRecommendation]:
175
+ """Generate ranked layout recommendations based on content and intent analysis."""
176
+ recommendations = []
177
+
178
+ content_type = content_analysis["content_type"]
179
+ intent_category = intent_analysis["intent_category"]
180
+ distinct_elements = content_analysis["distinct_elements"]
181
+
182
+ # Apply layout intelligence rules
183
+ if content_type == "comparison" or intent_category == "comparison":
184
+ if distinct_elements == 2:
185
+ recommendations.append(
186
+ LayoutRecommendation(
187
+ "Comparison",
188
+ 0.95,
189
+ "Two distinct elements perfect for side-by-side comparison",
190
+ "head-to-head comparisons and before/after scenarios",
191
+ )
192
+ )
193
+ recommendations.append(
194
+ LayoutRecommendation(
195
+ "Two Content",
196
+ 0.80,
197
+ "Alternative two-column layout for comparison content",
198
+ "detailed side-by-side content with more text",
199
+ )
200
+ )
201
+ elif 3 <= distinct_elements <= 4:
202
+ recommendations.append(
203
+ LayoutRecommendation(
204
+ "Four Columns",
205
+ 0.90,
206
+ f"{distinct_elements} elements ideal for structured comparison grid",
207
+ "feature matrices and multi-factor comparisons",
208
+ )
209
+ )
210
+ recommendations.append(
211
+ LayoutRecommendation(
212
+ "Comparison",
213
+ 0.70,
214
+ "Simplified two-way comparison focusing on main contrasts",
215
+ "high-level comparison without detailed breakdown",
216
+ )
217
+ )
218
+
219
+ elif content_type == "metrics_data" or intent_category == "data_presentation":
220
+ if content_analysis["data_elements"] >= 3:
221
+ recommendations.append(
222
+ LayoutRecommendation(
223
+ "Four Columns",
224
+ 0.85,
225
+ "Multiple metrics benefit from structured grid presentation",
226
+ "KPI dashboards and performance metrics",
227
+ )
228
+ )
229
+ recommendations.append(
230
+ LayoutRecommendation(
231
+ "Title and Content",
232
+ 0.75,
233
+ "Traditional layout works well for data with explanatory text",
234
+ "metrics with context and analysis",
235
+ )
236
+ )
237
+
238
+ elif content_type == "structured_list":
239
+ if 3 <= distinct_elements <= 4:
240
+ recommendations.append(
241
+ LayoutRecommendation(
242
+ "Four Columns",
243
+ 0.88,
244
+ "List structure maps well to column-based presentation",
245
+ "feature lists and categorized information",
246
+ )
247
+ )
248
+ recommendations.append(
249
+ LayoutRecommendation(
250
+ "Title and Content",
251
+ 0.85,
252
+ "Classic bulleted list presentation",
253
+ "traditional content delivery with clear hierarchy",
254
+ )
255
+ )
256
+ if distinct_elements == 2:
257
+ recommendations.append(
258
+ LayoutRecommendation(
259
+ "Two Content",
260
+ 0.80,
261
+ "Two main topics work well in side-by-side layout",
262
+ "dual-topic presentations",
263
+ )
264
+ )
265
+
266
+ elif content_type == "process_timeline":
267
+ recommendations.append(
268
+ LayoutRecommendation(
269
+ "Four Columns",
270
+ 0.80,
271
+ "Process steps map well to sequential columns",
272
+ "step-by-step processes and timelines",
273
+ )
274
+ )
275
+ recommendations.append(
276
+ LayoutRecommendation(
277
+ "Title and Content",
278
+ 0.75,
279
+ "Sequential list format for process explanation",
280
+ "detailed process documentation",
281
+ )
282
+ )
283
+
284
+ else: # narrative_text or general
285
+ recommendations.append(
286
+ LayoutRecommendation(
287
+ "Title and Content",
288
+ 0.85,
289
+ "Standard layout ideal for narrative and explanatory content",
290
+ "general information and detailed explanations",
291
+ )
292
+ )
293
+ if intent_category == "overview":
294
+ recommendations.append(
295
+ LayoutRecommendation(
296
+ "Section Header",
297
+ 0.70,
298
+ "High-level overview content works as section divider",
299
+ "topic introductions and agenda items",
300
+ )
301
+ )
302
+
303
+ # Add fallback recommendations if none were generated
304
+ if not recommendations:
305
+ recommendations.append(
306
+ LayoutRecommendation(
307
+ "Title and Content",
308
+ 0.70,
309
+ "Default layout suitable for most content types",
310
+ "general content presentation and information delivery",
311
+ )
312
+ )
313
+ if content_analysis["has_comparison"]:
314
+ recommendations.append(
315
+ LayoutRecommendation(
316
+ "Comparison",
317
+ 0.65,
318
+ "Content contains comparison elements",
319
+ "side-by-side comparisons and contrasts",
320
+ )
321
+ )
322
+
323
+ # Sort by confidence score
324
+ recommendations.sort(key=lambda x: x.confidence, reverse=True)
325
+
326
+ # Return top 3 recommendations
327
+ return recommendations[:3]
328
+
329
+ def _generate_content_suggestions(
330
+ self,
331
+ content_analysis: Dict[str, Any],
332
+ intent_analysis: Dict[str, Any],
333
+ layout_recommendations: List[LayoutRecommendation],
334
+ ) -> Dict[str, Any]:
335
+ """Generate content structuring and presentation suggestions."""
336
+ primary_layout = layout_recommendations[0] if layout_recommendations else None
337
+
338
+ if not primary_layout:
339
+ return {}
340
+
341
+ # Generate suggestions based on recommended layout
342
+ if primary_layout.layout == "Four Columns":
343
+ return {
344
+ "primary_message": "Structured comparison or categorized information",
345
+ "supporting_details": [
346
+ "distinct categories",
347
+ "balanced content distribution",
348
+ "clear labels",
349
+ ],
350
+ "recommended_structure": "title + categorized columns with headers",
351
+ "visual_approach": "balanced grid with consistent formatting",
352
+ }
353
+ elif primary_layout.layout == "Comparison":
354
+ return {
355
+ "primary_message": "Side-by-side comparison highlighting key differences",
356
+ "supporting_details": [
357
+ "contrasting elements",
358
+ "clear distinctions",
359
+ "decision factors",
360
+ ],
361
+ "recommended_structure": "title + left vs right comparison",
362
+ "visual_approach": "clear visual separation with balanced content",
363
+ }
364
+ elif primary_layout.layout == "Two Content":
365
+ return {
366
+ "primary_message": "Dual-topic presentation with equal emphasis",
367
+ "supporting_details": [
368
+ "complementary topics",
369
+ "balanced detail level",
370
+ "related themes",
371
+ ],
372
+ "recommended_structure": "title + two main content areas",
373
+ "visual_approach": "side-by-side layout with clear separation",
374
+ }
375
+ else: # Title and Content or others
376
+ return {
377
+ "primary_message": "Structured information with clear hierarchy",
378
+ "supporting_details": ["main points", "supporting details", "logical flow"],
379
+ "recommended_structure": "title + bulleted or paragraph content",
380
+ "visual_approach": "traditional layout with clear visual hierarchy",
381
+ }
382
+
383
+ def _generate_structured_frontmatter_preview(
384
+ self,
385
+ layout_recommendations: List[LayoutRecommendation],
386
+ content_piece: str,
387
+ message_intent: str,
388
+ ) -> Dict[str, Any]:
389
+ """Generate structured frontmatter preview for the recommended layout."""
390
+ if not layout_recommendations:
391
+ return {}
392
+
393
+ primary_layout = layout_recommendations[0].layout
394
+
395
+ # Generate title from content or intent
396
+ title = self._extract_title_from_content(content_piece, message_intent)
397
+
398
+ if primary_layout == "Four Columns":
399
+ return {
400
+ "preferred_format": "Four Columns",
401
+ "yaml_preview": f"""layout: Four Columns
402
+ title: {title}
403
+ columns:
404
+ - title: Category 1
405
+ content: "Key information for first category"
406
+ - title: Category 2
407
+ content: "Key information for second category"
408
+ - title: Category 3
409
+ content: "Key information for third category"
410
+ - title: Category 4
411
+ content: "Key information for fourth category" """,
412
+ "placeholder_mapping": {
413
+ "Title 1": title,
414
+ "Col 1 Title": "Category 1",
415
+ "Col 2 Title": "Category 2",
416
+ "Col 3 Title": "Category 3",
417
+ "Col 4 Title": "Category 4",
418
+ },
419
+ }
420
+ elif primary_layout == "Comparison":
421
+ return {
422
+ "preferred_format": "Comparison",
423
+ "yaml_preview": f"""layout: Comparison
424
+ title: {title}
425
+ comparison:
426
+ left:
427
+ title: Option A
428
+ content: "Benefits and characteristics of first option"
429
+ right:
430
+ title: Option B
431
+ content: "Benefits and characteristics of second option" """,
432
+ "placeholder_mapping": {
433
+ "Title 1": title,
434
+ "Text Placeholder 2": "Option A",
435
+ "Text Placeholder 4": "Option B",
436
+ },
437
+ }
438
+ else:
439
+ return {
440
+ "preferred_format": primary_layout,
441
+ "yaml_preview": f"""layout: {primary_layout}
442
+ title: {title}
443
+ content: |
444
+ - Key point from your content
445
+ - Supporting information
446
+ - Additional details""",
447
+ "placeholder_mapping": {
448
+ "Title 1": title,
449
+ "Content Placeholder 2": "Your content here",
450
+ },
451
+ }
452
+
453
+ def _count_distinct_elements(self, content: str) -> int:
454
+ """Count distinct content elements that could map to separate layout areas."""
455
+ # Look for bullet points, numbered lists, or natural separators
456
+ bullet_count = len(re.findall(r"[-*•]\s+", content))
457
+ number_list_count = len(re.findall(r"\d+\.\s+", content))
458
+ paragraph_count = len([p for p in content.split("\n\n") if p.strip()])
459
+
460
+ # Special handling for comparison content
461
+ if any(word in content.lower() for word in ["vs", "versus", "compared to", "against"]):
462
+ # For comparisons, assume at least 2 elements being compared
463
+ return max(2, bullet_count, number_list_count, min(paragraph_count, 4))
464
+
465
+ # For lists, count actual items
466
+ if bullet_count > 0:
467
+ return bullet_count
468
+ elif number_list_count > 0:
469
+ return number_list_count
470
+
471
+ # Check for comma-separated lists (like "feature1, feature2, feature3, and feature4")
472
+ comma_separated = [item.strip() for item in content.split(",") if item.strip()]
473
+ if len(comma_separated) >= 3:
474
+ return min(len(comma_separated), 4)
475
+
476
+ # Check for colon-separated lists (like "feature1: description, feature2: description")
477
+ colon_separated = len(re.findall(r"[^:]+:", content))
478
+ if colon_separated >= 2:
479
+ return min(colon_separated, 4)
480
+
481
+ # For paragraph content, minimum of 1, maximum of 4
482
+ return max(1, min(paragraph_count, 4))
483
+
484
+ def _determine_structure_pattern(self, content: str) -> str:
485
+ """Determine the structural pattern of the content."""
486
+ if re.search(r"[-*•]\s+.*[-*•]\s+", content):
487
+ return "bulleted_list"
488
+ elif re.search(r"\d+\.\s+.*\d+\.\s+", content):
489
+ return "numbered_list"
490
+ elif content.count("\n\n") >= 2:
491
+ return "paragraph_blocks"
492
+ else:
493
+ return "continuous_text"
494
+
495
+ def _determine_visual_emphasis(self, content: str, content_type: str) -> str:
496
+ """Determine what visual emphasis would work best."""
497
+ if content_type == "comparison":
498
+ return "balanced_comparison"
499
+ elif content_type == "metrics_data":
500
+ return "data_focused"
501
+ elif content_type == "process_timeline":
502
+ return "sequential_flow"
503
+ else:
504
+ return "content_hierarchy"
505
+
506
+ def _determine_emphasis_level(self, message_intent: str) -> str:
507
+ """Determine how much visual emphasis is needed."""
508
+ intent_lower = message_intent.lower()
509
+ if any(word in intent_lower for word in ["critical", "important", "key", "highlight"]):
510
+ return "high"
511
+ elif any(word in intent_lower for word in ["show", "present", "explain"]):
512
+ return "medium"
513
+ else:
514
+ return "standard"
515
+
516
+ def _determine_audience_focus(self, message_intent: str) -> str:
517
+ """Determine audience consideration from intent."""
518
+ intent_lower = message_intent.lower()
519
+ if any(word in intent_lower for word in ["executive", "board", "leadership"]):
520
+ return "executive"
521
+ elif any(word in intent_lower for word in ["technical", "detail", "implementation"]):
522
+ return "technical"
523
+ else:
524
+ return "general"
525
+
526
+ def _determine_communication_goal(self, message_intent: str) -> str:
527
+ """Determine the primary communication goal."""
528
+ intent_lower = message_intent.lower()
529
+ if any(word in intent_lower for word in ["decide", "choose", "recommend"]):
530
+ return "decision_support"
531
+ elif any(word in intent_lower for word in ["understand", "explain", "clarify"]):
532
+ return "comprehension"
533
+ elif any(word in intent_lower for word in ["convince", "persuade", "sell"]):
534
+ return "persuasion"
535
+ else:
536
+ return "information_sharing"
537
+
538
+ def _extract_title_from_content(self, content_piece: str, message_intent: str) -> str:
539
+ """Extract or generate an appropriate title from content and intent."""
540
+ # Try to extract from first line if it looks like a title
541
+ first_line = content_piece.split("\n")[0].strip()
542
+ if len(first_line) < 60 and not first_line.endswith("."):
543
+ return first_line
544
+
545
+ # Generate from message intent
546
+ intent_words = message_intent.split()[:3]
547
+ if intent_words:
548
+ return " ".join(word.capitalize() for word in intent_words)
549
+
550
+ # Fallback
551
+ return "Content Overview"
552
+
553
+ def _build_layout_intelligence(self) -> Dict[str, Any]:
554
+ """Build the layout intelligence mapping for content-to-layout recommendations."""
555
+ return {
556
+ "comparison_layouts": ["Comparison", "Two Content", "Four Columns"],
557
+ "data_layouts": ["Four Columns", "Title and Content"],
558
+ "list_layouts": ["Title and Content", "Four Columns", "Two Content"],
559
+ "process_layouts": ["Four Columns", "Title and Content", "Two Content"],
560
+ "narrative_layouts": ["Title and Content", "Section Header"],
561
+ }
562
+
563
+ def _get_available_layouts(self) -> List[str]:
564
+ """Get list of currently available PowerPoint layouts."""
565
+ return [
566
+ "Title Slide",
567
+ "Title and Content",
568
+ "Section Header",
569
+ "Two Content",
570
+ "Comparison",
571
+ "Title Only",
572
+ "Blank",
573
+ "Content with Caption",
574
+ "Picture with Caption",
575
+ "Four Columns",
576
+ ]
577
+
578
+
579
+ # Helper function for easy import
580
+ def recommend_slide_approach(
581
+ content_piece: str, message_intent: str, presentation_context: Optional[Dict] = None
582
+ ) -> Dict[str, Any]:
583
+ """
584
+ Convenience function for getting slide layout recommendations.
585
+
586
+ Args:
587
+ content_piece: Specific content to present
588
+ message_intent: What they want this content to communicate
589
+ presentation_context: Optional context from analyze_presentation_needs()
590
+
591
+ Returns:
592
+ Dictionary with layout recommendations and content suggestions
593
+ """
594
+ engine = LayoutRecommendationEngine()
595
+ return engine.recommend_slide_approach(content_piece, message_intent, presentation_context)