Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 93 additions & 11 deletions flask_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@

# Import HTML UI
from slidedeckai.ui.html_ui import HTML_UI
from slidedeckai.helpers.file_processor import FileProcessor
from openai import OpenAI

# Import orchestrators
from slidedeckai.agents.core_agents import PlanGeneratorOrchestrator
Expand Down Expand Up @@ -131,22 +133,75 @@ def index():
def create_plan():
"""Phase 1: Create layout-aware research plan with enforced diversity"""
try:
data = request.get_json()
query = data.get('query', '').strip()
template_key = data.get('template', 'Basic')
search_mode = data.get('search_mode', 'normal')
num_sections = data.get('num_sections', None)
api_key = os.getenv('OPENAI_API_KEY') # Default

# Check if this is a file upload request
if request.content_type.startswith('multipart/form-data'):
query = request.form.get('query', '').strip()
template_key = request.form.get('template', 'Basic')
search_mode = request.form.get('search_mode', 'normal')
num_sections = request.form.get('num_sections', None)

# Optional overrides
req_api_key = request.form.get('api_key')
if req_api_key:
api_key = req_api_key

# TODO: Handle Model overrides if PlanGeneratorOrchestrator supports it dynamically

if num_sections:
try:
num_sections = int(num_sections)
except:
num_sections = None

uploaded_files = request.files.getlist('files')
chart_file = request.files.get('chart_file')
extracted_text = ""
chart_data = None

# Process uploaded content files
if uploaded_files:
for file in uploaded_files:
if file.filename:
text = FileProcessor.extract_text(file)
if text:
extracted_text += f"\n\n--- Content from {file.filename} ---\n{text}"

# Process chart file if present
if chart_file and chart_file.filename:
# Use provided API key or env var for extraction
if not api_key:
return jsonify({'error': 'API key required for chart extraction'}), 400
client = OpenAI(api_key=api_key)
chart_data = FileProcessor.extract_chart_data(chart_file, client)
logger.info(f" 📊 Extracted chart data: {chart_data is not None}")

else:
data = request.get_json()
query = data.get('query', '').strip()
template_key = data.get('template', 'Basic')
search_mode = data.get('search_mode', 'normal')
num_sections = data.get('num_sections', None)
extracted_text = ""
chart_data = None

# Optional overrides
req_api_key = data.get('api_key')
if req_api_key:
api_key = req_api_key

if not query:
return jsonify({'error': 'Query required'}), 400

logger.info(f"🔥 Creating plan: {query}")
logger.info(f" Template: {template_key}")
logger.info(f" Mode: {search_mode}")
if extracted_text:
logger.info(f" 📄 Using uploaded content ({len(extracted_text)} chars)")

api_key = os.getenv('OPENAI_API_KEY')
if not api_key:
return jsonify({'error': 'OpenAI API key not configured'}), 500
return jsonify({'error': 'OpenAI API key not configured. Please provide it in settings or .env'}), 500

# Validate template exists
if template_key not in GlobalConfig.PPTX_TEMPLATE_FILES:
Expand All @@ -168,11 +223,16 @@ def create_plan():
search_mode=search_mode
)

llm_model = request.form.get('llm_model') if request.content_type.startswith('multipart/form-data') else data.get('llm_model')

# Generate plan with enforced diversity
# Pass extracted content if available
research_plan = orchestrator.generate_plan(
user_query=query,
template_layouts=layout_info['layouts'],
num_sections=num_sections
num_sections=num_sections,
extracted_content=extracted_text if extracted_text else None,
model_name=llm_model
)

# Cache plan
Expand All @@ -182,7 +242,9 @@ def create_plan():
'template_key': template_key,
'search_mode': search_mode,
'research_plan': research_plan,
'analyzer': analyzer
'analyzer': analyzer,
'chart_data': chart_data, # Store extracted chart data
'extracted_content': extracted_text # Store extracted text content
}

# Serialize plan
Expand Down Expand Up @@ -230,13 +292,33 @@ def execute_plan():
query = plan_data['query']
template_key = plan_data['template_key']
research_plan = plan_data['research_plan']
chart_data = plan_data.get('chart_data') # Retrieve chart data
extracted_content = plan_data.get('extracted_content') # Retrieve extracted content

# Use API key from request if provided (stateless execution)
# However, for consistency, if the user provided an API key during plan generation, we should probably stick to it or ask for it again.
# Ideally, we should receive it again here or store it in cache (not recommended for secrets).
# Let's assume the user has to provide it if not in env, or it's passed in data.
# But `html_ui` currently only sends `plan_id`.
# I'll stick to env var for now unless I update `execute` frontend call too.
# Wait, I should update frontend `approvePlan` to send API key if it was set in settings.
# But `approvePlan` logic is separate.
# Let's rely on `orchestrator`'s API key.
# Actually, `plans_cache` is in-memory. I can store the API key there TEMPORARILY for the session?
# A better practice is to pass it from frontend.

