Merged with main

This commit is contained in:
shiva raj badu 2025-08-05 20:32:05 +05:45
commit 40dde6ea44
No known key found for this signature in database
106 changed files with 6037 additions and 62370 deletions

View file

@ -1,10 +1,11 @@
.venv
.env
.next
out
build
.git
.gitignore
servers/fastapi/tmp
servers/fastapi/debug
servers/nextjs/node_modules
servers/fastapi/.venv
servers/nextjs/node_modules
servers/nextjs/.next
container.db

5
.gitignore vendored
View file

@ -12,4 +12,7 @@ tmp
debug
.fastembed_cache
my-doc.txt
generated_models
generated_models
nltk
chroma
container.db

View file

@ -3,8 +3,7 @@ FROM python:3.11-slim-bookworm
# Install Node.js and npm
RUN apt-get update && apt-get install -y \
nginx \
curl \
redis-server
curl
# Install Node.js 20 using NodeSource repository
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
@ -17,13 +16,17 @@ WORKDIR /app
# Set environment variables
ENV APP_DATA_DIRECTORY=/app_data
ENV TEMP_DIRECTORY=/tmp/presenton
ENV PYTHONPATH="${PYTHONPATH}:/app/servers/fastapi"
# Install ollama
RUN curl -fsSL https://ollama.com/install.sh | sh
# Install dependencies for FastAPI
COPY servers/fastapi/requirements.txt ./
RUN pip install -r requirements.txt
RUN pip install aiohttp aiomysql aiosqlite asyncpg fastapi[standard] \
pathvalidate pdfplumber nltk chromadb sqlmodel \
anthropic google-genai openai fastmcp
RUN pip install docling --extra-index-url https://download.pytorch.org/whl/cpu
# Install dependencies for Next.js
WORKDIR /app/servers/nextjs

View file

@ -3,8 +3,7 @@ FROM python:3.11-slim-bookworm
# Install Node.js and npm
RUN apt-get update && apt-get install -y \
nginx \
curl \
redis-server
curl
# Install Node.js 20 using NodeSource repository
@ -20,18 +19,21 @@ RUN ls -a
# Set environment variables
ENV APP_DATA_DIRECTORY=/app_data
ENV TEMP_DIRECTORY=/tmp/presenton
ENV PYTHONPATH="${PYTHONPATH}:/app/servers/fastapi"
# Install ollama
RUN curl -fsSL https://ollama.com/install.sh | sh
RUN curl -fsSL http://ollama.com/install.sh | sh
# Install dependencies for FastAPI
COPY servers/fastapi/requirements.txt ./
RUN pip install -r requirements.txt
RUN pip install aiohttp aiomysql aiosqlite asyncpg fastapi[standard] \
pathvalidate pdfplumber nltk chromadb sqlmodel \
anthropic google-genai openai fastmcp
RUN pip install docling --extra-index-url https://download.pytorch.org/whl/cpu
# Install dependencies for Next.js
WORKDIR /node_dependencies
COPY servers/nextjs/package.json servers/nextjs/package-lock.json ./
RUN npm install
RUN npm install
# Install chrome for puppeteer
RUN npx puppeteer browsers install chrome@138.0.7204.94 --install-deps
@ -45,4 +47,4 @@ COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
# Start the servers
CMD ["node", "/app/start.js", "--dev"]
CMD ["node", "/app/start.js", "--dev"]

View file

@ -89,6 +89,8 @@ You may want to directly provide your API KEYS as environment variables and keep
- **CUSTOM_LLM_URL=[Custom OpenAI Compatible URL]**: Provide this if **LLM** is set to **custom**
- **CUSTOM_LLM_API_KEY=[Custom OpenAI Compatible API KEY]**: Provide this if **LLM** is set to **custom**
- **CUSTOM_MODEL=[Custom Model ID]**: Provide this if **LLM** is set to **custom**
- **TOOL_CALLS=[Enable/Disable Tool Calls on Custom LLM]**: If **true**, **LLM** will use Tool Call instead of Json Schema for Structured Output.
- **DISABLE_THINKING=[Enable/Disable Thinking on Custom LLM]**: If **true**, Thinking will be disabled.
You can also set the following environment variables to customize the image generation provider and API keys:

View file

