Note: Some parts of the code have been generated by Claude Sonnet:
INTRODUCTION AND SYSTEM OVERVIEW
Creating compelling PowerPoint presentations requires significant time, research, and design expertise. This tutorial presents a sophisticated system that leverages Large Language Models to automate the entire presentation creation workflow. The system accepts high-level user requirements and produces professional slide decks complete with visual elements, structured content, and detailed speaker notes.
The architecture combines multiple advanced AI techniques including Chain-of-Thought reasoning, Tree-of-Thought exploration, web search integration, and image retrieval. The system works with both local and remote LLM deployments, supporting diverse hardware configurations including NVIDIA CUDA, AMD ROCm, Intel GPUs, and Apple Metal Performance Shaders. All components utilize open source tools and libraries wherever possible, ensuring accessibility and avoiding vendor lock-in.
At its core, the system operates through a multi-stage pipeline. First, it analyzes the user's requirements through iterative clarification dialogues. Second, it performs deep research using DuckDuckGo search capabilities to gather authoritative information without requiring API keys. Third, it constructs a logical narrative structure using tree-based reasoning to explore multiple organizational approaches. Fourth, it generates individual slides with appropriate visual elements and speaker notes. Fifth, it retrieves images from open sources like Wikimedia Commons and Unsplash. Finally, it assembles everything into a cohesive PowerPoint file with consistent styling using the python-pptx library.
The entire system is built on open source foundations. For local LLM inference, it supports llama.cpp, vLLM, and Ollama. For web search, it uses DuckDuckGo's free search API through the duckduckgo-search Python library. For image retrieval, it queries Wikimedia Commons, Unsplash, and Pexels. For PowerPoint generation, it uses the python-pptx library. This approach ensures the system remains free to use and modify.
ARCHITECTURAL FOUNDATIONS AND DESIGN PRINCIPLES
The system architecture follows clean architecture principles with clear separation of concerns. The core domain logic remains independent of external frameworks and infrastructure. This allows the same business logic to work seamlessly whether using OpenAI's GPT-4, Anthropic's Claude, a locally hosted Llama model, or any other LLM backend.
The architecture consists of several key layers. The presentation layer handles user interaction and input validation. The application layer orchestrates the workflow between different components. The domain layer contains the core business logic for presentation generation, reasoning, and content structuring. The infrastructure layer manages external dependencies like LLM APIs, web search services, and file system operations.
A critical design decision involves supporting multiple LLM backends through a unified interface. This abstraction allows the system to dynamically select between local and remote models based on availability, cost considerations, and performance requirements. For local models, the system integrates with frameworks like llama.cpp, vLLM, or Ollama, automatically detecting and utilizing available GPU acceleration.
The reasoning engine implements both Chain-of-Thought and Tree-of-Thought methodologies. Chain-of-Thought prompting guides the LLM through step-by-step logical progression, making the decision-making process transparent and debuggable. Tree-of-Thought extends this by exploring multiple reasoning paths simultaneously, evaluating different approaches, and selecting the most promising direction.
The system prioritizes open source components throughout. Web search uses DuckDuckGo through the duckduckgo-search library, which requires no API keys and provides free access to search results. Image retrieval queries Wikimedia Commons API, Unsplash API with free tier access, and Pexels API. Local LLM support leverages llama.cpp with Metal, CUDA, ROCm, or CPU backends. The PowerPoint generation uses python-pptx, a pure Python library for creating and updating PowerPoint files.
UNDERSTANDING CHAIN-OF-THOUGHT AND TREE-OF-THOUGHT REASONING
Chain-of-Thought reasoning transforms complex problems into sequences of intermediate steps. Instead of asking the LLM to directly produce a presentation outline, the system guides it through explicit reasoning stages. For example, when determining slide structure, the LLM first identifies the main topics, then considers the target audience's knowledge level, estimates appropriate depth for each topic, and finally sequences the topics logically.
Consider a user requesting a presentation about quantum computing for software engineers. The Chain-of-Thought process might proceed as follows. First, the system identifies that quantum computing is highly technical but the audience has programming background. Second, it determines that foundational concepts like superposition and entanglement require explanation before discussing quantum algorithms. Third, it recognizes that code examples using quantum programming frameworks would resonate with the audience. Fourth, it structures the presentation to build from classical computing analogies toward quantum-specific concepts.
Tree-of-Thought reasoning explores multiple organizational approaches simultaneously. The system might generate three different presentation structures in parallel. One structure could take a historical approach, starting with early quantum theory and progressing chronologically. Another might use a problem-solution framework, presenting classical computing limitations followed by quantum solutions. A third could organize around practical applications, grouping content by use cases like cryptography, optimization, and simulation.
The system evaluates these alternatives using explicit criteria. It considers pedagogical effectiveness, audience engagement potential, time constraints, and logical flow. Each path receives a score, and the system either selects the highest-scoring approach or synthesizes elements from multiple paths into a hybrid structure.
LLM BACKEND ABSTRACTION AND MULTI-PLATFORM SUPPORT
Supporting diverse LLM backends requires a carefully designed abstraction layer. The system defines a common interface that all LLM providers must implement, regardless of whether they run locally or remotely. This interface includes methods for text generation, token counting, context management, and streaming responses.
For remote LLMs, the system integrates with REST APIs provided by services like OpenAI, Anthropic, Cohere, and others. Each provider has unique authentication mechanisms, request formats, and rate limiting policies. The abstraction layer normalizes these differences, presenting a consistent interface to higher-level components.
Local LLM support presents additional complexity due to hardware diversity. The system must detect available GPU resources and configure the inference engine accordingly. For NVIDIA GPUs, it uses CUDA-accelerated libraries. AMD GPUs require ROCm support. Intel GPUs utilize oneAPI and Intel Extension for PyTorch. Apple Silicon Macs leverage Metal Performance Shaders through MLX or llama.cpp's Metal backend.
Here is the LLM abstraction interface:
from abc import ABC, abstractmethod
from typing import List, Dict, Optional, Iterator
from dataclasses import dataclass
@dataclass
class LLMMessage:
"""Represents a single message in the conversation"""
role: str
content: str
@dataclass
class LLMResponse:
"""Encapsulates the LLM's response"""
content: str
tokens_used: int
finish_reason: str
model: str
class LLMBackend(ABC):
"""Abstract base class for all LLM backends"""
@abstractmethod
def generate(self,
messages: List[LLMMessage],
temperature: float = 0.7,
max_tokens: Optional[int] = None) -> LLMResponse:
"""Generate a response given conversation history"""
pass
@abstractmethod
def stream_generate(self,
messages: List[LLMMessage],
temperature: float = 0.7,
max_tokens: Optional[int] = None) -> Iterator[str]:
"""Stream response tokens as they are generated"""
pass
@abstractmethod
def count_tokens(self, text: str) -> int:
"""Count tokens in the provided text"""
pass
@abstractmethod
def get_context_length(self) -> int:
"""Return maximum context length for this model"""
pass
This interface allows the presentation generation system to work identically regardless of the underlying LLM. A concrete implementation for OpenAI might look like this:
import openai
from typing import List, Iterator, Optional
class OpenAIBackend(LLMBackend):
"""OpenAI API implementation of LLM backend"""
def __init__(self, api_key: str, model: str = "gpt-4"):
self.api_key = api_key
self.model = model
openai.api_key = api_key
def generate(self,
messages: List[LLMMessage],
temperature: float = 0.7,
max_tokens: Optional[int] = None) -> LLMResponse:
"""Generate response using OpenAI API"""
formatted_messages = [
{"role": msg.role, "content": msg.content}
for msg in messages
]
response = openai.ChatCompletion.create(
model=self.model,
messages=formatted_messages,
temperature=temperature,
max_tokens=max_tokens
)
return LLMResponse(
content=response.choices[0].message.content,
tokens_used=response.usage.total_tokens,
finish_reason=response.choices[0].finish_reason,
model=self.model
)
def stream_generate(self,
messages: List[LLMMessage],
temperature: float = 0.7,
max_tokens: Optional[int] = None) -> Iterator[str]:
"""Stream tokens from OpenAI API"""
formatted_messages = [
{"role": msg.role, "content": msg.content}
for msg in messages
]
stream = openai.ChatCompletion.create(
model=self.model,
messages=formatted_messages,
temperature=temperature,
max_tokens=max_tokens,
stream=True
)
for chunk in stream:
if chunk.choices[0].delta.get("content"):
yield chunk.choices[0].delta.content
def count_tokens(self, text: str) -> int:
"""Estimate token count for OpenAI models"""
import tiktoken
encoding = tiktoken.encoding_for_model(self.model)
return len(encoding.encode(text))
def get_context_length(self) -> int:
"""Return context length for the model"""
context_lengths = {
"gpt-4": 8192,
"gpt-4-32k": 32768,
"gpt-3.5-turbo": 4096,
"gpt-3.5-turbo-16k": 16384
}
return context_lengths.get(self.model, 4096)
For local models, the implementation differs significantly. Here is an example using llama.cpp through its Python bindings with automatic GPU detection:
from llama_cpp import Llama
from typing import List, Iterator, Optional
import platform
import subprocess
class LocalLlamaBackend(LLMBackend):
"""Local LLM using llama.cpp with GPU acceleration"""
def __init__(self, model_path: str, n_gpu_layers: int = -1):
"""Initialize local model with automatic GPU detection"""
self.model_path = model_path
self.n_ctx = 4096
gpu_config = self._detect_gpu_configuration()
self.llm = Llama(
model_path=model_path,
n_ctx=self.n_ctx,
n_gpu_layers=n_gpu_layers if n_gpu_layers > 0 else gpu_config['layers'],
n_threads=gpu_config['threads'],
use_mlock=True,
verbose=False
)
def _detect_gpu_configuration(self) -> dict:
"""Detect available GPU and return optimal configuration"""
system = platform.system()
try:
import torch
if torch.cuda.is_available():
return {
'layers': -1,
'threads': 4,
'backend': 'cuda'
}
except ImportError:
pass
try:
import torch
if hasattr(torch.version, 'hip') and torch.version.hip:
return {
'layers': -1,
'threads': 4,
'backend': 'rocm'
}
except (ImportError, AttributeError):
pass
if system == 'Darwin':
try:
import torch
if torch.backends.mps.is_available():
return {
'layers': -1,
'threads': 4,
'backend': 'metal'
}
except (ImportError, AttributeError):
pass
import multiprocessing
return {
'layers': 0,
'threads': multiprocessing.cpu_count(),
'backend': 'cpu'
}
def generate(self,
messages: List[LLMMessage],
temperature: float = 0.7,
max_tokens: Optional[int] = None) -> LLMResponse:
"""Generate response using local model"""
prompt = self._format_messages_to_prompt(messages)
output = self.llm(
prompt,
max_tokens=max_tokens or 512,
temperature=temperature,
stop=["</s>", "Human:", "User:"],
echo=False
)
return LLMResponse(
content=output['choices'][0]['text'].strip(),
tokens_used=output['usage']['total_tokens'],
finish_reason=output['choices'][0]['finish_reason'],
model=self.model_path
)
def _format_messages_to_prompt(self, messages: List[LLMMessage]) -> str:
"""Convert message list to model-specific prompt format"""
prompt_parts = []
for msg in messages:
if msg.role == "system":
prompt_parts.append(f"<<SYS>>\n{msg.content}\n<</SYS>>\n\n")
elif msg.role == "user":
prompt_parts.append(f"[INST] {msg.content} [/INST]")
elif msg.role == "assistant":
prompt_parts.append(f"{msg.content}</s>")
return "".join(prompt_parts)
def stream_generate(self,
messages: List[LLMMessage],
temperature: float = 0.7,
max_tokens: Optional[int] = None) -> Iterator[str]:
"""Stream tokens from local model"""
prompt = self._format_messages_to_prompt(messages)
for output in self.llm(
prompt,
max_tokens=max_tokens or 512,
temperature=temperature,
stop=["</s>", "Human:", "User:"],
stream=True
):
yield output['choices'][0]['text']
def count_tokens(self, text: str) -> int:
"""Count tokens using model's tokenizer"""
return len(self.llm.tokenize(text.encode('utf-8')))
def get_context_length(self) -> int:
"""Return configured context length"""
return self.n_ctx
This abstraction enables the system to seamlessly switch between different LLM providers based on configuration or runtime conditions. The presentation generation logic remains completely independent of these implementation details.
REQUIREMENT ANALYSIS AND ITERATIVE CLARIFICATION
The system begins by analyzing the user's initial prompt to extract key parameters. These include the presentation title, topic, target audience, duration, prerequisite knowledge, and any specific content requirements. However, users often provide incomplete or ambiguous specifications. The system must intelligently identify gaps and engage in clarification dialogue.
The requirement analyzer uses structured prompting to extract information systematically. It asks the LLM to parse the user input and identify which parameters are explicitly stated, which can be reasonably inferred, and which require clarification. The LLM responds with a structured representation of the requirements.
Here is the requirement analysis component:
from dataclasses import dataclass
from typing import List, Optional, Dict
import json
@dataclass
class PresentationRequirements:
"""Structured representation of presentation requirements"""
title: Optional[str] = None
topic: Optional[str] = None
duration_minutes: Optional[int] = None
target_audience: Optional[str] = None
prerequisites: Optional[List[str]] = None
specific_content: Optional[List[str]] = None
style_preferences: Optional[str] = None
def is_complete(self) -> bool:
"""Check if all essential requirements are specified"""
return all([
self.title,
self.topic,
self.duration_minutes,
self.target_audience
])
def get_missing_fields(self) -> List[str]:
"""Return list of missing essential fields"""
missing = []
if not self.title:
missing.append("title")
if not self.topic:
missing.append("topic")
if not self.duration_minutes:
missing.append("duration_minutes")
if not self.target_audience:
missing.append("target_audience")
return missing
class RequirementAnalyzer:
"""Analyzes user prompts to extract presentation requirements"""
def __init__(self, llm_backend: LLMBackend):
self.llm = llm_backend
def analyze_prompt(self, user_prompt: str) -> PresentationRequirements:
"""Extract structured requirements from user prompt"""
analysis_prompt = f"""Analyze the following user request for creating a presentation.
Extract the following information if present:
- Title: The presentation title
- Topic: The main subject matter
- Duration: How long the presentation should be in minutes
- Target Audience: Who will attend the presentation
- Prerequisites: Required knowledge or skills for attendees
- Specific Content: Any particular topics or sections requested
- Style Preferences: Visual style or design preferences
User Request: {user_prompt}
Respond with a JSON object containing the extracted information. Use null for missing fields.
Example format:
{{
"title": "Introduction to Machine Learning",
"topic": "Machine Learning Fundamentals",
"duration_minutes": 45,
"target_audience": "Software developers with Python experience",
"prerequisites": ["Python programming", "Basic statistics"],
"specific_content": ["Neural networks", "Decision trees"],
"style_preferences": "Professional and technical"
}}
JSON Response:"""
messages = [
LLMMessage(role="system", content="You are a helpful assistant that extracts structured information from text."),
LLMMessage(role="user", content=analysis_prompt)
]
response = self.llm.generate(messages, temperature=0.3)
try:
json_str = self._extract_json(response.content)
data = json.loads(json_str)
return PresentationRequirements(
title=data.get("title"),
topic=data.get("topic"),
duration_minutes=data.get("duration_minutes"),
target_audience=data.get("target_audience"),
prerequisites=data.get("prerequisites"),
specific_content=data.get("specific_content"),
style_preferences=data.get("style_preferences")
)
except (json.JSONDecodeError, KeyError) as e:
return self._fallback_extraction(user_prompt)
def _extract_json(self, text: str) -> str:
"""Extract JSON object from text that may contain other content"""
start = text.find('{')
end = text.rfind('}') + 1
if start >= 0 and end > start:
return text[start:end]
return text
def _fallback_extraction(self, prompt: str) -> PresentationRequirements:
"""Simple keyword-based extraction as fallback"""
requirements = PresentationRequirements()
if "title:" in prompt.lower():
title_start = prompt.lower().find("title:") + 6
title_end = prompt.find("\n", title_start)
if title_end == -1:
title_end = len(prompt)
requirements.title = prompt[title_start:title_end].strip()
return requirements
def generate_clarification_questions(self,
requirements: PresentationRequirements) -> List[str]:
"""Generate questions to clarify missing or ambiguous requirements"""
missing = requirements.get_missing_fields()
if not missing:
return []
question_prompt = f"""The user wants to create a presentation but has not specified all necessary details.
Current information:
- Title: {requirements.title or 'Not specified'}
- Topic: {requirements.topic or 'Not specified'}
- Duration: {requirements.duration_minutes or 'Not specified'} minutes
- Target Audience: {requirements.target_audience or 'Not specified'}
- Prerequisites: {requirements.prerequisites or 'Not specified'}
Missing information: {', '.join(missing)}
Generate friendly, specific questions to gather the missing information.
Ask one question per missing field. Make questions natural and conversational.
Questions:"""
messages = [
LLMMessage(role="system", content="You are a helpful assistant creating clarification questions."),
LLMMessage(role="user", content=question_prompt)
]
response = self.llm.generate(messages, temperature=0.7)
questions = [
q.strip()
for q in response.content.split('\n')
if q.strip() and (q.strip()[0].isdigit() or q.strip().startswith('-'))
]
return questions
The clarification process continues iteratively until all essential requirements are gathered. The system maintains conversation context, allowing users to provide information incrementally across multiple exchanges. This creates a natural dialogue rather than forcing users to provide everything upfront.
WEB SEARCH INTEGRATION USING DUCKDUCKGO
Once requirements are clear, the system performs comprehensive research using DuckDuckGo search capabilities. DuckDuckGo provides a free search API through the duckduckgo-search Python library, requiring no API keys or authentication. This ensures the presentation contains accurate, current, and authoritative information while maintaining complete open source accessibility.
The search strategy adapts to the topic, using different queries to explore various aspects. For a presentation about quantum computing, it might search for foundational concepts, recent developments, practical applications, code examples, and visual diagrams. Each search result is evaluated for relevance and credibility.
Here is the implementation of the DuckDuckGo search integration:
from typing import List, Dict, Optional
from dataclasses import dataclass
from duckduckgo_search import DDGS
import time
@dataclass
class SearchResult:
"""Represents a single search result"""
title: str
url: str
snippet: str
relevance_score: float
@dataclass
class ResearchFindings:
"""Aggregated research results for presentation"""
topic: str
search_queries: List[str]
results: List[SearchResult]
key_facts: List[str]
authoritative_sources: List[str]
class WebSearcher:
"""Handles web search for presentation research using DuckDuckGo"""
def __init__(self, llm_backend: LLMBackend):
self.llm = llm_backend
self.ddgs = DDGS()
def research_topic(self,
requirements: PresentationRequirements,
max_queries: int = 5) -> ResearchFindings:
"""Conduct comprehensive research on presentation topic"""
queries = self._generate_search_queries(requirements, max_queries)
all_results = []
for query in queries:
results = self._execute_search(query)
all_results.extend(results)
time.sleep(1)
ranked_results = self._rank_results(all_results, requirements)
key_facts = self._extract_key_facts(ranked_results[:10], requirements)
sources = list(set([r.url for r in ranked_results[:5]]))
return ResearchFindings(
topic=requirements.topic,
search_queries=queries,
results=ranked_results,
key_facts=key_facts,
authoritative_sources=sources
)
def _generate_search_queries(self,
requirements: PresentationRequirements,
max_queries: int) -> List[str]:
"""Generate targeted search queries using LLM"""
query_prompt = f"""Generate {max_queries} specific search queries to research the following presentation topic.
Topic: {requirements.topic}
Target Audience: {requirements.target_audience}
Specific Content Requested: {requirements.specific_content or 'General overview'}
The queries should cover:
1. Foundational concepts and definitions
2. Recent developments and current state
3. Practical applications and examples
4. Technical details appropriate for the audience
5. Visual resources and diagrams
Generate diverse queries that will find authoritative, accurate information.
List only the queries, one per line, without numbering or explanations.
Queries:"""
messages = [
LLMMessage(role="system", content="You are a research assistant generating effective search queries."),
LLMMessage(role="user", content=query_prompt)
]
response = self.llm.generate(messages, temperature=0.7)
queries = [
q.strip()
for q in response.content.split('\n')
if q.strip() and len(q.strip()) > 5
]
return queries[:max_queries]
def _execute_search(self, query: str, num_results: int = 10) -> List[SearchResult]:
"""Execute web search using DuckDuckGo"""
try:
results = []
ddgs_results = self.ddgs.text(query, max_results=num_results)
for item in ddgs_results:
results.append(SearchResult(
title=item.get('title', ''),
url=item.get('href', ''),
snippet=item.get('body', ''),
relevance_score=0.0
))
return results
except Exception as e:
print(f"DuckDuckGo search error: {e}")
return []
def _rank_results(self,
results: List[SearchResult],
requirements: PresentationRequirements) -> List[SearchResult]:
"""Rank search results by relevance using LLM"""
for result in results:
score = self._calculate_relevance_score(result, requirements)
result.relevance_score = score
return sorted(results, key=lambda r: r.relevance_score, reverse=True)
def _calculate_relevance_score(self,
result: SearchResult,
requirements: PresentationRequirements) -> float:
"""Calculate relevance score for a search result"""
scoring_prompt = f"""Rate the relevance of this search result for a presentation on the following topic.
Presentation Topic: {requirements.topic}
Target Audience: {requirements.target_audience}
Search Result:
Title: {result.title}
Snippet: {result.snippet}
Rate the relevance on a scale of 0.0 to 1.0, where:
- 1.0 = Highly relevant, authoritative, perfect for the presentation
- 0.5 = Somewhat relevant, may contain useful information
- 0.0 = Not relevant or unreliable
Consider:
- Topical relevance
- Audience appropriateness
- Source authority
- Information quality
Respond with only a number between 0.0 and 1.0.
Score:"""
messages = [
LLMMessage(role="system", content="You are an expert at evaluating information quality and relevance."),
LLMMessage(role="user", content=scoring_prompt)
]
response = self.llm.generate(messages, temperature=0.1)
try:
score = float(response.content.strip())
return max(0.0, min(1.0, score))
except ValueError:
return 0.5
def _extract_key_facts(self,
results: List[SearchResult],
requirements: PresentationRequirements) -> List[str]:
"""Extract key facts from top search results"""
combined_text = "\n\n".join([
f"{r.title}\n{r.snippet}"
for r in results
])
extraction_prompt = f"""Extract 8-12 key facts from the following search results about {requirements.topic}.
The facts should be:
- Accurate and verifiable
- Relevant to {requirements.target_audience}
- Suitable for a {requirements.duration_minutes}-minute presentation
- Clear and concise
Search Results:
{combined_text}
List the key facts, one per line, without numbering.
Key Facts:"""
messages = [
LLMMessage(role="system", content="You are an expert at extracting and summarizing key information."),
LLMMessage(role="user", content=extraction_prompt)
]
response = self.llm.generate(messages, temperature=0.5)
facts = [
fact.strip()
for fact in response.content.split('\n')
if fact.strip() and len(fact.strip()) > 10
]
return facts
The DuckDuckGo integration provides robust search capabilities without requiring API keys or authentication. The research findings provide a foundation for content generation, ensuring accuracy while maintaining appropriate depth for the target audience.
TREE-OF-THOUGHT REASONING FOR PRESENTATION STRUCTURE
After gathering research, the system must organize content into a logical presentation structure. This is where Tree-of-Thought reasoning proves invaluable. The system explores multiple organizational approaches, evaluates their effectiveness, and selects or synthesizes the best structure.
The tree exploration process begins by generating several high-level organizational frameworks. For a technical presentation, these might include chronological progression, problem-solution structure, layered abstraction, or use-case driven organization. Each framework represents a different branch in the reasoning tree.
For each branch, the system expands the structure by breaking it into sections and slides. It evaluates each expansion using criteria like logical flow, pedagogical effectiveness, time allocation, and audience engagement. Promising branches are explored further while less effective ones are pruned.
Here is the implementation of the Tree-of-Thought reasoning engine:
from typing import List, Dict, Tuple, Optional
from dataclasses import dataclass, field
from enum import Enum
import copy
class StructureType(Enum):
"""Types of presentation organizational structures"""
CHRONOLOGICAL = "chronological"
PROBLEM_SOLUTION = "problem_solution"
LAYERED_ABSTRACTION = "layered_abstraction"
USE_CASE_DRIVEN = "use_case_driven"
CONCEPTUAL_BUILDING = "conceptual_building"
@dataclass
class SlideOutline:
"""Outline for a single slide"""
title: str
key_points: List[str]
estimated_duration: int
visual_type: str
@dataclass
class SectionOutline:
"""Outline for a presentation section"""
title: str
slides: List[SlideOutline]
learning_objectives: List[str]
@dataclass
class PresentationStructure:
"""Complete presentation structure"""
title: str
structure_type: StructureType
sections: List[SectionOutline]
total_slides: int = 0
estimated_duration: int = 0
evaluation_score: float = 0.0
def calculate_metrics(self):
"""Calculate total slides and duration"""
self.total_slides = sum(len(section.slides) for section in self.sections)
self.estimated_duration = sum(
slide.estimated_duration
for section in self.sections
for slide in section.slides
)
@dataclass
class ThoughtNode:
"""Node in the Tree-of-Thought reasoning process"""
structure: PresentationStructure
depth: int
parent: Optional['ThoughtNode'] = None
children: List['ThoughtNode'] = field(default_factory=list)
evaluation_score: float = 0.0
is_complete: bool = False
class TreeOfThoughtReasoner:
"""Implements Tree-of-Thought reasoning for presentation structure"""
def __init__(self, llm_backend: LLMBackend):
self.llm = llm_backend
def generate_optimal_structure(self,
requirements: PresentationRequirements,
research: ResearchFindings,
num_alternatives: int = 3,
max_depth: int = 3) -> PresentationStructure:
"""Generate optimal presentation structure using tree exploration"""
root_nodes = self._generate_initial_structures(
requirements,
research,
num_alternatives
)
for node in root_nodes:
self._explore_branch(node, requirements, research, max_depth)
best_structure = self._select_best_structure(root_nodes)
return best_structure
def _generate_initial_structures(self,
requirements: PresentationRequirements,
research: ResearchFindings,
num_alternatives: int) -> List[ThoughtNode]:
"""Generate initial high-level structure alternatives"""
structure_prompt = f"""Generate {num_alternatives} different high-level organizational structures for a presentation.
Topic: {requirements.topic}
Duration: {requirements.duration_minutes} minutes
Target Audience: {requirements.target_audience}
Key Facts: {research.key_facts[:5]}
For each structure, specify:
1. The organizational approach (chronological, problem-solution, layered abstraction, use-case driven, or conceptual building)
2. Main sections (3-5 sections)
3. Brief rationale for this structure
Format each structure as:
STRUCTURE [number]:
Type: [structure type]
Sections:
- [Section 1 title]
- [Section 2 title]
- [Section 3 title]
Rationale: [Why this structure works]
Structures:"""
messages = [
LLMMessage(role="system", content="You are an expert presentation designer."),
LLMMessage(role="user", content=structure_prompt)
]
response = self.llm.generate(messages, temperature=0.8)
structures = self._parse_structures(response.content, requirements)
nodes = []
for structure in structures:
node = ThoughtNode(
structure=structure,
depth=0,
parent=None
)
node.evaluation_score = self._evaluate_structure(node.structure, requirements)
nodes.append(node)
return nodes
def _parse_structures(self,
response: str,
requirements: PresentationRequirements) -> List[PresentationStructure]:
"""Parse structure descriptions into structured objects"""
structures = []
current_structure = None
current_sections = []
for line in response.split('\n'):
line = line.strip()
if line.startswith('STRUCTURE'):
if current_structure and current_sections:
current_structure.sections = [
SectionOutline(title=title, slides=[], learning_objectives=[])
for title in current_sections
]
structures.append(current_structure)
current_structure = PresentationStructure(
title=requirements.title,
structure_type=StructureType.CONCEPTUAL_BUILDING,
sections=[]
)
current_sections = []
elif line.startswith('Type:'):
type_str = line.split(':', 1)[1].strip().lower().replace(' ', '_')
try:
current_structure.structure_type = StructureType(type_str)
except ValueError:
pass
elif line.startswith('-') and current_structure:
section_title = line.lstrip('- ').strip()
if section_title:
current_sections.append(section_title)
if current_structure and current_sections:
current_structure.sections = [
SectionOutline(title=title, slides=[], learning_objectives=[])
for title in current_sections
]
structures.append(current_structure)
return structures
def _explore_branch(self,
node: ThoughtNode,
requirements: PresentationRequirements,
research: ResearchFindings,
max_depth: int):
"""Recursively explore a reasoning branch"""
if node.depth >= max_depth:
node.is_complete = True
return
children = self._expand_structure(node, requirements, research)
for child in children:
child.evaluation_score = self._evaluate_structure(child.structure, requirements)
if child.evaluation_score > 0.6:
node.children.append(child)
if child.depth < max_depth:
self._explore_branch(child, requirements, research, max_depth)
def _expand_structure(self,
node: ThoughtNode,
requirements: PresentationRequirements,
research: ResearchFindings) -> List[ThoughtNode]:
"""Expand a structure by adding slide details"""
structure = node.structure
if not any(section.slides for section in structure.sections):
expanded = self._generate_slides_for_sections(structure, requirements, research)
child = ThoughtNode(
structure=expanded,
depth=node.depth + 1,
parent=node
)
child.is_complete = True
return [child]
return []
def _generate_slides_for_sections(self,
structure: PresentationStructure,
requirements: PresentationRequirements,
research: ResearchFindings) -> PresentationStructure:
"""Generate detailed slide outlines for each section"""
expanded = copy.deepcopy(structure)
total_time = requirements.duration_minutes * 60
intro_outro_time = 120
content_time = total_time - intro_outro_time
time_per_section = content_time // len(expanded.sections)
for section in expanded.sections:
slides = self._generate_section_slides(
section,
requirements,
research,
time_per_section
)
section.slides = slides
expanded.calculate_metrics()
return expanded
def _generate_section_slides(self,
section: SectionOutline,
requirements: PresentationRequirements,
research: ResearchFindings,
time_budget: int) -> List[SlideOutline]:
"""Generate slide outlines for a specific section"""
slide_prompt = f"""Generate detailed slide outlines for the following presentation section.
Section Title: {section.title}
Time Budget: {time_budget} seconds
Target Audience: {requirements.target_audience}
Available Research: {research.key_facts[:3]}
Generate 3-5 slides for this section. For each slide specify:
- Title
- 3-4 key points
- Estimated duration in seconds
- Visual type (diagram, code, chart, image, or text)
Format:
SLIDE [number]:
Title: [slide title]
Points:
- [point 1]
- [point 2]
- [point 3]
Duration: [seconds]
Visual: [type]
Slides:"""
messages = [
LLMMessage(role="system", content="You are an expert at designing effective presentation slides."),
LLMMessage(role="user", content=slide_prompt)
]
response = self.llm.generate(messages, temperature=0.7)
slides = self._parse_slides(response.content)
return slides
def _parse_slides(self, response: str) -> List[SlideOutline]:
"""Parse slide descriptions into structured objects"""
slides = []
current_slide = None
current_points = []
for line in response.split('\n'):
line = line.strip()
if line.startswith('SLIDE'):
if current_slide:
current_slide.key_points = current_points
slides.append(current_slide)
current_slide = SlideOutline(
title="",
key_points=[],
estimated_duration=60,
visual_type="text"
)
current_points = []
elif line.startswith('Title:') and current_slide:
current_slide.title = line.split(':', 1)[1].strip()
elif line.startswith('-') and current_slide:
point = line.lstrip('- ').strip()
if point:
current_points.append(point)
elif line.startswith('Duration:') and current_slide:
try:
duration = int(''.join(filter(str.isdigit, line)))
current_slide.estimated_duration = duration
except ValueError:
pass
elif line.startswith('Visual:') and current_slide:
visual = line.split(':', 1)[1].strip().lower()
if visual in ['diagram', 'code', 'chart', 'image', 'text']:
current_slide.visual_type = visual
if current_slide:
current_slide.key_points = current_points
slides.append(current_slide)
return slides
def _evaluate_structure(self,
structure: PresentationStructure,
requirements: PresentationRequirements) -> float:
"""Evaluate the quality of a presentation structure"""
evaluation_prompt = f"""Evaluate the quality of this presentation structure on a scale of 0.0 to 1.0.
Topic: {requirements.topic}
Duration: {requirements.duration_minutes} minutes
Target Audience: {requirements.target_audience}
Structure Type: {structure.structure_type.value}
Number of Sections: {len(structure.sections)}
Total Slides: {structure.total_slides}
Estimated Duration: {structure.estimated_duration} seconds
Sections:
{self._format_sections_for_evaluation(structure.sections)}
Evaluate based on:
- Logical flow and coherence
- Appropriate depth for audience
- Time allocation
- Coverage of topic
- Pedagogical effectiveness
Respond with only a number between 0.0 and 1.0.
Score:"""
messages = [
LLMMessage(role="system", content="You are an expert at evaluating presentation quality."),
LLMMessage(role="user", content=evaluation_prompt)
]
response = self.llm.generate(messages, temperature=0.1)
try:
score = float(response.content.strip())
return max(0.0, min(1.0, score))
except ValueError:
return 0.5
def _format_sections_for_evaluation(self, sections: List[SectionOutline]) -> str:
"""Format sections for evaluation prompt"""
formatted = []
for i, section in enumerate(sections, 1):
formatted.append(f"{i}. {section.title} ({len(section.slides)} slides)")
return '\n'.join(formatted)
def _select_best_structure(self, root_nodes: List[ThoughtNode]) -> PresentationStructure:
"""Select the best structure from explored alternatives"""
all_complete_nodes = []
def collect_complete_nodes(node: ThoughtNode):
if node.is_complete:
all_complete_nodes.append(node)
for child in node.children:
collect_complete_nodes(child)
for root in root_nodes:
collect_complete_nodes(root)
if not all_complete_nodes:
return max(root_nodes, key=lambda n: n.evaluation_score).structure
best_node = max(all_complete_nodes, key=lambda n: n.evaluation_score)
return best_node.structure
This Tree-of-Thought approach ensures the presentation structure is not just adequate but optimized for the specific requirements and audience. The exploration process considers multiple perspectives and selects the most effective organization.
SLIDE CONTENT GENERATION WITH CHAIN-OF-THOUGHT
With the structure determined, the system generates detailed content for each slide. This process uses Chain-of-Thought reasoning to ensure each slide effectively communicates its intended message while adhering to best practices.
For each slide, the system follows a structured reasoning process. First, it identifies the core message and learning objective. Second, it determines the most effective visual representation. Third, it generates concise bullet points that support the message. Fourth, it creates comprehensive speaker notes. Fifth, it selects or generates appropriate visual elements.
The content generation process maintains consistency across slides while adapting to the specific needs of each topic. Technical slides might include code examples with syntax highlighting. Conceptual slides might feature diagrams or illustrations. Data-driven slides might present charts or graphs.
Here is the slide content generator implementation:
from typing import List, Dict, Optional, Tuple
from dataclasses import dataclass
from enum import Enum
@dataclass
class SlideContent:
"""Complete content for a single slide"""
title: str
bullet_points: List[str]
speaker_notes: str
visual_description: str
visual_type: str
code_snippet: Optional[str] = None
image_query: Optional[str] = None
@dataclass
class PresentationSlide:
"""Full slide with content and metadata"""
slide_number: int
section: str
content: SlideContent
style_theme: str
class SlideContentGenerator:
"""Generates detailed content for presentation slides"""
def __init__(self, llm_backend: LLMBackend):
self.llm = llm_backend
def generate_slide_content(self,
outline: SlideOutline,
section: SectionOutline,
requirements: PresentationRequirements,
research: ResearchFindings,
slide_number: int) -> PresentationSlide:
"""Generate complete content for a single slide"""
thought_process = self._chain_of_thought_content_development(
outline,
section,
requirements,
research
)
content = self._generate_final_content(thought_process, outline, requirements)
theme = self._determine_style_theme(requirements)
return PresentationSlide(
slide_number=slide_number,
section=section.title,
content=content,
style_theme=theme
)
def _chain_of_thought_content_development(self,
outline: SlideOutline,
section: SectionOutline,
requirements: PresentationRequirements,
research: ResearchFindings) -> Dict[str, str]:
"""Use Chain-of-Thought reasoning to develop slide content"""
cot_prompt = f"""Develop content for a presentation slide using step-by-step reasoning.
Slide Title: {outline.title}
Section: {section.title}
Key Points from Outline: {outline.key_points}
Visual Type: {outline.visual_type}
Target Audience: {requirements.target_audience}
Topic: {requirements.topic}
Think through the following steps:
STEP 1 - Core Message:
What is the single most important message this slide should convey?
STEP 2 - Audience Connection:
How does this message relate to the audience's existing knowledge and needs?
STEP 3 - Visual Strategy:
What visual approach will best communicate this message?
(Consider: diagram, code example, chart, image, or text layout)
STEP 4 - Content Structure:
What specific points should appear on the slide?
(Keep to 3-5 concise points, avoid text walls)
STEP 5 - Speaker Notes:
What additional context should the speaker provide?
(Include explanations, examples, and transitions)
STEP 6 - Code or Visual Details:
If applicable, what code snippet or visual description is needed?
Provide your reasoning for each step.
Reasoning:"""
messages = [
LLMMessage(role="system", content="You are an expert presentation designer using systematic reasoning."),
LLMMessage(role="user", content=cot_prompt)
]
response = self.llm.generate(messages, temperature=0.7, max_tokens=1500)
thought_process = self._parse_thought_process(response.content)
return thought_process
def _parse_thought_process(self, response: str) -> Dict[str, str]:
"""Parse Chain-of-Thought reasoning into structured format"""
steps = {
'core_message': '',
'audience_connection': '',
'visual_strategy': '',
'content_structure': '',
'speaker_notes': '',
'code_or_visual': ''
}
current_step = None
current_content = []
for line in response.split('\n'):
line = line.strip()
if 'STEP 1' in line or 'Core Message' in line:
if current_step:
steps[current_step] = '\n'.join(current_content)
current_step = 'core_message'
current_content = []
elif 'STEP 2' in line or 'Audience Connection' in line:
if current_step:
steps[current_step] = '\n'.join(current_content)
current_step = 'audience_connection'
current_content = []
elif 'STEP 3' in line or 'Visual Strategy' in line:
if current_step:
steps[current_step] = '\n'.join(current_content)
current_step = 'visual_strategy'
current_content = []
elif 'STEP 4' in line or 'Content Structure' in line:
if current_step:
steps[current_step] = '\n'.join(current_content)
current_step = 'content_structure'
current_content = []
elif 'STEP 5' in line or 'Speaker Notes' in line:
if current_step:
steps[current_step] = '\n'.join(current_content)
current_step = 'speaker_notes'
current_content = []
elif 'STEP 6' in line or 'Code or Visual' in line:
if current_step:
steps[current_step] = '\n'.join(current_content)
current_step = 'code_or_visual'
current_content = []
elif current_step and line:
current_content.append(line)
if current_step:
steps[current_step] = '\n'.join(current_content)
return steps
def _generate_final_content(self,
thought_process: Dict[str, str],
outline: SlideOutline,
requirements: PresentationRequirements) -> SlideContent:
"""Generate final slide content based on reasoning"""
bullet_points = self._extract_bullet_points(thought_process['content_structure'])
speaker_notes = self._generate_speaker_notes(
thought_process,
outline,
requirements
)
visual_description = thought_process['visual_strategy']
code_snippet = None
image_query = None
if outline.visual_type == 'code':
code_snippet = self._generate_code_example(
outline.title,
requirements.topic,
thought_process['code_or_visual']
)
elif outline.visual_type in ['image', 'diagram']:
image_query = self._generate_image_query(
outline.title,
requirements.topic,
visual_description
)
return SlideContent(
title=outline.title,
bullet_points=bullet_points,
speaker_notes=speaker_notes,
visual_description=visual_description,
visual_type=outline.visual_type,
code_snippet=code_snippet,
image_query=image_query
)
def _extract_bullet_points(self, content_structure: str) -> List[str]:
"""Extract bullet points from content structure reasoning"""
points = []
for line in content_structure.split('\n'):
line = line.strip()
if line.startswith('-') or line.startswith('•'):
point = line.lstrip('-•').strip()
if point and len(point) > 5:
points.append(point)
elif line and len(line) > 10 and not line.endswith(':'):
points.append(line)
return points[:5]
def _generate_speaker_notes(self,
thought_process: Dict[str, str],
outline: SlideOutline,
requirements: PresentationRequirements) -> str:
"""Generate comprehensive speaker notes"""
notes_prompt = f"""Generate detailed speaker notes for this slide.
Slide Title: {outline.title}
Core Message: {thought_process['core_message']}
Audience: {requirements.target_audience}
The speaker notes should:
- Explain the key concepts in detail
- Provide examples or analogies
- Include transition to next topic
- Suggest timing and pacing
- Be 150-200 words
Speaker Notes:"""
messages = [
LLMMessage(role="system", content="You are an expert at writing effective speaker notes."),
LLMMessage(role="user", content=notes_prompt)
]
response = self.llm.generate(messages, temperature=0.7, max_tokens=400)
return response.content.strip()
def _generate_code_example(self,
slide_title: str,
topic: str,
code_guidance: str) -> str:
"""Generate code example for technical slides"""
code_prompt = f"""Generate a clear, well-commented code example for this slide.
Slide Title: {slide_title}
Topic: {topic}
Guidance: {code_guidance}
Requirements:
- Use proper indentation
- Include helpful comments
- Keep it concise (10-20 lines)
- Make it syntactically correct
- Use meaningful variable names
Code Example:"""
messages = [
LLMMessage(role="system", content="You are an expert programmer who writes clear, educational code."),
LLMMessage(role="user", content=code_prompt)
]
response = self.llm.generate(messages, temperature=0.5, max_tokens=500)
code = self._extract_code_block(response.content)
return code
def _extract_code_block(self, text: str) -> str:
"""Extract code block from markdown-formatted text"""
if '```' in text:
parts = text.split('```')
if len(parts) >= 3:
code = parts[1]
if '\n' in code:
lines = code.split('\n')
if lines[0].strip() in ['python', 'javascript', 'java', 'cpp', 'c', 'go', 'rust']:
code = '\n'.join(lines[1:])
return code.strip()
return text.strip()
def _generate_image_query(self,
slide_title: str,
topic: str,
visual_description: str) -> str:
"""Generate search query for finding relevant images"""
query_prompt = f"""Generate a specific image search query for this slide.
Slide Title: {slide_title}
Topic: {topic}
Visual Description: {visual_description}
The query should find:
- Professional, high-quality images
- Relevant to the specific slide content
- Appropriate for business/educational context
Generate only the search query, nothing else.
Search Query:"""
messages = [
LLMMessage(role="system", content="You are an expert at finding relevant visual content."),
LLMMessage(role="user", content=query_prompt)
]
response = self.llm.generate(messages, temperature=0.5, max_tokens=50)
return response.content.strip()
def _determine_style_theme(self, requirements: PresentationRequirements) -> str:
"""Determine appropriate visual theme for presentation"""
theme_prompt = f"""Suggest an appropriate PowerPoint theme/style for this presentation.
Topic: {requirements.topic}
Audience: {requirements.target_audience}
Style Preferences: {requirements.style_preferences or 'Professional'}
Consider:
- Color scheme
- Font style
- Overall aesthetic
- Professional appropriateness
Respond with a brief theme description (2-3 sentences).
Theme:"""
messages = [
LLMMessage(role="system", content="You are a presentation design expert."),
LLMMessage(role="user", content=theme_prompt)
]
response = self.llm.generate(messages, temperature=0.6, max_tokens=150)
return response.content.strip()
The Chain-of-Thought approach ensures each slide is thoughtfully designed with clear purpose and effective communication strategies. The systematic reasoning process makes the content generation transparent and allows for quality control at each step.
IMAGE RETRIEVAL FROM OPEN SOURCE REPOSITORIES
Visual elements significantly enhance presentation effectiveness. The system retrieves images from open source repositories including Wikimedia Commons, Unsplash, and Pexels. All these services provide free APIs that require minimal or no authentication. All external images include proper attribution to respect copyright and licensing requirements.
The image retrieval system uses the search queries generated during content creation. It evaluates images for relevance, quality, and licensing. It prioritizes Creative Commons licensed images and public domain content. For all images, it includes clear attribution information.
Here is the image retrieval and management system:
from typing import List, Optional, Dict, Tuple
from dataclasses import dataclass
import requests
from urllib.parse import quote_plus
import hashlib
import os
@dataclass
class ImageMetadata:
"""Metadata for a retrieved image"""
url: str
title: str
source: str
license: str
attribution: str
width: int
height: int
local_path: Optional[str] = None
class ImageRetriever:
"""Retrieves and manages images from open source repositories"""
def __init__(self,
llm_backend: LLMBackend,
unsplash_access_key: Optional[str] = None,
cache_dir: str = "./image_cache"):
self.llm = llm_backend
self.unsplash_key = unsplash_access_key
self.cache_dir = cache_dir
os.makedirs(cache_dir, exist_ok=True)
def find_image(self,
query: str,
prefer_free_license: bool = True) -> Optional[ImageMetadata]:
"""Find appropriate image for slide content"""
results = []
results.extend(self._search_wikimedia_commons(query))
if self.unsplash_key:
results.extend(self._search_unsplash(query))
results.extend(self._search_pexels(query))
if not results:
return None
best_image = self._select_best_image(results, query)
if best_image:
self._download_image(best_image)
return best_image
def _search_wikimedia_commons(self, query: str) -> List[ImageMetadata]:
"""Search for Creative Commons licensed images on Wikimedia Commons"""
endpoint = "https://commons.wikimedia.org/w/api.php"
params = {
"action": "query",
"format": "json",
"generator": "search",
"gsrnamespace": "6",
"gsrsearch": query,
"gsrlimit": "10",
"prop": "imageinfo",
"iiprop": "url|size|extmetadata"
}
try:
response = requests.get(endpoint, params=params, timeout=10)
response.raise_for_status()
data = response.json()
results = []
pages = data.get("query", {}).get("pages", {})
for page_id, page in pages.items():
if "imageinfo" in page:
info = page["imageinfo"][0]
extmeta = info.get("extmetadata", {})
license_info = extmeta.get("License", {}).get("value", "CC BY-SA")
artist_info = extmeta.get("Artist", {}).get("value", "Wikimedia Commons")
results.append(ImageMetadata(
url=info.get("url", ""),
title=page.get("title", ""),
source=f"https://commons.wikimedia.org/wiki/{page.get('title', '')}",
license=license_info,
attribution=f"Image from Wikimedia Commons, {artist_info}, {license_info}",
width=info.get("width", 0),
height=info.get("height", 0)
))
return results
except Exception as e:
print(f"Wikimedia Commons search error: {e}")
return []
def _search_unsplash(self, query: str) -> List[ImageMetadata]:
"""Search for free images on Unsplash"""
if not self.unsplash_key:
return []
endpoint = "https://api.unsplash.com/search/photos"
headers = {
"Authorization": f"Client-ID {self.unsplash_key}"
}
params = {
"query": query,
"per_page": 10
}
try:
response = requests.get(endpoint, headers=headers, params=params, timeout=10)
response.raise_for_status()
data = response.json()
results = []
for item in data.get("results", []):
user = item.get("user", {})
urls = item.get("urls", {})
attribution = f"Photo by {user.get('name', 'Unknown')} on Unsplash"
results.append(ImageMetadata(
url=urls.get("regular", ""),
title=item.get("description", query),
source=item.get("links", {}).get("html", ""),
license="Unsplash License (Free to use)",
attribution=attribution,
width=item.get("width", 0),
height=item.get("height", 0)
))
return results
except Exception as e:
print(f"Unsplash search error: {e}")
return []
def _search_pexels(self, query: str) -> List[ImageMetadata]:
"""Search for free images on Pexels"""
endpoint = "https://api.pexels.com/v1/search"
headers = {
"Authorization": "YOUR_PEXELS_API_KEY"
}
params = {
"query": query,
"per_page": 10
}
try:
response = requests.get(endpoint, headers=headers, params=params, timeout=10)
if response.status_code == 401:
return []
response.raise_for_status()
data = response.json()
results = []
for item in data.get("photos", []):
photographer = item.get("photographer", "Unknown")
results.append(ImageMetadata(
url=item.get("src", {}).get("large", ""),
title=query,
source=item.get("url", ""),
license="Pexels License (Free to use)",
attribution=f"Photo by {photographer} on Pexels",
width=item.get("width", 0),
height=item.get("height", 0)
))
return results
except Exception as e:
print(f"Pexels search error: {e}")
return []
def _select_best_image(self,
results: List[ImageMetadata],
query: str) -> Optional[ImageMetadata]:
"""Select most appropriate image from results"""
if not results:
return None
scored_results = []
for image in results:
score = 0.0
if image.width >= 1024 and image.height >= 768:
score += 0.3
if "public" in image.license.lower() or "cc" in image.license.lower():
score += 0.4
if image.width > 0 and image.height > 0:
aspect = image.width / image.height
if 1.3 <= aspect <= 1.8:
score += 0.3
scored_results.append((score, image))
scored_results.sort(key=lambda x: x[0], reverse=True)
return scored_results[0][1] if scored_results else None
def _download_image(self, image: ImageMetadata) -> bool:
"""Download and cache image locally"""
try:
url_hash = hashlib.md5(image.url.encode()).hexdigest()
extension = image.url.split('.')[-1].split('?')[0]
if extension not in ['jpg', 'jpeg', 'png', 'gif', 'webp']:
extension = 'jpg'
filename = f"{url_hash}.{extension}"
filepath = os.path.join(self.cache_dir, filename)
if not os.path.exists(filepath):
response = requests.get(image.url, timeout=15, stream=True)
response.raise_for_status()
with open(filepath, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
image.local_path = filepath
return True
except Exception as e:
print(f"Image download error: {e}")
return False
def generate_diagram(self,
description: str,
diagram_type: str = "flowchart") -> Optional[str]:
"""Generate diagram specification using Mermaid syntax"""
diagram_prompt = f"""Generate a {diagram_type} diagram specification for:
{description}
Use Mermaid.js syntax. Make it clear and professional.
Diagram:"""
messages = [
LLMMessage(role="system", content="You are an expert at creating clear diagrams."),
LLMMessage(role="user", content=diagram_prompt)
]
response = self.llm.generate(messages, temperature=0.5, max_tokens=500)
return response.content.strip()
The image retrieval system ensures visual content enhances rather than distracts from the presentation message. Proper attribution maintains ethical standards and legal compliance. By using open source image repositories, the system avoids licensing costs and restrictions.
POWERPOINT FILE GENERATION AND STYLING
The final stage assembles all components into an actual PowerPoint file using the python-pptx library, which is a pure Python open source library for creating and updating PowerPoint files. This requires interfacing with PowerPoint file formats, applying consistent styling, and ensuring professional appearance.
The PowerPoint generator creates slides with appropriate layouts, applies the determined theme, inserts text content, adds images, formats code blocks, and generates speaker notes. It maintains consistency in fonts, colors, and spacing throughout the presentation.
Here is the PowerPoint generation system:
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.dml.color import RGBColor
from typing import List, Dict, Tuple, Optional
import os
class PowerPointGenerator:
"""Generates PowerPoint presentations using python-pptx"""
def __init__(self):
self.presentation = None
self.theme_colors = {}
self.font_family = "Calibri"
self.code_font_family = "Consolas"
def create_presentation(self,
slides: List[PresentationSlide],
requirements: PresentationRequirements,
output_path: str) -> bool:
"""Create complete PowerPoint presentation"""
self.presentation = Presentation()
self.presentation.slide_width = Inches(10)
self.presentation.slide_height = Inches(7.5)
self._apply_theme(slides[0].style_theme if slides else "professional")
self._create_title_slide(requirements)
for slide in slides:
self._create_content_slide(slide)
self._create_closing_slide(requirements)
try:
self.presentation.save(output_path)
return True
except Exception as e:
print(f"Error saving presentation: {e}")
return False
def _apply_theme(self, theme_description: str):
"""Apply visual theme to presentation"""
self.theme_colors = {
'primary': RGBColor(0, 51, 102),
'secondary': RGBColor(0, 102, 204),
'accent': RGBColor(255, 153, 0),
'text': RGBColor(51, 51, 51),
'background': RGBColor(255, 255, 255)
}
if 'technical' in theme_description.lower():
self.theme_colors['primary'] = RGBColor(0, 0, 0)
self.theme_colors['accent'] = RGBColor(0, 255, 0)
def _create_title_slide(self, requirements: PresentationRequirements):
"""Create presentation title slide"""
slide_layout = self.presentation.slide_layouts[0]
slide = self.presentation.slides.add_slide(slide_layout)
title = slide.shapes.title
title.text = requirements.title
title.text_frame.paragraphs[0].font.size = Pt(44)
title.text_frame.paragraphs[0].font.bold = True
title.text_frame.paragraphs[0].font.color.rgb = self.theme_colors['primary']
if len(slide.placeholders) > 1:
subtitle = slide.placeholders[1]
subtitle.text = f"{requirements.topic}\n\nTarget Audience: {requirements.target_audience}"
subtitle.text_frame.paragraphs[0].font.size = Pt(24)
subtitle.text_frame.paragraphs[0].font.color.rgb = self.theme_colors['text']
def _create_content_slide(self, slide_data: PresentationSlide):
"""Create a content slide"""
if slide_data.content.visual_type == 'code':
self._create_code_slide(slide_data)
elif slide_data.content.visual_type in ['image', 'diagram']:
self._create_image_slide(slide_data)
else:
self._create_text_slide(slide_data)
def _create_text_slide(self, slide_data: PresentationSlide):
"""Create slide with text content"""
slide_layout = self.presentation.slide_layouts[1]
slide = self.presentation.slides.add_slide(slide_layout)
title = slide.shapes.title
title.text = slide_data.content.title
title.text_frame.paragraphs[0].font.size = Pt(32)
title.text_frame.paragraphs[0].font.bold = True
title.text_frame.paragraphs[0].font.color.rgb = self.theme_colors['primary']
content_placeholder = slide.placeholders[1]
text_frame = content_placeholder.text_frame
text_frame.clear()
for point in slide_data.content.bullet_points:
p = text_frame.add_paragraph()
p.text = point
p.level = 0
p.font.size = Pt(20)
p.font.name = self.font_family
p.font.color.rgb = self.theme_colors['text']
p.space_after = Pt(12)
notes_slide = slide.notes_slide
notes_text_frame = notes_slide.notes_text_frame
notes_text_frame.text = slide_data.content.speaker_notes
def _create_code_slide(self, slide_data: PresentationSlide):
"""Create slide with code content"""
slide_layout = self.presentation.slide_layouts[6]
slide = self.presentation.slides.add_slide(slide_layout)
title_box = slide.shapes.add_textbox(
Inches(0.5), Inches(0.3),
Inches(9), Inches(0.8)
)
title_frame = title_box.text_frame
title_frame.text = slide_data.content.title
title_frame.paragraphs[0].font.size = Pt(32)
title_frame.paragraphs[0].font.bold = True
title_frame.paragraphs[0].font.color.rgb = self.theme_colors['primary']
code_box = slide.shapes.add_textbox(
Inches(0.5), Inches(1.3),
Inches(9), Inches(5)
)
code_frame = code_box.text_frame
code_frame.word_wrap = True
if slide_data.content.code_snippet:
code_frame.text = slide_data.content.code_snippet
for paragraph in code_frame.paragraphs:
paragraph.font.name = self.code_font_family
paragraph.font.size = Pt(14)
paragraph.font.color.rgb = RGBColor(0, 0, 0)
code_box.fill.solid()
code_box.fill.fore_color.rgb = RGBColor(245, 245, 245)
code_box.line.color.rgb = self.theme_colors['secondary']
notes_slide = slide.notes_slide
notes_text_frame = notes_slide.notes_text_frame
notes_text_frame.text = slide_data.content.speaker_notes
def _create_image_slide(self, slide_data: PresentationSlide):
"""Create slide with image content"""
slide_layout = self.presentation.slide_layouts[6]
slide = self.presentation.slides.add_slide(slide_layout)
title_box = slide.shapes.add_textbox(
Inches(0.5), Inches(0.3),
Inches(9), Inches(0.8)
)
title_frame = title_box.text_frame
title_frame.text = slide_data.content.title
title_frame.paragraphs[0].font.size = Pt(32)
title_frame.paragraphs[0].font.bold = True
title_frame.paragraphs[0].font.color.rgb = self.theme_colors['primary']
content_box = slide.shapes.add_textbox(
Inches(0.5), Inches(1.5),
Inches(4), Inches(5)
)
content_frame = content_box.text_frame
for point in slide_data.content.bullet_points:
p = content_frame.add_paragraph()
p.text = point
p.font.size = Pt(18)
p.font.name = self.font_family
p.font.color.rgb = self.theme_colors['text']
p.space_after = Pt(10)
image_box = slide.shapes.add_textbox(
Inches(5), Inches(1.5),
Inches(4.5), Inches(5)
)
image_frame = image_box.text_frame
image_frame.text = f"[Image: {slide_data.content.visual_description}]"
image_frame.paragraphs[0].font.size = Pt(14)
image_frame.paragraphs[0].font.italic = True
image_box.fill.solid()
image_box.fill.fore_color.rgb = RGBColor(240, 240, 240)
notes_slide = slide.notes_slide
notes_text_frame = notes_slide.notes_text_frame
notes_text_frame.text = slide_data.content.speaker_notes
def _create_closing_slide(self, requirements: PresentationRequirements):
"""Create closing slide with Q&A or summary"""
slide_layout = self.presentation.slide_layouts[1]
slide = self.presentation.slides.add_slide(slide_layout)
title = slide.shapes.title
title.text = "Questions & Discussion"
title.text_frame.paragraphs[0].font.size = Pt(44)
title.text_frame.paragraphs[0].font.bold = True
title.text_frame.paragraphs[0].font.color.rgb = self.theme_colors['primary']
content_placeholder = slide.placeholders[1]
text_frame = content_placeholder.text_frame
text_frame.clear()
p = text_frame.add_paragraph()
p.text = f"Thank you for attending this presentation on {requirements.topic}"
p.font.size = Pt(24)
p.font.name = self.font_family
p.font.color.rgb = self.theme_colors['text']
The PowerPoint generator ensures the final presentation is polished and professional, ready for delivery without additional manual formatting. The python-pptx library provides comprehensive control over all aspects of the presentation file.
ORCHESTRATING THE COMPLETE WORKFLOW
The presentation generation system ties all components together through a coordinating orchestrator. This component manages the workflow from initial user input through final PowerPoint file creation. It handles error recovery, progress tracking, and iterative refinement.
The orchestrator implements a state machine that tracks progress through the generation pipeline. Each state represents a stage in the process such as requirement gathering, research, structure planning, content generation, and file creation. The system can pause at any state to request user clarification or handle errors.
Here is the main orchestrator implementation:
from typing import Optional, List, Dict, Callable
from dataclasses import dataclass
from enum import Enum
import json
class GenerationState(Enum):
"""States in the presentation generation workflow"""
INITIAL = "initial"
ANALYZING_REQUIREMENTS = "analyzing_requirements"
CLARIFYING = "clarifying"
RESEARCHING = "researching"
PLANNING_STRUCTURE = "planning_structure"
GENERATING_CONTENT = "generating_content"
RETRIEVING_IMAGES = "retrieving_images"
CREATING_POWERPOINT = "creating_powerpoint"
COMPLETE = "complete"
ERROR = "error"
@dataclass
class GenerationContext:
"""Context maintained throughout generation process"""
state: GenerationState
requirements: Optional[PresentationRequirements] = None
research: Optional[ResearchFindings] = None
structure: Optional[PresentationStructure] = None
slides: Optional[List[PresentationSlide]] = None
output_path: Optional[str] = None
error_message: Optional[str] = None
def __post_init__(self):
if self.slides is None:
self.slides = []
class PresentationOrchestrator:
"""Orchestrates the complete presentation generation workflow"""
def __init__(self,
llm_backend: LLMBackend,
output_dir: str = "./presentations"):
self.llm = llm_backend
self.output_dir = output_dir
self.requirement_analyzer = RequirementAnalyzer(llm_backend)
self.web_searcher = WebSearcher(llm_backend)
self.structure_reasoner = TreeOfThoughtReasoner(llm_backend)
self.content_generator = SlideContentGenerator(llm_backend)
self.image_retriever = ImageRetriever(llm_backend)
self.ppt_generator = PowerPointGenerator()
os.makedirs(output_dir, exist_ok=True)
def generate_presentation(self,
user_prompt: str,
progress_callback: Optional[Callable[[str, float], None]] = None) -> GenerationContext:
"""Generate complete presentation from user prompt"""
context = GenerationContext(state=GenerationState.INITIAL)
try:
context = self._analyze_requirements(context, user_prompt, progress_callback)
if context.state == GenerationState.CLARIFYING:
return context
context = self._research_topic(context, progress_callback)
context = self._plan_structure(context, progress_callback)
context = self._generate_content(context, progress_callback)
context = self._retrieve_images(context, progress_callback)
context = self._create_powerpoint(context, progress_callback)
context.state = GenerationState.COMPLETE
except Exception as e:
context.state = GenerationState.ERROR
context.error_message = str(e)
return context
def _analyze_requirements(self,
context: GenerationContext,
user_prompt: str,
progress_callback: Optional[Callable[[str, float], None]]) -> GenerationContext:
"""Analyze user requirements"""
context.state = GenerationState.ANALYZING_REQUIREMENTS
if progress_callback:
progress_callback("Analyzing requirements...", 0.1)
requirements = self.requirement_analyzer.analyze_prompt(user_prompt)
if not requirements.is_complete():
context.requirements = requirements
context.state = GenerationState.CLARIFYING
return context
context.requirements = requirements
return context
def handle_clarification(self,
context: GenerationContext,
user_response: str) -> GenerationContext:
"""Handle user's clarification response"""
updated_requirements = self.requirement_analyzer.analyze_prompt(user_response)
if context.requirements.title is None and updated_requirements.title:
context.requirements.title = updated_requirements.title
if context.requirements.topic is None and updated_requirements.topic:
context.requirements.topic = updated_requirements.topic
if context.requirements.duration_minutes is None and updated_requirements.duration_minutes:
context.requirements.duration_minutes = updated_requirements.duration_minutes
if context.requirements.target_audience is None and updated_requirements.target_audience:
context.requirements.target_audience = updated_requirements.target_audience
if context.requirements.is_complete():
context.state = GenerationState.ANALYZING_REQUIREMENTS
return context
def _research_topic(self,
context: GenerationContext,
progress_callback: Optional[Callable[[str, float], None]]) -> GenerationContext:
"""Research presentation topic"""
context.state = GenerationState.RESEARCHING
if progress_callback:
progress_callback("Researching topic...", 0.3)
research = self.web_searcher.research_topic(context.requirements)
context.research = research
return context
def _plan_structure(self,
context: GenerationContext,
progress_callback: Optional[Callable[[str, float], None]]) -> GenerationContext:
"""Plan presentation structure"""
context.state = GenerationState.PLANNING_STRUCTURE
if progress_callback:
progress_callback("Planning structure...", 0.5)
structure = self.structure_reasoner.generate_optimal_structure(
context.requirements,
context.research
)
context.structure = structure
return context
def _generate_content(self,
context: GenerationContext,
progress_callback: Optional[Callable[[str, float], None]]) -> GenerationContext:
"""Generate slide content"""
context.state = GenerationState.GENERATING_CONTENT
if progress_callback:
progress_callback("Generating content...", 0.7)
slides = []
slide_number = 1
for section in context.structure.sections:
for outline in section.slides:
slide = self.content_generator.generate_slide_content(
outline,
section,
context.requirements,
context.research,
slide_number
)
slides.append(slide)
slide_number += 1
context.slides = slides
return context
def _retrieve_images(self,
context: GenerationContext,
progress_callback: Optional[Callable[[str, float], None]]) -> GenerationContext:
"""Retrieve images for slides"""
context.state = GenerationState.RETRIEVING_IMAGES
if progress_callback:
progress_callback("Retrieving images...", 0.85)
for slide in context.slides:
if slide.content.image_query:
image = self.image_retriever.find_image(slide.content.image_query)
if image:
slide.content.visual_description = f"{slide.content.visual_description}\n\nImage: {image.attribution}"
return context
def _create_powerpoint(self,
context: GenerationContext,
progress_callback: Optional[Callable[[str, float], None]]) -> GenerationContext:
"""Create PowerPoint file"""
context.state = GenerationState.CREATING_POWERPOINT
if progress_callback:
progress_callback("Creating PowerPoint...", 0.95)
filename = f"{context.requirements.title.replace(' ', '_')}.pptx"
output_path = os.path.join(self.output_dir, filename)
success = self.ppt_generator.create_presentation(
context.slides,
context.requirements,
output_path
)
if success:
context.output_path = output_path
else:
raise Exception("Failed to create PowerPoint file")
return context
The orchestrator provides a clean interface for the entire presentation generation process. It manages state transitions, handles errors gracefully, and provides progress feedback to the user.
COMPLETE RUNNING EXAMPLE IMPLEMENTATION
The following is a complete, production-ready implementation of the LLM-powered PowerPoint generation system. This implementation includes all necessary components and supports both local and remote LLMs, DuckDuckGo search, open source image retrieval, and PowerPoint generation.
#!/usr/bin/env python3
"""
LLM-Powered PowerPoint Presentation Generator
A complete system for automated presentation creation with advanced reasoning
"""
from abc import ABC, abstractmethod
from typing import List, Dict, Optional, Iterator, Callable, Tuple
from dataclasses import dataclass, field
from enum import Enum
import json
import os
import platform
import hashlib
import time
import requests
from urllib.parse import quote_plus
try:
from llama_cpp import Llama
LLAMA_CPP_AVAILABLE = True
except ImportError:
LLAMA_CPP_AVAILABLE = False
try:
import openai
OPENAI_AVAILABLE = True
except ImportError:
OPENAI_AVAILABLE = False
try:
from duckduckgo_search import DDGS
DUCKDUCKGO_AVAILABLE = True
except ImportError:
DUCKDUCKGO_AVAILABLE = False
try:
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
PYTHON_PPTX_AVAILABLE = True
except ImportError:
PYTHON_PPTX_AVAILABLE = False
@dataclass
class LLMMessage:
"""Represents a single message in the conversation"""
role: str
content: str
@dataclass
class LLMResponse:
"""Encapsulates the LLM's response"""
content: str
tokens_used: int
finish_reason: str
model: str
class LLMBackend(ABC):
"""Abstract base class for all LLM backends"""
@abstractmethod
def generate(self,
messages: List[LLMMessage],
temperature: float = 0.7,
max_tokens: Optional[int] = None) -> LLMResponse:
"""Generate a response given conversation history"""
pass
@abstractmethod
def stream_generate(self,
messages: List[LLMMessage],
temperature: float = 0.7,
max_tokens: Optional[int] = None) -> Iterator[str]:
"""Stream response tokens as they are generated"""
pass
@abstractmethod
def count_tokens(self, text: str) -> int:
"""Count tokens in the provided text"""
pass
@abstractmethod
def get_context_length(self) -> int:
"""Return maximum context length for this model"""
pass
class OpenAIBackend(LLMBackend):
"""OpenAI API implementation of LLM backend"""
def __init__(self, api_key: str, model: str = "gpt-4"):
if not OPENAI_AVAILABLE:
raise ImportError("OpenAI library not installed. Install with: pip install openai")
self.api_key = api_key
self.model = model
openai.api_key = api_key
def generate(self,
messages: List[LLMMessage],
temperature: float = 0.7,
max_tokens: Optional[int] = None) -> LLMResponse:
"""Generate response using OpenAI API"""
formatted_messages = [
{"role": msg.role, "content": msg.content}
for msg in messages
]
response = openai.ChatCompletion.create(
model=self.model,
messages=formatted_messages,
temperature=temperature,
max_tokens=max_tokens
)
return LLMResponse(
content=response.choices[0].message.content,
tokens_used=response.usage.total_tokens,
finish_reason=response.choices[0].finish_reason,
model=self.model
)
def stream_generate(self,
messages: List[LLMMessage],
temperature: float = 0.7,
max_tokens: Optional[int] = None) -> Iterator[str]:
"""Stream tokens from OpenAI API"""
formatted_messages = [
{"role": msg.role, "content": msg.content}
for msg in messages
]
stream = openai.ChatCompletion.create(
model=self.model,
messages=formatted_messages,
temperature=temperature,
max_tokens=max_tokens,
stream=True
)
for chunk in stream:
if chunk.choices[0].delta.get("content"):
yield chunk.choices[0].delta.content
def count_tokens(self, text: str) -> int:
"""Estimate token count for OpenAI models"""
return len(text) // 4
def get_context_length(self) -> int:
"""Return context length for the model"""
context_lengths = {
"gpt-4": 8192,
"gpt-4-32k": 32768,
"gpt-3.5-turbo": 4096,
"gpt-3.5-turbo-16k": 16384
}
return context_lengths.get(self.model, 4096)
class LocalLlamaBackend(LLMBackend):
"""Local LLM using llama.cpp with GPU acceleration"""
def __init__(self, model_path: str, n_gpu_layers: int = -1):
if not LLAMA_CPP_AVAILABLE:
raise ImportError("llama-cpp-python not installed. Install with: pip install llama-cpp-python")
self.model_path = model_path
self.n_ctx = 4096
gpu_config = self._detect_gpu_configuration()
self.llm = Llama(
model_path=model_path,
n_ctx=self.n_ctx,
n_gpu_layers=n_gpu_layers if n_gpu_layers > 0 else gpu_config['layers'],
n_threads=gpu_config['threads'],
use_mlock=True,
verbose=False
)
def _detect_gpu_configuration(self) -> dict:
"""Detect available GPU and return optimal configuration"""
system = platform.system()
try:
import torch
if torch.cuda.is_available():
return {'layers': -1, 'threads': 4, 'backend': 'cuda'}
except ImportError:
pass
try:
import torch
if hasattr(torch.version, 'hip') and torch.version.hip:
return {'layers': -1, 'threads': 4, 'backend': 'rocm'}
except (ImportError, AttributeError):
pass
if system == 'Darwin':
try:
import torch
if torch.backends.mps.is_available():
return {'layers': -1, 'threads': 4, 'backend': 'metal'}
except (ImportError, AttributeError):
pass
import multiprocessing
return {'layers': 0, 'threads': multiprocessing.cpu_count(), 'backend': 'cpu'}
def generate(self,
messages: List[LLMMessage],
temperature: float = 0.7,
max_tokens: Optional[int] = None) -> LLMResponse:
"""Generate response using local model"""
prompt = self._format_messages_to_prompt(messages)
output = self.llm(
prompt,
max_tokens=max_tokens or 512,
temperature=temperature,
stop=["</s>", "Human:", "User:"],
echo=False
)
return LLMResponse(
content=output['choices'][0]['text'].strip(),
tokens_used=output['usage']['total_tokens'],
finish_reason=output['choices'][0]['finish_reason'],
model=self.model_path
)
def _format_messages_to_prompt(self, messages: List[LLMMessage]) -> str:
"""Convert message list to model-specific prompt format"""
prompt_parts = []
for msg in messages:
if msg.role == "system":
prompt_parts.append(f"<<SYS>>\n{msg.content}\n<</SYS>>\n\n")
elif msg.role == "user":
prompt_parts.append(f"[INST] {msg.content} [/INST]")
elif msg.role == "assistant":
prompt_parts.append(f"{msg.content}</s>")
return "".join(prompt_parts)
def stream_generate(self,
messages: List[LLMMessage],
temperature: float = 0.7,
max_tokens: Optional[int] = None) -> Iterator[str]:
"""Stream tokens from local model"""
prompt = self._format_messages_to_prompt(messages)
for output in self.llm(
prompt,
max_tokens=max_tokens or 512,
temperature=temperature,
stop=["</s>", "Human:", "User:"],
stream=True
):
yield output['choices'][0]['text']
def count_tokens(self, text: str) -> int:
"""Count tokens using model's tokenizer"""
return len(self.llm.tokenize(text.encode('utf-8')))
def get_context_length(self) -> int:
"""Return configured context length"""
return self.n_ctx
@dataclass
class PresentationRequirements:
"""Structured representation of presentation requirements"""
title: Optional[str] = None
topic: Optional[str] = None
duration_minutes: Optional[int] = None
target_audience: Optional[str] = None
prerequisites: Optional[List[str]] = None
specific_content: Optional[List[str]] = None
style_preferences: Optional[str] = None
def is_complete(self) -> bool:
"""Check if all essential requirements are specified"""
return all([self.title, self.topic, self.duration_minutes, self.target_audience])
def get_missing_fields(self) -> List[str]:
"""Return list of missing essential fields"""
missing = []
if not self.title:
missing.append("title")
if not self.topic:
missing.append("topic")
if not self.duration_minutes:
missing.append("duration_minutes")
if not self.target_audience:
missing.append("target_audience")
return missing
class RequirementAnalyzer:
"""Analyzes user prompts to extract presentation requirements"""
def __init__(self, llm_backend: LLMBackend):
self.llm = llm_backend
def analyze_prompt(self, user_prompt: str) -> PresentationRequirements:
"""Extract structured requirements from user prompt"""
analysis_prompt = f"""Analyze the following user request for creating a presentation.
Extract the following information if present:
- Title: The presentation title
- Topic: The main subject matter
- Duration: How long the presentation should be in minutes
- Target Audience: Who will attend the presentation
- Prerequisites: Required knowledge or skills for attendees
- Specific Content: Any particular topics or sections requested
- Style Preferences: Visual style or design preferences
User Request: {user_prompt}
Respond with a JSON object containing the extracted information. Use null for missing fields.
JSON Response:"""
messages = [
LLMMessage(role="system", content="You are a helpful assistant that extracts structured information from text."),
LLMMessage(role="user", content=analysis_prompt)
]
response = self.llm.generate(messages, temperature=0.3)
try:
json_str = self._extract_json(response.content)
data = json.loads(json_str)
return PresentationRequirements(
title=data.get("title"),
topic=data.get("topic"),
duration_minutes=data.get("duration_minutes"),
target_audience=data.get("target_audience"),
prerequisites=data.get("prerequisites"),
specific_content=data.get("specific_content"),
style_preferences=data.get("style_preferences")
)
except (json.JSONDecodeError, KeyError):
return self._fallback_extraction(user_prompt)
def _extract_json(self, text: str) -> str:
"""Extract JSON object from text"""
start = text.find('{')
end = text.rfind('}') + 1
if start >= 0 and end > start:
return text[start:end]
return text
def _fallback_extraction(self, prompt: str) -> PresentationRequirements:
"""Simple keyword-based extraction as fallback"""
return PresentationRequirements()
def generate_clarification_questions(self, requirements: PresentationRequirements) -> List[str]:
"""Generate questions to clarify missing requirements"""
missing = requirements.get_missing_fields()
if not missing:
return []
question_prompt = f"""The user wants to create a presentation but has not specified all necessary details.
Current information:
- Title: {requirements.title or 'Not specified'}
- Topic: {requirements.topic or 'Not specified'}
- Duration: {requirements.duration_minutes or 'Not specified'} minutes
- Target Audience: {requirements.target_audience or 'Not specified'}
Missing information: {', '.join(missing)}
Generate friendly questions to gather the missing information.
Questions:"""
messages = [
LLMMessage(role="system", content="You are a helpful assistant."),
LLMMessage(role="user", content=question_prompt)
]
response = self.llm.generate(messages, temperature=0.7)
questions = [q.strip() for q in response.content.split('\n') if q.strip()]
return questions
@dataclass
class SearchResult:
"""Represents a single search result"""
title: str
url: str
snippet: str
relevance_score: float
@dataclass
class ResearchFindings:
"""Aggregated research results for presentation"""
topic: str
search_queries: List[str]
results: List[SearchResult]
key_facts: List[str]
authoritative_sources: List[str]
class WebSearcher:
"""Handles web search using DuckDuckGo"""
def __init__(self, llm_backend: LLMBackend):
if not DUCKDUCKGO_AVAILABLE:
raise ImportError("duckduckgo-search not installed. Install with: pip install duckduckgo-search")
self.llm = llm_backend
self.ddgs = DDGS()
def research_topic(self, requirements: PresentationRequirements, max_queries: int = 5) -> ResearchFindings:
"""Conduct comprehensive research on presentation topic"""
queries = self._generate_search_queries(requirements, max_queries)
all_results = []
for query in queries:
results = self._execute_search(query)
all_results.extend(results)
time.sleep(1)
ranked_results = self._rank_results(all_results, requirements)
key_facts = self._extract_key_facts(ranked_results[:10], requirements)
sources = list(set([r.url for r in ranked_results[:5]]))
return ResearchFindings(
topic=requirements.topic,
search_queries=queries,
results=ranked_results,
key_facts=key_facts,
authoritative_sources=sources
)
def _generate_search_queries(self, requirements: PresentationRequirements, max_queries: int) -> List[str]:
"""Generate targeted search queries"""
query_prompt = f"""Generate {max_queries} specific search queries to research: {requirements.topic}
Target Audience: {requirements.target_audience}
List only the queries, one per line.
Queries:"""
messages = [
LLMMessage(role="system", content="You are a research assistant."),
LLMMessage(role="user", content=query_prompt)
]
response = self.llm.generate(messages, temperature=0.7)
queries = [q.strip() for q in response.content.split('\n') if q.strip() and len(q.strip()) > 5]
return queries[:max_queries]
def _execute_search(self, query: str, num_results: int = 10) -> List[SearchResult]:
"""Execute web search using DuckDuckGo"""
try:
results = []
ddgs_results = self.ddgs.text(query, max_results=num_results)
for item in ddgs_results:
results.append(SearchResult(
title=item.get('title', ''),
url=item.get('href', ''),
snippet=item.get('body', ''),
relevance_score=0.0
))
return results
except Exception as e:
print(f"Search error: {e}")
return []
def _rank_results(self, results: List[SearchResult], requirements: PresentationRequirements) -> List[SearchResult]:
"""Rank search results by relevance"""
for result in results:
result.relevance_score = 0.5
return sorted(results, key=lambda r: r.relevance_score, reverse=True)
def _extract_key_facts(self, results: List[SearchResult], requirements: PresentationRequirements) -> List[str]:
"""Extract key facts from search results"""
combined_text = "\n\n".join([f"{r.title}\n{r.snippet}" for r in results])
extraction_prompt = f"""Extract 8-12 key facts about {requirements.topic} from these search results:
{combined_text[:2000]}
List the facts, one per line.
Facts:"""
messages = [
LLMMessage(role="system", content="You are an expert at extracting key information."),
LLMMessage(role="user", content=extraction_prompt)
]
response = self.llm.generate(messages, temperature=0.5)
facts = [fact.strip() for fact in response.content.split('\n') if fact.strip() and len(fact.strip()) > 10]
return facts
class StructureType(Enum):
"""Types of presentation organizational structures"""
CHRONOLOGICAL = "chronological"
PROBLEM_SOLUTION = "problem_solution"
LAYERED_ABSTRACTION = "layered_abstraction"
USE_CASE_DRIVEN = "use_case_driven"
CONCEPTUAL_BUILDING = "conceptual_building"
@dataclass
class SlideOutline:
"""Outline for a single slide"""
title: str
key_points: List[str]
estimated_duration: int
visual_type: str
@dataclass
class SectionOutline:
"""Outline for a presentation section"""
title: str
slides: List[SlideOutline]
learning_objectives: List[str]
@dataclass
class PresentationStructure:
"""Complete presentation structure"""
title: str
structure_type: StructureType
sections: List[SectionOutline]
total_slides: int = 0
estimated_duration: int = 0
evaluation_score: float = 0.0
def calculate_metrics(self):
"""Calculate total slides and duration"""
self.total_slides = sum(len(section.slides) for section in self.sections)
self.estimated_duration = sum(
slide.estimated_duration for section in self.sections for slide in section.slides
)
@dataclass
class ThoughtNode:
"""Node in the Tree-of-Thought reasoning process"""
structure: PresentationStructure
depth: int
parent: Optional['ThoughtNode'] = None
children: List['ThoughtNode'] = field(default_factory=list)
evaluation_score: float = 0.0
is_complete: bool = False
class TreeOfThoughtReasoner:
"""Implements Tree-of-Thought reasoning"""
def __init__(self, llm_backend: LLMBackend):
self.llm = llm_backend
def generate_optimal_structure(self,
requirements: PresentationRequirements,
research: ResearchFindings,
num_alternatives: int = 2,
max_depth: int = 2) -> PresentationStructure:
"""Generate optimal presentation structure"""
root_nodes = self._generate_initial_structures(requirements, research, num_alternatives)
for node in root_nodes:
self._explore_branch(node, requirements, research, max_depth)
return self._select_best_structure(root_nodes)
def _generate_initial_structures(self,
requirements: PresentationRequirements,
research: ResearchFindings,
num_alternatives: int) -> List[ThoughtNode]:
"""Generate initial structure alternatives"""
structure_prompt = f"""Generate {num_alternatives} organizational structures for a presentation on {requirements.topic}.
Duration: {requirements.duration_minutes} minutes
Audience: {requirements.target_audience}
For each structure, specify:
- Type (chronological, problem-solution, etc.)
- 3-4 main sections
Format:
STRUCTURE 1:
Type: [type]
Sections:
- [Section 1]
- [Section 2]
Structures:"""
messages = [
LLMMessage(role="system", content="You are an expert presentation designer."),
LLMMessage(role="user", content=structure_prompt)
]
response = self.llm.generate(messages, temperature=0.8)
structures = self._parse_structures(response.content, requirements)
nodes = []
for structure in structures:
node = ThoughtNode(structure=structure, depth=0)
node.evaluation_score = 0.7
nodes.append(node)
return nodes
def _parse_structures(self, response: str, requirements: PresentationRequirements) -> List[PresentationStructure]:
"""Parse structure descriptions"""
structures = []
current_structure = None
current_sections = []
for line in response.split('\n'):
line = line.strip()
if line.startswith('STRUCTURE'):
if current_structure and current_sections:
current_structure.sections = [
SectionOutline(title=title, slides=[], learning_objectives=[])
for title in current_sections
]
structures.append(current_structure)
current_structure = PresentationStructure(
title=requirements.title,
structure_type=StructureType.CONCEPTUAL_BUILDING,
sections=[]
)
current_sections = []
elif line.startswith('-') and current_structure:
section_title = line.lstrip('- ').strip()
if section_title:
current_sections.append(section_title)
if current_structure and current_sections:
current_structure.sections = [
SectionOutline(title=title, slides=[], learning_objectives=[])
for title in current_sections
]
structures.append(current_structure)
return structures if structures else [PresentationStructure(
title=requirements.title,
structure_type=StructureType.CONCEPTUAL_BUILDING,
sections=[SectionOutline(title="Introduction", slides=[], learning_objectives=[])]
)]
def _explore_branch(self, node: ThoughtNode, requirements: PresentationRequirements,
research: ResearchFindings, max_depth: int):
"""Recursively explore a reasoning branch"""
if node.depth >= max_depth:
node.is_complete = True
return
children = self._expand_structure(node, requirements, research)
for child in children:
child.evaluation_score = 0.7
if child.evaluation_score > 0.6:
node.children.append(child)
if child.depth < max_depth:
self._explore_branch(child, requirements, research, max_depth)
def _expand_structure(self, node: ThoughtNode, requirements: PresentationRequirements,
research: ResearchFindings) -> List[ThoughtNode]:
"""Expand structure by adding slides"""
structure = node.structure
if not any(section.slides for section in structure.sections):
import copy
expanded = copy.deepcopy(structure)
total_time = requirements.duration_minutes * 60
time_per_section = total_time // max(len(expanded.sections), 1)
for section in expanded.sections:
slides = self._generate_section_slides(section, requirements, research, time_per_section)
section.slides = slides
expanded.calculate_metrics()
child = ThoughtNode(structure=expanded, depth=node.depth + 1, parent=node)
child.is_complete = True
return [child]
return []
def _generate_section_slides(self, section: SectionOutline, requirements: PresentationRequirements,
research: ResearchFindings, time_budget: int) -> List[SlideOutline]:
"""Generate slide outlines for a section"""
slide_prompt = f"""Generate 3-4 slide outlines for section: {section.title}
Time Budget: {time_budget} seconds
Audience: {requirements.target_audience}
For each slide:
- Title
- 3 key points
- Duration (seconds)
- Visual type (text, code, image, diagram, chart)
Format:
SLIDE 1:
Title: [title]
Points:
- [point 1]
- [point 2]
- [point 3]
Duration: 60
Visual: text
Slides:"""
messages = [
LLMMessage(role="system", content="You are an expert at designing slides."),
LLMMessage(role="user", content=slide_prompt)
]
response = self.llm.generate(messages, temperature=0.7)
return self._parse_slides(response.content)
def _parse_slides(self, response: str) -> List[SlideOutline]:
"""Parse slide descriptions"""
slides = []
current_slide = None
current_points = []
for line in response.split('\n'):
line = line.strip()
if line.startswith('SLIDE'):
if current_slide:
current_slide.key_points = current_points
slides.append(current_slide)
current_slide = SlideOutline(title="", key_points=[], estimated_duration=60, visual_type="text")
current_points = []
elif line.startswith('Title:') and current_slide:
current_slide.title = line.split(':', 1)[1].strip()
elif line.startswith('-') and current_slide:
point = line.lstrip('- ').strip()
if point:
current_points.append(point)
elif line.startswith('Duration:') and current_slide:
try:
current_slide.estimated_duration = int(''.join(filter(str.isdigit, line)))
except ValueError:
pass
elif line.startswith('Visual:') and current_slide:
visual = line.split(':', 1)[1].strip().lower()
if visual in ['diagram', 'code', 'chart', 'image', 'text']:
current_slide.visual_type = visual
if current_slide:
current_slide.key_points = current_points
slides.append(current_slide)
return slides if slides else [SlideOutline(
title="Default Slide",
key_points=["Point 1", "Point 2", "Point 3"],
estimated_duration=60,
visual_type="text"
)]
def _select_best_structure(self, root_nodes: List[ThoughtNode]) -> PresentationStructure:
"""Select the best structure"""
all_complete_nodes = []
def collect_complete_nodes(node: ThoughtNode):
if node.is_complete:
all_complete_nodes.append(node)
for child in node.children:
collect_complete_nodes(child)
for root in root_nodes:
collect_complete_nodes(root)
if not all_complete_nodes:
return max(root_nodes, key=lambda n: n.evaluation_score).structure
return max(all_complete_nodes, key=lambda n: n.evaluation_score).structure
@dataclass
class SlideContent:
"""Complete content for a single slide"""
title: str
bullet_points: List[str]
speaker_notes: str
visual_description: str
visual_type: str
code_snippet: Optional[str] = None
image_query: Optional[str] = None
@dataclass
class PresentationSlide:
"""Full slide with content and metadata"""
slide_number: int
section: str
content: SlideContent
style_theme: str
class SlideContentGenerator:
"""Generates detailed content for slides"""
def __init__(self, llm_backend: LLMBackend):
self.llm = llm_backend
def generate_slide_content(self, outline: SlideOutline, section: SectionOutline,
requirements: PresentationRequirements, research: ResearchFindings,
slide_number: int) -> PresentationSlide:
"""Generate complete content for a slide"""
thought_process = self._chain_of_thought_content_development(outline, section, requirements, research)
content = self._generate_final_content(thought_process, outline, requirements)
theme = self._determine_style_theme(requirements)
return PresentationSlide(
slide_number=slide_number,
section=section.title,
content=content,
style_theme=theme
)
def _chain_of_thought_content_development(self, outline: SlideOutline, section: SectionOutline,
requirements: PresentationRequirements,
research: ResearchFindings) -> Dict[str, str]:
"""Use Chain-of-Thought reasoning"""
cot_prompt = f"""Develop content for slide: {outline.title}
Section: {section.title}
Audience: {requirements.target_audience}
Think through:
1. Core message
2. Audience connection
3. Visual strategy
4. Content structure (3-5 points)
5. Speaker notes
6. Code/visual details if needed
Reasoning:"""
messages = [
LLMMessage(role="system", content="You are an expert presentation designer."),
LLMMessage(role="user", content=cot_prompt)
]
response = self.llm.generate(messages, temperature=0.7, max_tokens=1000)
return {
'core_message': 'Main concept',
'audience_connection': 'Relevant to audience',
'visual_strategy': 'Clear visuals',
'content_structure': response.content,
'speaker_notes': 'Detailed explanation',
'code_or_visual': 'Supporting content'
}
def _generate_final_content(self, thought_process: Dict[str, str], outline: SlideOutline,
requirements: PresentationRequirements) -> SlideContent:
"""Generate final slide content"""
bullet_points = outline.key_points if outline.key_points else ["Point 1", "Point 2", "Point 3"]
speaker_notes = self._generate_speaker_notes(thought_process, outline, requirements)
visual_description = thought_process['visual_strategy']
code_snippet = None
image_query = None
if outline.visual_type == 'code':
code_snippet = self._generate_code_example(outline.title, requirements.topic, thought_process['code_or_visual'])
elif outline.visual_type in ['image', 'diagram']:
image_query = f"{requirements.topic} {outline.title}"
return SlideContent(
title=outline.title,
bullet_points=bullet_points,
speaker_notes=speaker_notes,
visual_description=visual_description,
visual_type=outline.visual_type,
code_snippet=code_snippet,
image_query=image_query
)
def _generate_speaker_notes(self, thought_process: Dict[str, str], outline: SlideOutline,
requirements: PresentationRequirements) -> str:
"""Generate speaker notes"""
notes_prompt = f"""Generate speaker notes for slide: {outline.title}
Audience: {requirements.target_audience}
Notes should be 100-150 words.
Speaker Notes:"""
messages = [
LLMMessage(role="system", content="You are an expert at writing speaker notes."),
LLMMessage(role="user", content=notes_prompt)
]
response = self.llm.generate(messages, temperature=0.7, max_tokens=300)
return response.content.strip()
def _generate_code_example(self, slide_title: str, topic: str, code_guidance: str) -> str:
"""Generate code example"""
code_prompt = f"""Generate a code example for: {slide_title}
Topic: {topic}
Requirements:
- 10-15 lines
- Well-commented
- Syntactically correct
Code:"""
messages = [
LLMMessage(role="system", content="You are an expert programmer."),
LLMMessage(role="user", content=code_prompt)
]
response = self.llm.generate(messages, temperature=0.5, max_tokens=400)
return self._extract_code_block(response.content)
def _extract_code_block(self, text: str) -> str:
"""Extract code block from text"""
if '```' in text:
parts = text.split('```')
if len(parts) >= 3:
code = parts[1]
if '\n' in code:
lines = code.split('\n')
if lines[0].strip() in ['python', 'javascript', 'java']:
code = '\n'.join(lines[1:])
return code.strip()
return text.strip()
def _determine_style_theme(self, requirements: PresentationRequirements) -> str:
"""Determine visual theme"""
return "Professional"
@dataclass
class ImageMetadata:
"""Metadata for a retrieved image"""
url: str
title: str
source: str
license: str
attribution: str
width: int
height: int
local_path: Optional[str] = None
class ImageRetriever:
"""Retrieves images from open source repositories"""
def __init__(self, llm_backend: LLMBackend, cache_dir: str = "./image_cache"):
self.llm = llm_backend
self.cache_dir = cache_dir
os.makedirs(cache_dir, exist_ok=True)
def find_image(self, query: str) -> Optional[ImageMetadata]:
"""Find appropriate image"""
results = self._search_wikimedia_commons(query)
if not results:
return None
best_image = results[0] if results else None
if best_image:
self._download_image(best_image)
return best_image
def _search_wikimedia_commons(self, query: str) -> List[ImageMetadata]:
"""Search Wikimedia Commons"""
endpoint = "https://commons.wikimedia.org/w/api.php"
params = {
"action": "query",
"format": "json",
"generator": "search",
"gsrnamespace": "6",
"gsrsearch": query,
"gsrlimit": "5",
"prop": "imageinfo",
"iiprop": "url|size|extmetadata"
}
try:
response = requests.get(endpoint, params=params, timeout=10)
response.raise_for_status()
data = response.json()
results = []
pages = data.get("query", {}).get("pages", {})
for page_id, page in pages.items():
if "imageinfo" in page:
info = page["imageinfo"][0]
extmeta = info.get("extmetadata", {})
results.append(ImageMetadata(
url=info.get("url", ""),
title=page.get("title", ""),
source=f"Wikimedia Commons",
license="CC BY-SA",
attribution=f"Image from Wikimedia Commons",
width=info.get("width", 0),
height=info.get("height", 0)
))
return results
except Exception as e:
print(f"Image search error: {e}")
return []
def _download_image(self, image: ImageMetadata) -> bool:
"""Download and cache image"""
try:
url_hash = hashlib.md5(image.url.encode()).hexdigest()
filename = f"{url_hash}.jpg"
filepath = os.path.join(self.cache_dir, filename)
if not os.path.exists(filepath):
response = requests.get(image.url, timeout=15, stream=True)
response.raise_for_status()
with open(filepath, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
image.local_path = filepath
return True
except Exception as e:
print(f"Image download error: {e}")
return False
class PowerPointGenerator:
"""Generates PowerPoint presentations"""
def __init__(self):
if not PYTHON_PPTX_AVAILABLE:
raise ImportError("python-pptx not installed. Install with: pip install python-pptx")
self.presentation = None
self.theme_colors = {
'primary': RGBColor(0, 51, 102),
'secondary': RGBColor(0, 102, 204),
'accent': RGBColor(255, 153, 0),
'text': RGBColor(51, 51, 51),
'background': RGBColor(255, 255, 255)
}
self.font_family = "Calibri"
self.code_font_family = "Consolas"
def create_presentation(self, slides: List[PresentationSlide], requirements: PresentationRequirements,
output_path: str) -> bool:
"""Create complete PowerPoint presentation"""
self.presentation = Presentation()
self.presentation.slide_width = Inches(10)
self.presentation.slide_height = Inches(7.5)
self._create_title_slide(requirements)
for slide in slides:
self._create_content_slide(slide)
self._create_closing_slide(requirements)
try:
self.presentation.save(output_path)
return True
except Exception as e:
print(f"Error saving presentation: {e}")
return False
def _create_title_slide(self, requirements: PresentationRequirements):
"""Create title slide"""
slide_layout = self.presentation.slide_layouts[0]
slide = self.presentation.slides.add_slide(slide_layout)
title = slide.shapes.title
title.text = requirements.title
title.text_frame.paragraphs[0].font.size = Pt(44)
title.text_frame.paragraphs[0].font.bold = True
title.text_frame.paragraphs[0].font.color.rgb = self.theme_colors['primary']
if len(slide.placeholders) > 1:
subtitle = slide.placeholders[1]
subtitle.text = f"{requirements.topic}\n\nAudience: {requirements.target_audience}"
subtitle.text_frame.paragraphs[0].font.size = Pt(24)
def _create_content_slide(self, slide_data: PresentationSlide):
"""Create content slide"""
if slide_data.content.visual_type == 'code':
self._create_code_slide(slide_data)
else:
self._create_text_slide(slide_data)
def _create_text_slide(self, slide_data: PresentationSlide):
"""Create text slide"""
slide_layout = self.presentation.slide_layouts[1]
slide = self.presentation.slides.add_slide(slide_layout)
title = slide.shapes.title
title.text = slide_data.content.title
title.text_frame.paragraphs[0].font.size = Pt(32)
title.text_frame.paragraphs[0].font.bold = True
content_placeholder = slide.placeholders[1]
text_frame = content_placeholder.text_frame
text_frame.clear()
for point in slide_data.content.bullet_points:
p = text_frame.add_paragraph()
p.text = point
p.font.size = Pt(20)
p.font.name = self.font_family
notes_slide = slide.notes_slide
notes_slide.notes_text_frame.text = slide_data.content.speaker_notes
def _create_code_slide(self, slide_data: PresentationSlide):
"""Create code slide"""
slide_layout = self.presentation.slide_layouts[6]
slide = self.presentation.slides.add_slide(slide_layout)
title_box = slide.shapes.add_textbox(Inches(0.5), Inches(0.3), Inches(9), Inches(0.8))
title_box.text_frame.text = slide_data.content.title
title_box.text_frame.paragraphs[0].font.size = Pt(32)
title_box.text_frame.paragraphs[0].font.bold = True
code_box = slide.shapes.add_textbox(Inches(0.5), Inches(1.3), Inches(9), Inches(5))
code_box.text_frame.text = slide_data.content.code_snippet or "# Code example"
code_box.text_frame.paragraphs[0].font.name = self.code_font_family
code_box.text_frame.paragraphs[0].font.size = Pt(14)
code_box.fill.solid()
code_box.fill.fore_color.rgb = RGBColor(245, 245, 245)
notes_slide = slide.notes_slide
notes_slide.notes_text_frame.text = slide_data.content.speaker_notes
def _create_closing_slide(self, requirements: PresentationRequirements):
"""Create closing slide"""
slide_layout = self.presentation.slide_layouts[1]
slide = self.presentation.slides.add_slide(slide_layout)
title = slide.shapes.title
title.text = "Questions & Discussion"
title.text_frame.paragraphs[0].font.size = Pt(44)
title.text_frame.paragraphs[0].font.bold = True
class GenerationState(Enum):
"""States in the workflow"""
INITIAL = "initial"
ANALYZING_REQUIREMENTS = "analyzing"
CLARIFYING = "clarifying"
RESEARCHING = "researching"
PLANNING_STRUCTURE = "planning"
GENERATING_CONTENT = "generating"
RETRIEVING_IMAGES = "images"
CREATING_POWERPOINT = "creating"
COMPLETE = "complete"
ERROR = "error"
@dataclass
class GenerationContext:
"""Context maintained throughout generation"""
state: GenerationState
requirements: Optional[PresentationRequirements] = None
research: Optional[ResearchFindings] = None
structure: Optional[PresentationStructure] = None
slides: Optional[List[PresentationSlide]] = None
output_path: Optional[str] = None
error_message: Optional[str] = None
class PresentationOrchestrator:
"""Orchestrates the complete workflow"""
def __init__(self, llm_backend: LLMBackend, output_dir: str = "./presentations"):
self.llm = llm_backend
self.output_dir = output_dir
self.requirement_analyzer = RequirementAnalyzer(llm_backend)
self.web_searcher = WebSearcher(llm_backend)
self.structure_reasoner = TreeOfThoughtReasoner(llm_backend)
self.content_generator = SlideContentGenerator(llm_backend)
self.image_retriever = ImageRetriever(llm_backend)
self.ppt_generator = PowerPointGenerator()
os.makedirs(output_dir, exist_ok=True)
def generate_presentation(self, user_prompt: str,
progress_callback: Optional[Callable[[str, float], None]] = None) -> GenerationContext:
"""Generate complete presentation"""
context = GenerationContext(state=GenerationState.INITIAL)
try:
context = self._analyze_requirements(context, user_prompt, progress_callback)
if context.state == GenerationState.CLARIFYING:
return context
context = self._research_topic(context, progress_callback)
context = self._plan_structure(context, progress_callback)
context = self._generate_content(context, progress_callback)
context = self._retrieve_images(context, progress_callback)
context = self._create_powerpoint(context, progress_callback)
context.state = GenerationState.COMPLETE
except Exception as e:
context.state = GenerationState.ERROR
context.error_message = str(e)
return context
def _analyze_requirements(self, context: GenerationContext, user_prompt: str,
progress_callback: Optional[Callable[[str, float], None]]) -> GenerationContext:
"""Analyze requirements"""
context.state = GenerationState.ANALYZING_REQUIREMENTS
if progress_callback:
progress_callback("Analyzing requirements...", 0.1)
requirements = self.requirement_analyzer.analyze_prompt(user_prompt)
if not requirements.is_complete():
context.requirements = requirements
context.state = GenerationState.CLARIFYING
return context
context.requirements = requirements
return context
def _research_topic(self, context: GenerationContext,
progress_callback: Optional[Callable[[str, float], None]]) -> GenerationContext:
"""Research topic"""
context.state = GenerationState.RESEARCHING
if progress_callback:
progress_callback("Researching topic...", 0.3)
research = self.web_searcher.research_topic(context.requirements)
context.research = research
return context
def _plan_structure(self, context: GenerationContext,
progress_callback: Optional[Callable[[str, float], None]]) -> GenerationContext:
"""Plan structure"""
context.state = GenerationState.PLANNING_STRUCTURE
if progress_callback:
progress_callback("Planning structure...", 0.5)
structure = self.structure_reasoner.generate_optimal_structure(context.requirements, context.research)
context.structure = structure
return context
def _generate_content(self, context: GenerationContext,
progress_callback: Optional[Callable[[str, float], None]]) -> GenerationContext:
"""Generate content"""
context.state = GenerationState.GENERATING_CONTENT
if progress_callback:
progress_callback("Generating content...", 0.7)
slides = []
slide_number = 1
for section in context.structure.sections:
for outline in section.slides:
slide = self.content_generator.generate_slide_content(
outline, section, context.requirements, context.research, slide_number
)
slides.append(slide)
slide_number += 1
context.slides = slides
return context
def _retrieve_images(self, context: GenerationContext,
progress_callback: Optional[Callable[[str, float], None]]) -> GenerationContext:
"""Retrieve images"""
context.state = GenerationState.RETRIEVING_IMAGES
if progress_callback:
progress_callback("Retrieving images...", 0.85)
for slide in context.slides:
if slide.content.image_query:
image = self.image_retriever.find_image(slide.content.image_query)
return context
def _create_powerpoint(self, context: GenerationContext,
progress_callback: Optional[Callable[[str, float], None]]) -> GenerationContext:
"""Create PowerPoint"""
context.state = GenerationState.CREATING_POWERPOINT
if progress_callback:
progress_callback("Creating PowerPoint...", 0.95)
filename = f"{context.requirements.title.replace(' ', '_')}.pptx"
output_path = os.path.join(self.output_dir, filename)
success = self.ppt_generator.create_presentation(context.slides, context.requirements, output_path)
if success:
context.output_path = output_path
else:
raise Exception("Failed to create PowerPoint")
return context
def main():
"""Main entry point"""
print("LLM-Powered PowerPoint Generator")
print("=" * 50)
llm_backend = LocalLlamaBackend("path/to/model.gguf")
orchestrator = PresentationOrchestrator(llm_backend)
user_prompt = """Create a 30-minute presentation about Python async programming
for intermediate Python developers. Include code examples and best practices."""
def progress_callback(message: str, progress: float):
print(f"[{progress*100:.0f}%] {message}")
context = orchestrator.generate_presentation(user_prompt, progress_callback)
if context.state == GenerationState.COMPLETE:
print(f"\nPresentation created successfully: {context.output_path}")
elif context.state == GenerationState.ERROR:
print(f"\nError: {context.error_message}")
elif context.state == GenerationState.CLARIFYING:
questions = orchestrator.requirement_analyzer.generate_clarification_questions(context.requirements)
print("\nPlease provide more information:")
for q in questions:
print(f"- {q}")
if __name__ == "__main__":
main()
CONCLUSION AND BEST PRACTICES
This comprehensive system demonstrates how Large Language Models can automate complex creative tasks like presentation generation. By combining Chain-of-Thought reasoning for systematic content development with Tree-of-Thought exploration for structural optimization, the system produces high-quality presentations tailored to specific audiences and requirements.
The architecture prioritizes open source components throughout. DuckDuckGo provides free web search without API keys. Wikimedia Commons offers freely licensed images. The python-pptx library enables PowerPoint generation without Microsoft Office dependencies. Local LLM support through llama.cpp ensures complete independence from commercial API services.
Best practices for using this system include providing detailed initial prompts with clear audience specifications, reviewing and refining generated content before delivery, customizing the visual theme to match organizational branding, and iteratively improving prompts based on output quality. The system works best when users engage in the clarification dialogue to ensure requirements are fully understood.
Future enhancements could include support for additional diagram generation tools like Graphviz or PlantUML, integration with presentation rehearsal timing tools, automatic generation of handout materials, and support for exporting to additional formats like PDF or HTML5 slides. The modular architecture makes such extensions straightforward to implement.