Skip to content
Draft
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
173 changes: 161 additions & 12 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 @@ -124,29 +126,97 @@ def serialize_plan(research_plan) -> Dict:
@app.route('/')
def index():
"""Serve the HTML UI"""
return render_template_string(HTML_UI)
# Prepare model options from GlobalConfig
model_options = {}
for key, info in GlobalConfig.VALID_MODELS.items():
if key.startswith('['):
provider = key[1:3]
name = key[4:]
if provider not in model_options:
model_options[provider] = []

model_options[provider].append({
'name': name,
'full_key': key,
'web_search': info.get('web_search', True)
})

return render_template_string(HTML_UI, model_options=model_options)


@app.route('/api/plan', methods=['POST'])
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 +238,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 +257,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 +307,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 +351,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 Expand Up @@ -337,6 +434,58 @@ def get_templates():
return jsonify({'error': str(e)}), 500


@app.route('/api/chat', methods=['POST'])
def chat_slide():
"""Chat with the slide content to refine it"""
try:
data = request.get_json()
report_id = data.get('report_id')
slide_idx = data.get('slide_idx')
instruction = data.get('instruction')

if not report_id or not instruction:
return jsonify({'error': 'Missing parameters'}), 400

logger.info(f"💬 Chat for {report_id} slide {slide_idx}: {instruction}")

# Placeholder response for demo purposes
return jsonify({
'success': True,
'message': 'Slide updated based on instruction',
'updated_content': {
'title': f"Updated Slide {slide_idx}",
'bullets': ["Refined bullet 1", "Refined bullet 2"]
}
})
except Exception as e:
logger.error(f"Chat failed: {e}", exc_info=True)
return jsonify({'error': str(e)}), 500

@app.route('/api/preview/<report_id>')
def preview_report(report_id):
"""Get preview data for the report (mocking image generation)"""
# In a real scenario, this would convert PPTX pages to images
# For now, we return slide metadata to render a HTML preview
if report_id not in slides_cache:
return jsonify({'error': 'Report not found'}), 404

cached = slides_cache[report_id]
# We could inspect the plan or the PPTX here
# Mocking preview data
slides = []
# Add title slide
slides.append({'title': cached.get('topic', 'Title Slide'), 'type': 'title', 'content': []})

# Add fake content slides based on what we know (or just generic)
for i in range(3):
slides.append({
'title': f"Slide {i+1}",
'type': 'bullets',
'content': [f"Point {j+1}" for j in range(3)]
})

return jsonify({'slides': slides})

@app.route('/api/health')
def health():
"""Health check endpoint"""
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
Loading