-
Notifications
You must be signed in to change notification settings - Fork 5
Feat: Add Prompt Management System with Versioning #319
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
56127c9
9fc65af
948f447
fcf4001
ef1933b
7067511
b3c5da8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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, | ||||||||||||||||||||||
), | ||||||||||||||||||||||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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
Suggested change
π€ Prompt for AI Agents
|
||||||||||||||||||||||
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 ### |
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], | ||
avirajsingh7 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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."} | ||
) |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
π€ Prompt for AI Agents
|
||||||||||||||||||||||||||||||||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
π€ Prompt for AI Agents
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
@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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
π€ Prompt for AI Agents
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
@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."} | ||||||||||||||||||||||||||||||||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Duplicate foreign key definition for prompt.active_version.
The FK from
prompt.active_version
toprompt_version.id
is defined both inline inop.create_table("prompt", ...)
and again viaop.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 thecreate_table
call.Apply this diff:
π Committable suggestion
π€ Prompt for AI Agents