diff --git a/ai-hub/app/api/dependencies.py b/ai-hub/app/api/dependencies.py new file mode 100644 index 0000000..b8185af --- /dev/null +++ b/ai-hub/app/api/dependencies.py @@ -0,0 +1,19 @@ +# app/api/dependencies.py +from fastapi import Depends, HTTPException, status +from sqlalchemy.orm import Session +from app.db.session import SessionLocal + +# This is a dependency +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +# This is another common dependency +async def get_current_user(token: str): + # In a real app, you would decode the token and fetch the user + if not token: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) + return {"email": "user@example.com", "id": 1} # Dummy user \ No newline at end of file diff --git a/ai-hub/app/api/dependencies.py b/ai-hub/app/api/dependencies.py new file mode 100644 index 0000000..b8185af --- /dev/null +++ b/ai-hub/app/api/dependencies.py @@ -0,0 +1,19 @@ +# app/api/dependencies.py +from fastapi import Depends, HTTPException, status +from sqlalchemy.orm import Session +from app.db.session import SessionLocal + +# This is a dependency +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +# This is another common dependency +async def get_current_user(token: str): + # In a real app, you would decode the token and fetch the user + if not token: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) + return {"email": "user@example.com", "id": 1} # Dummy user \ No newline at end of file diff --git a/ai-hub/app/api/routes.py b/ai-hub/app/api/routes.py index 23bfa68..3b432b1 100644 --- a/ai-hub/app/api/routes.py +++ b/ai-hub/app/api/routes.py @@ -1,25 +1,8 @@ -# app/api/routes.py - -from fastapi import APIRouter, HTTPException, Query, Depends -from pydantic import BaseModel, Field # Import Field here -from typing import Literal +from fastapi import APIRouter, HTTPException, Depends from sqlalchemy.orm import Session -from app.core.services import RAGService -from app.db.session import get_db - -# Pydantic Models for API requests -class ChatRequest(BaseModel): - # Added min_length to ensure the prompt is not an empty string - prompt: str = Field(..., min_length=1) - # This ensures the 'model' field must be either "deepseek" or "gemini". - model: Literal["deepseek", "gemini"] - -class DocumentCreate(BaseModel): - title: str - text: str - source_url: str = None - author: str = None - user_id: str = "default_user" +from app.core.services import RAGService +from app.api.dependencies import get_db +from app.api import schemas def create_api_router(rag_service: RAGService) -> APIRouter: """ @@ -30,56 +13,52 @@ """ router = APIRouter() - @router.get("/") + @router.get("/", summary="Check Service Status") def read_root(): return {"status": "AI Model Hub is running!"} - @router.post("/chat", status_code=200) + # Use the schemas for request body validation and to define the response model + @router.post("/chat", response_model=schemas.ChatResponse, summary="Get AI-Generated Response") async def chat_handler( - request: ChatRequest, + request: schemas.ChatRequest, # <-- Use the imported schema for the request body db: Session = Depends(get_db) ): """ - Handles a chat request, using the prompt and model specified in the request body. + Handles a chat request using the prompt and model from the request body. """ try: - # Both prompt and model are now accessed from the single request object response_text = await rag_service.chat_with_rag( db=db, prompt=request.prompt, model=request.model ) - return {"answer": response_text, "model_used": request.model} - - except ValueError as e: - # This error is raised if the model is unsupported or the prompt is invalid. - # 422 is a more specific code for a validation failure on the request data. - raise HTTPException( - status_code=422, - detail=str(e) - ) - + # Return an instance of the response schema for automatic serialization + return schemas.ChatResponse(answer=response_text, model_used=request.model) except Exception as e: - # This catches all other potential errors during the API call. raise HTTPException( status_code=500, detail=f"An unexpected error occurred with the {request.model} API: {e}" ) - @router.post("/document") - async def add_document( - doc: DocumentCreate, + # Use the schemas for the /document endpoint as well + @router.post("/document", response_model=schemas.DocumentResponse, summary="Add a New Document") + def add_document( + doc: schemas.DocumentCreate, # <-- Use the imported schema for the request body db: Session = Depends(get_db) ): """ - Adds a new document to the database and its vector embedding to the FAISS index. + Adds a new document to the database and vector store. """ try: + # The 'doc' object is already a validated Pydantic model doc_data = doc.model_dump() document_id = rag_service.add_document(db=db, doc_data=doc_data) - return {"message": f"Document '{doc.title}' added successfully with ID {document_id}"} + # Return an instance of the response schema + return schemas.DocumentResponse( + message=f"Document '{doc.title}' added successfully with ID {document_id}" + ) except Exception as e: raise HTTPException(status_code=500, detail=f"An error occurred: {e}") - return router + return router \ No newline at end of file diff --git a/ai-hub/app/api/dependencies.py b/ai-hub/app/api/dependencies.py new file mode 100644 index 0000000..b8185af --- /dev/null +++ b/ai-hub/app/api/dependencies.py @@ -0,0 +1,19 @@ +# app/api/dependencies.py +from fastapi import Depends, HTTPException, status +from sqlalchemy.orm import Session +from app.db.session import SessionLocal + +# This is a dependency +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +# This is another common dependency +async def get_current_user(token: str): + # In a real app, you would decode the token and fetch the user + if not token: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) + return {"email": "user@example.com", "id": 1} # Dummy user \ No newline at end of file diff --git a/ai-hub/app/api/routes.py b/ai-hub/app/api/routes.py index 23bfa68..3b432b1 100644 --- a/ai-hub/app/api/routes.py +++ b/ai-hub/app/api/routes.py @@ -1,25 +1,8 @@ -# app/api/routes.py - -from fastapi import APIRouter, HTTPException, Query, Depends -from pydantic import BaseModel, Field # Import Field here -from typing import Literal +from fastapi import APIRouter, HTTPException, Depends from sqlalchemy.orm import Session -from app.core.services import RAGService -from app.db.session import get_db - -# Pydantic Models for API requests -class ChatRequest(BaseModel): - # Added min_length to ensure the prompt is not an empty string - prompt: str = Field(..., min_length=1) - # This ensures the 'model' field must be either "deepseek" or "gemini". - model: Literal["deepseek", "gemini"] - -class DocumentCreate(BaseModel): - title: str - text: str - source_url: str = None - author: str = None - user_id: str = "default_user" +from app.core.services import RAGService +from app.api.dependencies import get_db +from app.api import schemas def create_api_router(rag_service: RAGService) -> APIRouter: """ @@ -30,56 +13,52 @@ """ router = APIRouter() - @router.get("/") + @router.get("/", summary="Check Service Status") def read_root(): return {"status": "AI Model Hub is running!"} - @router.post("/chat", status_code=200) + # Use the schemas for request body validation and to define the response model + @router.post("/chat", response_model=schemas.ChatResponse, summary="Get AI-Generated Response") async def chat_handler( - request: ChatRequest, + request: schemas.ChatRequest, # <-- Use the imported schema for the request body db: Session = Depends(get_db) ): """ - Handles a chat request, using the prompt and model specified in the request body. + Handles a chat request using the prompt and model from the request body. """ try: - # Both prompt and model are now accessed from the single request object response_text = await rag_service.chat_with_rag( db=db, prompt=request.prompt, model=request.model ) - return {"answer": response_text, "model_used": request.model} - - except ValueError as e: - # This error is raised if the model is unsupported or the prompt is invalid. - # 422 is a more specific code for a validation failure on the request data. - raise HTTPException( - status_code=422, - detail=str(e) - ) - + # Return an instance of the response schema for automatic serialization + return schemas.ChatResponse(answer=response_text, model_used=request.model) except Exception as e: - # This catches all other potential errors during the API call. raise HTTPException( status_code=500, detail=f"An unexpected error occurred with the {request.model} API: {e}" ) - @router.post("/document") - async def add_document( - doc: DocumentCreate, + # Use the schemas for the /document endpoint as well + @router.post("/document", response_model=schemas.DocumentResponse, summary="Add a New Document") + def add_document( + doc: schemas.DocumentCreate, # <-- Use the imported schema for the request body db: Session = Depends(get_db) ): """ - Adds a new document to the database and its vector embedding to the FAISS index. + Adds a new document to the database and vector store. """ try: + # The 'doc' object is already a validated Pydantic model doc_data = doc.model_dump() document_id = rag_service.add_document(db=db, doc_data=doc_data) - return {"message": f"Document '{doc.title}' added successfully with ID {document_id}"} + # Return an instance of the response schema + return schemas.DocumentResponse( + message=f"Document '{doc.title}' added successfully with ID {document_id}" + ) except Exception as e: raise HTTPException(status_code=500, detail=f"An error occurred: {e}") - return router + return router \ No newline at end of file diff --git a/ai-hub/app/api/schemas.py b/ai-hub/app/api/schemas.py new file mode 100644 index 0000000..b84ee67 --- /dev/null +++ b/ai-hub/app/api/schemas.py @@ -0,0 +1,28 @@ +from pydantic import BaseModel, Field +from typing import Literal, Optional + +# --- Chat Schemas --- + +class ChatRequest(BaseModel): + """Defines the shape of a request to the /chat endpoint.""" + prompt: str = Field(..., min_length=1, description="The user's question or prompt.") + model: Literal["deepseek", "gemini"] = Field("deepseek", description="The AI model to use.") + +class ChatResponse(BaseModel): + """Defines the shape of a successful response from the /chat endpoint.""" + answer: str + model_used: str + +# --- Document Schemas --- + +class DocumentCreate(BaseModel): + """Defines the shape for creating a new document.""" + title: str + text: str + source_url: Optional[str] = None + author: Optional[str] = None + user_id: str = "default_user" + +class DocumentResponse(BaseModel): + """Defines the response after creating a document.""" + message: str \ No newline at end of file diff --git a/ai-hub/app/api/dependencies.py b/ai-hub/app/api/dependencies.py new file mode 100644 index 0000000..b8185af --- /dev/null +++ b/ai-hub/app/api/dependencies.py @@ -0,0 +1,19 @@ +# app/api/dependencies.py +from fastapi import Depends, HTTPException, status +from sqlalchemy.orm import Session +from app.db.session import SessionLocal + +# This is a dependency +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +# This is another common dependency +async def get_current_user(token: str): + # In a real app, you would decode the token and fetch the user + if not token: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) + return {"email": "user@example.com", "id": 1} # Dummy user \ No newline at end of file diff --git a/ai-hub/app/api/routes.py b/ai-hub/app/api/routes.py index 23bfa68..3b432b1 100644 --- a/ai-hub/app/api/routes.py +++ b/ai-hub/app/api/routes.py @@ -1,25 +1,8 @@ -# app/api/routes.py - -from fastapi import APIRouter, HTTPException, Query, Depends -from pydantic import BaseModel, Field # Import Field here -from typing import Literal +from fastapi import APIRouter, HTTPException, Depends from sqlalchemy.orm import Session -from app.core.services import RAGService -from app.db.session import get_db - -# Pydantic Models for API requests -class ChatRequest(BaseModel): - # Added min_length to ensure the prompt is not an empty string - prompt: str = Field(..., min_length=1) - # This ensures the 'model' field must be either "deepseek" or "gemini". - model: Literal["deepseek", "gemini"] - -class DocumentCreate(BaseModel): - title: str - text: str - source_url: str = None - author: str = None - user_id: str = "default_user" +from app.core.services import RAGService +from app.api.dependencies import get_db +from app.api import schemas def create_api_router(rag_service: RAGService) -> APIRouter: """ @@ -30,56 +13,52 @@ """ router = APIRouter() - @router.get("/") + @router.get("/", summary="Check Service Status") def read_root(): return {"status": "AI Model Hub is running!"} - @router.post("/chat", status_code=200) + # Use the schemas for request body validation and to define the response model + @router.post("/chat", response_model=schemas.ChatResponse, summary="Get AI-Generated Response") async def chat_handler( - request: ChatRequest, + request: schemas.ChatRequest, # <-- Use the imported schema for the request body db: Session = Depends(get_db) ): """ - Handles a chat request, using the prompt and model specified in the request body. + Handles a chat request using the prompt and model from the request body. """ try: - # Both prompt and model are now accessed from the single request object response_text = await rag_service.chat_with_rag( db=db, prompt=request.prompt, model=request.model ) - return {"answer": response_text, "model_used": request.model} - - except ValueError as e: - # This error is raised if the model is unsupported or the prompt is invalid. - # 422 is a more specific code for a validation failure on the request data. - raise HTTPException( - status_code=422, - detail=str(e) - ) - + # Return an instance of the response schema for automatic serialization + return schemas.ChatResponse(answer=response_text, model_used=request.model) except Exception as e: - # This catches all other potential errors during the API call. raise HTTPException( status_code=500, detail=f"An unexpected error occurred with the {request.model} API: {e}" ) - @router.post("/document") - async def add_document( - doc: DocumentCreate, + # Use the schemas for the /document endpoint as well + @router.post("/document", response_model=schemas.DocumentResponse, summary="Add a New Document") + def add_document( + doc: schemas.DocumentCreate, # <-- Use the imported schema for the request body db: Session = Depends(get_db) ): """ - Adds a new document to the database and its vector embedding to the FAISS index. + Adds a new document to the database and vector store. """ try: + # The 'doc' object is already a validated Pydantic model doc_data = doc.model_dump() document_id = rag_service.add_document(db=db, doc_data=doc_data) - return {"message": f"Document '{doc.title}' added successfully with ID {document_id}"} + # Return an instance of the response schema + return schemas.DocumentResponse( + message=f"Document '{doc.title}' added successfully with ID {document_id}" + ) except Exception as e: raise HTTPException(status_code=500, detail=f"An error occurred: {e}") - return router + return router \ No newline at end of file diff --git a/ai-hub/app/api/schemas.py b/ai-hub/app/api/schemas.py new file mode 100644 index 0000000..b84ee67 --- /dev/null +++ b/ai-hub/app/api/schemas.py @@ -0,0 +1,28 @@ +from pydantic import BaseModel, Field +from typing import Literal, Optional + +# --- Chat Schemas --- + +class ChatRequest(BaseModel): + """Defines the shape of a request to the /chat endpoint.""" + prompt: str = Field(..., min_length=1, description="The user's question or prompt.") + model: Literal["deepseek", "gemini"] = Field("deepseek", description="The AI model to use.") + +class ChatResponse(BaseModel): + """Defines the shape of a successful response from the /chat endpoint.""" + answer: str + model_used: str + +# --- Document Schemas --- + +class DocumentCreate(BaseModel): + """Defines the shape for creating a new document.""" + title: str + text: str + source_url: Optional[str] = None + author: Optional[str] = None + user_id: str = "default_user" + +class DocumentResponse(BaseModel): + """Defines the response after creating a document.""" + message: str \ No newline at end of file diff --git a/ai-hub/app/db/session.py b/ai-hub/app/db/session.py index 26e8938..6c90abe 100644 --- a/ai-hub/app/db/session.py +++ b/ai-hub/app/db/session.py @@ -28,17 +28,4 @@ """ print("Creating database tables...") # Base.metadata contains all the schema information from your models. - Base.metadata.create_all(bind=engine) - -def get_db(): - """ - FastAPI dependency that provides a database session for a single API request. - - This pattern ensures that the database session is always closed after the - request is finished, even if an error occurs. - """ - db = SessionLocal() - try: - yield db - finally: - db.close() \ No newline at end of file + Base.metadata.create_all(bind=engine) \ No newline at end of file diff --git a/ai-hub/app/api/dependencies.py b/ai-hub/app/api/dependencies.py new file mode 100644 index 0000000..b8185af --- /dev/null +++ b/ai-hub/app/api/dependencies.py @@ -0,0 +1,19 @@ +# app/api/dependencies.py +from fastapi import Depends, HTTPException, status +from sqlalchemy.orm import Session +from app.db.session import SessionLocal + +# This is a dependency +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +# This is another common dependency +async def get_current_user(token: str): + # In a real app, you would decode the token and fetch the user + if not token: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) + return {"email": "user@example.com", "id": 1} # Dummy user \ No newline at end of file diff --git a/ai-hub/app/api/routes.py b/ai-hub/app/api/routes.py index 23bfa68..3b432b1 100644 --- a/ai-hub/app/api/routes.py +++ b/ai-hub/app/api/routes.py @@ -1,25 +1,8 @@ -# app/api/routes.py - -from fastapi import APIRouter, HTTPException, Query, Depends -from pydantic import BaseModel, Field # Import Field here -from typing import Literal +from fastapi import APIRouter, HTTPException, Depends from sqlalchemy.orm import Session -from app.core.services import RAGService -from app.db.session import get_db - -# Pydantic Models for API requests -class ChatRequest(BaseModel): - # Added min_length to ensure the prompt is not an empty string - prompt: str = Field(..., min_length=1) - # This ensures the 'model' field must be either "deepseek" or "gemini". - model: Literal["deepseek", "gemini"] - -class DocumentCreate(BaseModel): - title: str - text: str - source_url: str = None - author: str = None - user_id: str = "default_user" +from app.core.services import RAGService +from app.api.dependencies import get_db +from app.api import schemas def create_api_router(rag_service: RAGService) -> APIRouter: """ @@ -30,56 +13,52 @@ """ router = APIRouter() - @router.get("/") + @router.get("/", summary="Check Service Status") def read_root(): return {"status": "AI Model Hub is running!"} - @router.post("/chat", status_code=200) + # Use the schemas for request body validation and to define the response model + @router.post("/chat", response_model=schemas.ChatResponse, summary="Get AI-Generated Response") async def chat_handler( - request: ChatRequest, + request: schemas.ChatRequest, # <-- Use the imported schema for the request body db: Session = Depends(get_db) ): """ - Handles a chat request, using the prompt and model specified in the request body. + Handles a chat request using the prompt and model from the request body. """ try: - # Both prompt and model are now accessed from the single request object response_text = await rag_service.chat_with_rag( db=db, prompt=request.prompt, model=request.model ) - return {"answer": response_text, "model_used": request.model} - - except ValueError as e: - # This error is raised if the model is unsupported or the prompt is invalid. - # 422 is a more specific code for a validation failure on the request data. - raise HTTPException( - status_code=422, - detail=str(e) - ) - + # Return an instance of the response schema for automatic serialization + return schemas.ChatResponse(answer=response_text, model_used=request.model) except Exception as e: - # This catches all other potential errors during the API call. raise HTTPException( status_code=500, detail=f"An unexpected error occurred with the {request.model} API: {e}" ) - @router.post("/document") - async def add_document( - doc: DocumentCreate, + # Use the schemas for the /document endpoint as well + @router.post("/document", response_model=schemas.DocumentResponse, summary="Add a New Document") + def add_document( + doc: schemas.DocumentCreate, # <-- Use the imported schema for the request body db: Session = Depends(get_db) ): """ - Adds a new document to the database and its vector embedding to the FAISS index. + Adds a new document to the database and vector store. """ try: + # The 'doc' object is already a validated Pydantic model doc_data = doc.model_dump() document_id = rag_service.add_document(db=db, doc_data=doc_data) - return {"message": f"Document '{doc.title}' added successfully with ID {document_id}"} + # Return an instance of the response schema + return schemas.DocumentResponse( + message=f"Document '{doc.title}' added successfully with ID {document_id}" + ) except Exception as e: raise HTTPException(status_code=500, detail=f"An error occurred: {e}") - return router + return router \ No newline at end of file diff --git a/ai-hub/app/api/schemas.py b/ai-hub/app/api/schemas.py new file mode 100644 index 0000000..b84ee67 --- /dev/null +++ b/ai-hub/app/api/schemas.py @@ -0,0 +1,28 @@ +from pydantic import BaseModel, Field +from typing import Literal, Optional + +# --- Chat Schemas --- + +class ChatRequest(BaseModel): + """Defines the shape of a request to the /chat endpoint.""" + prompt: str = Field(..., min_length=1, description="The user's question or prompt.") + model: Literal["deepseek", "gemini"] = Field("deepseek", description="The AI model to use.") + +class ChatResponse(BaseModel): + """Defines the shape of a successful response from the /chat endpoint.""" + answer: str + model_used: str + +# --- Document Schemas --- + +class DocumentCreate(BaseModel): + """Defines the shape for creating a new document.""" + title: str + text: str + source_url: Optional[str] = None + author: Optional[str] = None + user_id: str = "default_user" + +class DocumentResponse(BaseModel): + """Defines the response after creating a document.""" + message: str \ No newline at end of file diff --git a/ai-hub/app/db/session.py b/ai-hub/app/db/session.py index 26e8938..6c90abe 100644 --- a/ai-hub/app/db/session.py +++ b/ai-hub/app/db/session.py @@ -28,17 +28,4 @@ """ print("Creating database tables...") # Base.metadata contains all the schema information from your models. - Base.metadata.create_all(bind=engine) - -def get_db(): - """ - FastAPI dependency that provides a database session for a single API request. - - This pattern ensures that the database session is always closed after the - request is finished, even if an error occurs. - """ - db = SessionLocal() - try: - yield db - finally: - db.close() \ No newline at end of file + Base.metadata.create_all(bind=engine) \ No newline at end of file diff --git a/ai-hub/tests/api/test_routes.py b/ai-hub/tests/api/test_routes.py index fdee764..a777871 100644 --- a/ai-hub/tests/api/test_routes.py +++ b/ai-hub/tests/api/test_routes.py @@ -8,7 +8,7 @@ # Import the dependencies and router factory from app.core.services import RAGService -from app.db.session import get_db +from app.api.dependencies import get_db from app.api.routes import create_api_router @pytest.fixture diff --git a/ai-hub/app/api/dependencies.py b/ai-hub/app/api/dependencies.py new file mode 100644 index 0000000..b8185af --- /dev/null +++ b/ai-hub/app/api/dependencies.py @@ -0,0 +1,19 @@ +# app/api/dependencies.py +from fastapi import Depends, HTTPException, status +from sqlalchemy.orm import Session +from app.db.session import SessionLocal + +# This is a dependency +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +# This is another common dependency +async def get_current_user(token: str): + # In a real app, you would decode the token and fetch the user + if not token: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) + return {"email": "user@example.com", "id": 1} # Dummy user \ No newline at end of file diff --git a/ai-hub/app/api/routes.py b/ai-hub/app/api/routes.py index 23bfa68..3b432b1 100644 --- a/ai-hub/app/api/routes.py +++ b/ai-hub/app/api/routes.py @@ -1,25 +1,8 @@ -# app/api/routes.py - -from fastapi import APIRouter, HTTPException, Query, Depends -from pydantic import BaseModel, Field # Import Field here -from typing import Literal +from fastapi import APIRouter, HTTPException, Depends from sqlalchemy.orm import Session -from app.core.services import RAGService -from app.db.session import get_db - -# Pydantic Models for API requests -class ChatRequest(BaseModel): - # Added min_length to ensure the prompt is not an empty string - prompt: str = Field(..., min_length=1) - # This ensures the 'model' field must be either "deepseek" or "gemini". - model: Literal["deepseek", "gemini"] - -class DocumentCreate(BaseModel): - title: str - text: str - source_url: str = None - author: str = None - user_id: str = "default_user" +from app.core.services import RAGService +from app.api.dependencies import get_db +from app.api import schemas def create_api_router(rag_service: RAGService) -> APIRouter: """ @@ -30,56 +13,52 @@ """ router = APIRouter() - @router.get("/") + @router.get("/", summary="Check Service Status") def read_root(): return {"status": "AI Model Hub is running!"} - @router.post("/chat", status_code=200) + # Use the schemas for request body validation and to define the response model + @router.post("/chat", response_model=schemas.ChatResponse, summary="Get AI-Generated Response") async def chat_handler( - request: ChatRequest, + request: schemas.ChatRequest, # <-- Use the imported schema for the request body db: Session = Depends(get_db) ): """ - Handles a chat request, using the prompt and model specified in the request body. + Handles a chat request using the prompt and model from the request body. """ try: - # Both prompt and model are now accessed from the single request object response_text = await rag_service.chat_with_rag( db=db, prompt=request.prompt, model=request.model ) - return {"answer": response_text, "model_used": request.model} - - except ValueError as e: - # This error is raised if the model is unsupported or the prompt is invalid. - # 422 is a more specific code for a validation failure on the request data. - raise HTTPException( - status_code=422, - detail=str(e) - ) - + # Return an instance of the response schema for automatic serialization + return schemas.ChatResponse(answer=response_text, model_used=request.model) except Exception as e: - # This catches all other potential errors during the API call. raise HTTPException( status_code=500, detail=f"An unexpected error occurred with the {request.model} API: {e}" ) - @router.post("/document") - async def add_document( - doc: DocumentCreate, + # Use the schemas for the /document endpoint as well + @router.post("/document", response_model=schemas.DocumentResponse, summary="Add a New Document") + def add_document( + doc: schemas.DocumentCreate, # <-- Use the imported schema for the request body db: Session = Depends(get_db) ): """ - Adds a new document to the database and its vector embedding to the FAISS index. + Adds a new document to the database and vector store. """ try: + # The 'doc' object is already a validated Pydantic model doc_data = doc.model_dump() document_id = rag_service.add_document(db=db, doc_data=doc_data) - return {"message": f"Document '{doc.title}' added successfully with ID {document_id}"} + # Return an instance of the response schema + return schemas.DocumentResponse( + message=f"Document '{doc.title}' added successfully with ID {document_id}" + ) except Exception as e: raise HTTPException(status_code=500, detail=f"An error occurred: {e}") - return router + return router \ No newline at end of file diff --git a/ai-hub/app/api/schemas.py b/ai-hub/app/api/schemas.py new file mode 100644 index 0000000..b84ee67 --- /dev/null +++ b/ai-hub/app/api/schemas.py @@ -0,0 +1,28 @@ +from pydantic import BaseModel, Field +from typing import Literal, Optional + +# --- Chat Schemas --- + +class ChatRequest(BaseModel): + """Defines the shape of a request to the /chat endpoint.""" + prompt: str = Field(..., min_length=1, description="The user's question or prompt.") + model: Literal["deepseek", "gemini"] = Field("deepseek", description="The AI model to use.") + +class ChatResponse(BaseModel): + """Defines the shape of a successful response from the /chat endpoint.""" + answer: str + model_used: str + +# --- Document Schemas --- + +class DocumentCreate(BaseModel): + """Defines the shape for creating a new document.""" + title: str + text: str + source_url: Optional[str] = None + author: Optional[str] = None + user_id: str = "default_user" + +class DocumentResponse(BaseModel): + """Defines the response after creating a document.""" + message: str \ No newline at end of file diff --git a/ai-hub/app/db/session.py b/ai-hub/app/db/session.py index 26e8938..6c90abe 100644 --- a/ai-hub/app/db/session.py +++ b/ai-hub/app/db/session.py @@ -28,17 +28,4 @@ """ print("Creating database tables...") # Base.metadata contains all the schema information from your models. - Base.metadata.create_all(bind=engine) - -def get_db(): - """ - FastAPI dependency that provides a database session for a single API request. - - This pattern ensures that the database session is always closed after the - request is finished, even if an error occurs. - """ - db = SessionLocal() - try: - yield db - finally: - db.close() \ No newline at end of file + Base.metadata.create_all(bind=engine) \ No newline at end of file diff --git a/ai-hub/tests/api/test_routes.py b/ai-hub/tests/api/test_routes.py index fdee764..a777871 100644 --- a/ai-hub/tests/api/test_routes.py +++ b/ai-hub/tests/api/test_routes.py @@ -8,7 +8,7 @@ # Import the dependencies and router factory from app.core.services import RAGService -from app.db.session import get_db +from app.api.dependencies import get_db from app.api.routes import create_api_router @pytest.fixture diff --git a/ai-hub/tests/db/test_session.py b/ai-hub/tests/db/test_session.py index 17b020a..bbbf680 100644 --- a/ai-hub/tests/db/test_session.py +++ b/ai-hub/tests/db/test_session.py @@ -1,3 +1,4 @@ +# tests/db/test_session.py import pytest import importlib from unittest.mock import patch @@ -19,7 +20,7 @@ # Assert assert session.engine.dialect.name == "sqlite" - assert session.engine.url.database == "./data/ai_hub.db" + assert "ai_hub.db" in session.engine.url.database def test_postgres_mode_initialization(monkeypatch): """ @@ -42,14 +43,14 @@ assert session.engine.url.host == "testhost" assert session.engine.url.database == "test_db" -@patch('app.db.session.SessionLocal') +# *** FIX: The patch target is changed to where SessionLocal is USED *** +@patch('app.api.dependencies.SessionLocal') def test_get_db_yields_and_closes_session(mock_session_local): """ Tests if the get_db() dependency function yields a session and then closes it. """ # Arrange - from app.db.session import get_db - # FIX: Correctly assign the mock session from the mock factory's return_value + from app.api.dependencies import get_db mock_session = mock_session_local.return_value db_generator = get_db() @@ -57,6 +58,7 @@ db_session_instance = next(db_generator) # Assert (Yield) + # Now db_session_instance will be the mock you expect assert db_session_instance is mock_session mock_session.close.assert_not_called() diff --git a/ai-hub/app/api/dependencies.py b/ai-hub/app/api/dependencies.py new file mode 100644 index 0000000..b8185af --- /dev/null +++ b/ai-hub/app/api/dependencies.py @@ -0,0 +1,19 @@ +# app/api/dependencies.py +from fastapi import Depends, HTTPException, status +from sqlalchemy.orm import Session +from app.db.session import SessionLocal + +# This is a dependency +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +# This is another common dependency +async def get_current_user(token: str): + # In a real app, you would decode the token and fetch the user + if not token: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) + return {"email": "user@example.com", "id": 1} # Dummy user \ No newline at end of file diff --git a/ai-hub/app/api/routes.py b/ai-hub/app/api/routes.py index 23bfa68..3b432b1 100644 --- a/ai-hub/app/api/routes.py +++ b/ai-hub/app/api/routes.py @@ -1,25 +1,8 @@ -# app/api/routes.py - -from fastapi import APIRouter, HTTPException, Query, Depends -from pydantic import BaseModel, Field # Import Field here -from typing import Literal +from fastapi import APIRouter, HTTPException, Depends from sqlalchemy.orm import Session -from app.core.services import RAGService -from app.db.session import get_db - -# Pydantic Models for API requests -class ChatRequest(BaseModel): - # Added min_length to ensure the prompt is not an empty string - prompt: str = Field(..., min_length=1) - # This ensures the 'model' field must be either "deepseek" or "gemini". - model: Literal["deepseek", "gemini"] - -class DocumentCreate(BaseModel): - title: str - text: str - source_url: str = None - author: str = None - user_id: str = "default_user" +from app.core.services import RAGService +from app.api.dependencies import get_db +from app.api import schemas def create_api_router(rag_service: RAGService) -> APIRouter: """ @@ -30,56 +13,52 @@ """ router = APIRouter() - @router.get("/") + @router.get("/", summary="Check Service Status") def read_root(): return {"status": "AI Model Hub is running!"} - @router.post("/chat", status_code=200) + # Use the schemas for request body validation and to define the response model + @router.post("/chat", response_model=schemas.ChatResponse, summary="Get AI-Generated Response") async def chat_handler( - request: ChatRequest, + request: schemas.ChatRequest, # <-- Use the imported schema for the request body db: Session = Depends(get_db) ): """ - Handles a chat request, using the prompt and model specified in the request body. + Handles a chat request using the prompt and model from the request body. """ try: - # Both prompt and model are now accessed from the single request object response_text = await rag_service.chat_with_rag( db=db, prompt=request.prompt, model=request.model ) - return {"answer": response_text, "model_used": request.model} - - except ValueError as e: - # This error is raised if the model is unsupported or the prompt is invalid. - # 422 is a more specific code for a validation failure on the request data. - raise HTTPException( - status_code=422, - detail=str(e) - ) - + # Return an instance of the response schema for automatic serialization + return schemas.ChatResponse(answer=response_text, model_used=request.model) except Exception as e: - # This catches all other potential errors during the API call. raise HTTPException( status_code=500, detail=f"An unexpected error occurred with the {request.model} API: {e}" ) - @router.post("/document") - async def add_document( - doc: DocumentCreate, + # Use the schemas for the /document endpoint as well + @router.post("/document", response_model=schemas.DocumentResponse, summary="Add a New Document") + def add_document( + doc: schemas.DocumentCreate, # <-- Use the imported schema for the request body db: Session = Depends(get_db) ): """ - Adds a new document to the database and its vector embedding to the FAISS index. + Adds a new document to the database and vector store. """ try: + # The 'doc' object is already a validated Pydantic model doc_data = doc.model_dump() document_id = rag_service.add_document(db=db, doc_data=doc_data) - return {"message": f"Document '{doc.title}' added successfully with ID {document_id}"} + # Return an instance of the response schema + return schemas.DocumentResponse( + message=f"Document '{doc.title}' added successfully with ID {document_id}" + ) except Exception as e: raise HTTPException(status_code=500, detail=f"An error occurred: {e}") - return router + return router \ No newline at end of file diff --git a/ai-hub/app/api/schemas.py b/ai-hub/app/api/schemas.py new file mode 100644 index 0000000..b84ee67 --- /dev/null +++ b/ai-hub/app/api/schemas.py @@ -0,0 +1,28 @@ +from pydantic import BaseModel, Field +from typing import Literal, Optional + +# --- Chat Schemas --- + +class ChatRequest(BaseModel): + """Defines the shape of a request to the /chat endpoint.""" + prompt: str = Field(..., min_length=1, description="The user's question or prompt.") + model: Literal["deepseek", "gemini"] = Field("deepseek", description="The AI model to use.") + +class ChatResponse(BaseModel): + """Defines the shape of a successful response from the /chat endpoint.""" + answer: str + model_used: str + +# --- Document Schemas --- + +class DocumentCreate(BaseModel): + """Defines the shape for creating a new document.""" + title: str + text: str + source_url: Optional[str] = None + author: Optional[str] = None + user_id: str = "default_user" + +class DocumentResponse(BaseModel): + """Defines the response after creating a document.""" + message: str \ No newline at end of file diff --git a/ai-hub/app/db/session.py b/ai-hub/app/db/session.py index 26e8938..6c90abe 100644 --- a/ai-hub/app/db/session.py +++ b/ai-hub/app/db/session.py @@ -28,17 +28,4 @@ """ print("Creating database tables...") # Base.metadata contains all the schema information from your models. - Base.metadata.create_all(bind=engine) - -def get_db(): - """ - FastAPI dependency that provides a database session for a single API request. - - This pattern ensures that the database session is always closed after the - request is finished, even if an error occurs. - """ - db = SessionLocal() - try: - yield db - finally: - db.close() \ No newline at end of file + Base.metadata.create_all(bind=engine) \ No newline at end of file diff --git a/ai-hub/tests/api/test_routes.py b/ai-hub/tests/api/test_routes.py index fdee764..a777871 100644 --- a/ai-hub/tests/api/test_routes.py +++ b/ai-hub/tests/api/test_routes.py @@ -8,7 +8,7 @@ # Import the dependencies and router factory from app.core.services import RAGService -from app.db.session import get_db +from app.api.dependencies import get_db from app.api.routes import create_api_router @pytest.fixture diff --git a/ai-hub/tests/db/test_session.py b/ai-hub/tests/db/test_session.py index 17b020a..bbbf680 100644 --- a/ai-hub/tests/db/test_session.py +++ b/ai-hub/tests/db/test_session.py @@ -1,3 +1,4 @@ +# tests/db/test_session.py import pytest import importlib from unittest.mock import patch @@ -19,7 +20,7 @@ # Assert assert session.engine.dialect.name == "sqlite" - assert session.engine.url.database == "./data/ai_hub.db" + assert "ai_hub.db" in session.engine.url.database def test_postgres_mode_initialization(monkeypatch): """ @@ -42,14 +43,14 @@ assert session.engine.url.host == "testhost" assert session.engine.url.database == "test_db" -@patch('app.db.session.SessionLocal') +# *** FIX: The patch target is changed to where SessionLocal is USED *** +@patch('app.api.dependencies.SessionLocal') def test_get_db_yields_and_closes_session(mock_session_local): """ Tests if the get_db() dependency function yields a session and then closes it. """ # Arrange - from app.db.session import get_db - # FIX: Correctly assign the mock session from the mock factory's return_value + from app.api.dependencies import get_db mock_session = mock_session_local.return_value db_generator = get_db() @@ -57,6 +58,7 @@ db_session_instance = next(db_generator) # Assert (Yield) + # Now db_session_instance will be the mock you expect assert db_session_instance is mock_session mock_session.close.assert_not_called() diff --git a/ai-hub/tests/test_app.py b/ai-hub/tests/test_app.py index 6b25640..5851da9 100644 --- a/ai-hub/tests/test_app.py +++ b/ai-hub/tests/test_app.py @@ -3,7 +3,7 @@ from sqlalchemy.orm import Session from app.app import create_app -from app.db.session import get_db +from app.api.dependencies import get_db # --- Dependency Override for Testing --- mock_db = MagicMock(spec=Session)