refactor: streamline MCP workflow by integrating file processing into outline generation and removing redundant states

This commit is contained in:
sudipnext 2025-08-05 20:04:56 +05:45
parent 18aee8fc78
commit 75abd8085d
14 changed files with 178 additions and 225 deletions

View file

@ -203,10 +203,77 @@ For detailed info checkout [API documentation](https://docs.presenton.ai/using-p
- [Create Presentations from CSV using AI](https://docs.presenton.ai/tutorial/generate-presentation-from-csv)
- [Create Data Reports Using AI](https://docs.presenton.ai/tutorial/create-data-reports-using-ai)
## 🏗️ MCP Architecture Overview
Presenton is built on a modular architecture featuring a FastAPI backend and a Next.js frontend. At its core is the **MCP (Model Context Protocol) server**, which orchestrates the entire presentation generation workflow using a robust state machine. This architecture ensures flexibility, reliability, and extensibility.
### MCP Workflow Highlights
- **Session Management:** Each presentation runs in its own session for isolation and tracking.
- **Outline Generation:** Automatically creates outlines, with or without input files.
- **Layout Selection:** Choose from built-in or custom layouts.
- **Content & Asset Generation:** Generates slide text, images, and icons using your selected AI models.
- **Export Options:** Seamlessly export presentations as PDF or PPTX files.
All workflow logic and tool APIs are organized in the `app_mcp` package. The orchestrator handles state transitions and error management, making it easy to extend or customize.
#### Key Files & Directories
- `.vscode/mcp.json`: VS Code integration and MCP server configuration.
- `servers/fastapi/app_mcp/`: Backend workflow logic and tool registration.
---
## ⚡ Quick Start: VS Code Integration
1. **Configure MCP:** Make sure `.vscode/mcp.json` points to your running MCP server (see example below).
2. **Start a Presentation:** Use the VS Code command palette or chat to run `start_presentation` with your topic.
3. **Advance Workflow:** Use `continue_workflow` to progress through outline, layout, and slide generation steps.
4. **Export:** Use `export_presentation` to download your presentation as PDF or PPTX.
5. **Check Progress:** Use `get_status` at any time to view your workflow status.
#### Example `.vscode/mcp.json`
```jsonc
{
"servers": {
"my-mcp-server-5f58fb2c": {
"url": "http://localhost:5000/mcp/",
"type": "http"
}
},
"inputs": []
}
```
---
### 🗣️ Using Chat Commands in VS Code
You can interact with Presenton directly from the VS Code chat window:
- **Step-by-step Workflow:**
Type a prompt like:
```plaintext
I want to create a presentation on "Artificial Intelligence in Healthcare". Can you please show me the step by step and verify things to me so that I can be sure that the presentation is good?
```
- **Direct Commands:**
For a faster workflow, use direct commands such as:
```plaintext
Start a presentation on "Artificial Intelligence in Healthcare" with general layout and 10 slides.
```
This integration gives you full control—whether you want a guided, step-by-step experience or prefer to automate the entire process with a single command.
---
## Roadmap
- [x] Support for custom HTML templates by developers
- [x] Support for accessing custom templates over API
- [ ] Implement MCP server
- [x] Implement MCP server
- [ ] Ability for users to change system prompt
- [X] Support external SQL database

View file

@ -1,7 +1,7 @@
from fastapi import APIRouter, HTTPException
import aiohttp
from typing import List, Any
from services.get_layout_by_name import get_layout_by_name
from utils.get_layout_by_name import get_layout_by_name
from models.presentation_layout import PresentationLayoutModel
LAYOUTS_ROUTER = APIRouter(prefix="/layouts", tags=["Layouts"])

View file

@ -2,21 +2,10 @@ from app_mcp.services.state_machine.states import PresentationState
TRANSITIONS = {
PresentationState.INIT: {
PresentationState.FILES_UPLOADED,
PresentationState.OUTLINE_REQUESTED
},
# Upload and summary flow
PresentationState.FILES_UPLOADED: {
PresentationState.SUMMARY_GENERATED,
PresentationState.UPLOAD_FAILED
},
PresentationState.SUMMARY_GENERATED: {
PresentationState.OUTLINE_REQUESTED,
PresentationState.SUMMARY_FAILED
},
# Outline generation flow
# Outline generation flow (now includes file processing)
PresentationState.OUTLINE_REQUESTED: {
PresentationState.OUTLINE_GENERATED,
PresentationState.OUTLINE_FAILED
@ -74,14 +63,6 @@ TRANSITIONS = {
},
# Error recovery transitions
PresentationState.UPLOAD_FAILED: {
PresentationState.INIT,
PresentationState.FILES_UPLOADED
},
PresentationState.SUMMARY_FAILED: {
PresentationState.FILES_UPLOADED,
PresentationState.OUTLINE_REQUESTED
},
PresentationState.OUTLINE_FAILED: {
PresentationState.OUTLINE_REQUESTED,
PresentationState.INIT
@ -102,10 +83,9 @@ TRANSITIONS = {
SUGGESTIONS = {
PresentationState.INIT: "Upload files or start with outline generation",
PresentationState.FILES_UPLOADED: "Generate summary from uploaded files",
PresentationState.SUMMARY_GENERATED: "Generate presentation outline",
PresentationState.OUTLINE_GENERATED: "Review and approve outline, or regenerate",
PresentationState.INIT: "Start with outline generation (files will be processed automatically if provided)",
PresentationState.OUTLINE_REQUESTED: "Generating presentation outline with file analysis if applicable",
PresentationState.OUTLINE_GENERATED: "Review and approve outline",
PresentationState.OUTLINE_APPROVED: "Select presentation layout",
PresentationState.LAYOUT_SELECTED: "Generate presentation",
PresentationState.PRESENTATION_READY: "Export presentation or request edits",
@ -117,9 +97,7 @@ SUGGESTIONS = {
PROGRESS_WEIGHTS = {
PresentationState.INIT: 0,
PresentationState.FILES_UPLOADED: 10,
PresentationState.SUMMARY_GENERATED: 20,
PresentationState.OUTLINE_REQUESTED: 25,
PresentationState.OUTLINE_REQUESTED: 20,
PresentationState.OUTLINE_GENERATED: 35,
PresentationState.OUTLINE_APPROVED: 40,
PresentationState.LAYOUT_REQUESTED: 45,
@ -134,8 +112,6 @@ PROGRESS_WEIGHTS = {
ERROR_STATES = {
PresentationState.UPLOAD_FAILED,
PresentationState.SUMMARY_FAILED,
PresentationState.OUTLINE_FAILED,
PresentationState.GENERATION_FAILED,
PresentationState.EXPORT_FAILED,

View file

@ -5,11 +5,8 @@ class PresentationState(Enum):
Represents the various states in the presentation workflow.
"""
INIT = auto()
# Upload and summary phase
FILES_UPLOADED = auto()
SUMMARY_GENERATED = auto()
# Outline generation phase
# Outline generation phase (now includes file processing)
OUTLINE_REQUESTED = auto()
OUTLINE_GENERATED = auto()
OUTLINE_APPROVED = auto()
@ -32,8 +29,6 @@ class PresentationState(Enum):
TEMPLATE_EDITING = auto()
# Error states
UPLOAD_FAILED = auto()
SUMMARY_FAILED = auto()
OUTLINE_FAILED = auto()
GENERATION_FAILED = auto()
EXPORT_FAILED = auto()

View file

@ -3,7 +3,6 @@ from dataclasses import asdict
from app_mcp.services.state_machine.machine import PresentationStateMachine
from app_mcp.services.state_machine.states import PresentationState
from utils.user_config import update_env_with_user_config
from app_mcp.wrapper.upload_and_generate_summary import upload_and_summarize_files
from app_mcp.wrapper.generate_outline import generate_outline
from app_mcp.wrapper.presentation_generation import process_post_outline_workflow
from app_mcp.wrapper.presentation_export import export_presentation_and_get_path
@ -73,50 +72,6 @@ class WorkflowOrchestrator:
"""Remove workflow session"""
return self._active_sessions.pop(session_id, None) is not None
async def execute_upload_and_summarize(self, session_id: str, files: List[Any]) -> Dict[str, Any]:
"""
Execute file upload and summary generation workflow step.
Args:
session_id (str): Unique identifier for the session.
files (List[Any]): List of files to be uploaded and summarized.
Returns:
Dict[str, Any]: Result containing status, state, progress, next action, and any
"""
fsm = self.get_session(session_id)
if not fsm:
raise ValueError(f"Session {session_id} not found")
try:
fsm.transition(PresentationState.FILES_UPLOADED)
result = await upload_and_summarize_files(files)
# Update context and transition to summary generated
context_updates = {
"summary": result["summary"],
"file_paths": result["file_paths"]
}
fsm.transition(PresentationState.SUMMARY_GENERATED, context_updates)
return {
"status": "success",
"state": fsm.state.name,
"progress": fsm.get_workflow_progress(),
"next_action": fsm.get_next_suggested_action(),
"result": result
}
except Exception as e:
fsm.transition(PresentationState.UPLOAD_FAILED, {"error_message": str(e)})
print(f"There was an error uploading and summarizing files: {e}")
return {
"status": "error",
"state": fsm.state.name,
"error": str(e),
"next_action": fsm.get_next_suggested_action()
}
async def execute_generate_outline(self, session_id: str, prompt: str, **kwargs) -> Dict[str, Any]:
"""
Execute outline generation workflow step
@ -136,7 +91,7 @@ class WorkflowOrchestrator:
fsm.transition(PresentationState.OUTLINE_REQUESTED)
result = await generate_outline(prompt, summary=fsm.context.summary, **kwargs)
result = await generate_outline(prompt, **kwargs)
# Update the Context and transition to outline generated
context_updates = {
@ -149,10 +104,9 @@ class WorkflowOrchestrator:
"status": "success",
"state": fsm.state.name,
"progress": fsm.get_workflow_progress(),
"next_action": "Review outline and approve, or request regeneration",
"next_action": "Review outline and approve",
"result": result,
"can_approve": True,
"can_regenerate": True
"can_approve": True
}
except Exception as e:

View file

@ -2,7 +2,6 @@
from app_mcp.tools.choose_layout import register_choose_layout
from app_mcp.tools.export_presentation import register_export_presentation
from app_mcp.tools.regenerate_outline import register_regenerate_outline
from app_mcp.tools.get_status import register_get_status
from app_mcp.tools.show_layouts import register_show_layouts
from app_mcp.tools.start_presentation import register_start_presentation
@ -13,7 +12,6 @@ from app_mcp.tools.continue_workflow import register_continue_workflow
__all__ = [
'register_choose_layout',
'register_export_presentation',
'register_regenerate_outline',
'register_get_status',
'register_show_layouts',
'register_start_presentation',
@ -27,7 +25,6 @@ def register_tools(mcp, orchestrator):
tools = [
register_choose_layout,
register_export_presentation,
register_regenerate_outline,
register_get_status,
register_show_layouts,
register_start_presentation,

View file

@ -44,11 +44,12 @@ def register_continue_workflow(mcp, orchestrator):
current_state = fsm.state.name
if current_state in ["FILES_UPLOADED", "SUMMARY_GENERATED", "INIT"]:
# Generate outline
if current_state in ["INIT"]:
# Generate outline (this now handles file processing internally)
prompt = fsm.context.metadata.get("original_prompt", "")
n_slides = fsm.context.metadata.get("n_slides", 8)
language = fsm.context.metadata.get("language", "English")
files = fsm.context.metadata.get("files", None)
if not prompt:
return {
@ -57,8 +58,13 @@ def register_continue_workflow(mcp, orchestrator):
"suggestion": "Call start_presentation with a valid prompt"
}
# Pass files to outline generation if they exist
kwargs = {"n_slides": n_slides, "language": language}
if files:
kwargs["files"] = files
result = await orchestrator.execute_generate_outline(
session_id, prompt, n_slides=n_slides, language=language
session_id, prompt, **kwargs
)
if result["status"] == "success":
@ -68,8 +74,9 @@ def register_continue_workflow(mcp, orchestrator):
"message": "Here's your presentation outline:",
"title": result["result"]["title"],
"outlines": result["result"]["outlines"],
"files_processed": bool(files),
"suggestion": "Take a look at the outline. If it looks good, use 'continue_workflow' again to proceed to layout selection.",
"next_step": "Call continue_workflow again to choose layouts, or use regenerate_outline to try different approach"
"next_step": "Call continue_workflow again to choose layouts"
}
return result

View file

@ -39,12 +39,7 @@ def register_export_presentation(mcp, orchestrator):
"session_id": session_id,
"message": f"🎉 Your presentation has been exported as {format.upper()}!",
"path": result["result"]["path"],
"suggestion": "You can download it now, or start creating another presentation.",
"available_actions": {
"download": "Download the presentation",
"new_presentation": "Create a new presentation",
"edit": "Make edits to this presentation"
}
"suggestion": "You can download it now, or start creating another presentation."
}
return result
except Exception as e:

View file

@ -42,7 +42,7 @@ def register_get_status(mcp, orchestrator):
# Provide user-friendly status messages
friendly_messages = {
"INIT": "Ready to start! Use 'start_presentation' to begin.",
"SUMMARY_GENERATED": "Files processed. Use 'continue_workflow' to generate outline.",
"OUTLINE_REQUESTED": "Generating outline with file analysis if applicable.",
"OUTLINE_GENERATED": "Outline created. Use 'continue_workflow' to proceed to layouts.",
"OUTLINE_APPROVED": "Outline approved. Use 'choose_layout' to select a theme.",
"LAYOUT_SELECTED": "Layout chosen. Use 'continue_workflow' to generate presentation.",
@ -52,8 +52,8 @@ def register_get_status(mcp, orchestrator):
next_actions = {
"INIT": "start_presentation",
"SUMMARY_GENERATED": "continue_workflow",
"OUTLINE_GENERATED": "continue_workflow (or regenerate_outline)",
"OUTLINE_REQUESTED": "Wait for outline generation to complete",
"OUTLINE_GENERATED": "continue_workflow",
"OUTLINE_APPROVED": "choose_layout",
"LAYOUT_SELECTED": "continue_workflow",
"PRESENTATION_READY": "export_presentation",

View file

@ -30,7 +30,6 @@ def register_help_me(mcp, orchestrator):
"helpful_commands": {
"get_status": "📊 Check your current progress anytime",
"show_layouts": "👀 Browse available themes and styles",
"regenerate_outline": "🔄 Try a different outline approach",
"help": "❓ Show this helpful guide"
},
"quick_start": {

View file

@ -1,63 +0,0 @@
from typing import Dict, Any, Optional, List
from app_mcp.tools.continue_workflow import register_continue_workflow
from app_mcp.services.state_machine.states import PresentationState
def register_regenerate_outline(mcp, orchestrator):
"""Register all workflow-related tools for chat-based interaction"""
@mcp.tool("regenerate_outline")
async def regenerate_outline(
session_id: str,
new_prompt: Optional[str] = None,
n_slides: Optional[int] = None,
language: Optional[str] = None
) -> Dict[str, Any]:
"""
🔄 Create a new outline with different requirements.
Not happy with the generated outline? Use this to:
- Try a different angle or focus for your topic
- Change the number of slides
- Adjust the language or tone
- Incorporate new requirements
Args:
session_id: Your presentation session ID
new_prompt: New description of what you want (optional)
n_slides: Different number of slides (optional)
language: Different language (optional)
"""
try:
fsm = orchestrator.get_session(session_id)
if not fsm:
return {"status": "error", "error": "Session not found"}
# Update parameters if provided
if new_prompt:
fsm.context.metadata["original_prompt"] = new_prompt
if n_slides:
fsm.context.metadata["n_slides"] = n_slides
if language:
fsm.context.metadata["language"] = language
# Reset to outline generation
if fsm.can_transition_to(PresentationState.OUTLINE_REQUESTED):
fsm.transition(PresentationState.OUTLINE_REQUESTED)
# Generate new outline
continue_workflow = register_continue_workflow(mcp, orchestrator)
result = await continue_workflow(session_id=session_id, action="continue")
if result["status"] == "success":
result["message"] = "I've created a new outline for you:"
return result
except Exception as e:
return {
"status": "error",
"error": str(e),
"session_id": session_id
}
return regenerate_outline

View file

@ -71,20 +71,21 @@ def register_start_presentation(mcp, orchestrator):
# Debug log to verify metadata update
print("DEBUG: Metadata after update:", fsm.context.metadata)
# Handle files if provided
# Handle files if provided - store them in context for later use
if files and len(files) > 0:
result = await orchestrator.execute_upload_and_summarize(session_id, files)
if result["status"] == "error":
return result
# Store files in context for integrated processing during outline generation
fsm.context.metadata.update({
"files": files
})
return {
"status": "success",
"session_id": session_id,
"message": "Great! I've uploaded and analyzed your files. Here's a summary:",
"summary": result["result"]["summary"],
"message": "Great! I've received your files and will analyze them during presentation creation.",
"prompt": prompt,
"suggestion": f"Now I can create a presentation outline based on your prompt '{prompt}' and the file content. Use 'continue_workflow' to proceed.",
"next_step": "Call continue_workflow to generate the outline"
"files_count": len(files),
"suggestion": f"Now I'll create a presentation outline based on your prompt '{prompt}' and analyze the uploaded files. Use 'continue_workflow' to proceed.",
"next_step": "Call continue_workflow to generate the outline with file analysis"
}
else:
# Direct outline generation without files

View file

@ -1,35 +1,91 @@
import json
from typing import Dict, Any, Optional
import os
from typing import Dict, Any, Optional, List, Annotated
from models.presentation_outline_model import PresentationOutlineModel
from utils.llm_calls.generate_presentation_outlines import generate_ppt_outline
from services import TEMP_FILE_SERVICE
from services.documents_loader import DocumentsLoader
from services.score_based_chunker import ScoreBasedChunker
from utils.validators import validate_files
from fastapi import UploadFile, File
from models.sse_response import SSEResponse
from constants.documents import UPLOAD_ACCEPTED_FILE_TYPES
import asyncio
async def generate_outline(
prompt: str,
n_slides: int = 8,
language: str = "English",
summary: Optional[str] = None,
files: Annotated[Optional[List[UploadFile]], File()] = None,
) -> Dict[str, Any]:
"""
Generate presentation outlines given a prompt, number of slides, language, and optional summary.
Generate presentation outlines given a prompt, number of slides, language, optional summary, and files.
Files are now processed directly within this function instead of a separate step.
Returns the parsed outline data.
"""
presentation_content_text = ""
async for chunk in generate_ppt_outline(
prompt,
n_slides,
language,
summary,
):
presentation_content_text += chunk
validate_files(files, True, True, 50, UPLOAD_ACCEPTED_FILE_TYPES)
temp_dir = TEMP_FILE_SERVICE.create_temp_dir()
file_paths = []
if files:
for upload in files:
file_path = os.path.join(temp_dir, upload.filename)
with open(file_path, "wb") as f:
f.write(await upload.read())
file_paths.append(file_path)
presentation_outlines = None
additional_context = ""
if file_paths:
documents_loader = DocumentsLoader(file_paths=file_paths)
await documents_loader.load_documents(temp_dir)
documents = documents_loader.documents
if documents:
additional_context = documents[0]
chunker = ScoreBasedChunker()
try:
chunks = await chunker.get_n_chunks(documents[0], n_slides)
presentation_outlines = PresentationOutlineModel(
slides=[chunk.to_slide_outline() for chunk in chunks]
)
except Exception as e:
print(e)
if not presentation_outlines:
presentation_outlines_text = ""
async for chunk in generate_ppt_outline(
prompt,
n_slides,
language,
additional_context,
):
# Give control to the event loop
await asyncio.sleep(0)
presentation_outlines_text += chunk
presentation_outlines_json = json.loads(presentation_outlines_text)
presentation_outlines = PresentationOutlineModel(
**presentation_outlines_json
)
# Truncate slides to n_slides
presentation_outlines.slides = presentation_outlines.slides[:n_slides]
# Compose title from first slide (if available)
title = ""
if presentation_outlines.slides and hasattr(presentation_outlines.slides[0], '__str__'):
title = str(presentation_outlines.slides[0])[:50]
title = title.replace("#", "").replace("/", "").replace("\\", "").replace("\n", "")
elif presentation_outlines.slides:
title = str(presentation_outlines.slides[0])[:50]
# Prepare outlines list
outlines = [slide.model_dump() for slide in presentation_outlines.slides]
presentation_content_json = json.loads(presentation_content_text)
presentation_content = PresentationOutlineModel(
**presentation_content_json)
outlines = [slide.model_dump()
for slide in presentation_content.slides[:n_slides]]
return {
"title": presentation_content.title,
"title": getattr(presentation_outlines, 'title', title),
"outlines": outlines,
"notes": presentation_content.notes
"notes": getattr(presentation_outlines, 'notes', []),
}

View file

@ -1,31 +0,0 @@
from typing import List, Dict, Any
from fastapi import UploadFile
from services import TEMP_FILE_SERVICE
from services.documents_loader import DocumentsLoader
from utils.randomizers import get_random_uuid
from utils.llm_calls.generate_document_summary import generate_document_summary
# Standalone function for workflow orchestrator
async def upload_and_summarize_files(
files: List[UploadFile]
) -> Dict[str, Any]:
"""
Upload files, generate a document summary, and return both summary and file paths.
"""
if not files:
raise ValueError("No files provided")
temp_dir = TEMP_FILE_SERVICE.create_temp_dir(get_random_uuid())
file_paths = []
for upload in files:
temp_path = TEMP_FILE_SERVICE.create_temp_file_path(upload.filename, temp_dir)
with open(temp_path, "wb") as f:
f.write(await upload.read())
file_paths.append(temp_path)
documents_loader = DocumentsLoader(file_paths=file_paths)
await documents_loader.load_documents(temp_dir)
summary = await generate_document_summary(documents_loader.documents)
return {
"summary": summary,
"file_paths": file_paths,
}