# Retrieve potential API key from plans_cache if I decided to store it there (I didn't).
# So I will check if data has api_key (I need to update frontend to send it).

api_key = data.get('api_key') or os.getenv('OPENAI_API_KEY')

logger.info(f"🚀 Executing plan {plan_id}")
logger.info(f" Query: {query}")
logger.info(f" Template: {template_key}")
logger.info(f" Sections: {len(research_plan.sections)}")
if chart_data:
logger.info(" 📊 Using pre-loaded chart data")

api_key = os.getenv('OPENAI_API_KEY')
if not api_key:
return jsonify({'error': 'OpenAI API key not configured'}), 500

Expand All @@ -254,7 +336,7 @@ def execute_plan():
template_path=template_file
)

output_path = orchestrator.execute_plan(research_plan, output_path)
output_path = orchestrator.execute_plan(research_plan, output_path, chart_data=chart_data, extracted_content=extracted_content)

# Cache results
report_id = datetime.now().strftime('%Y%m%d_%H%M%S')
Expand Down
9 changes: 8 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,11 @@ anyio==4.4.0

httpx~=0.27.2
huggingface-hub #~=0.24.5
ollama~=0.5.1
ollama~=0.5.1
pandas
openpyxl
openai
flask
flask-cors
scikit-learn
Pillow
3 changes: 2 additions & 1 deletion src/slidedeckai/agents/content_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import json
from typing import List, Dict
from openai import OpenAI
from slidedeckai.global_config import GlobalConfig

logger = logging.getLogger(__name__)

Expand All @@ -19,7 +20,7 @@ class ContentGenerator:
def __init__(self, api_key: str):
self.client = OpenAI(api_key=api_key)
# Use GPT-4 family for content generation (best available GPT-4 model by default)
self.model = "gpt-4.1-mini"
self.model = GlobalConfig.LLM_MODEL

def generate_subtitle(self, slide_title: str, purpose: str,
search_facts: List[str]) -> str:
Expand Down
67 changes: 47 additions & 20 deletions src/slidedeckai/agents/core_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from typing import List, Dict, Optional, Set
from pydantic import BaseModel, Field
from openai import OpenAI
from slidedeckai.global_config import GlobalConfig

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -55,28 +56,33 @@ def __init__(self, api_key: str, search_mode: str = 'normal'):
self.api_key = api_key
self.search_mode = search_mode
self.client = OpenAI(api_key=api_key)
self.model = "gpt-4o-mini"
self.model = GlobalConfig.LLM_MODEL_FAST
self.used_topics: Set[str] = set()

def generate_plan(self, user_query: str, template_layouts: Dict,
num_sections: Optional[int] = None) -> ResearchPlan:
"""Existing logic with FIX #1: Validate layouts upfront"""
num_sections: Optional[int] = None, extracted_content: Optional[str] = None,
model_name: Optional[str] = None) -> ResearchPlan:
"""Existing logic with FIX #1: Validate layouts upfront. Added support for extracted content."""

logger.info("🤖 Starting FULLY DYNAMIC planning...")
# Override model if provided
if model_name:
self.model = model_name

logger.info(f"🤖 Starting FULLY DYNAMIC planning using model: {self.model}")

# ✅ FIX #1: Validate layouts FIRST
template_layouts = {int(k): v for k, v in template_layouts.items()}

if not template_layouts:
raise ValueError("No layouts found in template!")

# STEP 1: Deep analysis
analysis = self._llm_deep_analysis(user_query)
# STEP 1: Deep analysis (using content if available)
analysis = self._llm_deep_analysis(user_query, extracted_content)
logger.info(f" 🧠 Analysis complete")

# STEP 2: Determine section count
target_sections = num_sections if num_sections else self._llm_determine_section_count(
user_query, analysis
user_query, analysis, extracted_content
)
logger.info(f" 📊 Target: {target_sections} sections")