@ -30,6 +30,13 @@ http {
proxy_connect_timeout 30m;
}
# MCP
location /mcp/ {
proxy_pass http://localhost:8001;
proxy_read_timeout 30m;
proxy_connect_timeout 30m;
}
location /docs {
proxy_pass http://localhost:8000/docs;
proxy_read_timeout 30m;

View file

@ -0,0 +1 @@
3.11

View file

@ -1,9 +1,11 @@
import json
from datetime import datetime
from fastapi import HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from models.ollama_model_status import OllamaModelStatus
from services import REDIS_SERVICE
from models.sql.ollama_pull_status import OllamaPullStatus
from services.database import get_container_db_async_session
from utils.ollama import pull_ollama_model
@ -15,6 +17,8 @@ async def pull_ollama_model_background_task(model: str):
)
log_event_count = 0
session = await get_container_db_async_session().__anext__()
try:
async for event in pull_ollama_model(model):
log_event_count += 1
@ -30,18 +34,13 @@ async def pull_ollama_model_background_task(model: str):
if "status" in event:
saved_model_status.status = event["status"]
REDIS_SERVICE.set(
f"ollama_models/{model}",
json.dumps(saved_model_status.model_dump(mode="json")),
)
await upsert_ollama_pull_status(session, model, saved_model_status)
except Exception as e:
saved_model_status.status = "error"
saved_model_status.done = True
REDIS_SERVICE.set(
f"ollama_models/{model}",
json.dumps(saved_model_status.model_dump(mode="json")),
)
await upsert_ollama_pull_status(session, model, saved_model_status)
await session.close()
raise HTTPException(
status_code=500,
detail=f"Failed to pull model: {e}",
@ -51,9 +50,27 @@ async def pull_ollama_model_background_task(model: str):
saved_model_status.status = "pulled"
saved_model_status.downloaded = saved_model_status.size
REDIS_SERVICE.set(
f"ollama_models/{model}",
json.dumps(saved_model_status.model_dump(mode="json")),
)
await upsert_ollama_pull_status(session, model, saved_model_status)
await session.close()
return saved_model_status
async def upsert_ollama_pull_status(
session: AsyncSession, model: str, model_status: OllamaModelStatus
):
stmt = select(OllamaPullStatus).where(OllamaPullStatus.id == model)
result = await session.execute(stmt)
existing_record = result.scalar_one_or_none()
if existing_record:
existing_record.status = model_status.model_dump(mode="json")
existing_record.last_updated = datetime.now()
else:
new_record = OllamaPullStatus(
id=model,
status=model_status.model_dump(mode="json"),
last_updated=datetime.now(),
)
session.add(new_record)
await session.commit()
await session.flush()

View file

@ -0,0 +1,27 @@
from fastapi import APIRouter, HTTPException
import aiohttp
from typing import List, Any
from services.get_layout_by_name import get_layout_by_name
from models.presentation_layout import PresentationLayoutModel
LAYOUTS_ROUTER = APIRouter(prefix="/layouts", tags=["Layouts"])
@LAYOUTS_ROUTER.get("/", summary="Get available layouts")
async def get_layouts():
url = "http://localhost:3000/api/layouts" # Adjust port if needed
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
if response.status != 200:
error_text = await response.text()
raise HTTPException(
status_code=response.status,
detail=f"Failed to fetch layouts: {error_text}"
)
layouts_json = await response.json()
# Optionally, parse into a Pydantic model if you have one matching the structure
return layouts_json
@LAYOUTS_ROUTER.get("/{layout_name}", summary="Get layout details by ID")
async def get_layout_detail(layout_name: str) -> PresentationLayoutModel:
return await get_layout_by_name(layout_name)

View file

@ -1,12 +1,15 @@
from datetime import datetime, timedelta
import json
from typing import List
from fastapi import APIRouter, BackgroundTasks, HTTPException
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from api.v1.ppt.background_tasks import pull_ollama_model_background_task
from constants.supported_ollama_models import SUPPORTED_OLLAMA_MODELS
from models.ollama_model_metadata import OllamaModelMetadata
from models.ollama_model_status import OllamaModelStatus
from services import REDIS_SERVICE
from models.sql.ollama_pull_status import OllamaPullStatus
from services.database import get_container_db_async_session
from utils.ollama import list_pulled_ollama_models
OLLAMA_ROUTER = APIRouter(prefix="/ollama", tags=["Ollama"])
@ -23,7 +26,11 @@ async def get_available_models():
@OLLAMA_ROUTER.get("/model/pull", response_model=OllamaModelStatus)
async def pull_model(model: str, background_tasks: BackgroundTasks):
async def pull_model(
model: str,
background_tasks: BackgroundTasks,
session: AsyncSession = Depends(get_container_db_async_session),
):
if model not in SUPPORTED_OLLAMA_MODELS:
raise HTTPException(
@ -46,21 +53,27 @@ async def pull_model(model: str, background_tasks: BackgroundTasks):
detail=f"Failed to check pulled models: {e}",
)
saved_model_status = REDIS_SERVICE.get(f"ollama_models/{model}")
saved_pull_status = None
saved_model_status = None
try:
saved_pull_status = await session.get(OllamaPullStatus, model)
saved_model_status = saved_pull_status.status
except Exception as e:
pass
# If the model is being pulled, return the model
if saved_model_status:
saved_model_status_json = json.loads(saved_model_status)
# If the model is being pulled, return the model
# ? If the model status is pulled in redis but was not found while listing pulled models,
# ? it means the model was deleted and we need to pull it again
if (
saved_model_status_json["status"] == "error"
or saved_model_status_json["status"] == "pulled"
saved_model_status["status"] == "error"
or saved_model_status["status"] == "pulled"
or saved_pull_status.last_updated < (datetime.now() - timedelta(seconds=10))
):
REDIS_SERVICE.delete(f"ollama_models/{model}")
await session.delete(saved_pull_status)
else:
return saved_model_status_json
return saved_model_status
# If the model is not being pulled, pull the model
background_tasks.add_task(pull_ollama_model_background_task, model)

View file

@ -7,7 +7,10 @@ from sqlalchemy.ext.asyncio import AsyncSession
from models.presentation_outline_model import PresentationOutlineModel
from models.sql.presentation import PresentationModel
from models.sse_response import SSECompleteResponse, SSEResponse, SSEStatusResponse
from services import TEMP_FILE_SERVICE
from services.database import get_async_session
from services.documents_loader import DocumentsLoader
from services.score_based_chunker import ScoreBasedChunker
from utils.llm_calls.generate_presentation_outlines import generate_ppt_outline
OUTLINES_ROUTER = APIRouter(prefix="/outlines", tags=["Outlines"])
@ -22,38 +25,66 @@ async def stream_outlines(
if not presentation:
raise HTTPException(status_code=404, detail="Presentation not found")
temp_dir = TEMP_FILE_SERVICE.create_temp_dir()
async def inner():
yield SSEStatusResponse(
status="Generating presentation outlines..."
).to_string()
presentation_content_text = ""
async for chunk in generate_ppt_outline(
presentation.prompt,
presentation.n_slides,
presentation.language,
presentation.summary,
):
# Give control to the event loop
await asyncio.sleep(0)
presentation_outlines = None
additional_context = ""
if presentation.file_paths:
documents_loader = DocumentsLoader(file_paths=presentation.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], presentation.n_slides
)
presentation_outlines = PresentationOutlineModel(
slides=[chunk.to_slide_outline() for chunk in chunks]
)
except Exception as e:
print(e)
yield SSEResponse(
event="response",
data=json.dumps({"type": "chunk", "chunk": chunk}),
).to_string()
presentation_content_text += chunk
if not presentation_outlines:
presentation_outlines_text = ""
async for chunk in generate_ppt_outline(
presentation.prompt,
presentation.n_slides,
presentation.language,
additional_context,
):
# Give control to the event loop
await asyncio.sleep(0)
presentation_content_json = json.loads(presentation_content_text)
yield SSEResponse(
event="response",
data=json.dumps({"type": "chunk", "chunk": chunk}),
).to_string()
presentation_outlines_text += chunk
presentation_content = PresentationOutlineModel(**presentation_content_json)
presentation_content.slides = presentation_content.slides[
presentation_outlines_json = json.loads(presentation_outlines_text)
presentation_outlines = PresentationOutlineModel(
**presentation_outlines_json
)
presentation_outlines.slides = presentation_outlines.slides[
: presentation.n_slides
]
presentation.title = presentation_content.title
presentation.outlines = [
each.model_dump() for each in presentation_content.slides
]
presentation.outlines = presentation_outlines.model_dump()
presentation.title = (
presentation_outlines.slides[0][:50]
.replace("#", "")
.replace("/", "")
.replace("\\", "")
.replace("\n", "")
)
sql_session.add(presentation)
await sql_session.commit()

View file

@ -2,7 +2,6 @@ import asyncio
import json
import os
import random
import importlib
from typing import Annotated, List, Literal, Optional
from fastapi import APIRouter, Body, Depends, File, HTTPException, UploadFile
from fastapi.responses import StreamingResponse
@ -12,14 +11,12 @@ from sqlmodel import select
from constants.documents import UPLOAD_ACCEPTED_FILE_TYPES
from models.presentation_and_path import PresentationPathAndEditPath
from models.presentation_from_template import GetPresentationUsingTemplateRequest
from models.presentation_outline_model import (
PresentationOutlineModel,
SlideOutlineModel,
)
from models.presentation_outline_model import PresentationOutlineModel
from models.pptx_models import PptxPresentationModel
from models.presentation_layout import PresentationLayoutModel
from models.presentation_structure_model import PresentationStructureModel
from models.presentation_with_slides import PresentationWithSlides
from services.score_based_chunker import ScoreBasedChunker
from utils.get_layout_by_name import get_layout_by_name
from services.icon_finder_service import IconFinderService
from services.image_generation_service import ImageGenerationService
@ -34,7 +31,6 @@ from services.documents_loader import DocumentsLoader
from models.sql.presentation import PresentationModel
from services.pptx_presentation_creator import PptxPresentationCreator
from utils.asset_directory_utils import get_exports_directory, get_images_directory
from utils.llm_calls.generate_document_summary import generate_document_summary
from utils.llm_calls.generate_presentation_structure import (
generate_presentation_structure,
)
@ -113,20 +109,12 @@ async def create_presentation(
):
presentation_id = get_random_uuid()
summary = None
if file_paths:
temp_dir = TEMP_FILE_SERVICE.create_temp_dir(presentation_id)
documents_loader = DocumentsLoader(file_paths=file_paths)
await documents_loader.load_documents(temp_dir)
summary = await generate_document_summary(documents_loader.documents)
presentation = PresentationModel(
id=presentation_id,
prompt=prompt,
n_slides=n_slides,
language=language,
summary=summary,
file_paths=file_paths,
)
sql_session.add(presentation)
@ -138,7 +126,7 @@ async def create_presentation(
@PRESENTATION_ROUTER.post("/prepare", response_model=PresentationModel)
async def prepare_presentation(
presentation_id: Annotated[str, Body()],
outlines: Annotated[List[SlideOutlineModel], Body()],
outlines: Annotated[List[str], Body()],
layout: Annotated[PresentationLayoutModel, Body()],
title: Annotated[Optional[str], Body()] = None,
sql_session: AsyncSession = Depends(get_async_session),
@ -173,7 +161,7 @@ async def prepare_presentation(
presentation_structure.slides[index] = random_slide_index
sql_session.add(presentation)
presentation.outlines = [each.model_dump() for each in outlines]
presentation.outlines = PresentationOutlineModel(slides=outlines).model_dump()
presentation.title = title or presentation.title
presentation.set_layout(layout)
presentation.set_structure(presentation_structure)
@ -328,37 +316,48 @@ async def generate_presentation_api(
presentation_id = get_random_uuid()
temp_dir = TEMP_FILE_SERVICE.create_temp_dir()
# 1. Save uploaded files
file_paths = []
if files:
temp_dir = TEMP_FILE_SERVICE.create_temp_dir()
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)
# 2. Create Presentation Summary (if documents are provided)
summary = None
# 3. Generate Outlines
presentation_outlines = None
additional_context = ""
if file_paths:
temp_dir = TEMP_FILE_SERVICE.create_temp_dir(presentation_id)
documents_loader = DocumentsLoader(file_paths=file_paths)
await documents_loader.load_documents(temp_dir)
summary = await generate_document_summary(documents_loader.documents)
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)
# 3. Generate Outlines
presentation_content_text = ""
async for chunk in generate_ppt_outline(
prompt,
n_slides,
language,
summary,
):
presentation_content_text += chunk
if not presentation_outlines:
presentation_outlines_text = ""
async for chunk in generate_ppt_outline(
prompt,
n_slides,
language,
additional_context,
):
presentation_outlines_text += chunk
presentation_content_json = json.loads(presentation_content_text)
presentation_content = PresentationOutlineModel(**presentation_content_json)
outlines = presentation_content.slides[:n_slides]
presentation_outlines_json = json.loads(presentation_outlines_text)
presentation_outlines = PresentationOutlineModel(**presentation_outlines_json)
outlines = presentation_outlines.slides[:n_slides]
total_outlines = len(outlines)
print("-" * 40)
@ -374,11 +373,8 @@ async def generate_presentation_api(
else:
presentation_structure: PresentationStructureModel = (
await generate_presentation_structure(
presentation_outline=PresentationOutlineModel(
title=presentation_content.title,
slides=outlines,
),
presentation_layout=layout_model,
presentation_outlines,
layout_model,
)
)
@ -397,9 +393,7 @@ async def generate_presentation_api(
prompt=prompt,
n_slides=n_slides,
language=language,
title=presentation_content.title,
summary=summary,
outlines=[each.model_dump() for each in outlines],
outlines=presentation_outlines.model_dump(),
layout=layout_model.model_dump(),
structure=presentation_structure.model_dump(),
)
@ -445,7 +439,7 @@ async def generate_presentation_api(
# 9. Export
presentation_and_path = await export_presentation(
presentation_id, presentation_content.title, export_as
presentation_id, presentation.title or get_random_uuid(), export_as
)
return PresentationPathAndEditPath(
@ -482,7 +476,7 @@ async def from_template(
await sql_session.commit()
presentation_and_path = await export_presentation(
new_presentation.id, new_presentation.title, data.export_as
new_presentation.id, new_presentation.title or get_random_uuid(), data.export_as
)
return PresentationPathAndEditPath(

View file

@ -0,0 +1 @@
# This file marks the mcp directory as a Python package.

View file

@ -0,0 +1,13 @@
from fastmcp import FastMCP
from app_mcp.tools import register_tools
from app_mcp.services.workflow_orchestrator import WorkflowOrchestrator
def create_mcp_server():
mcp = FastMCP("PresentonMCP")
orchestrator = WorkflowOrchestrator()
register_tools(mcp, orchestrator)
return mcp
uvicorn_config = {
"reload": True,
}

View file

@ -0,0 +1,143 @@
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
PresentationState.OUTLINE_REQUESTED: {
PresentationState.OUTLINE_GENERATED,
PresentationState.OUTLINE_FAILED
},
PresentationState.OUTLINE_GENERATED: {
PresentationState.OUTLINE_APPROVED,
PresentationState.OUTLINE_REQUESTED,
PresentationState.OUTLINE_FAILED
},
PresentationState.OUTLINE_APPROVED: {
PresentationState.LAYOUT_REQUESTED
},
# Layout selection flow
PresentationState.LAYOUT_REQUESTED: {
PresentationState.LAYOUT_SELECTED
},
PresentationState.LAYOUT_SELECTED: {
PresentationState.GENERATION_IN_PROGRESS,
PresentationState.LAYOUT_REQUESTED
},
# Presentation generation flow
PresentationState.GENERATION_IN_PROGRESS: {
PresentationState.PRESENTATION_READY,
PresentationState.GENERATION_FAILED
},
PresentationState.PRESENTATION_READY: {
PresentationState.EXPORT_REQUESTED,
PresentationState.EDIT_REQUESTED,
PresentationState.OUTLINE_REQUESTED
},
# Export flow
PresentationState.EXPORT_REQUESTED: {
PresentationState.EXPORT_IN_PROGRESS
},
PresentationState.EXPORT_IN_PROGRESS: {
PresentationState.EXPORT_COMPLETE,
PresentationState.EXPORT_FAILED
},
PresentationState.EXPORT_COMPLETE: {
PresentationState.EDIT_REQUESTED,
PresentationState.EXPORT_REQUESTED,
PresentationState.INIT
},
# Edit and revision flow
PresentationState.EDIT_REQUESTED: {
PresentationState.TEMPLATE_EDITING
},
PresentationState.TEMPLATE_EDITING: {
PresentationState.PRESENTATION_READY,
PresentationState.EDIT_FAILED
},
# 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
},
PresentationState.GENERATION_FAILED: {
PresentationState.LAYOUT_SELECTED,
PresentationState.OUTLINE_APPROVED
},
PresentationState.EXPORT_FAILED: {
PresentationState.EXPORT_REQUESTED,
PresentationState.PRESENTATION_READY
},
PresentationState.EDIT_FAILED: {
PresentationState.EDIT_REQUESTED,
PresentationState.PRESENTATION_READY
}
}
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.OUTLINE_APPROVED: "Select presentation layout",
PresentationState.LAYOUT_SELECTED: "Generate presentation",
PresentationState.PRESENTATION_READY: "Export presentation or request edits",
PresentationState.EXPORT_REQUESTED: "Choose export format and generate",
PresentationState.EXPORT_COMPLETE: "Download presentation or start new one",
PresentationState.EDIT_REQUESTED: "Make template-based edits",
}
PROGRESS_WEIGHTS = {
PresentationState.INIT: 0,
PresentationState.FILES_UPLOADED: 10,
PresentationState.SUMMARY_GENERATED: 20,
PresentationState.OUTLINE_REQUESTED: 25,
PresentationState.OUTLINE_GENERATED: 35,
PresentationState.OUTLINE_APPROVED: 40,
PresentationState.LAYOUT_REQUESTED: 45,
PresentationState.LAYOUT_SELECTED: 50,
PresentationState.GENERATION_IN_PROGRESS: 70,
PresentationState.PRESENTATION_READY: 85,
PresentationState.EXPORT_REQUESTED: 90,
PresentationState.EXPORT_IN_PROGRESS: 95,
PresentationState.EXPORT_COMPLETE: 100,
PresentationState.TEMPLATE_EDITING: 60,
}
ERROR_STATES = {
PresentationState.UPLOAD_FAILED,
PresentationState.SUMMARY_FAILED,
PresentationState.OUTLINE_FAILED,
PresentationState.GENERATION_FAILED,
PresentationState.EXPORT_FAILED,
PresentationState.EDIT_FAILED
}

View file

@ -0,0 +1,20 @@
from typing import Dict, Set, Optional, Any
from dataclasses import dataclass
@dataclass
class StateContext:
"""Context data that travels with the state machine"""
presentation_id: Optional[str] = None
summary: Optional[str] = None
title: Optional[str] = None
outlines: Optional[list] = None
layout: Optional[str] = None
file_paths: Optional[list] = None
export_format: Optional[str] = None
export_path: Optional[str] = None
error_message: Optional[str] = None
metadata: Dict[str, Any] = None
def __post_init__(self):
if self.metadata is None:
self.metadata = {}

View file

@ -0,0 +1,101 @@
from typing import Dict, Set, Any
from app_mcp.services.state_machine.context import StateContext
from app_mcp.services.state_machine.states import PresentationState
from app_mcp.services.state_machine.constants import TRANSITIONS, SUGGESTIONS, PROGRESS_WEIGHTS, ERROR_STATES
class PresentationStateMachine:
def __init__(self):
self.state = PresentationState.INIT
self.context = StateContext()
self._state_history = [PresentationState.INIT]
self._transitions = TRANSITIONS
self._error_states = ERROR_STATES
self._suggestions = SUGGESTIONS
self._progress_weights = PROGRESS_WEIGHTS
def transition(self, new_state: PresentationState, context_updates: Dict[str, Any] = None):
"""
Transition to new state with optional context updates
Args:
new_state (PresentationState): The state to transition to
context_updates (Dict[str, Any], optional): Context data to update during transition
Raises:
ValueError: If the transition is not valid
"""
if not self.is_valid_transition(new_state):
raise ValueError(f"Invalid transition from {self.state} to {new_state}")
# Update context if provided
if context_updates:
for key, value in context_updates.items():
if hasattr(self.context, key):
setattr(self.context, key, value)
else:
self.context.metadata[key] = value
# Record state history
self._state_history.append(new_state)
self.state = new_state
def is_valid_transition(self, new_state: PresentationState) -> bool:
"""Check if transition to new state is valid"""
return new_state in self._transitions.get(self.state, set())
def get_available_transitions(self) -> Set[PresentationState]:
"""Get all valid transitions from current state"""
return self._transitions.get(self.state, set())
def can_transition_to(self, target_state: PresentationState) -> bool:
"""Check if can transition to target state"""
return target_state in self.get_available_transitions()
def is_terminal_state(self) -> bool:
"""Check if current state is terminal (no outgoing transitions)"""
return len(self.get_available_transitions()) == 0
def is_error_state(self) -> bool:
"""Check if current state is an error state"""
return self.state in self._error_states
def get_workflow_progress(self) -> float:
"""Calculate workflow progress as percentage"""
return self._progress_weights.get(self.state, 0)
def get_next_suggested_action(self) -> str:
"""Get suggested next action based on current state"""
return self._suggestions.get(self.state, "No suggestions available")
def reset(self):
"""Reset state machine to initial state"""
self.state = PresentationState.INIT
self.context = StateContext()
self._state_history = [PresentationState.INIT]
def get_state_history(self) -> list:
"""Get history of states visited"""
return self._state_history.copy()
def rollback_to_previous_state(self) -> bool:
"""Rollback to previous state if possible"""
if len(self._state_history) < 2:
return False
# Remove current state from history
self._state_history.pop()
previous_state = self._state_history[-1]
if self.is_valid_transition(previous_state):
self.state = previous_state
return True
else:
self._state_history.append(self.state)
return False
def __str__(self):
return f"PresentationStateMachine(state={self.state.name}, progress={self.get_workflow_progress()}%)"
def __repr__(self):
return (f"PresentationStateMachine(state={self.state.name}, "
f"context={self.context}, "
f"history_length={len(self._state_history)})")

View file

@ -0,0 +1,40 @@
from enum import Enum, auto
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_REQUESTED = auto()
OUTLINE_GENERATED = auto()
OUTLINE_APPROVED = auto()
# Layout selection phase
LAYOUT_REQUESTED = auto()
LAYOUT_SELECTED = auto()
# Presentation generation phase
GENERATION_IN_PROGRESS = auto()
PRESENTATION_READY = auto()
# Export phase
EXPORT_REQUESTED = auto()
EXPORT_IN_PROGRESS = auto()
EXPORT_COMPLETE = auto()
# Edit and revision loops
EDIT_REQUESTED = auto()
TEMPLATE_EDITING = auto()
# Error states
UPLOAD_FAILED = auto()
SUMMARY_FAILED = auto()
OUTLINE_FAILED = auto()
GENERATION_FAILED = auto()
EXPORT_FAILED = auto()
EDIT_FAILED = auto()

View file

@ -0,0 +1,354 @@
from typing import Dict, Any, Optional, List
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
from app_mcp.wrapper.list_layout import list_layouts
class WorkflowOrchestrator:
"""
Orchestrates the presentation generation workflow using FSM
- Handles session management
- Executes
- file uploads
- summary generation
- outline generation
- layout selection
- presentation generation
- export
- Provides status and context management
- Allows for session-based operations
- Supports error handling and recovery
"""
def __init__(self):
"""
Initiating:
- The environment with user configuration from the user config file.
- The Finite State Machine (FSM) for presentation workflow.
- Active sessions dictionary to manage multiple workflows.
"""
try:
update_env_with_user_config()
except Exception as e:
print(f"Error updating environment with user config: {e}")
self.fsm = PresentationStateMachine()
self._active_sessions: Dict[str, PresentationStateMachine] = {}
def create_session(self, session_id: str) -> PresentationStateMachine:
"""
Create a new workflow session with the given session ID.
If a session with the same ID already exists, it will be replaced.
Session will Remain for the lifetime of the application.
Args:
session_id (str): Unique identifier for the session.
"""
if not session_id or not isinstance(session_id, str):
raise ValueError("Session ID must be a non-empty string")
session_id = session_id.strip()
if not session_id:
raise ValueError("Session ID cannot be empty")
if session_id in self._active_sessions:
self.remove_session(session_id)
print(f"Session {session_id} already exists, replacing it.")
self._active_sessions[session_id] = PresentationStateMachine()
return self._active_sessions[session_id]
def get_session(self, session_id: str) -> Optional[PresentationStateMachine]:
"""Get existing workflow session"""
if not session_id or not isinstance(session_id, str):
return None
return self._active_sessions.get(session_id.strip())
def remove_session(self, session_id: str) -> bool:
"""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
Args:
session_id (str): Unique identifier for the session.
prompt (str): The prompt to generate the outline.
**kwargs: Additional parameters for outline generation.
Returns:
Dict[str, Any]: Result containing status, state, progress, next action, and generated outline.
"""
fsm = self.get_session(session_id)
if not fsm:
raise ValueError(f"Session {session_id} not found")
try:
fsm.transition(PresentationState.OUTLINE_REQUESTED)
result = await generate_outline(prompt, summary=fsm.context.summary, **kwargs)
# Update the Context and transition to outline generated
context_updates = {
"title": result["title"],
"outlines": result["outlines"]
}
fsm.transition(PresentationState.OUTLINE_GENERATED, context_updates)
return {
"status": "success",
"state": fsm.state.name,
"progress": fsm.get_workflow_progress(),
"next_action": "Review outline and approve, or request regeneration",
"result": result,
"can_approve": True,
"can_regenerate": True
}
except Exception as e:
fsm.transition(PresentationState.OUTLINE_FAILED, {"error_message": str(e)})
print(f"Error generating outline for session {session_id}: {e}")
return {
"status": "error",
"state": fsm.state.name,
"error": str(e),
"next_action": fsm.get_next_suggested_action()
}
async def approve_outline(self, session_id: str) -> Dict[str, Any]:
"""
Approve the generated outline
Args:
session_id (str): Unique identifier for the session.
Returns:
Dict[str, Any]: Result containing status, state, progress, next action.
"""
fsm = self.get_session(session_id)
if not fsm:
raise ValueError(f"Session {session_id} not found")
if fsm.state != PresentationState.OUTLINE_GENERATED:
raise ValueError(f"Cannot approve outline in state {fsm.state.name}")
fsm.transition(PresentationState.OUTLINE_APPROVED)
return {
"status": "success",
"state": fsm.state.name,
"progress": fsm.get_workflow_progress(),
"next_action": fsm.get_next_suggested_action()
}
async def execute_layout_selection(self, session_id: str, layout: str) -> Dict[str, Any]:
"""
Execute layout selection workflow step
Args:
session_id (str): Unique identifier for the session.
layout (str): Selected layout for the presentation.
Returns:
Dict[str, Any]: Result containing status, state, progress, next action, and selected layout.
"""
fsm = self.get_session(session_id)
if not fsm:
raise ValueError(f"Session {session_id} not found")
try:
fsm.transition(PresentationState.LAYOUT_REQUESTED)
#Updating the context and transitioning to LAYOUT_SELECTED
context_updates = {"layout": layout}
fsm.transition(PresentationState.LAYOUT_SELECTED, context_updates)
return {
"status": "success",
"state": fsm.state.name,
"progress": fsm.get_workflow_progress(),
"next_action": fsm.get_next_suggested_action(),
"selected_layout": layout
}
except Exception as e:
print(f"Error selecting layout for session {session_id}: {e}")
return {
"status": "error",
"error": str(e),
"next_action": "Please select a valid layout"
}
async def execute_presentation_generation(self, session_id: str, **kwargs) -> Dict[str, Any]:
"""
Execute presentation generation workflow step
Args:
session_id (str): Unique identifier for the session.
**kwargs: Additional parameters for presentation generation.
Returns:
Dict[str, Any]: Result containing status, state, progress, next action, and generated presentation.
"""
fsm = self.get_session(session_id)
if not fsm:
raise ValueError(f"Session {session_id} not found")
try:
fsm.transition(PresentationState.GENERATION_IN_PROGRESS)
notes = kwargs.get('notes', [])
result = await process_post_outline_workflow(
title=fsm.context.title,
outlines=fsm.context.outlines,
notes=notes,
layout=fsm.context.layout,
prompt=fsm.context.metadata.get('original_prompt', ""),
sql_session=None,
**kwargs
)
#Updating the Context and transitioning to PRESENTATION_READY
context_updates = {"presentation_id": result["presentation_id"]}
fsm.transition(PresentationState.PRESENTATION_READY, 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.GENERATION_FAILED, {"error_message": str(e)})
print(f"Error generating presentation for session {session_id}: {e}")
return {
"status": "error",
"state": fsm.state.name,
"error": str(e),
"next_action": fsm.get_next_suggested_action()
}
async def execute_export(self, session_id: str, export_format: str = "pptx") -> Dict[str, Any]:
"""
Execute presentation export workflow step
Args:
session_id (str): Unique identifier for the session.
export_format (str): Format to export the presentation (e.g., "pptx", "pdf").
Returns:
Dict[str, Any]: Result containing status, state, progress, next action, and export
"""
fsm = self.get_session(session_id)
if not fsm:
raise ValueError(f"Session {session_id} not found")
try:
# Transition to EXPORT_REQUESTED state
fsm.transition(PresentationState.EXPORT_REQUESTED, {"export_format": export_format})
fsm.transition(PresentationState.EXPORT_IN_PROGRESS)
result = await export_presentation_and_get_path(
presentation_id=fsm.context.presentation_id,
title=fsm.context.title,
export_as=export_format
)
print("RResult of export:", result)
#Updating the Context and transitioning to EXPORT_COMPLETE
context_updates = {"export_path": result["path"]}
fsm.transition(PresentationState.EXPORT_COMPLETE, context_updates)
return {
"status": "success",
"state": fsm.state.name,
"progress": fsm.get_workflow_progress(),
"next_action": "Download your presentation or start a new one",
"result": result
}
except Exception as e:
fsm.transition(PresentationState.EXPORT_FAILED, {"error_message": str(e)})
print(f"Error exporting presentation for session {session_id}: {e}")
return {
"status": "error",
"state": fsm.state.name,
"error": str(e),
"next_action": fsm.get_next_suggested_action()
}
async def get_available_layouts(self) -> List[Any]:
"""
Get available presentation layouts
"""
return await list_layouts()
def get_workflow_status(self, session_id: str) -> Dict[str, Any]:
"""Get current workflow status"""
fsm = self.get_session(session_id)
if not fsm:
return {"error": "Session not found"}
return {
"session_id": session_id,
"current_state": fsm.state.name,
"progress": fsm.get_workflow_progress(),
"next_action": fsm.get_next_suggested_action(),
"available_transitions": [s.name for s in fsm.get_available_transitions()],
"is_error_state": fsm.is_error_state(),
"context": asdict(fsm.context),
"state_history": [s.name for s in fsm.get_state_history()]
}
def get_all_sessions(self) -> Dict[str, Dict[str, Any]]:
"""
Get status of all active sessions
"""
return {
session_id: self.get_workflow_status(session_id)
for session_id in self._active_sessions.keys()
}

View file

@ -0,0 +1,38 @@
"""MCP Tools package for presentation generation."""
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
from app_mcp.tools.help_me import register_help_me
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',
'register_help_me',
'register_continue_workflow',
'register_tools',
]
def register_tools(mcp, orchestrator):
"""Register all MCP tools in a fancy way."""
tools = [
register_choose_layout,
register_export_presentation,
register_regenerate_outline,
register_get_status,
register_show_layouts,
register_start_presentation,
register_help_me,
register_continue_workflow
]
for tool in tools:
tool(mcp, orchestrator)

View file

@ -0,0 +1,46 @@
from typing import Dict, Any
def register_choose_layout(mcp, orchestrator):
"""Register all workflow-related tools for chat-based interaction"""
@mcp.tool("choose_layout")
async def choose_layout(session_id: str, layout_name: str) -> Dict[str, Any]:
"""
🎨 Select a visual style and theme for your presentation.
Choose from available professional layouts that determine:
- Color scheme and visual design
- Slide structure and layout patterns
- Font choices and styling
- Overall presentation aesthetic
Use 'show_layouts' first to see all available options. Only show the layout name and short description.
Args:
session_id: Your presentation session ID
layout_name: Name of the layout you want to use
"""
try:
result = await orchestrator.execute_layout_selection(session_id, layout_name)
if result["status"] == "success":
return {
"status": "success",
"session_id": session_id,
"message": f"Perfect! I've selected the '{layout_name}' layout for your presentation.",
"suggestion": "Now I'll generate all the slides with content, images, and styling. This might take a minute or two.",
"available_actions": {
"continue": "Start generating the presentation",
"change_layout": "Actually, let me pick a different layout"
}
}
return result
except Exception as e:
return {
"status": "error",
"error": str(e),
"session_id": session_id
}
return choose_layout

View file

@ -0,0 +1,122 @@
from typing import Dict, Any
def register_continue_workflow(mcp, orchestrator):
"""Register all workflow-related tools for chat-based interaction"""
@mcp.tool("continue_workflow")
async def continue_workflow(
session_id: str,
action: str = "continue"
) -> Dict[str, Any]:
"""
Move to the next step in creating your presentation.
This tool automatically determines what should happen next based on where
you are in the process:
- After starting: Generates your presentation outline
- After outline: Shows available layouts to choose from
- After layout: Creates your complete presentation
Just call this when you're ready to proceed to the next step!
Args:
session_id: Your presentation session ID
action: What to do next (usually just "continue")
"""
try:
# Validate session_id
if not session_id or not isinstance(session_id, str):
return {
"status": "error",
"error": "Valid session_id is required",
"suggestion": "Use the same session_id from start_presentation"
}
session_id = session_id.strip()
fsm = orchestrator.get_session(session_id)
if not fsm:
return {
"status": "error",
"error": "Session not found. Please start a new presentation first.",
"suggestion": "Call start_presentation to begin"
}
current_state = fsm.state.name
if current_state in ["FILES_UPLOADED", "SUMMARY_GENERATED", "INIT"]:
# Generate outline
prompt = fsm.context.metadata.get("original_prompt", "")
n_slides = fsm.context.metadata.get("n_slides", 8)
language = fsm.context.metadata.get("language", "English")
if not prompt:
return {
"status": "error",
"error": "No prompt found in session. Please start over.",
"suggestion": "Call start_presentation with a valid prompt"
}
result = await orchestrator.execute_generate_outline(
session_id, prompt, n_slides=n_slides, language=language
)
if result["status"] == "success":
return {
"status": "success",
"session_id": session_id,
"message": "Here's your presentation outline:",
"title": result["result"]["title"],
"outlines": result["result"]["outlines"],
"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"
}
return result
elif current_state == "OUTLINE_GENERATED":
# Auto-approve and move to layouts
await orchestrator.approve_outline(session_id)
layouts = await orchestrator.get_available_layouts()
return {
"status": "success",
"session_id": session_id,
"message": "Great! Now let's choose a visual style for your presentation.",
"available_layouts": layouts,
"suggestion": "Choose a layout that fits your content and audience. Use 'choose_layout' with the layout name.",
"next_step": "Call choose_layout with your preferred layout name"
}
elif current_state == "LAYOUT_SELECTED":
# Generate presentation
result = await orchestrator.execute_presentation_generation(session_id)
if result["status"] == "success":
return {
"status": "success",
"session_id": session_id,
"message": "🎉 Your presentation is ready!",
"title": result["result"]["title"],
"presentation_id": result["result"]["presentation_id"],
"suggestion": "Your presentation has been generated successfully! Use 'export_presentation' to download it.",
"next_step": "Call export_presentation with format 'pptx' or 'pdf'"
}
return result
else:
return {
"status": "info",
"message": f"Currently in {current_state} state.",
"suggestion": "Use get_status to see what actions are available.",
"next_step": "Call get_status for guidance"
}
except Exception as e:
return {
"status": "error",
"error": f"Workflow error: {str(e)}",
"session_id": session_id if 'session_id' in locals() else "unknown",
"suggestion": "Use get_status to check your current progress"
}
return continue_workflow

View file

@ -0,0 +1,56 @@
from typing import Dict, Any
def register_export_presentation(mcp, orchestrator):
"""Register all workflow-related tools for chat-based interaction"""
@mcp.tool("export_presentation")
async def export_presentation(
session_id: str,
format: str = "pptx",
export_path: str = None
) -> Dict[str, Any]:
"""
📁 Download your finished presentation in your preferred format.
Export your completed presentation as:
- "pptx" - PowerPoint format (editable, best for sharing and presenting)
- "pdf" - PDF format (read-only, best for viewing and printing)
The exported file will be ready for download immediately.
Args:
session_id: Your presentation session ID
format: Export format - either "pptx" or "pdf"
"""
try:
if format.lower() not in ["pdf", "pptx"]:
return {
"status": "error",
"error": "Please choose either 'pdf' or 'pptx' format",
"session_id": session_id
}
result = await orchestrator.execute_export(session_id, format.lower())
print("Export result:", result)
if result["status"] == "success":
return {
"status": "success",
"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"
}
}
return result
except Exception as e:
return {
"status": "error",
"error": str(e),
"session_id": session_id
}
return export_presentation

View file

@ -0,0 +1,83 @@
from typing import Dict, Any, Optional, List
def register_get_status(mcp, orchestrator):
"""Register all workflow-related tools for chat-based interaction"""
@mcp.tool("get_status")
def get_status(session_id: str) -> Dict[str, Any]:
"""
📊 Check your presentation creation progress.
See exactly where you are in the process:
- What step you're currently on
- How much progress you've made
- What you can do next
- Any issues that need attention
Perfect for checking in if you're unsure what to do next!
Args:
session_id: Your presentation session ID
"""
try:
if not session_id or not isinstance(session_id, str):
return {
"status": "error",
"error": "Valid session_id is required"
}
session_id = session_id.strip()
status = orchestrator.get_workflow_status(session_id)
if "error" in status:
return {
"status": "error",
"error": "Session not found. Start a new presentation with 'start_presentation'.",
"available_sessions": list(orchestrator._active_sessions.keys())
}
state = status["current_state"]
# 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_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.",
"PRESENTATION_READY": "Presentation generated! Use 'export_presentation' to download.",
"EXPORT_COMPLETE": "All done! Presentation exported successfully."
}
next_actions = {
"INIT": "start_presentation",
"SUMMARY_GENERATED": "continue_workflow",
"OUTLINE_GENERATED": "continue_workflow (or regenerate_outline)",
"OUTLINE_APPROVED": "choose_layout",
"LAYOUT_SELECTED": "continue_workflow",
"PRESENTATION_READY": "export_presentation",
"EXPORT_COMPLETE": "Download file or start_presentation for new one"
}
return {
"status": "success",
"session_id": session_id,
"current_step": state,
"progress": f"{status['progress']:.0f}%",
"message": friendly_messages.get(state, f"Currently in {state} state"),
"next_action": next_actions.get(state, status["next_action"]),
"context": {
"prompt": status["context"].get("metadata", {}).get("original_prompt"),
"n_slides": status["context"].get("metadata", {}).get("n_slides"),
"language": status["context"].get("metadata", {}).get("language")
}
}
except Exception as e:
return {
"status": "error",
"error": f"Status check failed: {str(e)}",
"suggestion": "Try start_presentation to begin a new session"
}
return get_status

View file

@ -0,0 +1,49 @@
from typing import Dict, Any, Optional, List
def register_help_me(mcp, orchestrator):
"""Register all workflow-related tools for chat-based interaction"""
@mcp.tool("help")
def help() -> Dict[str, Any]:
"""
Get help and guidance for creating presentations.
Shows you:
- Step-by-step workflow guide
- Available commands and what they do
- Example usage to get you started
- Tips for best results
Perfect for first-time users or when you need a refresher!
"""
return {
"status": "info",
"message": "🎯 Complete Guide to Creating Presentations",
"workflow": {
"step_1": "🚀 start_presentation - Begin with your topic and optional files",
"step_2": "📋 continue_workflow - Generate and review your outline",
"step_3": "🎨 choose_layout - Pick a visual style that fits your content",
"step_4": "⚡ continue_workflow - Generate your complete presentation",
"step_5": "📁 export_presentation - Download as PowerPoint or PDF"
},
"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": {
"with_files": "start_presentation(session_id='my-session', prompt='Your topic', files=[uploaded_files])",
"text_only": "start_presentation(session_id='my-session', prompt='Create a presentation about sustainable energy')",
"custom": "start_presentation(session_id='my-session', prompt='Your topic', n_slides=10, language='Spanish')"
},
"tips": [
"💡 Be specific in your prompt for better results",
"📎 Upload relevant files to enhance your content",
"🎨 Choose layouts that match your audience and purpose",
"📊 Use get_status anytime to see what's next"
]
}
return help

View file

@ -0,0 +1,63 @@
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

@ -0,0 +1,39 @@
from typing import Dict, Any
def register_show_layouts(mcp, orchestrator):
"""Register all workflow-related tools for chat-based interaction"""
@mcp.tool("show_layouts")
async def show_layouts(session_id: str) -> Dict[str, Any]:
"""
👀 Browse all available presentation themes and layouts.
See the complete list of professional layouts including:
- Business and corporate themes
- Creative and modern designs
- Academic and educational styles
- Technical and data-focused layouts
Each layout comes with its own color scheme, fonts, and slide structures.
Args:
session_id: Your presentation session ID
"""
try:
layouts = await orchestrator.get_available_layouts()
return {
"status": "success",
"session_id": session_id,
"message": "Here are all the available presentation layouts:",
"layouts": layouts,
"suggestion": "Choose one using 'choose_layout' with the layout name."
}
except Exception as e:
return {
"status": "error",
"error": str(e),
"session_id": session_id
}
return show_layouts

View file

@ -0,0 +1,110 @@
from typing import List, Dict, Any, Optional
def register_start_presentation(mcp, orchestrator):
"""Register all workflow-related tools for chat-based interaction"""
@mcp.tool("start_presentation")
async def start_presentation(
session_id: str,
prompt: str,
files: Optional[List] = None,
n_slides: int = 8,
language: str = "English"
) -> Dict[str, Any]:
"""
🚀 Start creating a new presentation with your idea!
This is your entry point to create presentations. You can:
- Start with just a text prompt describing what you want
- Upload files (PDFs, docs, etc.) to base your presentation on
- Specify how many slides you want (default: 8)
- Choose the language for your presentation
Examples:
- "Create a presentation about climate change solutions"
- "Make slides about our Q4 financial results" (with uploaded files)
- "Build a training deck for new employees"
Args:
session_id: Unique identifier for your presentation session
prompt: Describe what your presentation should be about
files: Optional list of files to analyze and include
n_slides: Number of slides to generate (default: 8)
language: Presentation language (default: English)
"""
try:
if not session_id or not isinstance(session_id, str) or len(session_id.strip()) == 0:
return {
"status": "error",
"error": "Session ID is required and must be a non-empty string",
"example": "Use something like: session_id='my_presentation_123'"
}
if not prompt or not isinstance(prompt, str) or len(prompt.strip()) == 0:
return {
"status": "error",
"error": "Prompt is required and must be a non-empty string",
"example": "prompt='Create a presentation about AI in healthcare'"
}
# Clean session_id
session_id = session_id.strip()
# Create session
orchestrator.create_session(session_id)
# Store initial parameters
fsm = orchestrator.get_session(session_id)
if not fsm:
return {
"status": "error",
"error": "Failed to create session",
"session_id": session_id
}
fsm.context.metadata.update({
"original_prompt": prompt.strip(),
"n_slides": max(1, min(50, n_slides)), # Validate slide count
"language": language.strip() if language else "English"
})
# Debug log to verify metadata update
print("DEBUG: Metadata after update:", fsm.context.metadata)
# Handle files if provided
if files and len(files) > 0:
result = await orchestrator.execute_upload_and_summarize(session_id, files)
if result["status"] == "error":
return result
return {
"status": "success",
"session_id": session_id,
"message": "Great! I've uploaded and analyzed your files. Here's a summary:",
"summary": result["result"]["summary"],
"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"
}
else:
# Direct outline generation without files
return {
"status": "success",
"session_id": session_id,
"message": f"Perfect! Let's create a presentation about: '{prompt}'",
"suggestion": "I'll generate an outline with the key topics and structure. Use 'continue_workflow' to proceed.",
"next_step": "Call continue_workflow to generate the outline",
"parameters": {
"n_slides": fsm.context.metadata.get("n_slides", 8), # Ensure n_slides is retrieved correctly
"language": fsm.context.metadata.get("language", "English") # Ensure language is retrieved correctly
}
}
except Exception as e:
return {
"status": "error",
"error": f"Unexpected error: {str(e)}",
"session_id": session_id if 'session_id' in locals() else "unknown",
"suggestion": "Please try again with a valid session_id and prompt"
}
return start_presentation

View file

@ -0,0 +1,52 @@
from typing import Dict, Any
from models.sql.presentation import PresentationModel
from models.sql.slide import SlideModel
from models.presentation_from_template import GetPresentationUsingTemplateRequest
from utils.dict_utils import deep_update
from utils.export_utils import export_presentation
from sqlmodel import select
from fastapi import HTTPException
class EditFromTemplateTools:
def __init__(self):
pass
def register(self, mcp):
@mcp.tool("edit_from_template")
async def edit_from_template(
data: GetPresentationUsingTemplateRequest,
sql_session
) -> Dict[str, Any]:
"""
Create a new presentation from a template and updated slide data, then export.
"""
presentation = await sql_session.get(PresentationModel, data.presentation_id)
if not presentation:
raise HTTPException(status_code=404, detail="Presentation not found")
slides = await sql_session.scalars(
select(SlideModel).where(SlideModel.presentation == data.presentation_id)
)
new_presentation = presentation.get_new_presentation()
new_slides = []
for each_slide in slides:
updated_content = None
new_slide_data = list(filter(lambda x: x.index == each_slide.index, data.data))
if new_slide_data:
updated_content = deep_update(each_slide.content, new_slide_data[0].content)
new_slides.append(
each_slide.get_new_slide(new_presentation.id, updated_content)
)
sql_session.add(new_presentation)
sql_session.add_all(new_slides)
await sql_session.commit()
presentation_and_path = await export_presentation(
new_presentation.id, new_presentation.title, data.export_as
)
return {
**presentation_and_path.model_dump(),
"edit_path": f"/presentation?id={new_presentation.id}",
}

View file

@ -0,0 +1,35 @@
import json
from typing import Dict, Any, Optional
from models.presentation_outline_model import PresentationOutlineModel
from utils.llm_calls.generate_presentation_outlines import generate_ppt_outline
async def generate_outline(
prompt: str,
n_slides: int = 8,
language: str = "English",
summary: Optional[str] = None,
) -> Dict[str, Any]:
"""
Generate presentation outlines given a prompt, number of slides, language, and optional summary.
Returns the parsed outline data.
"""
presentation_content_text = ""
async for chunk in generate_ppt_outline(
prompt,
n_slides,
language,
summary,
):
presentation_content_text += chunk
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,
"outlines": outlines,
"notes": presentation_content.notes
}

View file

@ -0,0 +1,8 @@
from typing import List, Any
from api.v1.ppt.endpoints.layouts import get_layouts
async def list_layouts() -> List[Any]:
"""
Retrieve and return a list of all available presentation layouts.
"""
return await get_layouts()

View file

@ -0,0 +1,25 @@
from typing import Literal, Dict, Any
from utils.export_utils import export_presentation
# Standalone function for workflow orchestrator
async def export_presentation_and_get_path(
presentation_id: str,
title: str,
export_as: Literal["pptx", "pdf"] = "pptx"
) -> Dict[str, Any]:
"""
Export the presentation and return the export path and edit path.
"""
presentation_and_path = await export_presentation(
presentation_id, title, export_as
)
# model_dump() is assumed to return a dict with the export path and related info
data = presentation_and_path.model_dump()
print("Exported presentation data:", data)
# Map export_path to path if needed
return {
**data,
"edit_path": f"/presentation?id={presentation_id}",
"export_path": data["path"],
}

View file

@ -0,0 +1,128 @@
import random
from typing import List, Dict, Any, Optional
from models.presentation_outline_model import SlideOutlineModel
from models.presentation_layout import PresentationLayoutModel
from models.presentation_structure_model import PresentationStructureModel
from models.sql.presentation import PresentationModel
from models.sql.slide import SlideModel
from utils.get_layout_by_name import get_layout_by_name
from utils.llm_calls.generate_presentation_structure import generate_presentation_structure
from utils.llm_calls.generate_slide_content import get_slide_content_from_type_and_outline
from services.image_generation_service import ImageGenerationService
from services.icon_finder_service import IconFinderService
from utils.asset_directory_utils import get_images_directory
from utils.process_slides import process_slide_and_fetch_assets
from models.presentation_outline_model import PresentationOutlineModel
from utils.randomizers import get_random_uuid
import asyncio
from services.database import get_async_session
from sqlalchemy.ext.asyncio import AsyncSession
# Standalone function for workflow orchestrator
async def process_post_outline_workflow(
title: str,
outlines: List[Dict[str, Any]],
notes: Optional[str]=[],
layout: str = "general",
language: str = "English",
prompt: str = "",
n_slides: int = 8,
sql_session: Optional[AsyncSession] = None,
) -> Dict[str, Any]:
"""
Process the workflow after outlines are generated: layout, structure, slides, assets, save, and ask for export.
"""
# 1. Parse Layout
layout_model: PresentationLayoutModel = await get_layout_by_name(layout)
total_slide_layouts = len(layout_model.slides)
# 2. Generate Structure
if layout_model.ordered:
presentation_structure = layout_model.to_presentation_structure()
else:
presentation_structure: PresentationStructureModel = (
await generate_presentation_structure(
presentation_outline=PresentationOutlineModel(
title=title,
slides=outlines,
notes=notes,
),
presentation_layout=layout_model,
)
)
presentation_structure.slides = presentation_structure.slides[:n_slides]
for index in range(n_slides):
random_slide_index = random.randint(0, total_slide_layouts - 1)
if index >= n_slides:
presentation_structure.slides.append(random_slide_index)
continue
if presentation_structure.slides[index] >= total_slide_layouts:
presentation_structure.slides[index] = random_slide_index
# 3. Create PresentationModel
presentation_id = get_random_uuid()
presentation = PresentationModel(
id=presentation_id,
title=title,
n_slides=n_slides,
language=language,
outlines=outlines,
prompt=prompt,
notes=notes,
layout=layout_model.model_dump(),
structure=presentation_structure.model_dump(),
)
image_generation_service = ImageGenerationService(get_images_directory())
icon_finder_service = IconFinderService()
async_asset_generation_tasks = []
# 4. Generate slide content and save slides
slides: List[SlideModel] = []
for i, slide_layout_index in enumerate(presentation_structure.slides):
slide_layout = layout_model.slides[slide_layout_index]
slide_content = await get_slide_content_from_type_and_outline(
slide_layout, SlideOutlineModel(**outlines[i]), language
)
slide = SlideModel(
presentation=presentation_id,
layout_group=layout_model.name,
layout=slide_layout.id,
index=i,
content=slide_content,
)
async_asset_generation_tasks.append(
process_slide_and_fetch_assets(
image_generation_service, icon_finder_service, slide
)
)
slides.append(slide)
generated_assets_lists = await asyncio.gather(*async_asset_generation_tasks)
generated_assets = []
for assets_list in generated_assets_lists:
generated_assets.extend(assets_list)
# 5. Save PresentationModel and Slides
if sql_session is None:
from services.database import get_async_session
async for session in get_async_session():
session.add(presentation)
session.add_all(slides)
session.add_all(generated_assets)
await session.commit()
else:
sql_session.add(presentation)
sql_session.add_all(slides)
sql_session.add_all(generated_assets)
await sql_session.commit()
# 6. Ask user if they want to export and in which format
return {
"presentation_id": presentation_id,
"title": title,
"message": "Presentation is ready. Would you like to export? (pdf or pptx)",
"export_options": ["pdf", "pptx"]
}

View file

@ -0,0 +1,31 @@
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,
}

View file

@ -1,25 +0,0 @@
{
"_name_or_path": "sentence-transformers/all-MiniLM-L6-v2",
"architectures": [
"BertModel"
],
"attention_probs_dropout_prob": 0.1,
"classifier_dropout": null,
"gradient_checkpointing": false,
"hidden_act": "gelu",
"hidden_dropout_prob": 0.1,
"hidden_size": 384,
"initializer_range": 0.02,
"intermediate_size": 1536,
"layer_norm_eps": 1e-12,
"max_position_embeddings": 512,
"model_type": "bert",
"num_attention_heads": 12,
"num_hidden_layers": 6,
"pad_token_id": 0,
"position_embedding_type": "absolute",
"transformers_version": "4.27.4",
"type_vocab_size": 2,
"use_cache": true,
"vocab_size": 30522
}

View file

@ -1,7 +0,0 @@
{
"cls_token": "[CLS]",
"mask_token": "[MASK]",
"pad_token": "[PAD]",
"sep_token": "[SEP]",
"unk_token": "[UNK]"
}

File diff suppressed because it is too large Load diff

View file

@ -1,15 +0,0 @@
{
"cls_token": "[CLS]",
"do_basic_tokenize": true,
"do_lower_case": true,
"mask_token": "[MASK]",
"model_max_length": 512,
"never_split": null,
"pad_token": "[PAD]",
"sep_token": "[SEP]",
"special_tokens_map_file": "/Users/hammad/.cache/huggingface/hub/models--sentence-transformers--all-MiniLM-L6-v2/snapshots/7dbbc90392e2f80f3d3c277d6e90027e55de9125/special_tokens_map.json",
"strip_accents": null,
"tokenize_chinese_chars": true,
"tokenizer_class": "BertTokenizer",
"unk_token": "[UNK]"
}

File diff suppressed because it is too large Load diff

View file

@ -1,419 +0,0 @@
import json
from typing import List, Literal, Optional
from pydantic import BaseModel, Field, HttpUrl, EmailStr
from models.presentation_layout import PresentationLayoutModel, SlideLayoutModel
from models.presentation_outline_model import PresentationOutlineModel
from utils.dict_utils import get_dict_at_path, get_dict_paths_with_key
from utils.schema_utils import remove_fields_from_schema
class ContactInfoModel(BaseModel):
email: Optional[EmailStr] = Field(None, description="Contact email")
phone: Optional[str] = Field(
None, min_length=5, max_length=50, description="Contact phone number"
)
website: Optional[HttpUrl] = Field(None, description="Website URL")
class ImageModel(BaseModel):
__image_url__: str = Field(description="Image URL")
__image_prompt__: str = Field(description="Image prompt")
# First Slide Layout
class FirstSlideModel(BaseModel):
title: str = Field(
min_length=3,
max_length=100,
description="Main title of the presentation",
)
subtitle: Optional[str] = Field(
min_length=10, max_length=200, description="Optional subtitle or tagline"
)
author: Optional[str] = Field(
min_length=2,
max_length=100,
description="Author or presenter name",
)
date: Optional[str] = Field(description="Presentation date")
company: Optional[str] = Field(
min_length=2,
max_length=100,
description="Company or organization name",
)
backgroundImage: Optional[ImageModel] = Field(
description="Background image for the slide"
)
# Bullet Point Slide Layout
class BulletPointSlideModel(BaseModel):
title: str = Field(
min_length=3,
max_length=100,
description="Title of the slide",
)
subtitle: Optional[str] = Field(
min_length=3,
max_length=150,
description="Optional subtitle or description",
)
icon: Optional[str] = Field(description="Icon to display in the slide")
bulletPoints: List[str] = Field(
min_length=2,
max_length=8,
description="List of bullet points (2-8 items)",
)
# Image Slide Layout
class ImageSlideModel(BaseModel):
title: str = Field(
min_length=3,
max_length=100,
description="Title of the slide",
)
subtitle: Optional[str] = Field(
min_length=3,
max_length=150,
description="Optional subtitle or description",
)
image: HttpUrl = Field(
description="Main image URL",
)
imageCaption: Optional[str] = Field(
min_length=5,
max_length=200,
description="Optional image caption or description",
)
content: Optional[str] = Field(
min_length=10,
max_length=600,
description="Optional supporting content text",
)
backgroundImage: Optional[HttpUrl] = Field(
description="URL to background image for the slide"
)
# Statistics Slide Layout
class StatisticItemModel(BaseModel):
value: str = Field(
min_length=1,
max_length=20,
description="Statistical value (e.g., '250%', '$1.2M', '99.9%')",
)
label: str = Field(
min_length=3, max_length=100, description="Description of the statistic"
)
trend: Optional[str] = Field(
description="Trend direction indicator", pattern="^(up|down|neutral)$"
)
context: Optional[str] = Field(
min_length=5,
max_length=200,
description="Additional context or time period",
)
class StatisticsSlideModel(BaseModel):
title: str = Field(
min_length=3,
max_length=100,
description="Title of the slide",
)
subtitle: Optional[str] = Field(
min_length=3,
max_length=150,
description="Optional subtitle or description",
)
statistics: List[StatisticItemModel] = Field(
min_length=2,
max_length=6,
description="List of statistics (2-6 items)",
)
backgroundImage: Optional[HttpUrl] = Field(
description="URL to background image for the slide"
)
# Quote Slide Layout
class QuoteSlideModel(BaseModel):
title: str = Field(
min_length=3,
max_length=100,
description="Title of the slide",
)
subtitle: Optional[str] = Field(
min_length=3,
max_length=150,
description="Optional subtitle or description",
)
quote: str = Field(
min_length=10,
max_length=500,
description="The main quote or testimonial",
)
author: str = Field(
min_length=2,
max_length=100,
description="Quote author name",
)
authorTitle: Optional[str] = Field(
min_length=2, max_length=100, description="Author job title or position"
)
company: Optional[str] = Field(
min_length=2, max_length=100, description="Author company or organization"
)
authorImage: Optional[HttpUrl] = Field(description="URL to author photo")
backgroundImage: Optional[HttpUrl] = Field(
description="URL to background image for the slide"
)
# Timeline Slide Layout
class TimelineItemModel(BaseModel):
date: str = Field(min_length=2, max_length=50, description="Date or time period")
title: str = Field(
min_length=3, max_length=100, description="Event or milestone title"
)
description: str = Field(
min_length=10, max_length=300, description="Event description"
)
status: str = Field(
description="Timeline item status",
pattern="^(completed|current|upcoming)$",
)
class TimelineSlideModel(BaseModel):
title: str = Field(
min_length=3,
max_length=100,
description="Title of the slide",
)
subtitle: Optional[str] = Field(
min_length=3,
max_length=150,
description="Optional subtitle or description",
)
timelineItems: List[TimelineItemModel] = Field(
min_length=2,
max_length=6,
description="Timeline events (2-6 items)",
)
backgroundImage: Optional[HttpUrl] = Field(
description="URL to background image for the slide"
)
# Team Slide Layout
class TeamMemberModel(BaseModel):
name: str = Field(min_length=2, max_length=100, description="Team member name")
title: str = Field(min_length=2, max_length=100, description="Job title or role")
image: Optional[HttpUrl] = Field(description="URL to team member photo")
bio: Optional[str] = Field(
min_length=10,
max_length=300,
description="Brief biography or description",
)
email: Optional[EmailStr] = Field(description="Contact email")
linkedin: Optional[HttpUrl] = Field(description="LinkedIn profile URL")
class TeamSlideModel(BaseModel):
title: str = Field(
min_length=3,
max_length=100,
description="Title of the slide",
)
subtitle: Optional[str] = Field(
min_length=3,
max_length=150,
description="Optional subtitle or team description",
)
teamMembers: List[TeamMemberModel] = Field(
min_length=1,
max_length=6,
description="Team members (1-6 people)",
)
backgroundImage: Optional[HttpUrl] = Field(
description="URL to background image for the slide"
)
# Process Slide Layout
class ProcessStepModel(BaseModel):
step: int = Field(ge=1, le=10, description="Step number")
title: str = Field(min_length=3, max_length=100, description="Step title")
description: str = Field(
min_length=10, max_length=200, description="Step description"
)
class ProcessSlideModel(BaseModel):
title: str = Field(
min_length=3,
max_length=100,
description="Title of the slide",
)
subtitle: Optional[str] = Field(
min_length=3,
max_length=150,
description="Optional subtitle or description",
)
processSteps: List[ProcessStepModel] = Field(
min_length=2,
max_length=6,
description="Process steps (2-6 items)",
)
backgroundImage: Optional[HttpUrl] = Field(
description="URL to background image for the slide"
)
# Two Column Slide Layout
class ColumnContentModel(BaseModel):
title: str = Field(min_length=3, max_length=100, description="Column title")
content: str = Field(min_length=10, max_length=800, description="Column content")
class TwoColumnSlideModel(BaseModel):
title: str = Field(
min_length=3,
max_length=100,
description="Title of the slide",
)
subtitle: Optional[str] = Field(
min_length=3,
max_length=150,
description="Optional subtitle or description",
)
leftColumn: ColumnContentModel = Field(
description="Left column content",
)
rightColumn: ColumnContentModel = Field(
description="Right column content",
)
backgroundImage: Optional[HttpUrl] = Field(
description="URL to background image for the slide"
)
# Conclusion Slide Layout
class ConclusionSlideModel(BaseModel):
title: str = Field(
min_length=3,
max_length=100,
description="Title of the slide",
)
subtitle: Optional[str] = Field(
min_length=3,
max_length=150,
description="Optional subtitle or description",
)
keyTakeaways: List[str] = Field(
min_length=2,
max_length=6,
description="Key takeaways or summary points (2-6 items)",
)
callToAction: Optional[str] = Field(
min_length=5,
max_length=150,
description="Optional call to action or next steps",
)
contactInfo: Optional[ContactInfoModel] = Field(
description="Optional contact information"
)
backgroundImage: Optional[HttpUrl] = Field(
description="URL to background image for the slide"
)
# Content Slide Layout
class ContentSlideModel(BaseModel):
title: str = Field(
min_length=3,
max_length=100,
description="Title of the slide",
)
subtitle: Optional[str] = Field(
min_length=3,
max_length=150,
description="Optional subtitle or description",
)
content: str = Field(
min_length=10,
max_length=1000,
description="Main content text",
)
backgroundImage: Optional[HttpUrl] = Field(
description="URL to background image for the slide"
)
# Create the presentation layout with all slide types
presentation_layout = PresentationLayoutModel(
name="Complete Presentation Layout",
slides=[
SlideLayoutModel(
id="first-slide",
name="First Slide",
json_schema=FirstSlideModel.model_json_schema(),
),
# SlideLayoutModel(
# id="bullet-point-slide",
# name="Bullet Point Slide",
# json_schema=BulletPointSlideModel.model_json_schema(),
# ),
# SlideLayoutModel(
# id="image-slide",
# name="Image Slide",
# json_schema=ImageSlideModel.model_json_schema(),
# ),
# SlideLayoutModel(
# id="statistics-slide",
# name="Statistics Slide",
# json_schema=StatisticsSlideModel.model_json_schema(),
# ),
# SlideLayoutModel(
# id="quote-slide",
# name="Quote Slide",
# json_schema=QuoteSlideModel.model_json_schema(),
# ),
# SlideLayoutModel(
# id="timeline-slide",
# name="Timeline Slide",
# json_schema=TimelineSlideModel.model_json_schema(),
# ),
# SlideLayoutModel(
# id="team-slide",
# name="Team Slide",
# json_schema=TeamSlideModel.model_json_schema(),
# ),
# SlideLayoutModel(
# id="process-slide",
# name="Process Slide",
# json_schema=ProcessSlideModel.model_json_schema(),
# ),
# SlideLayoutModel(
# id="two-column-slide",
# name="Two Column Slide",
# json_schema=TwoColumnSlideModel.model_json_schema(),
# ),
# SlideLayoutModel(
# id="conclusion-slide",
# name="Conclusion Slide",
# json_schema=ConclusionSlideModel.model_json_schema(),
# ),
# SlideLayoutModel(
# id="content-slide",
# name="Content Slide",
# json_schema=ContentSlideModel.model_json_schema(),
# ),
],
)
print(json.dumps(StatisticsSlideModel.model_json_schema()))

View file

@ -0,0 +1,24 @@
import sys
import os
import argparse
import asyncio
from app_mcp.server import create_mcp_server, uvicorn_config
async def main():
parser = argparse.ArgumentParser(description="Run the FastAPI server")
parser.add_argument(
"--port", type=int, default=8001, help="Port number to run the server on"
)
args = parser.parse_args()
mcp = create_mcp_server()
await mcp.run_async(
transport="http",
host="0.0.0.0",
port=args.port,
uvicorn_config=uvicorn_config
)
if __name__ == "__main__":
asyncio.run(main())

View file

@ -0,0 +1,11 @@
from pydantic import BaseModel
class DocumentChunk(BaseModel):
heading: str
content: str
heading_index: int
score: float
def to_slide_outline(self) -> str:
return f"{self.heading}\n{self.content}"

View file

@ -1,31 +1,13 @@
from typing import List, Optional
from pydantic import BaseModel, Field
class SlideOutlineModel(BaseModel):
title: str = Field(
description="Title of the slide in about 3 to 5 words",
)
body: str = Field(
description="Content of the slide in markdown format",
)
from typing import List
from pydantic import BaseModel
class PresentationOutlineModel(BaseModel):
title: str = Field(
description="Title of the presentation in about 3 to 8 words",
)
slides: List[SlideOutlineModel] = Field(description="List of slides")
slides: List[str]
def to_string(self):
message = f"# Presentation Title: {self.title} \n\n"
message = ""
for i, slide in enumerate(self.slides):
message += f"## Slide {i+1}:\n"
message += f" - Title: {slide.title} \n"
message += f" - Body: {slide.body} \n"
# if self.notes:
# message += f"# Notes: \n"
# for note in self.notes:
# message += f" - {note} \n"
message += f" - Content: {slide} \n"
return message

View file

@ -4,7 +4,7 @@ from datetime import datetime
from pydantic import BaseModel
from models.presentation_layout import PresentationLayoutModel
from models.presentation_outline_model import SlideOutlineModel
from models.presentation_outline_model import PresentationOutlineModel
from models.presentation_structure_model import PresentationStructureModel
from models.sql.presentation import PresentationModel
from models.sql.slide import SlideModel
@ -16,9 +16,7 @@ class PresentationWithSlides(BaseModel):
n_slides: int
language: str
title: Optional[str] = None
notes: Optional[List[str]]
outlines: Optional[List[SlideOutlineModel]]
summary: Optional[str]
outlines: Optional[PresentationOutlineModel]
created_at: datetime
updated_at: datetime
layout: Optional[PresentationLayoutModel]

View file

@ -2,7 +2,7 @@ from datetime import datetime
from typing import Optional
from sqlalchemy import JSON, Column, DateTime
from sqlmodel import SQLModel, Field
from sqlmodel import Field, SQLModel
from utils.randomizers import get_random_uuid

View file

@ -1,4 +1,4 @@
from sqlmodel import SQLModel, Field, Column, JSON
from sqlmodel import Field, Column, JSON, SQLModel
from utils.randomizers import get_random_uuid

View file

@ -0,0 +1,8 @@
from datetime import datetime
from sqlmodel import Field, Column, JSON, SQLModel, DateTime
class OllamaPullStatus(SQLModel, table=True):
id: str = Field(primary_key=True)
last_updated: datetime = Field(sa_column=Column(DateTime, default=datetime.now))
status: dict = Field(sa_column=Column(JSON))

View file

@ -1,13 +1,10 @@
from datetime import datetime
from typing import List, Optional
from sqlalchemy import JSON, Column, DateTime
from sqlmodel import SQLModel, Field
from sqlmodel import Field, SQLModel
from models.presentation_layout import PresentationLayoutModel
from models.presentation_outline_model import (
PresentationOutlineModel,
SlideOutlineModel,
)
from models.presentation_outline_model import PresentationOutlineModel
from models.presentation_structure_model import PresentationStructureModel
from utils.randomizers import get_random_uuid
@ -18,9 +15,8 @@ class PresentationModel(SQLModel, table=True):
n_slides: int
language: str
title: Optional[str] = None
notes: Optional[List[str]] = Field(sa_column=Column(JSON), default=None)
outlines: Optional[List[dict]] = Field(sa_column=Column(JSON), default=None)
summary: Optional[str] = None
file_paths: Optional[List[str]] = Field(sa_column=Column(JSON), default=None)
outlines: Optional[dict] = Field(sa_column=Column(JSON), default=None)
created_at: datetime = Field(sa_column=Column(DateTime, default=datetime.now))
updated_at: datetime = Field(sa_column=Column(DateTime, default=datetime.now))
layout: Optional[dict] = Field(sa_column=Column(JSON), default=None)
@ -33,9 +29,8 @@ class PresentationModel(SQLModel, table=True):
n_slides=self.n_slides,
language=self.language,
title=self.title,
notes=self.notes,
file_paths=self.file_paths,
outlines=self.outlines,
summary=self.summary,
layout=self.layout,
structure=self.structure,
)
@ -43,11 +38,7 @@ class PresentationModel(SQLModel, table=True):
def get_presentation_outline(self):
if not self.outlines:
return None
return PresentationOutlineModel(
title=self.title,
slides=[SlideOutlineModel(**each) for each in self.outlines],
# notes=self.notes,
)
return PresentationOutlineModel(**self.outlines)
def get_layout(self):
return PresentationLayoutModel(**self.layout)

