Merged with main
This commit is contained in:
commit
40dde6ea44
106 changed files with 6037 additions and 62370 deletions
|
|
@ -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
5
.gitignore
vendored
|
|
@ -12,4 +12,7 @@ tmp
|
|||
debug
|
||||
.fastembed_cache
|
||||
my-doc.txt
|
||||
generated_models
|
||||
generated_models
|
||||
nltk
|
||||
chroma
|
||||
container.db
|
||||
11
Dockerfile
11
Dockerfile
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
1
servers/fastapi/.python-version
Normal file
1
servers/fastapi/.python-version
Normal file
|
|
@ -0,0 +1 @@
|
|||
3.11
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
27
servers/fastapi/api/v1/ppt/endpoints/layouts.py
Normal file
27
servers/fastapi/api/v1/ppt/endpoints/layouts.py
Normal 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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
1
servers/fastapi/app_mcp/__init__.py
Normal file
1
servers/fastapi/app_mcp/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
# This file marks the mcp directory as a Python package.
|
||||
13
servers/fastapi/app_mcp/server.py
Normal file
13
servers/fastapi/app_mcp/server.py
Normal 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,
|
||||
}
|
||||
0
servers/fastapi/app_mcp/services/__init__.py
Normal file
0
servers/fastapi/app_mcp/services/__init__.py
Normal file
143
servers/fastapi/app_mcp/services/state_machine/constants.py
Normal file
143
servers/fastapi/app_mcp/services/state_machine/constants.py
Normal 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
|
||||
}
|
||||
20
servers/fastapi/app_mcp/services/state_machine/context.py
Normal file
20
servers/fastapi/app_mcp/services/state_machine/context.py
Normal 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 = {}
|
||||
101
servers/fastapi/app_mcp/services/state_machine/machine.py
Normal file
101
servers/fastapi/app_mcp/services/state_machine/machine.py
Normal 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)})")
|
||||
40
servers/fastapi/app_mcp/services/state_machine/states.py
Normal file
40
servers/fastapi/app_mcp/services/state_machine/states.py
Normal 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()
|
||||
354
servers/fastapi/app_mcp/services/workflow_orchestrator.py
Normal file
354
servers/fastapi/app_mcp/services/workflow_orchestrator.py
Normal 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()
|
||||
}
|
||||
38
servers/fastapi/app_mcp/tools/__init__.py
Normal file
38
servers/fastapi/app_mcp/tools/__init__.py
Normal 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)
|
||||
46
servers/fastapi/app_mcp/tools/choose_layout.py
Normal file
46
servers/fastapi/app_mcp/tools/choose_layout.py
Normal 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
|
||||
122
servers/fastapi/app_mcp/tools/continue_workflow.py
Normal file
122
servers/fastapi/app_mcp/tools/continue_workflow.py
Normal 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
|
||||
56
servers/fastapi/app_mcp/tools/export_presentation.py
Normal file
56
servers/fastapi/app_mcp/tools/export_presentation.py
Normal 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
|
||||
83
servers/fastapi/app_mcp/tools/get_status.py
Normal file
83
servers/fastapi/app_mcp/tools/get_status.py
Normal 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
|
||||
49
servers/fastapi/app_mcp/tools/help_me.py
Normal file
49
servers/fastapi/app_mcp/tools/help_me.py
Normal 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
|
||||
63
servers/fastapi/app_mcp/tools/regenerate_outline.py
Normal file
63
servers/fastapi/app_mcp/tools/regenerate_outline.py
Normal 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
|
||||
39
servers/fastapi/app_mcp/tools/show_layouts.py
Normal file
39
servers/fastapi/app_mcp/tools/show_layouts.py
Normal 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
|
||||
110
servers/fastapi/app_mcp/tools/start_presentation.py
Normal file
110
servers/fastapi/app_mcp/tools/start_presentation.py
Normal 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
|
||||
52
servers/fastapi/app_mcp/wrapper/edit_from_template.py
Normal file
52
servers/fastapi/app_mcp/wrapper/edit_from_template.py
Normal 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}",
|
||||
}
|
||||
35
servers/fastapi/app_mcp/wrapper/generate_outline.py
Normal file
35
servers/fastapi/app_mcp/wrapper/generate_outline.py
Normal 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
|
||||
}
|
||||
8
servers/fastapi/app_mcp/wrapper/list_layout.py
Normal file
8
servers/fastapi/app_mcp/wrapper/list_layout.py
Normal 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()
|
||||
25
servers/fastapi/app_mcp/wrapper/presentation_export.py
Normal file
25
servers/fastapi/app_mcp/wrapper/presentation_export.py
Normal 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"],
|
||||
}
|
||||
128
servers/fastapi/app_mcp/wrapper/presentation_generation.py
Normal file
128
servers/fastapi/app_mcp/wrapper/presentation_generation.py
Normal 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"]
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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
|
||||
}
|
||||
Binary file not shown.
|
|
@ -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
|
|
@ -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
|
|
@ -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()))
|
||||
24
servers/fastapi/mcp_server.py
Normal file
24
servers/fastapi/mcp_server.py
Normal 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())
|
||||
11
servers/fastapi/models/document_chunk.py
Normal file
11
servers/fastapi/models/document_chunk.py
Normal 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}"
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
8
servers/fastapi/models/sql/ollama_pull_status.py
Normal file
8
servers/fastapi/models/sql/ollama_pull_status.py
Normal 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))
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
28
servers/fastapi/pyproject.toml
Normal file
28
servers/fastapi/pyproject.toml
Normal 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"
|
||||
|
|
@ -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
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
from services.redis_service import RedisService
|
||||
from services.temp_file_service import TempFileService
|
||||
|
||||
|
||||
TEMP_FILE_SERVICE = TempFileService()
|
||||
REDIS_SERVICE = RedisService()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
27
servers/fastapi/services/docling_service.py
Normal file
27
servers/fastapi/services/docling_service.py
Normal 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()
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
199
servers/fastapi/services/score_based_chunker.py
Normal file
199
servers/fastapi/services/score_based_chunker.py
Normal 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
|
||||
421
servers/fastapi/tests/test_mcp_server.py
Normal file
421
servers/fastapi/tests/test_mcp_server.py
Normal 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())
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
"""
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
4
servers/fastapi/utils/parsers.py
Normal file
4
servers/fastapi/utils/parsers.py
Normal 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"
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
3149
servers/fastapi/uv.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ const OutlinePage: React.FC = () => {
|
|||
selectedLayoutGroup,
|
||||
setActiveTab
|
||||
);
|
||||
|
||||
if (!presentation_id) {
|
||||
return <EmptyStateView />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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 >
|
||||
);
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue