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
4 changes: 1 addition & 3 deletions app/alembic/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,7 @@ def run_migrations_online() -> None:
)

with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata
)
context.configure(connection=connection, target_metadata=target_metadata)

with context.begin_transaction():
context.run_migrations()
Expand Down
40 changes: 40 additions & 0 deletions app/alembic/versions/417469bac4de_removed_auth_type_from_otp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""removed auth-type from OTP

Revision ID: 417469bac4de
Revises: a6d1c6c13842
Create Date: 2025-08-31 09:57:49.661025

"""

from typing import Sequence, Union

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision: str = "417469bac4de"
down_revision: Union[str, Sequence[str], None] = "a6d1c6c13842"
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.drop_column("otps", "provider_id")
op.drop_column("otps", "auth_type")
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"otps",
sa.Column("auth_type", sa.VARCHAR(), autoincrement=False, nullable=False),
)
op.add_column(
"otps",
sa.Column("provider_id", sa.VARCHAR(), autoincrement=False, nullable=True),
)
# ### end Alembic commands ###
40 changes: 21 additions & 19 deletions app/alembic/versions/a4b5fabfdeac_add_otp_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,43 +5,45 @@
Create Date: 2025-07-24 18:30:31.802426

"""

from typing import Sequence, Union

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision: str = 'a4b5fabfdeac'
down_revision: Union[str, Sequence[str], None] = 'ccf5dc612027'
revision: str = "a4b5fabfdeac"
down_revision: Union[str, Sequence[str], None] = "ccf5dc612027"
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('otps',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(), nullable=False),
sa.Column('otp_code', sa.String(), nullable=False),
sa.Column('name', sa.String(), nullable=False),
sa.Column('password', sa.String(), nullable=True),
sa.Column('is_active', sa.Integer(), nullable=True),
sa.Column('provider_id', sa.String(), nullable=True),
sa.Column('auth_type', sa.String(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('expires_at', sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint('id')
op.create_table(
"otps",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("email", sa.String(), nullable=False),
sa.Column("otp_code", sa.String(), nullable=False),
sa.Column("name", sa.String(), nullable=False),
sa.Column("password", sa.String(), nullable=True),
sa.Column("is_active", sa.Integer(), nullable=True),
sa.Column("provider_id", sa.String(), nullable=True),
sa.Column("auth_type", sa.String(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=True),
sa.Column("expires_at", sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f('ix_otps_email'), 'otps', ['email'], unique=False)
op.create_index(op.f('ix_otps_id'), 'otps', ['id'], unique=False)
op.create_index(op.f("ix_otps_email"), "otps", ["email"], unique=False)
op.create_index(op.f("ix_otps_id"), "otps", ["id"], unique=False)
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_otps_id'), table_name='otps')
op.drop_index(op.f('ix_otps_email'), table_name='otps')
op.drop_table('otps')
op.drop_index(op.f("ix_otps_id"), table_name="otps")
op.drop_index(op.f("ix_otps_email"), table_name="otps")
op.drop_table("otps")
# ### end Alembic commands ###
38 changes: 38 additions & 0 deletions app/alembic/versions/a6d1c6c13842_add_role_field.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""add role field

Revision ID: a6d1c6c13842
Revises: a4b5fabfdeac
Create Date: 2025-08-31 09:15:05.419403

"""

from typing import Sequence, Union

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision: str = "a6d1c6c13842"
down_revision: Union[str, Sequence[str], None] = "a4b5fabfdeac"
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! ###
userrole_enum = sa.Enum("ADMIN", "USER", name="userrole")
userrole_enum.create(op.get_bind())

# Then add the column using the enum type
op.add_column("users", sa.Column("role", userrole_enum, nullable=False))
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("users", "role")
# Then drop the enum type
sa.Enum(name="userrole").drop(op.get_bind())
# ### end Alembic commands ###
4 changes: 3 additions & 1 deletion app/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@
api_router = APIRouter()

# TODO: Add routes as needed
api_router.include_router(questions.router, prefix="/questions", tags=["question_generation"])
api_router.include_router(
questions.router, prefix="/questions", tags=["question_generation"]
)
30 changes: 24 additions & 6 deletions app/api/routes/questions.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import json

from fastapi import APIRouter

from app.schemas.utils import UserPrompt
from app.services.questions import add_daily_question_to_mongodb
from app.services.questions import \
add_daily_question_to_store as add_question_in_chroma_db
from app.schemas.utils import TechDescription, UserPrompt
from app.services.questions import (add_daily_question_to_mongodb,
add_tech_description_to_store)
from app.services.questions import \
generate_daily_question as generate_daily_question_service
from app.services.questions import get_most_recent_daily_question
Expand All @@ -18,9 +19,26 @@ def generate_daily_question(request: UserPrompt):
or user's input.
"""
response = generate_daily_question_service(user_prompt=request.user_prompt)
add_daily_question_to_mongodb(response) # Store question in MongoDB
try:
question = json.loads(response) # Parse the JSON string to a dictionary
except json.JSONDecodeError:
return {"error": "Failed to parse the generated question."}
add_daily_question_to_mongodb(question) # Store question in MongoDB
return response


@router.post("/add_tech_description")
def add_tech_description(request: TechDescription):
"""
Add a technical description to the vector store.
Whenever admin add a new tech description/Tag, it will be stored in ChromaDB.
"""
add_tech_description_to_store(
description=request.description, metadata=request.metadata
)
return {"message": "Tech description added successfully."}


@router.get("/get_daily_question")
def get_daily_question():
"""
Expand All @@ -29,4 +47,4 @@ def get_daily_question():
recent_question = get_most_recent_daily_question()
if recent_question:
return recent_question
return {"message": "No daily question found."}
return {"message": "No daily question found."}
1 change: 1 addition & 0 deletions app/config/config_mongo_fb.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def __init__(self, uri: str, db_name: str):
def get_collection(self, collection_name: str):
return self.db[collection_name]


# MongoDB connection setup
MONGO_URI = "mongodb://mongodb:27017"
MONGO_DB_NAME = "questions_db"
Expand Down
19 changes: 11 additions & 8 deletions app/config/vectorestore.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@
# Using singleton pattern for ChromaDB to ensure a single instance is used across the application
class ChromaDB:
"""Singleton class for managing ChromaDB collections and operations."""
_instance = None

_instance = None

def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance

def __init__(self):
self.path = settings.CHROMA_PATH
self.model = settings.EMBEDDING_MODEL
Expand All @@ -26,7 +27,7 @@ def __init__(self):
self._client = PersistentClient(path=self.path)

self._collections: Dict[str, Chroma] = {}

def get_collection(self, name: str) -> Chroma:
"""Get or create a Chroma-backed collection."""
if name not in self._collections:
Expand All @@ -36,21 +37,23 @@ def get_collection(self, name: str) -> Chroma:
embedding_function=self._embeddings,
)
return self._collections[name]

def add_documents(self, collection: str, documents: List[Document]) -> None:
"""
Add documents to the ChromaDB collection.
"""
store = self.get_collection(collection)
store.add_documents(documents)

def as_retriever(self,collection: str, search_kwargs: Optional[Dict[str, Any]] = None) -> Any:

def as_retriever(
self, collection: str, search_kwargs: Optional[Dict[str, Any]] = None
) -> Any:
"""
Get a retriever for the ChromaDB collection.
search_kwargs: can be used to specify additional search parameters like 'k' for KNN.
"""
store = self.get_collection(collection)
return store.as_retriever(search_kwargs=search_kwargs)


chroma_db = ChromaDB()

chroma_db = ChromaDB()
9 changes: 6 additions & 3 deletions app/llm/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ def build_chat_model() -> ChatHuggingFace:
)
return ChatHuggingFace(llm=llm)

#TODO: Improve this function

# TODO: Improve this function
def invoke_with_retries(chain, payload: Dict[str, Any]):
last_err = None
for attempt in range(1, settings.LLM_RETRIES + 1):
Expand All @@ -26,5 +27,7 @@ def invoke_with_retries(chain, payload: Dict[str, Any]):
except Exception as e: # noqa: BLE001
last_err = e
# Exponential backoff with cap
time.sleep(min(2 ** attempt, 8))
raise RuntimeError(f"LLM invocation failed after {settings.LLM_RETRIES} attempts") from last_err
time.sleep(min(2**attempt, 8))
raise RuntimeError(
f"LLM invocation failed after {settings.LLM_RETRIES} attempts"
) from last_err
14 changes: 3 additions & 11 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,13 @@
app = FastAPI(
title="Generator Service",
description="API for generating and managing coding problems",
version="0.1.0"
)

# CORS middleware configuration
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # In production, replace with specific origins
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
version="0.1.0",
)

app.include_router(api_router, prefix=settings.API_V1_STR, tags=["v1"])


# Health check endpoint
@app.get("/health")
async def health_check():
return {"status": "healthy", "service": "JamAndFlow-generator"}
return {"status": "healthy", "service": "JamAndFlow-generator"}
2 changes: 1 addition & 1 deletion app/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Add models here to be imported by Alembic
# example:
# from .models.user import User # noqa: F401, F403
# from .models.user import User # noqa: F401, F403
24 changes: 14 additions & 10 deletions app/pipelines/daily_question_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,21 @@

# using tech_description collection to retrieve context.
# It will return top k relevant document based on the user prompt.
# The context structure stores tech-stack and relevant real-world concepts
# The context structure stores tech-stack and relevant real-world concepts
# On which the question can be framed.
retriever = chroma_db.as_retriever(collection="tech_description",
search_kwargs={"k": settings.RETRIEVE_K})
retriever = chroma_db.as_retriever(
collection="tech_description", search_kwargs={"k": settings.RETRIEVE_K}
)

prompt = ChatPromptTemplate.from_messages(
[
SystemMessagePromptTemplate.from_template(system_template),
HumanMessagePromptTemplate.from_template(human_template),
]
)

prompt = ChatPromptTemplate.from_messages([
SystemMessagePromptTemplate.from_template(system_template),
HumanMessagePromptTemplate.from_template(human_template),
])

#TODO: we might required a context for last n question for uniquness and variety
# TODO: we might required a context for last n question for uniquness and variety
# But for now the concern is on the limit of the tokens hence not implementing it
def _retrieve_context(inputs: Dict):
"""It will only trigger when user gives a contet if not it will return 'No extra context'"""
Expand All @@ -33,9 +37,9 @@ def _retrieve_context(inputs: Dict):
context = " ".join([d.page_content for d in docs]) if docs else "No extra context"
return {"context": context, "user_prompt": inputs["user_prompt"]}


context_retriever = RunnableParallel(
user_question=lambda x: x["user_prompt"],
context=_retrieve_context
user_question=lambda x: x["user_prompt"], context=_retrieve_context
)

chat_model = build_chat_model()
Expand Down
Loading