View file

@ -1,5 +1,5 @@
from typing import Optional
from sqlmodel import SQLModel, Field, Column, JSON
from sqlmodel import Field, Column, JSON, SQLModel
from utils.randomizers import get_random_uuid

View file

@ -32,4 +32,6 @@ class UserConfig(BaseModel):
PIXABAY_API_KEY: Optional[str] = None
# Reasoning
TOOL_CALLS: Optional[bool] = None
DISABLE_THINKING: Optional[bool] = None
EXTENDED_REASONING: Optional[bool] = None

View file

@ -0,0 +1,28 @@
[project]
name = "presenton-backend"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.11,<3.12"
dependencies = [
"aiohttp>=3.12.15",
"aiomysql>=0.2.0",
"aiosqlite>=0.21.0",
"anthropic>=0.60.0",
"asyncpg>=0.30.0",
"chromadb>=1.0.15",
"docling>=2.43.0",
"fastapi[standard]>=0.116.1",
"fastmcp>=2.11.0",
"google-genai>=1.28.0",
"nltk>=3.9.1",
"openai>=1.98.0",
"pathvalidate>=3.3.1",
"pdfplumber>=0.11.7",
"python-pptx>=1.0.2",
"redis>=6.2.0",
"sqlmodel>=0.0.24",
]
[[tool.uv.index]]
url = "https://download.pytorch.org/whl/cpu"

