diff --git a/alembic/script.py.mako b/alembic/script.py.mako index 1101630..a2ccc2a 100644 --- a/alembic/script.py.mako +++ b/alembic/script.py.mako @@ -9,6 +9,7 @@ from typing import Sequence, Union from alembic import op import sqlalchemy as sa +import pgvector.sqlalchemy ${imports if imports else ""} # revision identifiers, used by Alembic. diff --git a/alembic/versions/2f98f76cb765_create_file_relationships_table.py b/alembic/versions/2f98f76cb765_create_file_relationships_table.py deleted file mode 100644 index 3e24c02..0000000 --- a/alembic/versions/2f98f76cb765_create_file_relationships_table.py +++ /dev/null @@ -1,42 +0,0 @@ -"""create_file_relationships_table - -Revision ID: 2f98f76cb765 -Revises: cdcdffd36592 -Create Date: 2026-06-11 16:18:13.091065 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '2f98f76cb765' -down_revision: Union[str, Sequence[str], None] = 'cdcdffd36592' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('file_relationships', - sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), nullable=False), - sa.Column('source_file_id', sa.UUID(), nullable=False), - sa.Column('target_file_id', sa.UUID(), nullable=False), - sa.Column('similarity_score', sa.Float(), nullable=False), - sa.Column('relation_type', sa.String(length=50), server_default='semantic_similarity', nullable=False), - sa.ForeignKeyConstraint(['source_file_id'], ['files.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['target_file_id'], ['files.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('source_file_id', 'target_file_id', name='uq_file_relationship') - ) - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('file_relationships') - # ### end Alembic commands ### diff --git a/alembic/versions/c1850b307113_create_files_table.py b/alembic/versions/c1850b307113_create_files_table.py deleted file mode 100644 index 5822e0d..0000000 --- a/alembic/versions/c1850b307113_create_files_table.py +++ /dev/null @@ -1,46 +0,0 @@ -"""create_files_table - -Revision ID: c1850b307113 -Revises: -Create Date: 2026-06-11 16:12:34.237674 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -# revision identifiers, used by Alembic. -revision: str = 'c1850b307113' -down_revision: Union[str, Sequence[str], None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('files', - sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), nullable=False), - sa.Column('file_path', sa.Text(), nullable=False), - sa.Column('file_hash', sa.String(length=64), nullable=False), - sa.Column('mime_type', sa.String(length=50), nullable=False), - sa.Column('last_modified', sa.DateTime(timezone=True), nullable=False), - sa.Column('indexed_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), - sa.Column('tags', postgresql.ARRAY(sa.Text()), server_default='{}', nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('file_path') - ) - op.create_index('files_path_hash_idx', 'files', ['file_path', 'file_hash'], unique=False) - op.create_index('files_tags_idx', 'files', ['tags'], unique=False, postgresql_using='gin') - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_index('files_tags_idx', table_name='files', postgresql_using='gin') - op.drop_index('files_path_hash_idx', table_name='files') - op.drop_table('files') - # ### end Alembic commands ### diff --git a/alembic/versions/cdcdffd36592_create_file_content_table.py b/alembic/versions/cdcdffd36592_create_file_content_table.py deleted file mode 100644 index 6707dea..0000000 --- a/alembic/versions/cdcdffd36592_create_file_content_table.py +++ /dev/null @@ -1,46 +0,0 @@ -"""create_file_content_table - -Revision ID: cdcdffd36592 -Revises: c1850b307113 -Create Date: 2026-06-11 16:14:39.195041 - -""" -from typing import Sequence, Union -import pgvector -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -# revision identifiers, used by Alembic. -revision: str = 'cdcdffd36592' -down_revision: Union[str, Sequence[str], None] = 'c1850b307113' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - op.execute("CREATE EXTENSION IF NOT EXISTS vector;") - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('file_content', - sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), nullable=False), - sa.Column('file_id', sa.UUID(), nullable=False), - sa.Column('chunk_index', sa.Integer(), nullable=False), - sa.Column('content_text', sa.Text(), nullable=False), - sa.Column('embedding', pgvector.sqlalchemy.vector.VECTOR(dim=384), nullable=False), - sa.Column('keyword_tokens', postgresql.TSVECTOR(), sa.Computed("to_tsvector('english', content_text)", persisted=True), nullable=True), - sa.ForeignKeyConstraint(['file_id'], ['files.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_index('content_embedding_idx', 'file_content', ['embedding'], unique=False, postgresql_using='hnsw', postgresql_with={'m': 16, 'ef_construction': 64}, postgresql_ops={'embedding': 'vector_cosine_ops'}) - op.create_index('content_fts_idx', 'file_content', ['keyword_tokens'], unique=False, postgresql_using='gin') - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_index('content_fts_idx', table_name='file_content', postgresql_using='gin') - op.drop_index('content_embedding_idx', table_name='file_content', postgresql_using='hnsw', postgresql_with={'m': 16, 'ef_construction': 64}, postgresql_ops={'embedding': 'vector_cosine_ops'}) - op.drop_table('file_content') - # ### end Alembic commands ### diff --git a/alembic/versions/ecca7b879961_genesis_migration.py b/alembic/versions/ecca7b879961_genesis_migration.py new file mode 100644 index 0000000..689ceb2 --- /dev/null +++ b/alembic/versions/ecca7b879961_genesis_migration.py @@ -0,0 +1,75 @@ +"""genesis migration + +Revision ID: ecca7b879961 +Revises: +Create Date: 2026-06-20 16:46:17.090119 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import pgvector.sqlalchemy +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = 'ecca7b879961' +down_revision: Union[str, Sequence[str], None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('files', + sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), nullable=False), + sa.Column('file_path', sa.Text(), nullable=False), + sa.Column('file_hash', sa.String(length=64), nullable=False), + sa.Column('mime_type', sa.String(length=50), nullable=False), + sa.Column('last_modified', sa.DateTime(timezone=True), nullable=False), + sa.Column('indexed_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('tags', postgresql.ARRAY(sa.Text()), server_default='{}', nullable=False), + sa.Column('context', sa.Text(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('file_path') + ) + op.create_index('files_path_hash_idx', 'files', ['file_path', 'file_hash'], unique=False) + op.create_index('files_tags_idx', 'files', ['tags'], unique=False, postgresql_using='gin') + op.create_table('file_content', + sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), nullable=False), + sa.Column('file_id', sa.UUID(), nullable=False), + sa.Column('chunk_index', sa.Integer(), nullable=False), + sa.Column('content_text', sa.Text(), nullable=False), + sa.Column('embedding', pgvector.sqlalchemy.vector.VECTOR(dim=384), nullable=False), + sa.Column('keyword_tokens', postgresql.TSVECTOR(), sa.Computed("to_tsvector('english', content_text)", persisted=True), nullable=True), + sa.ForeignKeyConstraint(['file_id'], ['files.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('content_embedding_idx', 'file_content', ['embedding'], unique=False, postgresql_using='hnsw', postgresql_with={'m': 16, 'ef_construction': 64}, postgresql_ops={'embedding': 'vector_cosine_ops'}) + op.create_index('content_fts_idx', 'file_content', ['keyword_tokens'], unique=False, postgresql_using='gin') + op.create_table('file_relationships', + sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), nullable=False), + sa.Column('source_file_id', sa.UUID(), nullable=False), + sa.Column('target_file_id', sa.UUID(), nullable=False), + sa.Column('similarity_score', sa.Float(), nullable=False), + sa.Column('relation_type', sa.String(length=50), server_default='semantic_similarity', nullable=False), + sa.ForeignKeyConstraint(['source_file_id'], ['files.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['target_file_id'], ['files.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('source_file_id', 'target_file_id', name='uq_file_relationship') + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('file_relationships') + op.drop_index('content_fts_idx', table_name='file_content', postgresql_using='gin') + op.drop_index('content_embedding_idx', table_name='file_content', postgresql_using='hnsw', postgresql_with={'m': 16, 'ef_construction': 64}, postgresql_ops={'embedding': 'vector_cosine_ops'}) + op.drop_table('file_content') + op.drop_index('files_tags_idx', table_name='files', postgresql_using='gin') + op.drop_index('files_path_hash_idx', table_name='files') + op.drop_table('files') + # ### end Alembic commands ### diff --git a/app/main.py b/app/main.py index adaaf66..fbd693a 100644 --- a/app/main.py +++ b/app/main.py @@ -1,7 +1,7 @@ from fastapi import FastAPI from app.routes import search from app.routes import upload - +from app.routes import files app = FastAPI( title="WhereTF Backend" @@ -9,6 +9,7 @@ app.include_router(search.router) app.include_router(upload.router) +app.include_router(files.router) @app.get("/health", tags=["System"]) def health_check(): diff --git a/app/models/file.py b/app/models/file.py index cd02ede..8c3515e 100644 --- a/app/models/file.py +++ b/app/models/file.py @@ -19,8 +19,10 @@ class File(Base): indexed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) tags: Mapped[list[str]] = mapped_column(ARRAY(Text), server_default='{}') + + # NEWLY ADDED : The context column for storing user-defined descriptions + context: Mapped[str | None] = mapped_column(Text, nullable=True) - # Link to the contents table contents = relationship("FileContent", back_populates="file", cascade="all, delete-orphan") # Indexes from the image diff --git a/app/routes/files.py b/app/routes/files.py new file mode 100644 index 0000000..6f98cb1 --- /dev/null +++ b/app/routes/files.py @@ -0,0 +1,69 @@ +# code for file-idexing,addition and updation of tags and deleting a specific file + +import uuid +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from sqlalchemy import select +from pydantic import BaseModel +from typing import List, Optional + +from app.database import get_db +from app.models import File + +router = APIRouter(prefix="/files", tags=["files"]) + +# --- Pydantic Schema for the PATCH request --- +class FileUpdatePayload(BaseModel): + tags: Optional[List[str]] = None + context: Optional[str] = None + +# 1. THE DASHBOARD: Get all files +@router.get("/") +def get_all_files(db: Session = Depends(get_db)): + """Returns a list of all indexed files for the frontend UI.""" + files = db.execute(select(File)).scalars().all() + + return [ + { + "id": str(f.id), + "file_path": f.file_path, + "mime_type": f.mime_type, + "tags": f.tags, + "context": f.context, + "last_modified": f.last_modified + } + for f in files + ] + +# 2. THE METADATA MANAGER: Update tags and context +@router.patch("/{file_id}") +def update_file_metadata(file_id: uuid.UUID, payload: FileUpdatePayload, db: Session = Depends(get_db)): + """Updates the user-defined tags and context for a specific file.""" + db_file = db.scalar(select(File).where(File.id == file_id)) + + if not db_file: + raise HTTPException(status_code=404, detail="File not found") + + if payload.tags is not None: + db_file.tags = payload.tags + if payload.context is not None: + db_file.context = payload.context + + db.commit() + db.refresh(db_file) + + return {"status": "success", "message": "Metadata updated", "file_id": str(db_file.id)} + +# 3. THE ERASER: Delete a file and its vectors +@router.delete("/{file_id}") +def delete_file(file_id: uuid.UUID, db: Session = Depends(get_db)): + """Deletes a file and all its associated AI chunks automatically.""" + db_file = db.scalar(select(File).where(File.id == file_id)) + + if not db_file: + raise HTTPException(status_code=404, detail="File not found") + + db.delete(db_file) + db.commit() + + return {"status": "success", "message": f"File and vectors wiped successfully."} \ No newline at end of file