b32669caa7
Problema: i template puntavano a un path host hardcoded
(stylesheet: /home/adriano/.../themes/tielogic.css), quindi il file .md
generato non era portabile — su un'altra macchina md-to-pdf non trovava
il CSS e produceva PDF senza stile.
Soluzione: il Renderer legge il CSS da Settings.inline_stylesheet_path
(default /app/themes/tielogic.css nel container) e lo inietta come
blocco <style>...</style> subito dopo il frontmatter YAML del Markdown
restituito dall'LLM. Il file .md risultante è autocontenuto e portabile.
- renderer.py: nuovo arg inline_stylesheet_path + funzione
_inject_inline_stylesheet (idempotente, gestisce Markdown senza
frontmatter, no-op se CSS vuoto)
- config.py: Settings.inline_stylesheet_path: Path | None
- main.py: passa il path al Renderer
- mcp-docugen.Dockerfile: COPY themes ./themes nello stage builder per
trasportare /app/themes/tielogic.css nell'immagine runtime
- templates_seed/{offerta,report-analisi}/template.md: rimossa la riga
`stylesheet:` dal frontmatter di output + regola tassativa che vieta
all'LLM di emettere blocchi <style> di sua iniziativa (evita
conflitti di cascade visti in test)
- 4 nuovi test unit (76 totali): iniezione dopo frontmatter, prepend
quando frontmatter assente, no-op CSS vuoto, integrazione full E2E
via Renderer.generate
scripts/bundle-css.py: utility per fixare file .md legacy che
referenziavano stylesheet: come path host (sostituisce la riga con
<style> inline pescando il CSS dal repo)
README aggiornato con rationale e workflow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
114 lines
3.6 KiB
Python
114 lines
3.6 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
import os
|
|
from contextlib import asynccontextmanager
|
|
|
|
import uvicorn
|
|
from fastapi import FastAPI
|
|
|
|
from mcp_docugen.auth import ApiKeyAuthMiddleware
|
|
from mcp_docugen.config import Settings
|
|
from mcp_docugen.generation_store import GenerationStore
|
|
from mcp_docugen.http_routes import build_http_app
|
|
from mcp_docugen.llm_client import OpenRouterClient
|
|
from mcp_docugen.mcp_tools import build_mcp_server
|
|
from mcp_docugen.renderer import Renderer
|
|
from mcp_docugen.template_seed import seed_templates
|
|
from mcp_docugen.template_store import TemplateStore
|
|
|
|
logger = logging.getLogger("mcp_docugen")
|
|
|
|
|
|
async def build_app(settings: Settings | None = None) -> FastAPI:
|
|
settings = settings or Settings()
|
|
settings.data_dir.mkdir(parents=True, exist_ok=True)
|
|
templates_dir = settings.data_dir / "templates"
|
|
generated_dir = settings.data_dir / "generated"
|
|
db_path = settings.data_dir / "mcp_docugen.db"
|
|
|
|
template_store = TemplateStore(base_dir=templates_dir)
|
|
seed_templates(settings.templates_seed_dir, templates_dir)
|
|
generation_store = GenerationStore(
|
|
db_path=db_path, generated_dir=generated_dir
|
|
)
|
|
await generation_store.init()
|
|
|
|
llm = OpenRouterClient(
|
|
api_key=settings.openrouter_api_key,
|
|
base_url=settings.openrouter_base_url,
|
|
timeout=settings.llm_timeout_seconds,
|
|
)
|
|
renderer = Renderer(
|
|
template_store=template_store,
|
|
generation_store=generation_store,
|
|
llm=llm,
|
|
public_base_url=settings.public_base_url,
|
|
default_model=settings.llm_model_default,
|
|
asset_ttl_days=settings.asset_ttl_days,
|
|
max_image_size_mb=settings.max_image_size_mb,
|
|
inline_stylesheet_path=settings.inline_stylesheet_path,
|
|
)
|
|
|
|
mcp = build_mcp_server(template_store, renderer)
|
|
mcp_asgi = mcp.streamable_http_app()
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
async with mcp.session_manager.run():
|
|
cleanup_task = asyncio.create_task(
|
|
_periodic_cleanup(generation_store, interval_seconds=24 * 3600)
|
|
)
|
|
try:
|
|
yield
|
|
finally:
|
|
cleanup_task.cancel()
|
|
try:
|
|
await cleanup_task
|
|
except asyncio.CancelledError:
|
|
pass
|
|
|
|
app = build_http_app(template_store, generation_store)
|
|
app.router.lifespan_context = lifespan
|
|
# streamable_http_app() espone /mcp internamente; monto a root.
|
|
app.mount("/", mcp_asgi)
|
|
|
|
app.add_middleware(
|
|
ApiKeyAuthMiddleware,
|
|
api_key=settings.api_key,
|
|
exempt_paths={"/health", "/docs", "/redoc", "/openapi.json"},
|
|
)
|
|
|
|
app.state.settings = settings
|
|
app.state.template_store = template_store
|
|
app.state.generation_store = generation_store
|
|
app.state.renderer = renderer
|
|
app.state.llm = llm
|
|
|
|
return app
|
|
|
|
|
|
async def _periodic_cleanup(
|
|
generation_store: GenerationStore, interval_seconds: int
|
|
) -> None:
|
|
while True:
|
|
try:
|
|
removed = await generation_store.cleanup_expired()
|
|
if removed:
|
|
logger.info("cleanup_expired removed %d assets", removed)
|
|
except Exception as exc: # noqa: BLE001
|
|
logger.error("cleanup task error: %s", exc)
|
|
await asyncio.sleep(interval_seconds)
|
|
|
|
|
|
def run() -> None:
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s %(levelname)s %(name)s %(message)s",
|
|
)
|
|
host = os.environ.get("HOST", "0.0.0.0")
|
|
port = int(os.environ.get("PORT", "9100"))
|
|
app = asyncio.run(build_app())
|
|
uvicorn.run(app, host=host, port=port)
|