View file

@ -1,149 +0,0 @@
aiohappyeyeballs==2.6.1
aiohttp==3.12.14
aiomysql==0.2.0
aiosignal==1.4.0
aiosqlite==0.21.0
annotated-types==0.7.0
anthropic==0.60.0
anyio==4.9.0
argcomplete==3.6.2
async-timeout==5.0.1
asyncpg==0.30.0
attrs==25.3.0
backoff==2.2.1
bcrypt==4.3.0
black==25.1.0
build==1.2.2.post1
cachetools==5.5.2
certifi==2025.7.14
cffi==1.17.1
charset-normalizer==3.4.2
chromadb==1.0.15
click==8.2.1
coloredlogs==15.0.1
cryptography==45.0.5
distro==1.9.0
dnspython==2.7.0
durationpy==0.10
email_validator==2.2.0
fastapi==0.116.1
fastapi-cli==0.0.8
fastapi-cloud-cli==0.1.4
fastembed==0.7.1
filelock==3.18.0
flatbuffers==25.2.10
frozenlist==1.7.0
fsspec==2025.7.0
genson==1.3.0
google-auth==2.40.3
google-genai==1.25.0
googleapis-common-protos==1.70.0
greenlet==3.2.3
grpcio==1.74.0
h11==0.16.0
h2==4.2.0
hf-xet==1.1.5
hpack==4.1.0
httpcore==1.0.9
httptools==0.6.4
httpx==0.28.1
huggingface-hub==0.34.1
humanfriendly==10.0
hyperframe==6.1.0
idna==3.10
importlib_metadata==8.7.0
importlib_resources==6.5.2
inflect==7.5.0
iniconfig==2.1.0
isort==6.0.1
Jinja2==3.1.6
jiter==0.10.0
jsonschema==4.25.0
jsonschema-specifications==2025.4.1
kubernetes==33.1.0
loguru==0.7.3
lxml==6.0.0
markdown-it-py==3.0.0
MarkupSafe==3.0.2
mdurl==0.1.2
mmh3==5.1.0
more-itertools==10.7.0
mpmath==1.3.0
multidict==6.6.3
mypy_extensions==1.1.0
numpy==2.3.2
oauthlib==3.3.1
onnxruntime==1.22.1
openai==1.95.1
opentelemetry-api==1.35.0
opentelemetry-exporter-otlp-proto-common==1.35.0
opentelemetry-exporter-otlp-proto-grpc==1.35.0
opentelemetry-proto==1.35.0
opentelemetry-sdk==1.35.0
opentelemetry-semantic-conventions==0.56b0
orjson==3.11.1
overrides==7.7.0
packaging==25.0
pathspec==0.12.1
pathvalidate==3.3.1
pdfminer.six==20250506
pdfplumber==0.11.7
pillow==11.3.0
platformdirs==4.3.8
pluggy==1.6.0
portalocker==3.2.0
posthog==5.4.0
propcache==0.3.2
protobuf==6.31.1
py_rust_stemmers==0.1.5
pyasn1==0.6.1
pyasn1_modules==0.4.2
pybase64==1.4.2
pycparser==2.22
pydantic==2.11.7
pydantic_core==2.33.2
Pygments==2.19.2
pypdfium2==4.30.1
PyPika==0.48.9
pyproject_hooks==1.2.0
pytest==8.4.1
python-dateutil==2.9.0.post0
python-docx==1.2.0
python-dotenv==1.1.1
python-multipart==0.0.20
python-pptx==1.0.2
PyYAML==6.0.2
redis==6.2.0
referencing==0.36.2
requests==2.32.4
requests-oauthlib==2.0.0
rich==14.0.0
rich-toolkit==0.14.8
rignore==0.6.2
rpds-py==0.26.0
rsa==4.9.1
sentry-sdk==2.32.0
shellingham==1.5.4
six==1.17.0
sniffio==1.3.1
SQLAlchemy==2.0.41
sqlmodel==0.0.24
starlette==0.47.1
sympy==1.14.0
tenacity==8.5.0
tokenizers==0.21.2
tomli==2.2.1
tqdm==4.67.1
typeguard==4.4.4
typer==0.16.0
typing-inspection==0.4.1
typing_extensions==4.14.1
urllib3==2.5.0
uvicorn==0.35.0
uvloop==0.21.0
watchfiles==1.1.0
websocket-client==1.8.0
websockets==15.0.1
xlsxwriter==3.2.5
yarl==1.20.1
zipp==3.23.0