Expand All @@ -90,7 +96,7 @@ def generate_plan(self, user_query: str, template_layouts: Dict,

# STEP 4: Generate topics
section_topics = self._llm_generate_all_topics(
user_query, analysis, target_sections, template_capabilities
user_query, analysis, target_sections, template_capabilities, extracted_content
)
logger.info(f" 📝 Generated {len(section_topics)} unique topics")

Expand All @@ -106,7 +112,8 @@ def generate_plan(self, user_query: str, template_layouts: Dict,
section_num=i,
blueprint=blueprint,
query=user_query,
template_layouts=template_layouts
template_layouts=template_layouts,
extracted_content=extracted_content
)
sections.append(section)
logger.info(f" ✅ Slide {i}: {section.section_title}")
Expand Down Expand Up @@ -237,7 +244,8 @@ def _llm_match_topics_to_layouts_validated(self, topics: List[Dict],
raise RuntimeError("Layout matching failed unexpectedly")

def _generate_detailed_slide_plan(self, section_num: int, blueprint: Dict,
query: str, template_layouts: Dict) -> SectionPlan:
query: str, template_layouts: Dict,
extracted_content: Optional[str] = None) -> SectionPlan:
"""FIX #3: GUARANTEE unique subtitles with retry logic"""

layout_idx = blueprint['layout_idx']
Expand Down Expand Up @@ -295,7 +303,7 @@ def _generate_detailed_slide_plan(self, section_num: int, blueprint: Dict,
# CONTENT
content_phs = layout['placeholders']['content']
self._assign_content_dynamically(
specs, content_phs, blueprint, query
specs, content_phs, blueprint, query, extracted_content
)

return SectionPlan(
Expand Down Expand Up @@ -370,12 +378,17 @@ def _llm_generate_subtitle_guaranteed_unique(self, purpose: str, position: str,
return unique_heading

# Keep all other existing methods unchanged
def _llm_deep_analysis(self, query: str) -> Dict:
"""Existing - unchanged"""
def _llm_deep_analysis(self, query: str, extracted_content: Optional[str] = None) -> Dict:
"""Existing - modified to use content"""

context_str = f"Context from files:\n{extracted_content[:2000]}..." if extracted_content else ""

prompt = f"""You are an expert business analyst. Analyze this presentation request:

"{query}"

{context_str}

Your task:
1. Understand the MAIN SUBJECT (company, topic, product, etc.)
2. Understand the CONTEXT (financial report, market analysis, product launch, etc.)
Expand Down Expand Up @@ -426,13 +439,14 @@ def _llm_deep_analysis(self, query: str) -> Dict:
"aspects": [f"Aspect {i+1}" for i in range(6)]
}

def _llm_determine_section_count(self, query: str, analysis: Dict) -> int:
def _llm_determine_section_count(self, query: str, analysis: Dict, extracted_content: Optional[str] = None) -> int:
"""Existing - unchanged"""
aspects = analysis.get('aspects', [])

prompt = f"""Given this presentation request:
Query: "{query}"
Identified aspects: {len(aspects)}
{'Content available: Yes' if extracted_content else ''}

How many slides should this presentation have?

Expand Down Expand Up @@ -501,17 +515,21 @@ def _dynamic_template_analysis(self, layouts: Dict) -> Dict:
}

def _llm_generate_all_topics(self, query: str, analysis: Dict,
count: int, capabilities: Dict) -> List[Dict]:
count: int, capabilities: Dict, extracted_content: Optional[str] = None) -> List[Dict]:
"""Existing - unchanged"""
aspects = analysis.get('aspects', [])
main_subject = analysis.get('main_subject', query)

content_prompt = f"Base your topics on this content:\n{extracted_content[:3000]}..." if extracted_content else ""

prompt = f"""Create {count} COMPLETELY DIFFERENT slide topics for this presentation:

Main Subject: {main_subject}
Context: {analysis.get('context', 'analysis')}
Aspects to cover: {json.dumps(aspects, indent=2)}

{content_prompt}

Template capabilities:
- Can display charts: {len(capabilities['chart_capable'])} layouts
- Can display tables: {len(capabilities['table_capable'])} layouts
Expand Down Expand Up @@ -572,7 +590,7 @@ def _llm_generate_all_topics(self, query: str, analysis: Dict,
]

def _assign_content_dynamically(self, specs: List, content_phs: List,
blueprint: Dict, query: str):
blueprint: Dict, query: str, extracted_content: Optional[str] = None):
"""Existing - unchanged"""
if not content_phs:
return
Expand All @@ -586,7 +604,7 @@ def _assign_content_dynamically(self, specs: List, content_phs: List,
primary_type = self._determine_content_type(enforced, largest)

search_query = self._llm_generate_search_query(
query, purpose, primary_type, "primary"
query, purpose, primary_type, "primary", extracted_content
)

specs.append(PlaceholderContentSpec(
Expand Down Expand Up @@ -614,7 +632,7 @@ def _assign_content_dynamically(self, specs: List, content_phs: List,
else:
ct = 'bullets'

sq = self._llm_generate_search_query(query, purpose, ct, f"supporting_{i}")
sq = self._llm_generate_search_query(query, purpose, ct, f"supporting_{i}", extracted_content)

specs.append(PlaceholderContentSpec(
placeholder_idx=ph['idx'],
Expand Down Expand Up @@ -650,8 +668,17 @@ def _determine_content_type(self, enforced: str, ph: Dict) -> str:
return 'bullets'

def _llm_generate_search_query(self, main_query: str, purpose: str,
content_type: str, role: str) -> SearchQuery:
"""Existing - unchanged"""
content_type: str, role: str, extracted_content: Optional[str] = None) -> SearchQuery:
"""Existing - updated to handle content extraction source"""

if extracted_content:
# If we have extracted content, the "search query" becomes a "extraction instruction"
return SearchQuery(
query=f"Extract info about {purpose} for {content_type}",
purpose=f"{purpose} - {role}",
expected_source_type='extracted_content'
)

prompt = f"""Generate a specific search query:

Main topic: {main_query}
Expand Down
Loading