Skip to content
Closed

Codex #2203

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .env → .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ FRONTEND_HOST=http://localhost:5173
# Environment: local, staging, production
ENVIRONMENT=local

PROJECT_NAME="Full Stack FastAPI Project"
PROJECT_NAME="TemplateForge AI"
STACK_NAME=full-stack-fastapi-project

# Backend
Expand All @@ -40,6 +40,14 @@ POSTGRES_PASSWORD=changethis

SENTRY_DSN=

# Google OAuth (Web client ID)
GOOGLE_OAUTH_CLIENT_ID=your-google-web-client-id.apps.googleusercontent.com

# LLM (Gemini)
# Do not commit real keys. Set locally or via CI/CD secret manager.
GEMINI_API_KEY=your-gemini-api-key
GEMINI_MODEL=gemini-2.5-flash-lite

# Configure these with your own Docker registry images
DOCKER_IMAGE_BACKEND=backend
DOCKER_IMAGE_FRONTEND=frontend
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@ node_modules/
/playwright-report/
/blob-report/
/playwright/.cache/

# Environment files / secrets
.env
.env.*
!.env.example
!.env.*.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""Add template and generation models

Revision ID: 6f44bc66fd3f
Revises: fe56fa70289e
Create Date: 2026-02-21 23:20:00.000000