View file

@ -1,6 +1,4 @@
from services.redis_service import RedisService
from services.temp_file_service import TempFileService
TEMP_FILE_SERVICE = TempFileService()
REDIS_SERVICE = RedisService()

View file

@ -37,6 +37,25 @@ async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
yield session
# Container DB (Lives inside the container)
container_db_url = "sqlite+aiosqlite:////app/container.db"
container_db_engine: AsyncEngine = create_async_engine(
container_db_url, connect_args={"check_same_thread": False}
)
container_db_async_session_maker = async_sessionmaker(
container_db_engine, expire_on_commit=False
)
async def get_container_db_async_session() -> AsyncGenerator[AsyncSession, None]:
async with container_db_async_session_maker() as session:
yield session
# Create Database and Tables
async def create_db_and_tables():
async with sql_engine.begin() as conn:
await conn.run_sync(SQLModel.metadata.create_all)
async with container_db_engine.begin() as conn:
await conn.run_sync(SQLModel.metadata.create_all)

View file

@ -0,0 +1,27 @@
from docling.document_converter import DocumentConverter, PdfFormatOption
from docling.datamodel.pipeline_options import PdfPipelineOptions
from docling.datamodel.base_models import InputFormat
class DoclingService:
def __init__(self):
self.pipeline_options = PdfPipelineOptions()
self.pipeline_options.do_ocr = False
self.converter = DocumentConverter(
format_options={
InputFormat.DOCX: PdfFormatOption(
pipeline_options=self.pipeline_options,
),
InputFormat.PPTX: PdfFormatOption(
pipeline_options=self.pipeline_options,
),
InputFormat.PDF: PdfFormatOption(
pipeline_options=self.pipeline_options,
),
}
)
def parse_to_markdown(self, file_path: str) -> str:
result = self.converter.convert(file_path)
return result.document.export_to_markdown()

View file

@ -1,9 +1,8 @@
import mimetypes
from fastapi import HTTPException
import os, pdfplumber, asyncio
import os, asyncio
from typing import List, Tuple
from docx import Document
from pptx import Presentation
import pdfplumber
from constants.documents import (
PDF_MIME_TYPES,
@ -11,6 +10,7 @@ from constants.documents import (
TEXT_MIME_TYPES,
WORD_TYPES,
)
from services.docling_service import DoclingService
class DocumentsLoader:
@ -18,6 +18,8 @@ class DocumentsLoader:
def __init__(self, file_paths: List[str]):
self._file_paths = file_paths
self.docling_service = DoclingService()
self._documents: List[str] = []
self._images: List[List[str]] = []
@ -76,9 +78,7 @@ class DocumentsLoader:
document: str = ""
if load_text:
with pdfplumber.open(file_path) as pdf:
for page in pdf.pages:
document += await asyncio.to_thread(page.extract_text)
document = self.docling_service.parse_to_markdown(file_path)
if load_images:
image_paths = await self.get_page_images_from_pdf_async(file_path, temp_dir)
@ -90,23 +90,10 @@ class DocumentsLoader:
return await asyncio.to_thread(file.read)
def load_msword(self, file_path: str) -> str:
document = Document(file_path)
text = "\n".join([paragraph.text for paragraph in document.paragraphs])
return text
return self.docling_service.parse_to_markdown(file_path)
def load_powerpoint(self, file_path: str) -> str:
presentation = Presentation(file_path)
extracted_text = ""
for index, slide in enumerate(presentation.slides):
extracted_text += f"# Slide {index + 1}\n"
for shape in slide.shapes:
if shape.has_text_frame:
for paragraph in shape.text_frame.paragraphs:
extracted_text += f"{paragraph.text}\n"
extracted_text += "\n"
extracted_text += "\n\n"
return extracted_text
return self.docling_service.parse_to_markdown(file_path)
def get_page_images_from_pdf(self, file_path: str, temp_dir: str):
with pdfplumber.open(file_path) as pdf:

View file

@ -15,11 +15,14 @@ from utils.get_env import (
get_anthropic_api_key_env,
get_custom_llm_api_key_env,
get_custom_llm_url_env,
get_disable_thinking_env,
get_google_api_key_env,
get_ollama_url_env,
get_openai_api_key_env,
get_tool_calls_env,
)
from utils.llm_provider import get_llm_provider
from utils.parsers import parse_bool_or_none
from utils.schema_utils import ensure_strict_json_schema
@ -28,13 +31,17 @@ class LLMClient:
self.llm_provider = get_llm_provider()
self._client = self._get_client()
# Supports json_schema
def supports_json_schema(self, model: str) -> bool:
if model.startswith("deepseek"):
# ? Use tool calls
def use_tool_calls(self) -> bool:
if self.llm_provider != LLMProvider.CUSTOM:
return False
if model.startswith("claude"):
return parse_bool_or_none(get_tool_calls_env()) or False
# ? Disable thinking
def disable_thinking(self) -> bool:
if self.llm_provider != LLMProvider.CUSTOM:
return False
return True
return parse_bool_or_none(get_disable_thinking_env()) or False
# ? Clients
def _get_client(self):
@ -121,6 +128,9 @@ class LLMClient:
model=model,
messages=[message.model_dump() for message in messages],
max_completion_tokens=max_tokens,
extra_body={
"enable_thinking": not self.disable_thinking(),
},
)
return response.choices[0].message.content
@ -212,7 +222,7 @@ class LLMClient:
max_tokens: Optional[int] = None,
):
client: AsyncOpenAI = self._client
supports_json_schema = self.supports_json_schema(model)
use_tool_calls = self.use_tool_calls()
response_schema = response_format
if strict:
response_schema = ensure_strict_json_schema(
@ -220,7 +230,7 @@ class LLMClient:
path=(),
root=response_schema,
)
if supports_json_schema:
if not use_tool_calls:
response = await client.chat.completions.create(
model=model,
messages=[message.model_dump() for message in messages],
@ -235,6 +245,9 @@ class LLMClient:
),
},
max_completion_tokens=max_tokens,
extra_body={
"enable_thinking": not self.disable_thinking(),
},
)
content = response.choices[0].message.content
else:
@ -254,6 +267,9 @@ class LLMClient:
],
tool_choice="required",
max_completion_tokens=max_tokens,
extra_body={
"enable_thinking": not self.disable_thinking(),
},
)
tool_calls = response.choices[0].message.tool_calls
if tool_calls:
@ -396,6 +412,9 @@ class LLMClient:
model=model,
messages=[message.model_dump() for message in messages],
max_completion_tokens=max_tokens,
extra_body={
"enable_thinking": not self.disable_thinking(),
},
) as stream:
async for event in stream:
if event.type == "content.delta":
@ -482,7 +501,7 @@ class LLMClient:
max_tokens: Optional[int] = None,
):
client: AsyncOpenAI = self._client
supports_json_schema = self.supports_json_schema(model)
use_tool_calls = self.use_tool_calls()
response_schema = response_format
if strict:
response_schema = ensure_strict_json_schema(
@ -490,7 +509,7 @@ class LLMClient:
path=(),
root=response_schema,
)
if supports_json_schema:
if not use_tool_calls:
async with client.chat.completions.stream(
model=model,
messages=[message.model_dump() for message in messages],
@ -505,6 +524,9 @@ class LLMClient:
},
}
),
extra_body={
"enable_thinking": not self.disable_thinking(),
},
) as stream:
async for event in stream:
if event.type == "content.delta":
@ -526,6 +548,9 @@ class LLMClient:
}
],
tool_choice="required",
extra_body={
"enable_thinking": not self.disable_thinking(),
},
) as stream:
async for event in stream:
if event.type == "tool_calls.function.arguments.delta":

View file

