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
63 changes: 55 additions & 8 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,18 +133,55 @@ 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)
# 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)
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:
api_key = os.getenv('OPENAI_API_KEY')
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

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:
Expand All @@ -169,10 +208,12 @@ def create_plan():
)

# 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
)

# Cache plan
Expand All @@ -182,7 +223,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,11 +273,15 @@ 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

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:
Expand All @@ -254,7 +301,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
Binary file added src/slidedeckai/__pycache__/__init__.cpython-312.pyc
Binary file not shown.
Binary file added src/slidedeckai/__pycache__/_version.cpython-312.pyc
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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
60 changes: 41 additions & 19 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,12 +56,12 @@ 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) -> ResearchPlan:
"""Existing logic with FIX #1: Validate layouts upfront. Added support for extracted content."""

logger.info("🤖 Starting FULLY DYNAMIC planning...")

Expand All @@ -70,13 +71,13 @@ def generate_plan(self, user_query: str, template_layouts: Dict,
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 +91,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 +107,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 +239,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 +298,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 +373,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 +434,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 +510,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 +585,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 +599,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 +627,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 +663,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