"""

from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql


# revision identifiers, used by Alembic.
revision = "6f44bc66fd3f"
down_revision = "fe56fa70289e"
branch_labels = None
depends_on = None


template_category_enum = postgresql.ENUM(
"cover_letter",
"email",
"proposal",
"other",
name="templatecategory",
create_type=False,
)
template_language_enum = postgresql.ENUM(
"fr",
"en",
"zh",
"other",
name="templatelanguage",
create_type=False,
)


def upgrade() -> None:
bind = op.get_bind()
template_category_enum.create(bind, checkfirst=True)
template_language_enum.create(bind, checkfirst=True)

op.create_table(
"template",
sa.Column("name", sa.String(length=255), nullable=False),
sa.Column("category", template_category_enum, nullable=False),
sa.Column("language", template_language_enum, nullable=False),
sa.Column("tags", sa.JSON(), nullable=False),
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("is_archived", sa.Boolean(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(["user_id"], ["user.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)

op.create_table(
"templateversion",
sa.Column("content", sa.Text(), nullable=False),
sa.Column("variables_schema", sa.JSON(), nullable=False),
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("template_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("version", sa.Integer(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("created_by", postgresql.UUID(as_uuid=True), nullable=False),
sa.ForeignKeyConstraint(
["template_id"], ["template.id"], ondelete="CASCADE"
),
sa.ForeignKeyConstraint(["created_by"], ["user.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("template_id", "version"),
)

op.create_table(
"generation",
sa.Column("title", sa.String(length=255), nullable=False),
sa.Column("input_text", sa.Text(), nullable=False),
sa.Column("extracted_values", sa.JSON(), nullable=False),
sa.Column("output_text", sa.Text(), nullable=False),
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("template_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("template_version_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(["user_id"], ["user.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(
["template_id"], ["template.id"], ondelete="CASCADE"
),
sa.ForeignKeyConstraint(
["template_version_id"], ["templateversion.id"], ondelete="CASCADE"
),
sa.PrimaryKeyConstraint("id"),
)


def downgrade() -> None:
op.drop_table("generation")
op.drop_table("templateversion")
op.drop_table("template")

bind = op.get_bind()
template_language_enum.drop(bind, checkfirst=True)
template_category_enum.drop(bind, checkfirst=True)
14 changes: 13 additions & 1 deletion backend/app/api/main.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
from fastapi import APIRouter

from app.api.routes import items, login, private, users, utils
from app.api.routes import (
generate,
generations,
items,
login,
private,
templates,
users,
utils,
)
from app.core.config import settings

api_router = APIRouter()
api_router.include_router(login.router)
api_router.include_router(users.router)
api_router.include_router(utils.router)
api_router.include_router(items.router)
api_router.include_router(templates.router)
api_router.include_router(generate.router)
api_router.include_router(generations.router)


if settings.ENVIRONMENT == "local":
Expand Down
65 changes: 65 additions & 0 deletions backend/app/api/routes/generate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import logging
from typing import Any, NoReturn

from fastapi import APIRouter, HTTPException

from app.api.deps import CurrentUser, SessionDep
from app.models import (
ExtractVariablesRequest,
ExtractVariablesResponse,
RenderTemplateRequest,
RenderTemplateResponse,
)
from app.services import generation_service
from app.services.exceptions import ServiceError

router = APIRouter(prefix="/generate", tags=["generate"])
logger = logging.getLogger(__name__)


def _raise_http_from_service_error(exc: ServiceError) -> NoReturn:
raise HTTPException(status_code=exc.status_code, detail=exc.detail)


@router.post("/extract", response_model=ExtractVariablesResponse)
def extract_variables(
*,
session: SessionDep,
current_user: CurrentUser,
extract_in: ExtractVariablesRequest,
) -> Any:
try:
return generation_service.extract_values_for_user(
session=session,
current_user=current_user,
extract_in=extract_in,
)
except ServiceError as exc:
logger.warning(
"Generate extract failed (status=%s): %s",
exc.status_code,
exc.detail,
)
_raise_http_from_service_error(exc)


@router.post("/render", response_model=RenderTemplateResponse)
def render_template(
*,
session: SessionDep,
current_user: CurrentUser,
render_in: RenderTemplateRequest,
) -> Any:
try:
return generation_service.render_for_user(
session=session,
current_user=current_user,
render_in=render_in,
)
except ServiceError as exc:
logger.warning(
"Generate render failed (status=%s): %s",
exc.status_code,
exc.detail,
)
_raise_http_from_service_error(exc)
90 changes: 90 additions & 0 deletions backend/app/api/routes/generations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import uuid
from typing import Any, NoReturn

from fastapi import APIRouter, HTTPException

from app.api.deps import CurrentUser, SessionDep
from app.models import (
GenerationCreate,
GenerationPublic,
GenerationsPublic,
GenerationUpdate,
)
from app.services import generation_service
from app.services.exceptions import ServiceError

router = APIRouter(prefix="/generations", tags=["generations"])


def _raise_http_from_service_error(exc: ServiceError) -> NoReturn:
raise HTTPException(status_code=exc.status_code, detail=exc.detail)


@router.get("/", response_model=GenerationsPublic)
def read_generations(
session: SessionDep, current_user: CurrentUser, skip: int = 0, limit: int = 100
) -> Any:
generations, count = generation_service.list_generations_for_user(
session=session,
current_user=current_user,
skip=skip,
limit=limit,
)
data = [GenerationPublic.model_validate(generation) for generation in generations]
return GenerationsPublic(data=data, count=count)


@router.get("/{generation_id}", response_model=GenerationPublic)
def read_generation(
session: SessionDep, current_user: CurrentUser, generation_id: uuid.UUID
) -> Any:
try:
generation = generation_service.get_generation_for_user(
session=session,
current_user=current_user,
generation_id=generation_id,
)
except ServiceError as exc:
_raise_http_from_service_error(exc)

return GenerationPublic.model_validate(generation)


@router.post("/", response_model=GenerationPublic)
def create_generation(
*,
session: SessionDep,
current_user: CurrentUser,
generation_in: GenerationCreate,
) -> Any:
try:
generation = generation_service.create_generation_for_user(
session=session,
current_user=current_user,
generation_in=generation_in,
)
except ServiceError as exc:
_raise_http_from_service_error(exc)

return GenerationPublic.model_validate(generation)


@router.patch("/{generation_id}", response_model=GenerationPublic)
def update_generation(
*,
session: SessionDep,
current_user: CurrentUser,
generation_id: uuid.UUID,
generation_in: GenerationUpdate,
) -> Any:
try:
generation = generation_service.update_generation_for_user(
session=session,
current_user=current_user,
generation_id=generation_id,
generation_in=generation_in,
)
except ServiceError as exc:
_raise_http_from_service_error(exc)

return GenerationPublic.model_validate(generation)
Loading
Loading