@ -0,0 +1,199 @@
import asyncio
from typing import List
import nltk
from models.document_chunk import DocumentChunk
try:
nltk.data.find("tokenizers/punkt", paths=["./nltk"])
except LookupError:
nltk.download("punkt", download_dir="./nltk")
class ScoreBasedChunker:
def extract_sentences(self, text: str, min_sentences: int) -> List[str]:
sentences = self.extract_sentences_markdown(text)
if len(sentences) < min_sentences:
sentences = self.extract_sentences_nltk(text)
if len(sentences) < min_sentences:
sentences = self.extract_sentences_by_stop_words(text)
if len(sentences) < min_sentences:
sentences = self.extract_sentences_by_new_line(text)
if len(sentences) < min_sentences:
raise ValueError(
f"Only {len(sentences)} sentences found, requested {min_sentences}"
)
return sentences
def extract_sentences_markdown(self, text: str) -> List[str]:
lines = text.split("\n")
sentences = []
for line in lines:
line = line.strip()
if line:
if line.startswith("#"):
sentences.append(line)
else:
if line.endswith((".", "!", "?")):
sentences.append(line)
else:
sentences.append(line)
return sentences
def extract_sentences_nltk(self, text: str) -> List[str]:
sentences = nltk.sent_tokenize(text)
return sentences
def extract_sentences_by_stop_words(self, text: str) -> List[str]:
sentences = []
current_sentence = ""
for char in text:
current_sentence += char
if char in ".!?":
sentences.append(current_sentence.strip())
current_sentence = ""
if current_sentence.strip():
sentences.append(current_sentence.strip())
return [s for s in sentences if s]
def extract_sentences_by_new_line(self, text: str) -> List[str]:
sentences = text.split("\n")
result = []
for i, sentence in enumerate(sentences):
if i < len(sentences) - 1:
result.append(sentence + "\n")
else:
result.append(sentence)
return result
def score_sentences_for_heading(self, sentences: List[str]) -> List[float]:
sentences_scores = []
last_heading_index = -1
first_heading_found = False
for i, sentence in enumerate(sentences):
score = 0.0
if sentence.strip().startswith("#"):
heading_level = len(sentence) - len(sentence.lstrip("#"))
if heading_level <= 3:
score += 10.0 - (heading_level - 1) * 2.0
else:
score += 4.0 - (heading_level - 4) * 0.5
if not first_heading_found:
score += 5.0
first_heading_found = True
if last_heading_index != -1:
distance = i - last_heading_index
distance_bonus = min(5.0, distance * 0.5)
score += distance_bonus
last_heading_index = i
sentences_scores.append(score)
return sentences_scores
def get_chunks(
self, sentences: List[str], sentences_scores: List[float], top_k: int = 10
) -> List[DocumentChunk]:
if not sentences_scores:
sentences_scores = self.score_sentences_for_heading(sentences)
chunks = []
heading_scores = []
for i, score in enumerate(sentences_scores):
if score > 0:
heading_scores.append((i, score))
if len(heading_scores) == 0:
return chunks
heading_scores.sort(key=lambda x: (-x[1], x[0]))
if len(heading_scores) <= top_k:
selected_headings = [idx for idx, _ in heading_scores]
selected_headings.sort()
else:
score_groups = {}
for idx, score in heading_scores:
rounded_score = round(score)
if rounded_score not in score_groups:
score_groups[rounded_score] = []
score_groups[rounded_score].append(idx)
sorted_groups = sorted(
score_groups.items(), key=lambda x: x[0], reverse=True
)
selected_headings = []
for score, headings in sorted_groups:
headings.sort()
remaining_needed = top_k - len(selected_headings)
if remaining_needed <= 0:
break
if len(headings) <= remaining_needed:
selected_headings.extend(headings)
else:
if remaining_needed == 1:
mid_idx = len(headings) // 2
selected_headings.append(headings[mid_idx])
elif remaining_needed == 2:
selected_headings.append(headings[0])
selected_headings.append(headings[-1])
else:
step = (len(headings) - 1) / (remaining_needed - 1)
for i in range(remaining_needed):
index = int(round(i * step))
if index < len(headings):
selected_headings.append(headings[index])
selected_headings.sort()
for i, heading_idx in enumerate(selected_headings):
heading = sentences[heading_idx]
if i + 1 < len(selected_headings):
next_heading_idx = selected_headings[i + 1]
content_end = next_heading_idx
else:
content_end = len(sentences)
content_sentences = sentences[heading_idx + 1 : content_end]
content = " ".join(content_sentences).strip()
chunk = DocumentChunk(
heading=heading,
content=content,
heading_index=heading_idx,
score=sentences_scores[heading_idx],
)
chunks.append(chunk)
return chunks
async def get_n_chunks(self, text: str, n: int) -> List[DocumentChunk]:
sentences = await asyncio.to_thread(self.extract_sentences, text, n)
sentences_scores = await asyncio.to_thread(
self.score_sentences_for_heading, sentences
)
chunks = await asyncio.to_thread(
self.get_chunks, sentences, sentences_scores, n
)
if len(chunks) < n:
raise ValueError(f"Only {len(chunks)} chunks found, requested {n}")
return chunks

View file

@ -0,0 +1,421 @@
import asyncio
import pytest
from fastmcp import FastMCP, Client
from app_mcp.tools.start_presentation import register_start_presentation
from app_mcp.tools.help_me import register_help_me
from app_mcp.tools.continue_workflow import register_continue_workflow
from app_mcp.tools.regenerate_outline import register_regenerate_outline
from app_mcp.tools.export_presentation import register_export_presentation
from app_mcp.tools.show_layouts import register_show_layouts
from app_mcp.tools.get_status import register_get_status
from app_mcp.tools.choose_layout import register_choose_layout
from app_mcp.services.state_machine.machine import PresentationStateMachine
from app_mcp.services.state_machine.context import StateContext
from unittest.mock import patch, MagicMock
@pytest.fixture
def mcp_server():
with patch("app_mcp.services.workflow_orchestrator.WorkflowOrchestrator") as MockOrchestrator:
mock_orchestrator = MockOrchestrator.return_value
mcp = FastMCP("TestServer")
#Mocking the StateContext Too
mock_context = StateContext()
mock_context.metadata = {}
mock_fsm = MagicMock(spec=PresentationStateMachine)
mock_fsm.context = mock_context
mock_orchestrator.get_session.return_value = mock_fsm
# Register all tool functions with the mocked orchestrator
register_start_presentation(mcp=mcp, orchestrator=mock_orchestrator)
register_help_me(mcp=mcp, orchestrator=mock_orchestrator)
register_continue_workflow(mcp=mcp, orchestrator=mock_orchestrator)
register_regenerate_outline(mcp=mcp, orchestrator=mock_orchestrator)
register_export_presentation(mcp=mcp, orchestrator=mock_orchestrator)
register_show_layouts(mcp=mcp, orchestrator=mock_orchestrator)
register_get_status(mcp=mcp, orchestrator=mock_orchestrator)
register_choose_layout(mcp=mcp, orchestrator=mock_orchestrator)
return mcp
# Grouped test classes for each tool
class TestStartPresentation:
"""
Tests for the start_presentation tool
"""
def test_success(self, mcp_server):
"""
Test successful start_presentation call with all required parameters.
Checks for correct status, session_id, and parameter values in response.
"""
async def run():
async with Client(mcp_server) as client:
params = {
"session_id": "test_session",
"prompt": "Test Presentation",
"files": None,
"n_slides": 5,
"language": "English"
}
result = await client.call_tool("start_presentation", params)
assert result.data["status"] == "success"
assert result.data["session_id"] == "test_session"
assert "message" in result.data
assert "suggestion" in result.data
assert "next_step" in result.data
assert "parameters" in result.data
assert result.data["parameters"]["n_slides"] == 5
assert result.data["parameters"]["language"] == "English"
asyncio.run(run())
def test_missing_session_id(self, mcp_server):
"""
Test start_presentation with missing session_id.
Expects error status and appropriate error message.
"""
async def run():
async with Client(mcp_server) as client:
params = {"prompt": "Test Presentation", "session_id": ""}
result = await client.call_tool("start_presentation", params)
assert result.data["status"] == "error"
assert "Session ID is required" in result.data["error"]
asyncio.run(run())
def test_missing_prompt(self, mcp_server):
"""
Test start_presentation with missing prompt.
Expects error status and appropriate error message.
"""
async def run():
async with Client(mcp_server) as client:
params = {"session_id": "test_session", "prompt": ""}
result = await client.call_tool("start_presentation", params)
assert result.data["status"] == "error"
assert "Prompt is required" in result.data["error"]
asyncio.run(run())
def test_invalid_prompt_type(self, mcp_server):
"""
Test start_presentation with invalid prompt type (None).
Expects error status and appropriate error message.
"""
async def run():
async with Client(mcp_server) as client:
params = {"session_id": "test_session",
"prompt": ""}
result = await client.call_tool("start_presentation", params)
assert result.data["status"] == "error"
assert "Prompt is required" in result.data["error"]
asyncio.run(run())
class TestHelp:
"""
Tests for the help tool
"""
def test_help(self, mcp_server):
"""
Test help tool with no parameters.
Checks for info status and presence of help fields in response.
"""
async def run():
async with Client(mcp_server) as client:
result = await client.call_tool("help", {})
data = result.data
assert data["status"] == "info"
assert "message" in data
assert "workflow" in data
assert "helpful_commands" in data
assert "quick_start" in data
assert "tips" in data
assert "step_1" in data["workflow"]
assert "get_status" in data["helpful_commands"]
assert isinstance(data["tips"], list)
asyncio.run(run())
class TestContinueWorkflow:
"""
Tests for the continue_workflow tool
"""
def test_success(self, mcp_server):
"""
Test continue_workflow with valid session_id.
Checks for correct status and required fields in response.
"""
async def run():
async with Client(mcp_server) as client:
params = {"session_id": "test_session"}
result = await client.call_tool("continue_workflow", params)
data = result.data
assert "status" in data
assert data["status"] in ["success", "error", "info"]
if data["status"] == "success":
assert data["session_id"] == "test_session"
assert "next_step" in data
if data["status"] == "error":
assert "error" in data
asyncio.run(run())
def test_missing_session_id(self, mcp_server):
"""
Test continue_workflow with missing session_id.
Expects error status and appropriate error message.
"""
async def run():
async with Client(mcp_server) as client:
params = {"session_id": ""}
result = await client.call_tool("continue_workflow", params)
data = result.data
assert data["status"] == "error"
assert "Valid session_id is required" in data["error"]
asyncio.run(run())
class TestRegenerateOutline:
"""
Tests for the regenerate_outline tool
"""
def test_success(self, mcp_server):
"""
Test regenerate_outline with valid session_id.
Checks for correct status and required fields in response.
"""
async def run():
async with Client(mcp_server) as client:
params = {"session_id": "test_session"}
result = await client.call_tool("regenerate_outline", params)
data = result.data
assert "status" in data
assert data["status"] in ["success", "error"]
if data["status"] == "success":
assert "message" in data
assert "session_id" in data
if data["status"] == "error":
assert "error" in data
asyncio.run(run())
class TestExportPresentation:
"""
Tests for the export_presentation tool
"""
def test_success_pptx(self, mcp_server):
"""
Test export_presentation with format 'pptx'.
Checks for success status, correct session_id, and pptx path in response.
"""
async def run():
async with Client(mcp_server) as client:
params = {"session_id": "test_session", "format": "pptx"}
result = await client.call_tool("export_presentation", params)
data = result.data
assert "status" in data
if data["status"] == "success":
assert data["session_id"] == "test_session"
assert data["message"].endswith("PPTX!")
assert "path" in data
assert "suggestion" in data
assert "available_actions" in data
if data["status"] == "error":
assert "error" in data
asyncio.run(run())
def test_success_pdf(self, mcp_server):
"""
Test export_presentation with format 'pdf'.
Checks for success status, correct session_id, and pdf path in response.
"""
async def run():
async with Client(mcp_server) as client:
params = {"session_id": "test_session", "format": "pdf"}
result = await client.call_tool("export_presentation", params)
data = result.data
assert "status" in data
if data["status"] == "success":
assert data["session_id"] == "test_session"
assert data["message"].endswith("PDF!")
assert "path" in data
if data["status"] == "error":
assert "error" in data
asyncio.run(run())
def test_invalid_format(self, mcp_server):
"""
Test export_presentation with invalid format (not 'pdf' or 'pptx').
Expects error status and appropriate error message.
"""
async def run():
async with Client(mcp_server) as client:
params = {"session_id": "test_session", "format": "docx"}
result = await client.call_tool("export_presentation", params)
data = result.data
assert data["status"] == "error"
assert "Please choose either 'pdf' or 'pptx' format" in data["error"]
asyncio.run(run())
def test_missing_session_id(self, mcp_server):
"""
Test export_presentation with missing session_id.
Expects error status and session_id error in response.
"""
async def run():
async with Client(mcp_server) as client:
params = {"session_id": "", "format": "pptx"}
result = await client.call_tool("export_presentation", params)
data = result.data
assert data["status"] == "error"
assert "session_id" in data
asyncio.run(run())
class TestShowLayouts:
"""
Tests for the show_layouts tool
"""
def test_success(self, mcp_server):
"""
Test show_layouts with valid session_id.
Checks for success status, layouts list, and suggestion in response.
"""
async def run():
async with Client(mcp_server) as client:
params = {"session_id": "test_session"}
result = await client.call_tool("show_layouts", params)
data = result.data
assert "status" in data
if data["status"] == "success":
assert data["session_id"] == "test_session"
assert "layouts" in data
assert isinstance(
data["layouts"], list) or data["layouts"] is not None
assert "message" in data
assert "suggestion" in data
if data["status"] == "error":
assert "error" in data
asyncio.run(run())
def test_missing_session_id(self, mcp_server):
"""
Test show_layouts with missing session_id.
Expects error status and session_id error in response.
"""
async def run():
async with Client(mcp_server) as client:
params = {"session_id": ""}
result = await client.call_tool("show_layouts", params)
data = result.data
assert data["status"] == "error"
assert "session_id" in data
asyncio.run(run())
class TestGetStatus:
"""
Tests for the get_status tool
"""
def test_success(self, mcp_server):
"""
Test get_status with valid session_id.
Checks for success status, progress, and context in response.
"""
async def run():
async with Client(mcp_server) as client:
params = {"session_id": "test_session"}
result = await client.call_tool("get_status", params)
data = result.data
assert "status" in data
if data["status"] == "success":
assert data["session_id"] == "test_session"
assert "current_step" in data
assert "progress" in data
assert "message" in data
assert "next_action" in data
assert "context" in data
if data["status"] == "error":
assert "error" in data
asyncio.run(run())
def test_missing_session_id(self, mcp_server):
"""
Test get_status with missing session_id.
Expects error status and appropriate error message.
"""
async def run():
async with Client(mcp_server) as client:
params = {"session_id": ""}
result = await client.call_tool("get_status", params)
data = result.data
assert data["status"] == "error"
assert "Valid session_id is required" in data["error"]
asyncio.run(run())
class TestChooseLayout:
"""
Tests for the choose_layout tool
"""
def test_success(self, mcp_server):
"""
Test choose_layout with valid session_id and layout_name.
Checks for success status, available actions, and suggestion in response.
"""
async def run():
async with Client(mcp_server) as client:
params = {"session_id": "test_session",
"layout_name": "default"}
result = await client.call_tool("choose_layout", params)
data = result.data
assert "status" in data
if data["status"] == "success":
assert data["session_id"] == "test_session"
assert "message" in data
assert "suggestion" in data
assert "available_actions" in data
if data["status"] == "error":
assert "error" in data
asyncio.run(run())
def test_missing_session_id(self, mcp_server):
"""
Test choose_layout with missing session_id.
Expects error status and session_id error in response.
"""
async def run():
async with Client(mcp_server) as client:
params = {"session_id": "", "layout_name": "default"}
result = await client.call_tool("choose_layout", params)
data = result.data
assert data["status"] == "error"
assert "session_id" in data
asyncio.run(run())
def test_missing_layout_name(self, mcp_server):
"""
Test choose_layout with missing layout_name.
Checks for error status if layout_name is required.
"""
async def run():
async with Client(mcp_server) as client:
params = {"session_id": "test_session", "layout_name": ""}
result = await client.call_tool("choose_layout", params)
data = result.data
assert "status" in data
if data["status"] == "error":
assert "error" in data
asyncio.run(run())

View file

