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
1 change: 1 addition & 0 deletions alembic/script.py.mako
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
42 changes: 0 additions & 42 deletions alembic/versions/2f98f76cb765_create_file_relationships_table.py

This file was deleted.

46 changes: 0 additions & 46 deletions alembic/versions/c1850b307113_create_files_table.py

This file was deleted.

46 changes: 0 additions & 46 deletions alembic/versions/cdcdffd36592_create_file_content_table.py

This file was deleted.

75 changes: 75 additions & 0 deletions alembic/versions/ecca7b879961_genesis_migration.py
Original file line number Diff line number Diff line change
@@ -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 ###
3 changes: 2 additions & 1 deletion app/main.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
from fastapi import FastAPI
from app.routes import search
from app.routes import upload

from app.routes import files

app = FastAPI(
title="WhereTF Backend"
)

app.include_router(search.router)
app.include_router(upload.router)
app.include_router(files.router)

@app.get("/health", tags=["System"])
def health_check():
Expand Down
4 changes: 3 additions & 1 deletion app/models/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
69 changes: 69 additions & 0 deletions app/routes/files.py
Original file line number Diff line number Diff line change
@@ -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."}
Loading