Sunday, November 01, 2026

BUILDING AN INTELLIGENT LLM-POWERED POWERPOINT GENERATION SYSTEM

 


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.