Skip to content
Merged
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,87 @@
"""add_openai_conversation_table

Revision ID: e9dd35eff62c
Revises: e8ee93526b37
Create Date: 2025-07-25 18:26:38.132146

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


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


def upgrade():
op.create_table(
"openai_conversation",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("response_id", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column(
"ancestor_response_id", sqlmodel.sql.sqltypes.AutoString(), nullable=False
),
sa.Column(
"previous_response_id", sqlmodel.sql.sqltypes.AutoString(), nullable=True
),
sa.Column("user_question", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("response", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column("model", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("assistant_id", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column("project_id", sa.Integer(), nullable=False),
sa.Column("organization_id", sa.Integer(), nullable=False),
sa.Column("is_deleted", sa.Boolean(), nullable=False),
sa.Column("inserted_at", sa.DateTime(), nullable=False),
sa.Column("updated_at", sa.DateTime(), nullable=False),
sa.Column("deleted_at", sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint("id"),
sa.ForeignKeyConstraint(
["organization_id"], ["organization.id"], ondelete="CASCADE"
),
sa.ForeignKeyConstraint(["project_id"], ["project.id"], ondelete="CASCADE"),
)
op.create_index(
op.f("ix_openai_conversation_ancestor_response_id"),
"openai_conversation",
["ancestor_response_id"],
unique=False,
)
op.create_index(
op.f("ix_openai_conversation_previous_response_id"),
"openai_conversation",
["previous_response_id"],
unique=False,
)
op.create_index(
op.f("ix_openai_conversation_response_id"),
"openai_conversation",
["response_id"],
unique=False,
)
op.create_foreign_key(
None, "openai_conversation", "project", ["project_id"], ["id"]
)
op.create_foreign_key(
None, "openai_conversation", "organization", ["organization_id"], ["id"]
)


def downgrade():
op.drop_constraint(None, "openai_conversation", type_="foreignkey")
op.drop_constraint(None, "openai_conversation", type_="foreignkey")
op.drop_index(
op.f("ix_openai_conversation_response_id"), table_name="openai_conversation"
)
op.drop_index(
op.f("ix_openai_conversation_previous_response_id"),
table_name="openai_conversation",
)
op.drop_index(
op.f("ix_openai_conversation_ancestor_response_id"),
table_name="openai_conversation",
)
op.drop_table("openai_conversation")
2 changes: 2 additions & 0 deletions backend/app/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
documents,
login,
organization,
openai_conversation,
project,
project_user,
responses,
Expand All @@ -27,6 +28,7 @@
api_router.include_router(documents.router)
api_router.include_router(login.router)
api_router.include_router(onboarding.router)
api_router.include_router(openai_conversation.router)
api_router.include_router(organization.router)
api_router.include_router(project.router)
api_router.include_router(project_user.router)
Expand Down
152 changes: 152 additions & 0 deletions backend/app/api/routes/openai_conversation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
from typing import Annotated

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

from app.api.deps import get_db, get_current_user_org_project
from app.crud import (
get_conversation_by_id,
get_conversation_by_response_id,
get_conversation_by_ancestor_id,
get_conversations_by_project,
get_conversations_count_by_project,
create_conversation,
delete_conversation,
)
from app.models import (
UserProjectOrg,
OpenAIConversationPublic,
)
from app.utils import APIResponse

router = APIRouter(prefix="/openai-conversation", tags=["OpenAI Conversations"])


@router.get(
"/{conversation_id}",
response_model=APIResponse[OpenAIConversationPublic],
summary="Get a single conversation by its ID",
)
def get_conversation_route(
conversation_id: int = Path(..., description="The conversation ID to fetch"),
session: Session = Depends(get_db),
current_user: UserProjectOrg = Depends(get_current_user_org_project),
):
"""
Fetch a single conversation by its ID.
"""
conversation = get_conversation_by_id(
session, conversation_id, current_user.project_id
)
if not conversation:
raise HTTPException(
status_code=404, detail=f"Conversation with ID {conversation_id} not found."
)
return APIResponse.success_response(conversation)


@router.get(
"/response/{response_id}",
response_model=APIResponse[OpenAIConversationPublic],
summary="Get a conversation by its OpenAI response ID",
)
def get_conversation_by_response_id_route(
response_id: str = Path(..., description="The OpenAI response ID to fetch"),
session: Session = Depends(get_db),
current_user: UserProjectOrg = Depends(get_current_user_org_project),
):
"""
Fetch a conversation by its OpenAI response ID.
"""
conversation = get_conversation_by_response_id(
session, response_id, current_user.project_id
)
if not conversation:
raise HTTPException(
status_code=404,
detail=f"Conversation with response ID {response_id} not found.",
)
return APIResponse.success_response(conversation)


@router.get(
"/ancestor/{ancestor_response_id}",
response_model=APIResponse[OpenAIConversationPublic],
summary="Get a conversation by its ancestor response ID",
)
def get_conversation_by_ancestor_id_route(
ancestor_response_id: str = Path(
..., description="The ancestor response ID to fetch"
),
session: Session = Depends(get_db),
current_user: UserProjectOrg = Depends(get_current_user_org_project),
):
"""
Fetch a conversation by its ancestor response ID.
"""
conversation = get_conversation_by_ancestor_id(
session, ancestor_response_id, current_user.project_id
)
if not conversation:
raise HTTPException(
status_code=404,
detail=f"Conversation with ancestor response ID {ancestor_response_id} not found.",
)
return APIResponse.success_response(conversation)


@router.get(
"/",
response_model=APIResponse[list[OpenAIConversationPublic]],
summary="List all conversations in the current project",
)
def list_conversations_route(
session: Session = Depends(get_db),
current_user: UserProjectOrg = Depends(get_current_user_org_project),
skip: int = Query(0, ge=0, description="How many items to skip"),
limit: int = Query(100, ge=1, le=100, description="Maximum items to return"),
):
"""
List all conversations in the current project.
"""
conversations = get_conversations_by_project(
session=session,
project_id=current_user.project_id,
skip=skip, # ← Pagination offset
limit=limit, # ← Page size
)

# Get total count for pagination metadata
total = get_conversations_count_by_project(
session=session,
project_id=current_user.project_id,
)

return APIResponse.success_response(
data=conversations, metadata={"skip": skip, "limit": limit, "total": total}
)


@router.delete("/{conversation_id}", response_model=APIResponse)
def delete_conversation_route(
conversation_id: Annotated[int, Path(description="Conversation ID to delete")],
session: Session = Depends(get_db),
current_user: UserProjectOrg = Depends(get_current_user_org_project),
):
"""
Soft delete a conversation by marking it as deleted.
"""
deleted_conversation = delete_conversation(
session=session,
conversation_id=conversation_id,
project_id=current_user.project_id,
)

if not deleted_conversation:
raise HTTPException(
status_code=404, detail=f"Conversation with ID {conversation_id} not found."
)

return APIResponse.success_response(
data={"message": "Conversation deleted successfully."}
)
10 changes: 10 additions & 0 deletions backend/app/crud/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,13 @@
get_assistants_by_project,
delete_assistant,
)

from .openai_conversation import (
get_conversation_by_id,
get_conversation_by_response_id,
get_conversation_by_ancestor_id,
get_conversations_by_project,
get_conversations_count_by_project,
create_conversation,
delete_conversation,
)
Loading