@ -1,29 +1,15 @@
from typing import List, Optional
from typing import List
from pydantic import Field
from models.presentation_outline_model import (
PresentationOutlineModel,
SlideOutlineModel,
)
from models.presentation_outline_model import PresentationOutlineModel
from models.presentation_structure_model import PresentationStructureModel
class SlideOutlineModelWithValidation(SlideOutlineModel):
title: str = Field(
description="Title of the slide in about 3 to 5 words",
min_length=10,
max_length=50,
)
def get_presentation_outline_model_with_n_slides(n_slides: int):
class PresentationOutlineModelWithNSlides(PresentationOutlineModel):
title: str = Field(
description="Title of the presentation in about 3 to 8 words",
min_length=10,
max_length=50,
)
slides: List[SlideOutlineModelWithValidation] = Field(
description="List of slides", min_items=n_slides, max_items=n_slides
slides: List[str] = Field(
description="Markdown content for each slide",
min_items=n_slides,
max_items=n_slides,
)
return PresentationOutlineModelWithNSlides

View file

@ -97,5 +97,13 @@ def get_redis_password_env():
return os.getenv("REDIS_PASSWORD")
def get_tool_calls_env():
return os.getenv("TOOL_CALLS")
def get_disable_thinking_env():
return os.getenv("DISABLE_THINKING")
def get_extended_reasoning_env():
return os.getenv("EXTENDED_REASONING")

View file

@ -1,44 +0,0 @@
import asyncio
from typing import List
from models.llm_message import LLMMessage
from services.llm_client import LLMClient
from utils.llm_provider import get_model
sysmte_prompt = """
Generate a blog-style summary of the provided document in **more than 2000 words**.
Maintain as much information as possible.
### Output Format
- Provide the summary in a **blog format** with an **engaging introduction** and a **clear structure**.
- Ensure the **logical flow** of the document is preserved.
### Notes
- **Retain the main ideas and essential details** from the document.
- **Show line-breaks** clearly.
- If **slides structure is mentioned** in document, structure the summary in the same way.
"""
async def generate_document_summary(documents: List[str]):
client = LLMClient()
model = get_model()
coroutines = []
for document in documents:
truncated_text = document[:200000]
coroutine = client.generate(
model=model,
messages=[
LLMMessage(role="system", content=sysmte_prompt),
LLMMessage(role="user", content=truncated_text),
],
)
coroutines.append(coroutine)
completions: List[str] = await asyncio.gather(*coroutines)
combined = "\n\n\n\n".join(completions)
return combined

View file

@ -7,42 +7,13 @@ from utils.get_dynamic_models import get_presentation_outline_model_with_n_slide
from utils.llm_provider import get_model
system_prompt = """
You are an expert presentation creator. Generate structured presentations based on user requirements and format them according to the specified JSON schema with markdown content.
You are an expert presentation creator. Generate structured presentations based on user requirements and format them according to the specified JSON schema with markdown content.
## Core Requirements
### Input Processing
1. **Extract key information** from the user's prompt:
- Main topic/subject matter
- Required number of slides
- Target language for output
- Specific content requirements or focus areas
- Target audience (if specified)
- Presentation style or tone preferences
## Content Generation Guidelines
### Presentation Title
- Create a **concise, descriptive title** that captures the essence of the topic
- Use **plain text format** (no markdown formatting)
- Make it **engaging and professional**
- Ensure it reflects the main theme and target audience
### Slide Titles
- Generate **clear, specific titles** for each slide
- Use **plain text format** (no markdown, no "Slide 1", "Slide 2" prefixes)
- Make each title **descriptive and informative**
- Ensure titles create a **logical flow** through the presentation
- Keep titles **concise but meaningful**
## Special Considerations
### Slide Count Compliance
- Generate **exactly** the number of slides requested
- Distribute content **evenly** across slides
- Create **balanced information flow**
- Provide content for each slide in markdown format.
- Make sure that flow of the presentation is logical and consistent.
- Place greater emphasis on numerical data.
- If Additional Information is provided, divide it into slides.
- Make sure that content follows language guidelines.
"""

View file

@ -1,29 +1,28 @@
from models.llm_message import LLMMessage
from models.presentation_layout import SlideLayoutModel
from models.presentation_outline_model import SlideOutlineModel
from services.llm_client import LLMClient
from utils.llm_provider import get_model
from utils.schema_utils import remove_fields_from_schema
system_prompt = """
Generate structured slide based on provided title and outline, follow mentioned steps and notes and provide structured output.
Generate structured slide based on provided outline, follow mentioned steps and notes and provide structured output.
# Steps
1. Analyze the outline and title.
2. Generate structured slide based on the outline and title.
1. Analyze the outline.
2. Generate structured slide based on the outline.
# Notes
- Slide body should not use words like "This slide", "This presentation".
- Rephrase the slide body to make it flow naturally.
- Provide prompt to generate image on "__image_prompt__" property.
- Provide query to search icon on "__icon_query__" property.
- Do not use markdown formatting in slide body.
- Only use markdown to highlight important points.
- Make sure to follow language guidelines.
**Strictly follow the max and min character limit for every property in the slide.**
"""
def get_user_prompt(title: str, outline: str, language: str):
def get_user_prompt(outline: str, language: str):
return f"""
## Icon Query And Image Prompt Language
English
@ -31,15 +30,12 @@ def get_user_prompt(title: str, outline: str, language: str):
## Slide Content Language
{language}
## Slide Title
{title}
## Slide Outline
{outline}
"""
def get_messages(title: str, outline: str, language: str):
def get_messages(outline: str, language: str):
return [
LLMMessage(
@ -48,13 +44,13 @@ def get_messages(title: str, outline: str, language: str):
),
LLMMessage(
role="user",
content=get_user_prompt(title, outline, language),
content=get_user_prompt(outline, language),
),
]
async def get_slide_content_from_type_and_outline(
slide_layout: SlideLayoutModel, outline: SlideOutlineModel, language: str
slide_layout: SlideLayoutModel, outline: str, language: str
):
client = LLMClient()
model = get_model()
@ -66,8 +62,7 @@ async def get_slide_content_from_type_and_outline(
response = await client.generate_structured(
model=model,
messages=get_messages(
outline.title,
outline.body,
outline,
language,
),
response_format=response_schema,

View file

@ -0,0 +1,4 @@
def parse_bool_or_none(value: str | None) -> bool | None:
if value is None:
return None
return value.lower() == "true"

View file

@ -69,5 +69,13 @@ def set_pixabay_api_key_env(value):
os.environ["PIXABAY_API_KEY"] = value
def set_tool_calls_env(value):
os.environ["TOOL_CALLS"] = value
def set_disable_thinking_env(value):
os.environ["DISABLE_THINKING"] = value
def set_extended_reasoning_env(value):
os.environ["EXTENDED_REASONING"] = value

View file

@ -8,6 +8,7 @@ from utils.get_env import (
get_custom_llm_api_key_env,
get_custom_llm_url_env,
get_custom_model_env,
get_disable_thinking_env,
get_google_api_key_env,
get_google_model_env,
get_llm_provider_env,
@ -16,17 +17,20 @@ from utils.get_env import (
get_openai_api_key_env,
get_openai_model_env,
get_pexels_api_key_env,
get_tool_calls_env,
get_user_config_path_env,
get_image_provider_env,
get_pixabay_api_key_env,
get_extended_reasoning_env,
)
from utils.parsers import parse_bool_or_none
from utils.set_env import (
set_anthropic_api_key_env,
set_anthropic_model_env,
set_custom_llm_api_key_env,
set_custom_llm_url_env,
set_custom_model_env,
set_disable_thinking_env,
set_extended_reasoning_env,
set_google_api_key_env,
set_google_model_env,
@ -38,6 +42,7 @@ from utils.set_env import (
set_pexels_api_key_env,
set_image_provider_env,
set_pixabay_api_key_env,
set_tool_calls_env,
)
@ -53,12 +58,6 @@ def get_user_config():
print("Error while loading user config")
pass
new_extended_reasoning = (
existing_config.EXTENDED_REASONING or get_extended_reasoning_env()
)
if new_extended_reasoning is not None:
new_extended_reasoning = bool(new_extended_reasoning)
return UserConfig(
LLM=existing_config.LLM or get_llm_provider_env(),
OPENAI_API_KEY=existing_config.OPENAI_API_KEY or get_openai_api_key_env(),
@ -77,7 +76,12 @@ def get_user_config():
IMAGE_PROVIDER=existing_config.IMAGE_PROVIDER or get_image_provider_env(),
PIXABAY_API_KEY=existing_config.PIXABAY_API_KEY or get_pixabay_api_key_env(),
PEXELS_API_KEY=existing_config.PEXELS_API_KEY or get_pexels_api_key_env(),
EXTENDED_REASONING=new_extended_reasoning,
TOOL_CALLS=existing_config.TOOL_CALLS
or parse_bool_or_none(get_tool_calls_env()),
DISABLE_THINKING=existing_config.DISABLE_THINKING
or parse_bool_or_none(get_disable_thinking_env()),
EXTENDED_REASONING=existing_config.EXTENDED_REASONING
or parse_bool_or_none(get_extended_reasoning_env()),
)
@ -113,6 +117,10 @@ def update_env_with_user_config():
set_pixabay_api_key_env(user_config.PIXABAY_API_KEY)
if user_config.PEXELS_API_KEY:
set_pexels_api_key_env(user_config.PEXELS_API_KEY)
if user_config.TOOL_CALLS:
set_tool_calls_env(str(user_config.TOOL_CALLS))
if user_config.DISABLE_THINKING:
set_disable_thinking_env(str(user_config.DISABLE_THINKING))
if user_config.EXTENDED_REASONING:
if user_config.EXTENDED_REASONING:
set_extended_reasoning_env(str(user_config.EXTENDED_REASONING))

3149
servers/fastapi/uv.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,9 @@
import { useEditor, EditorContent } from "@tiptap/react"
import StarterKit from "@tiptap/starter-kit"
import { Markdown } from "tiptap-markdown"
import { useEffect } from "react"
export default function MarkdownEditor({ content, onChange }: { content: string; onChange: (content: string) => void }) {
const editor = useEditor({
extensions: [StarterKit, Markdown],
content: content,

View file

@ -207,7 +207,7 @@ const DocumentsPreviewPage: React.FC = () => {
return (
<div className={`border-r border-gray-200 fixed xl:relative w-full z-50 xl:z-auto
transition-all duration-300 ease-in-out max-w-[200px] md:max-w-[300px] h-[85vh] rounded-md p-5`}>
transition-all duration-300 bg-white ease-in-out max-w-[200px] md:max-w-[300px] h-[85vh] rounded-md p-5`}>
<X
onClick={() => setIsOpen(false)}
className="text-black mb-4 ml-auto mr-0 cursor-pointer hover:text-gray-600"

View file

@ -15,11 +15,10 @@ import {
} from "@dnd-kit/sortable";
import { OutlineItem } from "./OutlineItem";
import { Button } from "@/components/ui/button";
import { SlideOutline } from "@/store/slices/presentationGeneration";
import { FileText } from "lucide-react";
interface OutlineContentProps {
outlines: SlideOutline[] | null;
outlines: string[] | null;
isLoading: boolean;
isStreaming: boolean;
onDragEnd: (event: any) => void;
@ -33,6 +32,7 @@ const OutlineContent: React.FC<OutlineContentProps> = ({
onDragEnd,
onAddSlide
}) => {
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
@ -84,12 +84,12 @@ const OutlineContent: React.FC<OutlineContentProps> = ({
onDragEnd={onDragEnd}
>
<SortableContext
items={outlines?.map((item, index) => ({ id: item.title || `slide-${index}` })) || []}
items={outlines?.map((item, index) => ({ id: `slide-${index}` })) || []}
strategy={verticalListSortingStrategy}
>
{outlines?.map((item, index) => (
<OutlineItem
key={item.title || `slide-${index}`}
key={`slide-${index}`}
index={index + 1}
slideOutline={item}
isStreaming={isStreaming}

View file

@ -3,14 +3,14 @@ import { CSS } from "@dnd-kit/utilities"
import { Trash2 } from "lucide-react"
import { RootState } from "@/store/store"
import { useDispatch, useSelector } from "react-redux"
import { deleteSlideOutline, setOutlines, SlideOutline } from "@/store/slices/presentationGeneration"
import { deleteSlideOutline, setOutlines } from "@/store/slices/presentationGeneration"
import ToolTip from "@/components/ToolTip"
import MarkdownEditor from "../../components/MarkdownEditor"
import { useEffect } from "react"
interface OutlineItemProps {
slideOutline: SlideOutline,
slideOutline: string,
index: number
isStreaming: boolean
}
@ -26,7 +26,7 @@ export function OutlineItem({
const dispatch = useDispatch()
useEffect(() => {
if (isStreaming && slideOutline.body) {
if (isStreaming && slideOutline) {
const outlineItem = document.getElementById(`outline-item-${index}`);
if (outlineItem) {
outlineItem.scrollIntoView({
@ -38,7 +38,7 @@ export function OutlineItem({
}
}, [outlines.length]);
const handleSlideChange = (newOutline: SlideOutline) => {
const handleSlideChange = (newOutline: string) => {
if (isStreaming) return;
const newData = outlines?.map((each, idx) => {
if (idx === index - 1) {
@ -60,7 +60,7 @@ export function OutlineItem({
transform,
transition,
isDragging,
} = useSortable({ id: slideOutline.title || index })
} = useSortable({ id: index })
const style = {
transform: CSS.Transform.toString(transform),
@ -96,24 +96,16 @@ export function OutlineItem({
{/* Main Title Input - Add onFocus handler */}
<div id={`outline-item-${index}`} className="flex flex-col basis-full gap-2">
<input
type="text"
defaultValue={slideOutline.title || ''}
onBlur={(e) => handleSlideChange({ ...slideOutline, title: e.target.value })}
className="text-lg mt-4 sm:text-xl flex-1 font-semibold bg-transparent outline-none"
placeholder="Title goes here"
/>
{/* Editable Markdown Content */}
{isStreaming ? <textarea
defaultValue={slideOutline.body || ''}
onBlur={(e) => handleSlideChange({ ...slideOutline, body: e.target.value })}
defaultValue={slideOutline || ''}
onBlur={(e) => handleSlideChange(e.target.value)}
className="text-sm flex-1 font-normal bg-transparent outline-none overflow-y-hidden"
placeholder="Content goes here"
/> : <MarkdownEditor
key={index}
content={slideOutline.body || ''}
onChange={(content) => handleSlideChange({ ...slideOutline, body: content })}
content={slideOutline || ''}
onChange={(content) => handleSlideChange(content)}
/>}
</div>

View file

@ -32,7 +32,6 @@ const OutlinePage: React.FC = () => {
selectedLayoutGroup,
setActiveTab
);
if (!presentation_id) {
return <EmptyStateView />;
}

View file

@ -1,7 +1,7 @@
import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { toast } from "sonner";
import { setOutlines, SlideOutline } from "@/store/slices/presentationGeneration";
import { setOutlines } from "@/store/slices/presentationGeneration";
import { jsonrepair } from "jsonrepair";
import { StreamState } from "../types/index";
import { RootState } from "@/store/store";
@ -49,7 +49,7 @@ export const useOutlineStreaming = (presentationId: string | null) => {
case "complete":
try {
const outlinesData: SlideOutline[] = data.presentation.outlines;
const outlinesData: string[] = data.presentation.outlines.slides;
dispatch(setOutlines(outlinesData));
setStreamState({ isStreaming: false, isLoading: false });
eventSource.close();

View file

@ -14,13 +14,14 @@ import {
usePresentationStreaming,
usePresentationData,
usePresentationNavigation,
useAutoSave
useAutoSave,
} from "../hooks";
import { PresentationPageProps } from "../types";
import LoadingState from "./LoadingState";
const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id }) => {
const PresentationPage: React.FC<PresentationPageProps> = ({
presentation_id,
}) => {
// State management
const [loading, setLoading] = useState(true);
const [selectedSlide, setSelectedSlide] = useState(0);
@ -28,7 +29,6 @@ const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id })
const [error, setError] = useState(false);
const [isMobilePanelOpen, setIsMobilePanelOpen] = useState(false);
const { presentationData, isStreaming } = useSelector(
(state: RootState) => state.presentationGeneration
);
@ -37,7 +37,6 @@ const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id })
const { isSaving } = useAutoSave({
debounceMs: 2000,
enabled: !!presentationData && !isStreaming,
});
// Custom hooks
@ -54,7 +53,12 @@ const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id })
toggleFullscreen,
handlePresentExit,
handleSlideChange,
} = usePresentationNavigation(presentation_id, selectedSlide, setSelectedSlide, setIsFullscreen);
} = usePresentationNavigation(
presentation_id,
selectedSlide,
setSelectedSlide,
setIsFullscreen
);
// Initialize streaming
usePresentationStreaming(
@ -65,13 +69,10 @@ const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id })
fetchUserSlides
);
const onSlideChange = (newSlide: number) => {
handleSlideChange(newSlide, presentationData);
};
// Presentation Mode View
if (isPresentMode) {
return (
@ -94,15 +95,11 @@ const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id })
role="alert"
>
<AlertCircle className="w-16 h-16 mb-4 text-red-500" />
<h2 className="text-xl font-semibold mb-2">
Something went wrong
</h2>
<h2 className="text-xl font-semibold mb-2">Something went wrong</h2>
<p className="text-center mb-4">
We couldn't load your presentation. Please try again.
</p>
<Button onClick={() => window.location.reload()}>
Refresh Page
</Button>
<Button onClick={() => window.location.reload()}>Refresh Page</Button>
</div>
</div>
);
@ -110,12 +107,8 @@ const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id })
return (
<div className="h-screen flex overflow-hidden flex-col">
<div className="fixed right-6 top-[5.2rem] z-50">
{isSaving && (
<Loader2 className="w-6 h-6 animate-spin text-blue-500" />
)}
{isSaving && <Loader2 className="w-6 h-6 animate-spin text-blue-500" />}
</div>
<Header presentation_id={presentation_id} currentSlide={selectedSlide} />
@ -123,7 +116,7 @@ const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id })
<div
style={{
background: '#c8c7c9',
background: "#c8c7c9",
}}
className="flex flex-1 relative pt-6"
>
@ -136,11 +129,14 @@ const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id })
/>
<div className="flex-1 h-[calc(100vh-100px)] overflow-y-auto">
<div id="presentation-slides-wrapper" className="mx-auto flex flex-col items-center overflow-hidden justify-center p-2 sm:p-6 pt-0">
<div
id="presentation-slides-wrapper"
className="mx-auto flex flex-col items-center overflow-hidden justify-center p-2 sm:p-6 pt-0"
>
{!presentationData ||
loading ||
!presentationData?.slides ||
presentationData?.slides.length === 0 ? (
loading ||
!presentationData?.slides ||
presentationData?.slides.length === 0 ? (
<div className="relative w-full h-[calc(100vh-120px)] mx-auto">
<div className="">
{Array.from({ length: 2 }).map((_, index) => (
@ -163,7 +159,6 @@ const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id })
slide={slide}
index={index}
presentationId={presentation_id}
/>
))}
</>

View file

@ -1,4 +1,4 @@
import { useCallback, useEffect } from 'react';
import { useCallback, useEffect } from "react";
import { useDispatch } from "react-redux";
import { toast } from "sonner";
import { setPresentationData } from "@/store/slices/presentationGeneration";
@ -26,11 +26,7 @@ export const usePresentationData = (
}
}, [presentationId, dispatch, setLoading, setError]);
useEffect(() => {
fetchUserSlides();
}, [fetchUserSlides]);
return {
fetchUserSlides,
};
};
};

View file

@ -1,6 +1,10 @@
import { useEffect, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import { clearPresentationData, setPresentationData, setStreaming } from "@/store/slices/presentationGeneration";
import {
clearPresentationData,
setPresentationData,
setStreaming,
} from "@/store/slices/presentationGeneration";
import { jsonrepair } from "jsonrepair";
import { RootState } from "@/store/store";
@ -11,8 +15,6 @@ export const usePresentationStreaming = (
setError: (error: boolean) => void,
fetchUserSlides: () => void
) => {
const { presentationData } = useSelector((state: RootState) => state.presentationGeneration);
const dispatch = useDispatch();
const previousSlidesLength = useRef(0);
@ -64,7 +66,7 @@ export const usePresentationStreaming = (
dispatch(setStreaming(false));
setLoading(false);
eventSource.close();
// Remove stream parameter from URL
const newUrl = new URL(window.location.href);
newUrl.searchParams.delete("stream");
@ -81,7 +83,7 @@ export const usePresentationStreaming = (
setLoading(false);
dispatch(setStreaming(false));
eventSource.close();
// Remove stream parameter from URL
const newUrl = new URL(window.location.href);
newUrl.searchParams.delete("stream");
@ -102,9 +104,7 @@ export const usePresentationStreaming = (
if (stream) {
initializeStream();
} else {
if(!presentationData || presentationData.slides.length === 0){
fetchUserSlides();
}
fetchUserSlides();
}
return () => {
@ -113,4 +113,4 @@ export const usePresentationStreaming = (
}
};
}, [presentationId, stream, dispatch, setLoading, setError, fetchUserSlides]);
};
};

View file

@ -8,11 +8,11 @@ import { handleSaveLLMConfig } from "@/utils/storeHelpers";
import {
checkIfSelectedOllamaModelIsPulled,
pullOllamaModel,
LLMConfig
} from "@/utils/providerUtils";
import { useRouter } from "next/navigation";
import LLMProviderSelection from "@/components/LLMSelection";
import Header from "../dashboard/components/Header";
import { LLMConfig } from "@/types/llm_config";
// Button state interface
interface ButtonState {

View file

@ -6,6 +6,7 @@ import { hasValidLLMConfig } from '@/utils/storeHelpers';
import { usePathname, useRouter } from 'next/navigation';
import { useDispatch } from 'react-redux';
import { checkIfSelectedOllamaModelIsPulled } from '@/utils/providerUtils';
import { LLMConfig } from '@/types/llm_config';
export function ConfigurationInitializer({ children }: { children: React.ReactNode }) {
const dispatch = useDispatch();

View file

@ -26,8 +26,8 @@ export async function GET(request: Request) {
waitUntil: "networkidle0",
timeout: 80000,
});
await page.waitForSelector("[data-layouts]", { timeout: 10000 });
await page.waitForSelector("[data-layouts]", { timeout: 30000 });
// Extract both data-layouts and data-group-settings attributes
const { dataLayouts, dataGroupSettings } = await page.$eval(

View file

@ -1,5 +1,6 @@
import { NextResponse } from "next/server";
import fs from "fs";
import { LLMConfig } from "@/types/llm_config";
const userConfigPath = process.env.USER_CONFIG_PATH!;
const canChangeKeys = process.env.CAN_CHANGE_KEYS !== "false";
@ -50,14 +51,16 @@ export async function POST(request: Request) {
userConfig.PIXABAY_API_KEY || existingConfig.PIXABAY_API_KEY,
IMAGE_PROVIDER: userConfig.IMAGE_PROVIDER || existingConfig.IMAGE_PROVIDER,
PEXELS_API_KEY: userConfig.PEXELS_API_KEY || existingConfig.PEXELS_API_KEY,
USE_CUSTOM_URL:
userConfig.USE_CUSTOM_URL === undefined
? existingConfig.USE_CUSTOM_URL
: userConfig.USE_CUSTOM_URL,
TOOL_CALLS: userConfig.TOOL_CALLS === undefined ? existingConfig.TOOL_CALLS : userConfig.TOOL_CALLS,
DISABLE_THINKING: userConfig.DISABLE_THINKING === undefined ? existingConfig.DISABLE_THINKING : userConfig.DISABLE_THINKING,
EXTENDED_REASONING:
userConfig.EXTENDED_REASONING === undefined
? existingConfig.EXTENDED_REASONING
: userConfig.EXTENDED_REASONING,
USE_CUSTOM_URL:
userConfig.USE_CUSTOM_URL === undefined
? existingConfig.USE_CUSTOM_URL
: userConfig.USE_CUSTOM_URL,
};
fs.writeFileSync(userConfigPath, JSON.stringify(mergedConfig));
return NextResponse.json(mergedConfig);

View file

@ -41,6 +41,7 @@ body {
--chart-5: 27 87% 67%;
--radius: 0.5rem;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
@ -70,13 +71,14 @@ body {
}
@layer base {
body {
@apply bg-background text-foreground;
}
}
strong{
@apply font-black ;
strong {
@apply font-black;
}
@ -97,13 +99,17 @@ input[type="number"] {
-moz-appearance: textfield;
}
thead, tbody tr {
display: table;
width: 100%;
table-layout: fixed;/* even columns width , fix width of table too*/
thead,
tbody tr {
display: table;
width: 100%;
table-layout: fixed;
/* even columns width , fix width of table too*/
}
thead {
width: calc( 100% - 1em )/* scrollbar is average 1em/16px width, remove it from thead width */
width: calc(100% - 1em)
/* scrollbar is average 1em/16px width, remove it from thead width */
}
/* Add this to your global CSS or a specific CSS module */
@ -111,37 +117,54 @@ thead {
from {
width: 0;
}
to {
width: 100%;
}
}
.typing-effect {
overflow: hidden; /* Ensures the text is hidden until revealed */
white-space: nowrap; /* Prevents text from wrapping */
display: inline-block; /* Ensures the width is respected */
animation: typing 2s steps(10, end); /* Adjust duration and steps for effect */
animation-fill-mode: forwards; /* Retain the final state of the animation */
animation-delay: 1s; /* Optional: delay before starting the animation */
overflow: hidden;
/* Ensures the text is hidden until revealed */
white-space: nowrap;
/* Prevents text from wrapping */
display: inline-block;
/* Ensures the width is respected */
animation: typing 2s steps(10, end);
/* Adjust duration and steps for effect */
animation-fill-mode: forwards;
/* Retain the final state of the animation */
animation-delay: 1s;
/* Optional: delay before starting the animation */
}
.typing-effect-complete {
border-right: none; /* Remove the cursor after animation */
border-right: none;
/* Remove the cursor after animation */
}
.blinking-cursor {
animation: blink 1s step-end infinite;
animation: blink 1s step-end infinite;
}
@keyframes blink {
from, to { opacity: 1; }
50% { opacity: 0; }
from,
to {
opacity: 1;
}
50% {
opacity: 0;
}
}
.hide-scrollbar::-webkit-scrollbar {
@apply hidden;
}
.hide-scrollbar {
-ms-overflow-style: none;
-ms-overflow-style: none;
scrollbar-width: none;
}
@ -180,36 +203,39 @@ thead {
/* word animation */
@keyframes slideUp {
0% {
transform: translateY(0);
}
100% {
transform: translateY(-50%);
}
0% {
transform: translateY(0);
}
100% {
transform: translateY(-50%);
}
}
@keyframes slideDown {
0% {
transform: translateY(-50%);
}
100% {
transform: translateY(0);
}
0% {
transform: translateY(-50%);
}
100% {
transform: translateY(0);
}
}
.animate-slideUp {
animation: slideUp 20s linear infinite;
animation: slideUp 20s linear infinite;
}
.animate-slideDown {
animation: slideDown 20s linear infinite;
animation: slideDown 20s linear infinite;
}
/* Add hover pause */
.animate-slideUp:hover,
.animate-slideDown:hover {
animation-play-state: paused;
animation-play-state: paused;
}
/* box animation */
.research-mode-bg {
@ -237,18 +263,20 @@ thead {
height: 0;
opacity: 0.8;
}
100% {
width: 200%;
height: 200%;
opacity: 1;
}
}
/* Markdown Styles */
.markdown-content {
@apply prose prose-slate max-w-none;
}
.markdown-content h1 {
/* .markdown-content h1 {
@apply text-xl font-bold mb-4 text-gray-900;
}
@ -323,7 +351,7 @@ thead {
.markdown-content td {
@apply border border-gray-300 px-4 py-2;
}
} */
/* Override Tailwind Typography prose heading sizes for markdown editor */
.prose h1 {
@ -383,7 +411,7 @@ thead {
.mdxeditor-button[data-active=true] {
@apply bg-gray-100;
}
}
/* tippy-box */
.tippy-box {
@ -396,8 +424,4 @@ thead {
float: left;
height: 0;
pointer-events: none;
}
}

View file

@ -13,18 +13,23 @@ import {
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
import { cn } from "@/lib/utils";
import { toast } from "sonner";
import { Switch } from "./ui/switch";
interface CustomConfigProps {
customLlmUrl: string;
customLlmApiKey: string;
customModel: string;
onInputChange: (value: string, field: string) => void;
toolCalls: boolean;
disableThinking: boolean;
onInputChange: (value: string | boolean, field: string) => void;
}
export default function CustomConfig({
customLlmUrl,
customLlmApiKey,
customModel,
toolCalls,
disableThinking,
onInputChange,
}: CustomConfigProps) {
const [customModels, setCustomModels] = useState<string[]>([]);
@ -225,6 +230,39 @@ export default function CustomConfig({
</div>
</div>
)}
</div>
{/* Tool Calls Toggle */}
<div>
<div className="flex items-center justify-between mb-4 bg-green-50 p-2 rounded-sm">
<label className="text-sm font-medium text-gray-700">
Use Tool Calls
</label>
<Switch
checked={toolCalls}
onCheckedChange={(checked) => onInputChange(checked, "tool_calls")}
/>
</div>
<p className="mt-2 text-sm text-gray-500 flex items-center gap-2">
<span className="block w-1 h-1 rounded-full bg-gray-400"></span>
If enabled, Tool Calls will be used instead of JSON Schema for Structured Output.
</p>
</div>
{/* Disable Thinking Toggle */}
<div>
<div className="flex items-center justify-between mb-4 bg-green-50 p-2 rounded-sm">
<label className="text-sm font-medium text-gray-700">
Disable Thinking
</label>
<Switch
checked={disableThinking}
onCheckedChange={(checked) => onInputChange(checked, "disable_thinking")}
/>
</div>
<p className="mt-2 text-sm text-gray-500 flex items-center gap-2">
<span className="block w-1 h-1 rounded-full bg-gray-400"></span>
If enabled, Thinking will be disabled.
</p>
</div>
</div >
);
}

View file

@ -9,9 +9,9 @@ import { handleSaveLLMConfig } from "@/utils/storeHelpers";
import LLMProviderSelection from "./LLMSelection";
import {
checkIfSelectedOllamaModelIsPulled,
LLMConfig,
pullOllamaModel,
} from "@/utils/providerUtils";
import { LLMConfig } from "@/types/llm_config";
// Button state interface
interface ButtonState {

View file

@ -19,11 +19,11 @@ import AnthropicConfig from "./AnthropicConfig";
import OllamaConfig from "./OllamaConfig";
import CustomConfig from "./CustomConfig";
import {
LLMConfig,
updateLLMConfig,
changeProvider as changeProviderUtil,
} from "@/utils/providerUtils";
import { IMAGE_PROVIDERS, LLM_PROVIDERS } from "@/utils/providerConstants";
import { LLMConfig } from "@/types/llm_config";
// Button state interface
interface ButtonState {
@ -188,6 +188,8 @@ export default function LLMProviderSelection({
customLlmUrl={llmConfig.CUSTOM_LLM_URL || ""}
customLlmApiKey={llmConfig.CUSTOM_LLM_API_KEY || ""}
customModel={llmConfig.CUSTOM_MODEL || ""}
toolCalls={llmConfig.TOOL_CALLS || false}
disableThinking={llmConfig.DISABLE_THINKING || false}
onInputChange={input_field_changed}
/>
</TabsContent>

View file

@ -8,15 +8,15 @@ export const layoutName = 'Classic Dark Pie Chart and Metrics'
export const layoutDescription = 'A modern slide with dark background, metrics on the left, and pie chart visualization on the right.'
const chartDataSchema = z.object({
name: z.string().meta({ description: "Data point name" }),
name: z.string().min(2).max(30).meta({ description: "Data point name" }),
value: z.number().meta({ description: "Data point value" }),
});
const pieChartAndMetricsSchema = z.object({
title: z.string().min(3).max(100).default('Introduction to Nepal\'s Trade').meta({
title: z.string().min(3).max(80).default('Introduction to Nepal\'s Trade').meta({
description: "Main title of the slide",
}),
description: z.string().min(10).max(200).default('Nepal\'s landlocked geography heavily influences its trade, fostering reliance on India and China.').meta({
description: z.string().min(10).max(100).default('Nepal\'s landlocked geography heavily influences its trade, fostering reliance on India and China.').meta({
description: "Description text",
}),
metrics: z.array(z.object({
@ -37,13 +37,7 @@ const pieChartAndMetricsSchema = z.object({
{ name: 'Other GDP', value: 50.6 },
]).meta({
description: "Pie chart data",
}),
showLegend: z.boolean().default(true).meta({
description: "Whether to show chart legend",
}),
showTooltip: z.boolean().default(true).meta({
description: "Whether to show chart tooltip",
}),
})
})
const chartConfig = {
@ -70,7 +64,7 @@ interface PieChartAndMetricsLayoutProps {
}
const PieChartAndMetricsLayout: React.FC<PieChartAndMetricsLayoutProps> = ({ data: slideData }) => {
const { title, description, metrics, chartData, showLegend = true, showTooltip = true } = slideData;
const { title, description, metrics, chartData } = slideData;
const CustomLegend = () => (
<div className="flex justify-center space-x-8 mt-4">
@ -89,7 +83,7 @@ const PieChartAndMetricsLayout: React.FC<PieChartAndMetricsLayoutProps> = ({ dat
const renderPieChart = () => {
return (
<PieChart>
{showTooltip && <ChartTooltip content={<ChartTooltipContent />} />}
<ChartTooltip content={<ChartTooltipContent />} />
<Pie
data={chartData}
fill="#8b5cf6"
@ -149,7 +143,7 @@ const PieChartAndMetricsLayout: React.FC<PieChartAndMetricsLayoutProps> = ({ dat
<ChartContainer config={chartConfig} className="h-[500px] w-[500px]">
{renderPieChart()}
</ChartContainer>
{showLegend && <CustomLegend />}
<CustomLegend />
</div>
</div>
</div>

View file

@ -8,15 +8,15 @@ export const layoutName = 'Classic Dark Bar Graph'
export const layoutDescription = 'A modern slide with dark background, gradient title, bar chart visualization, and footer text.'
const barDataSchema = z.object({
name: z.string().meta({ description: "Product name" }),
name: z.string().min(2).max(30).meta({ description: "Product name" }),
value: z.number().meta({ description: "Export value in millions" }),
});
const barGraphSchema = z.object({
title: z.string().min(3).max(100).default('Export Overview: Key Products').meta({
title: z.string().min(3).max(80).default('Export Overview: Key Products').meta({
description: "Main title of the slide",
}),
description: z.string().min(10).max(150).default('Nepal\'s total exports were $1.3 billion in 2022, a 21% decrease from 2021, but showed a 47.5% YoY increase by Nov 2024.').meta({
description: z.string().min(10).max(120).default('Nepal\'s total exports were $1.3 billion in 2022, a 21% decrease from 2021, but showed a 47.5% YoY increase by Nov 2024.').meta({
description: "Description text",
}),
chartData: z.array(barDataSchema).min(2).max(6).default([
@ -28,12 +28,6 @@ const barGraphSchema = z.object({
]).meta({
description: "Bar chart data",
}),
showLegend: z.boolean().default(true).meta({
description: "Whether to show chart legend",
}),
showTooltip: z.boolean().default(true).meta({
description: "Whether to show chart tooltip",
}),
})
const chartConfig = {
@ -62,7 +56,7 @@ interface BarGraphLayoutProps {
}
const BarGraphLayout: React.FC<BarGraphLayoutProps> = ({ data: slideData }) => {
const { title, description, chartData, showLegend = false, showTooltip = true } = slideData;
const { title, description, chartData } = slideData;
const CustomLegend = () => (
<div className="flex justify-center space-x-8 mt-8">
@ -98,9 +92,9 @@ const BarGraphLayout: React.FC<BarGraphLayoutProps> = ({ data: slideData }) => {
/>
<YAxis
tick={{ fill: '#ffffff', fontSize: 16, fontWeight: 600 }}
tickFormatter={(value) => `$${value.toFixed(0)}.00`}
tickFormatter={(value) => value.toFixed(0)}
/>
{showTooltip && <ChartTooltip content={<ChartTooltipContent />} />}
<ChartTooltip content={<ChartTooltipContent />} />
<Bar
dataKey="value"
fill="#8b5cf6"
@ -141,7 +135,7 @@ const BarGraphLayout: React.FC<BarGraphLayoutProps> = ({ data: slideData }) => {
<ChartContainer config={chartConfig} className="h-[300px] w-full">
{renderBarChart()}
</ChartContainer>
{showLegend && <CustomLegend />}
<CustomLegend />
</div>
</div>
</div>

View file

@ -19,7 +19,7 @@ const comparisonSectionSchema = z.object({
});
const comparisonSchema = z.object({
title: z.string().min(3).max(100).default('Key Commodities in Focus').meta({
title: z.string().min(3).max(80).default('Key Commodities in Focus').meta({
description: "Main title of the slide",
}),
comparisonSections: z.array(comparisonSectionSchema).min(2).max(2).default([

View file

@ -14,10 +14,10 @@ const metricItemSchema = z.object({
});
const metricsSchema = z.object({
title: z.string().min(3).max(100).default('Top Export Destinations').meta({
title: z.string().min(3).max(80).default('Top Export Destinations').meta({
description: "Main title of the slide",
}),
description: z.string().min(10).max(200).default('Nepal exports 760 products to 132 countries, with a strong focus on regional trade.').meta({
description: z.string().min(10).max(120).default('Nepal exports 760 products to 132 countries, with a strong focus on regional trade.').meta({
description: "Description text",
}),
metrics: z.array(metricItemSchema).min(2).max(6).default([

View file

@ -7,12 +7,12 @@ export const layoutName = 'Classic Dark Bullet Point with Description'
export const layoutDescription = 'A modern slide with dark background, image on the left (2/5), and bullet points with descriptions in boxes on the right (3/5).'
const bulletPointSchema = z.object({
title: z.string().min(3).max(80).meta({ description: "Bullet point title" }),
content: z.string().min(10).max(150).meta({ description: "Bullet point content (max 150 characters)" }),
title: z.string().min(3).max(60).meta({ description: "Bullet point title" }),
content: z.string().min(10).max(120).meta({ description: "Bullet point content (max 150 characters)" }),
});
const bulletPointWithDescriptionSchema = z.object({
title: z.string().min(3).max(100).default('Trade Policies and Challenges').meta({
title: z.string().min(3).max(80).default('Trade Policies and Challenges').meta({
description: "Main title of the slide",
}),
bulletPoints: z.array(bulletPointSchema).min(2).max(3).default([

View file

@ -3,10 +3,7 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
export interface SlideOutline {
title?: string;
body?: string;
}
export interface PresentationData {
@ -26,7 +23,7 @@ interface PresentationGenerationState {
presentation_id: string | null;
isLoading: boolean;
isStreaming: boolean | null;
outlines: SlideOutline[];
outlines: string[];
error: string | null;
presentationData: PresentationData | null;
isSlidesRendered: boolean;
@ -63,7 +60,7 @@ const presentationGenerationSlice = createSlice({
state.presentation_id = action.payload;
state.error = null;
},
// Slides rendered
// Slides rendereimport { useEffect } from "react"d
setSlidesRendered: (state, action: PayloadAction<boolean>) => {
state.isSlidesRendered = action.payload;
},
@ -80,7 +77,7 @@ const presentationGenerationSlice = createSlice({
state.outlines = [];
},
// Set outlines
setOutlines: (state, action: PayloadAction<SlideOutline[]>) => {
setOutlines: (state, action: PayloadAction<string[]>) => {
state.outlines = action.payload;
},
// Set presentation data

Some files were not shown because too many files have changed in this diff Show more