Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
"""Add prompt and version table

Revision ID: 757f50ada8ef
Revises: e9dd35eff62c
Create Date: 2025-08-14 11:45:07.186686

"""
from alembic import op
import sqlalchemy as sa
import sqlmodel.sql.sqltypes


# revision identifiers, used by Alembic.
revision = "757f50ada8ef"
down_revision = "e9dd35eff62c"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"prompt",
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(length=50), nullable=False),
sa.Column(
"description", sqlmodel.sql.sqltypes.AutoString(length=500), nullable=True
),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("active_version", sa.Uuid(), nullable=False),
sa.Column("project_id", sa.Integer(), nullable=False),
sa.Column("inserted_at", sa.DateTime(), nullable=False),
sa.Column("updated_at", sa.DateTime(), nullable=False),
sa.Column("is_deleted", sa.Boolean(), nullable=False),
sa.Column("deleted_at", sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(
["active_version"],
["prompt_version.id"],
initially="DEFERRED",
deferrable=True,
use_alter=True,
),
Comment on lines +35 to +41
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Duplicate foreign key definition for prompt.active_version.

The FK from prompt.active_version to prompt_version.id is defined both inline in op.create_table("prompt", ...) and again via op.create_foreign_key(...) (Lines 80–89). This can lead to migration failures due to duplicate constraints.

Keep the explicit op.create_foreign_key(...) after both tables exist, and remove the inline FK from the create_table call.

Apply this diff:

-        sa.ForeignKeyConstraint(
-            ["active_version"],
-            ["prompt_version.id"],
-            initially="DEFERRED",
-            deferrable=True,
-            use_alter=True,
-        ),
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
sa.ForeignKeyConstraint(
["active_version"],
["prompt_version.id"],
initially="DEFERRED",
deferrable=True,
use_alter=True,
),
πŸ€– Prompt for AI Agents
In backend/app/alembic/versions/757f50ada8ef_add_prompt_and_version_table.py
around lines 35 to 41, the foreign key from prompt.active_version to
prompt_version.id is declared inline inside op.create_table (the
sa.ForeignKeyConstraint block) and is duplicated later via
op.create_foreign_key; remove the inline sa.ForeignKeyConstraint lines (the
entire block at 35–41) from the create_table call so the table defines
active_version as a plain Column/Integer, and keep the existing
op.create_foreign_key(...) that runs after both tables exist to create the FK
constraint.

sa.ForeignKeyConstraint(
["project_id"],
["project.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_prompt_name"), "prompt", ["name"], unique=False)
op.create_index(
op.f("ix_prompt_project_id"), "prompt", ["project_id"], unique=False
)
op.create_index(
"ix_prompt_project_id_is_deleted",
"prompt",
["project_id", "is_deleted"],
unique=False,
)
op.create_table(
"prompt_version",
sa.Column("instruction", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column(
"commit_message",
sqlmodel.sql.sqltypes.AutoString(length=512),
nullable=True,
),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("prompt_id", sa.Uuid(), nullable=False),
sa.Column("version", sa.Integer(), nullable=False),
sa.Column("inserted_at", sa.DateTime(), nullable=False),
sa.Column("updated_at", sa.DateTime(), nullable=False),
sa.Column("is_deleted", sa.Boolean(), nullable=False),
sa.Column("deleted_at", sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(
["prompt_id"],
["prompt.id"],
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("prompt_id", "version"),
)
op.create_foreign_key(
None,
"prompt",
"prompt_version",
["active_version"],
["id"],
initially="DEFERRED",
deferrable=True,
use_alter=True,
)
Comment on lines +80 to +89
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Duplicate/invalid FK creation: remove op.create_foreign_key and the unsupported use_alter argument.

You already declare the active_version FK in the prompt table with use_alter=True (which Alembic emits as a separate ALTER after both tables exist). The extra op.create_foreign_key block introduces a duplicate FK and includes an unsupported parameter (use_alter), which will raise a TypeError.

Apply this diff to remove the redundant/invalid FK creation:

-    op.create_foreign_key(
-        None,
-        "prompt",
-        "prompt_version",
-        ["active_version"],
-        ["id"],
-        initially="DEFERRED",
-        deferrable=True,
-        use_alter=True,
-    )
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
op.create_foreign_key(
None,
"prompt",
"prompt_version",
["active_version"],
["id"],
initially="DEFERRED",
deferrable=True,
use_alter=True,
)
πŸ€– Prompt for AI Agents
In backend/app/alembic/versions/757f50ada8ef_add_prompt_and_version_table.py
around lines 80 to 89, there is a redundant/invalid op.create_foreign_key call
that duplicates the already-declared active_version FK and includes the
unsupported use_alter argument; remove the entire op.create_foreign_key block
(or at minimum drop the duplicate FK creation and the use_alter parameter) so
the FK is declared only once and no unsupported parameters are passed to
Alembic.

op.create_index(
op.f("ix_prompt_version_prompt_id"),
"prompt_version",
["prompt_id"],
unique=False,
)
op.create_index(
"ix_prompt_version_prompt_id_is_deleted",
"prompt_version",
["prompt_id", "is_deleted"],
unique=False,
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index("ix_prompt_version_prompt_id_is_deleted", table_name="prompt_version")
op.drop_index(op.f("ix_prompt_version_prompt_id"), table_name="prompt_version")
op.drop_table("prompt_version")
op.drop_index("ix_prompt_project_id_is_deleted", table_name="prompt")
op.drop_index(op.f("ix_prompt_project_id"), table_name="prompt")
op.drop_index(op.f("ix_prompt_name"), table_name="prompt")
op.drop_table("prompt")
# ### end Alembic commands ###
4 changes: 4 additions & 0 deletions backend/app/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
openai_conversation,
project,
project_user,
prompts,
prompt_versions,
responses,
private,
threads,
Expand All @@ -32,6 +34,8 @@
api_router.include_router(organization.router)
api_router.include_router(project.router)
api_router.include_router(project_user.router)
api_router.include_router(prompts.router)
api_router.include_router(prompt_versions.router)
api_router.include_router(responses.router)
api_router.include_router(threads.router)
api_router.include_router(users.router)
Expand Down
55 changes: 55 additions & 0 deletions backend/app/api/routes/prompt_versions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import logging
from uuid import UUID

from fastapi import APIRouter, Depends
from sqlmodel import Session

from app.api.deps import CurrentUserOrgProject, get_db
from app.crud import create_prompt_version, delete_prompt_version
from app.models import PromptVersionCreate, PromptVersionPublic
from app.utils import APIResponse


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


@router.post(
"/{prompt_id}/versions",
response_model=APIResponse[PromptVersionPublic],
status_code=201,
)
def create_prompt_version_route(
prompt_version_in: PromptVersionCreate,
prompt_id: UUID,
current_user: CurrentUserOrgProject,
session: Session = Depends(get_db),
):
version = create_prompt_version(
session=session,
prompt_id=prompt_id,
prompt_version_in=prompt_version_in,
project_id=current_user.project_id,
)
return APIResponse.success_response(version)


@router.delete("/{prompt_id}/versions/{version_id}", response_model=APIResponse)
def delete_prompt_version_route(
prompt_id: UUID,
version_id: UUID,
current_user: CurrentUserOrgProject,
session: Session = Depends(get_db),
):
"""
Delete a prompt version by ID.
"""
delete_prompt_version(
session=session,
prompt_id=prompt_id,
version_id=version_id,
project_id=current_user.project_id,
)
return APIResponse.success_response(
data={"message": "Prompt version deleted successfully."}
)
134 changes: 134 additions & 0 deletions backend/app/api/routes/prompts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import logging
from uuid import UUID

from fastapi import APIRouter, Depends, Path, Query
from sqlmodel import Session

from app.api.deps import CurrentUserOrgProject, get_db
from app.crud import (
create_prompt,
delete_prompt,
get_prompt_by_id,
get_prompts,
count_prompts_in_project,
update_prompt,
)
from app.models import (
PromptCreate,
PromptPublic,
PromptUpdate,
PromptWithVersion,
PromptWithVersions,
)
Comment on lines +16 to +22
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ› οΈ Refactor suggestion

Missing import for PromptVersionPublic used in response shaping below.

You construct responses with version(s). Import PromptVersionPublic to ensure explicit conversion and correct OpenAPI schema generation.

Apply this diff:

 from app.models import (
     PromptCreate,
     PromptPublic,
     PromptUpdate,
     PromptWithVersion,
     PromptWithVersions,
+    PromptVersionPublic,
 )
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
from app.models import (
PromptCreate,
PromptPublic,
PromptUpdate,
PromptWithVersion,
PromptWithVersions,
)
from app.models import (
PromptCreate,
PromptPublic,
PromptUpdate,
PromptWithVersion,
PromptWithVersions,
PromptVersionPublic,
)
πŸ€– Prompt for AI Agents
In backend/app/api/routes/prompts.py around lines 16 to 22, the import list for
models is missing PromptVersionPublic which is later used to shape responses;
update the import tuple to include PromptVersionPublic so responses explicitly
convert versions using that Pydantic model and the OpenAPI schema includes the
version shape.

from app.utils import APIResponse


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


@router.post("/", response_model=APIResponse[PromptWithVersion], status_code=201)
def create_prompt_route(
prompt_in: PromptCreate,
current_user: CurrentUserOrgProject,
session: Session = Depends(get_db),
):
"""
Create a new prompt under the specified organization and project.
"""
prompt, version = create_prompt(
session=session, prompt_in=prompt_in, project_id=current_user.project_id
)
prompt_with_version = PromptWithVersion(**prompt.model_dump(), version=version)
return APIResponse.success_response(prompt_with_version)
Comment on lines +42 to +43
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ› οΈ Refactor suggestion

Ensure correct serialization for nested version in the create response.

Instantiate the public schema to avoid leaking internal fields and to ensure Pydantic validation is deterministic.

Apply this diff:

-    prompt_with_version = PromptWithVersion(**prompt.model_dump(), version=version)
+    prompt_with_version = PromptWithVersion(
+        **prompt.model_dump(),
+        version=PromptVersionPublic.model_validate(version),
+    )
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
prompt_with_version = PromptWithVersion(**prompt.model_dump(), version=version)
return APIResponse.success_response(prompt_with_version)
prompt_with_version = PromptWithVersion(
**prompt.model_dump(),
version=PromptVersionPublic.model_validate(version),
)
return APIResponse.success_response(prompt_with_version)
πŸ€– Prompt for AI Agents
In backend/app/api/routes/prompts.py around lines 42 to 43, replace the current
creation of the response (which directly uses the internal Prompt model) with
instantiation of the public Pydantic response schema so internal fields are not
leaked and validation is deterministic; build a data dict from
prompt.model_dump(), add the version field, then pass that dict into the public
response schema class (instead of using the internal model) and return
APIResponse.success_response(the_public_schema_instance).



@router.get(
"/",
response_model=APIResponse[list[PromptPublic]],
)
def get_prompts_route(
current_user: CurrentUserOrgProject,
skip: int = Query(
0, ge=0, description="Number of prompts to skip (for pagination)."
),
limit: int = Query(100, gt=0, description="Maximum number of prompts to return."),
session: Session = Depends(get_db),
):
"""
Get all prompts for the specified organization and project.
"""
prompts = get_prompts(
session=session,
project_id=current_user.project_id,
skip=skip,
limit=limit,
)
total = count_prompts_in_project(
session=session, project_id=current_user.project_id
)
metadata = {"pagination": {"total": total, "skip": skip, "limit": limit}}
return APIResponse.success_response(prompts, metadata=metadata)


@router.get(
"/{prompt_id}",
response_model=APIResponse[PromptWithVersions],
summary="Get a single prompt by its ID by default returns the active version",
)
def get_prompt_by_id_route(
current_user: CurrentUserOrgProject,
prompt_id: UUID = Path(..., description="The ID of the prompt to fetch"),
include_versions: bool = Query(
False, description="Whether to include all versions of the prompt."
),
session: Session = Depends(get_db),
):
"""
Get a single prompt by its ID.
"""
prompt, versions = get_prompt_by_id(
session=session,
prompt_id=prompt_id,
project_id=current_user.project_id,
include_versions=include_versions,
)
prompt_with_versions = PromptWithVersions(**prompt.model_dump(), versions=versions)
return APIResponse.success_response(prompt_with_versions)
Comment on lines +96 to +97
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ› οΈ Refactor suggestion

Serialize versions to their public schema for consistent API responses.

Convert ORM models to PromptVersionPublic to avoid accidental leakage of internal fields and to be robust to Pydantic parsing differences.

Apply this diff:

-    prompt_with_versions = PromptWithVersions(**prompt.model_dump(), versions=versions)
+    versions_public = [PromptVersionPublic.model_validate(v) for v in versions]
+    prompt_with_versions = PromptWithVersions(
+        **prompt.model_dump(),
+        versions=versions_public,
+    )
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
prompt_with_versions = PromptWithVersions(**prompt.model_dump(), versions=versions)
return APIResponse.success_response(prompt_with_versions)
versions_public = [PromptVersionPublic.model_validate(v) for v in versions]
prompt_with_versions = PromptWithVersions(
**prompt.model_dump(),
versions=versions_public,
)
return APIResponse.success_response(prompt_with_versions)
πŸ€– Prompt for AI Agents
In backend/app/api/routes/prompts.py around lines 96-97, the code returns
PromptWithVersions using raw ORM version objects which can leak internal fields
and be brittle; convert each version to the public schema before returning.
Replace building PromptWithVersions(..., versions=versions) with a serialized
list (e.g., map each version through PromptVersionPublic using the version's
model dump or model_validate) so versions passed to PromptWithVersions are
PromptVersionPublic instances (or validated dicts) and then return
APIResponse.success_response with that sanitized PromptWithVersions object.



@router.patch("/{prompt_id}", response_model=APIResponse[PromptPublic])
def update_prompt_route(
current_user: CurrentUserOrgProject,
prompt_update: PromptUpdate,
prompt_id: UUID = Path(..., description="The ID of the prompt to Update"),
session: Session = Depends(get_db),
):
"""
Update a prompt's name or description.
"""

prompt = update_prompt(
session=session,
prompt_id=prompt_id,
project_id=current_user.project_id,
prompt_update=prompt_update,
)
return APIResponse.success_response(prompt)


@router.delete("/{prompt_id}", response_model=APIResponse)
def delete_prompt_route(
current_user: CurrentUserOrgProject,
prompt_id: UUID = Path(..., description="The ID of the prompt to delete"),
session: Session = Depends(get_db),
):
"""
Delete a prompt by ID.
"""
delete_prompt(
session=session, prompt_id=prompt_id, project_id=current_user.project_id
)
return APIResponse.success_response(
data={"message": "Prompt deleted successfully."}
)
11 changes: 11 additions & 0 deletions backend/app/crud/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,14 @@
create_conversation,
delete_conversation,
)

from .prompt_versions import create_prompt_version, delete_prompt_version

from .prompts import (
create_prompt,
count_prompts_in_project,
delete_prompt,
get_prompt_by_id,
get_prompts,
update_prompt,
)
Loading