diff --git a/ai-hub/app/api/dependencies.py b/ai-hub/app/api/dependencies.py index e410b71..8a0a3bd 100644 --- a/ai-hub/app/api/dependencies.py +++ b/ai-hub/app/api/dependencies.py @@ -1,14 +1,11 @@ # app/api/dependencies.py from fastapi import Depends, HTTPException, status -from typing import List +from typing import List,Any from sqlalchemy.orm import Session from app.db.session import SessionLocal from app.core.retrievers.base_retriever import Retriever from app.core.services.document import DocumentService from app.core.services.rag import RAGService -from app.core.services.tts import TTSService -from app.core.services.stt import STTService -from app.core.services.workspace import WorkspaceService from app.core.vector_store.faiss_store import FaissVectorStore @@ -29,12 +26,47 @@ class ServiceContainer: - def __init__(self, vector_store: FaissVectorStore, retrievers: List[Retriever], tts_service: TTSService, stt_service: STTService, workspace_service: WorkspaceService): - # Initialize all services within the container + """ + A flexible container for managing and providing various application services. + Services are added dynamically using the `with_service` method. + """ + def __init__(self): + # Use a dictionary to store services, mapping their names to instances + self._services = {} + self.document_service = None + self.rag_service = None + + def with_service(self, name: str, service: Any): + """ + Adds a service to the container. + + Args: + name (str): The name to assign to the service (e.g., 'tts_service'). + service (Any): The service instance to add. + """ + setattr(self, name, service) + return self + + def with_document_service(self, vector_store: FaissVectorStore): + """ + Adds a DocumentService instance to the container. + """ self.document_service = DocumentService(vector_store=vector_store) - self.rag_service = RAGService( - retrievers=retrievers - ) - self.tts_service = tts_service - self.stt_service = stt_service - self.workspace_service= workspace_service + return self + + def with_rag_service(self, retrievers: List[Retriever]): + """ + Adds a RAGService instance to the container. + """ + self.rag_service = RAGService(retrievers=retrievers) + return self + + def __getattr__(self, name: str) -> Any: + """ + Allows services to be accessed directly as attributes (e.g., container.rag_service). + """ + # This allows direct access to services that are not explicitly defined in __init__ + try: + return self.__getattribute__(name) + except AttributeError: + raise AttributeError(f"'{self.__class__.__name__}' object has no service named '{name}'") \ No newline at end of file diff --git a/ai-hub/app/api/routes/sessions.py b/ai-hub/app/api/routes/sessions.py index 5ff6fb6..c69c5ea 100644 --- a/ai-hub/app/api/routes/sessions.py +++ b/ai-hub/app/api/routes/sessions.py @@ -13,7 +13,7 @@ db: Session = Depends(get_db) ): try: - new_session = services.rag_service.create_session( + new_session = services.session_service.create_session( db=db, user_id=request.user_id, provider_name=request.provider_name diff --git a/ai-hub/app/app.py b/ai-hub/app/app.py index 1297a9a..a6c782c 100644 --- a/ai-hub/app/app.py +++ b/ai-hub/app/app.py @@ -14,6 +14,7 @@ from app.api.routes.api import create_api_router from app.utils import print_config from app.api.dependencies import ServiceContainer +from app.core.services.session import SessionService from app.core.services.tts import TTSService from app.core.services.stt import STTService # NEW: Added the missing import for STTService from app.core.services.workspace import WorkspaceService # NEW: Added the missing import for STTService @@ -87,9 +88,6 @@ model_name = settings.TTS_MODEL_NAME, voice_name=settings.TTS_VOICE_NAME ) - - # 5. Initialize the TTSService - tts_service = TTSService(tts_provider=tts_provider) # 6. Get the concrete STT provider from the factory stt_provider = get_stt_provider( @@ -97,21 +95,16 @@ api_key=settings.STT_API_KEY, model_name=settings.STT_MODEL_NAME ) - # 7. Initialize the STTService - stt_service = STTService(stt_provider=stt_provider) - - # 8. Initialize the WorkspaceService - workspace_service = WorkspaceService() # 9. Initialize the Service Container with all services # This replaces the previous, redundant initialization - services = ServiceContainer( - vector_store=vector_store, - retrievers=retrievers, - tts_service=tts_service, - stt_service=stt_service, - workspace_service=workspace_service - ) + services = ServiceContainer() + services.with_rag_service(retrievers=retrievers) + services.with_document_service(vector_store=vector_store) + services.with_service("stt_service",service=STTService(stt_provider=stt_provider)) + services.with_service("tts_service",service=TTSService(tts_provider=tts_provider)) + services.with_service("workspace_service", service=WorkspaceService()) + services.with_service("session_service", service=SessionService()) # Create and include the API router, injecting the service api_router = create_api_router(services=services) diff --git a/ai-hub/app/core/services/rag.py b/ai-hub/app/core/services/rag.py index 4965630..1f4848c 100644 --- a/ai-hub/app/core/services/rag.py +++ b/ai-hub/app/core/services/rag.py @@ -1,10 +1,8 @@ import asyncio -from typing import List, Dict, Any, Tuple +from typing import List, Tuple from sqlalchemy.orm import Session, joinedload -from sqlalchemy.exc import SQLAlchemyError import dspy -from app.core.vector_store.faiss_store import FaissVectorStore from app.db import models from app.core.retrievers.faiss_db_retriever import FaissDBRetriever from app.core.retrievers.base_retriever import Retriever @@ -13,25 +11,12 @@ class RAGService: """ - Service class for managing conversational RAG sessions. - This class orchestrates the RAG pipeline and manages chat sessions. + Service for orchestrating conversational RAG pipelines. + Manages chat interactions and message history for a session. """ - def __init__(self, retrievers: List[Retriever]): + def __init__(self, retrievers: List[Retriever]): self.retrievers = retrievers self.faiss_retriever = next((r for r in retrievers if isinstance(r, FaissDBRetriever)), None) - # --- Session Management --- - - def create_session(self, db: Session, user_id: str, provider_name: str) -> models.Session: - """Creates a new chat session in the database.""" - try: - new_session = models.Session(user_id=user_id, provider_name=provider_name, title=f"New Chat Session") - db.add(new_session) - db.commit() - db.refresh(new_session) - return new_session - except SQLAlchemyError as e: - db.rollback() - raise async def chat_with_rag( self, @@ -42,7 +27,7 @@ load_faiss_retriever: bool = False ) -> Tuple[str, str]: """ - Handles a message within a session, including saving history and getting a response. + Processes a user prompt within a session, saves the chat history, and returns a response. """ session = db.query(models.Session).options( joinedload(models.Session.messages) @@ -51,13 +36,16 @@ if not session: raise ValueError(f"Session with ID {session_id} not found.") + # Save user message user_message = models.Message(session_id=session_id, sender="user", content=prompt) db.add(user_message) db.commit() db.refresh(user_message) + # Get the appropriate LLM provider llm_provider = get_llm_provider(provider_name) + # Configure retrievers for the pipeline current_retrievers = [] if load_faiss_retriever: if self.faiss_retriever: @@ -67,7 +55,7 @@ rag_pipeline = DspyRagPipeline(retrievers=current_retrievers) - # Use dspy.context to configure the language model for this specific async task + # Run the RAG pipeline to get a response with dspy.context(lm=llm_provider): answer_text = await rag_pipeline.forward( question=prompt, @@ -75,6 +63,7 @@ db=db ) + # Save assistant's response assistant_message = models.Message(session_id=session_id, sender="assistant", content=answer_text) db.add(assistant_message) db.commit() @@ -84,7 +73,7 @@ def get_message_history(self, db: Session, session_id: int) -> List[models.Message]: """ - Retrieves all messages for a given session. + Retrieves all messages for a given session, ordered by creation time. """ session = db.query(models.Session).options( joinedload(models.Session.messages) diff --git a/ai-hub/app/core/services/session.py b/ai-hub/app/core/services/session.py new file mode 100644 index 0000000..d1b25a1 --- /dev/null +++ b/ai-hub/app/core/services/session.py @@ -0,0 +1,34 @@ +# app/core/session.py + +from sqlalchemy.orm import Session +from sqlalchemy.exc import SQLAlchemyError +from app.db import models + +class SessionService: + def __init__(self): + pass + + def create_session(self, db: Session, user_id: str, provider_name: str) -> models.Session: + """ + Creates a new chat session in the database. + + Args: + db (Session): The SQLAlchemy database session. + user_id (str): The ID of the user creating the session. + provider_name (str): The name of the LLM provider for the session. + + Returns: + models.Session: The newly created session object. + + Raises: + SQLAlchemyError: If a database error occurs during session creation. + """ + try: + new_session = models.Session(user_id=user_id, provider_name=provider_name, title=f"New Chat Session") + db.add(new_session) + db.commit() + db.refresh(new_session) + return new_session + except SQLAlchemyError as e: + db.rollback() + raise \ No newline at end of file diff --git a/ai-hub/integration_tests/conftest.py b/ai-hub/integration_tests/conftest.py index 60a854d..6be7916 100644 --- a/ai-hub/integration_tests/conftest.py +++ b/ai-hub/integration_tests/conftest.py @@ -1,7 +1,7 @@ import httpx import pytest_asyncio -BASE_URL = "http://127.0.0.1:8000" +BASE_URL = "http://127.0.0.1:8001" @pytest_asyncio.fixture(scope="session") def base_url(): diff --git a/ai-hub/run_integration_tests.sh b/ai-hub/run_integration_tests.sh index 5a2ecb8..a2102ac 100644 --- a/ai-hub/run_integration_tests.sh +++ b/ai-hub/run_integration_tests.sh @@ -2,6 +2,9 @@ # A script to automate running tests locally. # It starts the FastAPI server, runs the specified tests, and then shuts down the server. +# +# To run without starting the server, use the --skip-server flag: +# ./run_tests.sh --skip-server # --- Configuration --- # You can define aliases for your test file paths here. @@ -23,6 +26,14 @@ export FAISS_INDEX_PATH="data/integration_test_faiss_index.bin" export LOG_LEVEL="DEBUG" +# Check for the --skip-server flag as an inline parameter. +# The user can pass this as the first argument to the script. +SKIP_SERVER=false +if [ "$1" == "--skip-server" ]; then + SKIP_SERVER=true + shift # Remove the --skip-server argument from the list +fi + # --- User Interaction --- echo "--- AI Hub Test Runner ---" echo "Select a test suite to run:" @@ -44,43 +55,49 @@ TEST_PATH=${TEST_PATHS[0]} fi -# --- Pre-test Cleanup --- -# Check for and remove old test files to ensure a clean test environment. -echo "--- Checking for and removing old test files ---" -if [ -f "$LOCAL_DB_PATH" ]; then - echo "Removing old database file: $LOCAL_DB_PATH" - rm "$LOCAL_DB_PATH" +# --- Conditional Server Startup and Cleanup --- +if [ "$SKIP_SERVER" = false ]; then + # --- Pre-test Cleanup --- + # Check for and remove old test files to ensure a clean test environment. + echo "--- Checking for and removing old test files ---" + if [ -f "$LOCAL_DB_PATH" ]; then + echo "Removing old database file: $LOCAL_DB_PATH" + rm "$LOCAL_DB_PATH" + fi + if [ -f "$FAISS_INDEX_PATH" ]; then + echo "Removing old FAISS index file: $FAISS_INDEX_PATH" + rm "$FAISS_INDEX_PATH" + fi + echo "Cleanup complete." + + echo "--- Starting AI Hub Server for Tests ---" + + # Start the uvicorn server in the background + # We bind it to 127.0.0.1 to ensure it's not accessible from outside the local machine. + uvicorn app.main:app --host 127.0.0.1 --port 8001 --reload & + + # Get the Process ID (PID) of the background server + SERVER_PID=$! + + # Define a cleanup function to be called on exit + cleanup() { + echo "" + echo "--- Shutting Down Server (PID: $SERVER_PID) ---" + kill $SERVER_PID + } + + # Register the cleanup function to run when the script exits + # This ensures the server is stopped even if tests fail or the script is interrupted (e.g., with Ctrl+C). + trap cleanup EXIT + + echo "Server started with PID: $SERVER_PID. Waiting for it to initialize..." + + # Wait a few seconds to ensure the server is fully up and running + sleep 5 +else + echo "--- Skipping server startup. Running tests against an existing server. ---" fi -if [ -f "$FAISS_INDEX_PATH" ]; then - echo "Removing old FAISS index file: $FAISS_INDEX_PATH" - rm "$FAISS_INDEX_PATH" -fi -echo "Cleanup complete." -echo "--- Starting AI Hub Server for Tests ---" - -# Start the uvicorn server in the background -# We bind it to 127.0.0.1 to ensure it's not accessible from outside the local machine. -uvicorn app.main:app --host 127.0.0.1 --port 8000 --reload & - -# Get the Process ID (PID) of the background server -SERVER_PID=$! - -# Define a cleanup function to be called on exit -cleanup() { - echo "" - echo "--- Shutting Down Server (PID: $SERVER_PID) ---" - kill $SERVER_PID -} - -# Register the cleanup function to run when the script exits -# This ensures the server is stopped even if tests fail or the script is interrupted (e.g., with Ctrl+C). -trap cleanup EXIT - -echo "Server started with PID: $SERVER_PID. Waiting for it to initialize..." - -# Wait a few seconds to ensure the server is fully up and running -sleep 5 echo "--- Running tests in: $TEST_PATH ---" # Execute the Python tests using pytest on the specified path @@ -90,6 +107,6 @@ # Capture the exit code of the test script TEST_EXIT_CODE=$? -# The 'trap' will automatically call the cleanup function now. +# The 'trap' will automatically call the cleanup function if it was set. # Exit with the same code as the test script (0 for success, non-zero for failure). -exit $TEST_EXIT_CODE \ No newline at end of file +exit $TEST_EXIT_CODE diff --git a/ai-hub/tests/api/routes/conftest.py b/ai-hub/tests/api/routes/conftest.py index e76f8bf..bd784ce 100644 --- a/ai-hub/tests/api/routes/conftest.py +++ b/ai-hub/tests/api/routes/conftest.py @@ -9,6 +9,7 @@ from app.core.services.document import DocumentService from app.core.services.tts import TTSService from app.core.services.stt import STTService +from app.core.services.session import SessionService from app.api.routes.api import create_api_router # Change the scope to "function" so the fixture is re-created for each test @@ -25,6 +26,7 @@ mock_document_service = MagicMock(spec=DocumentService) mock_tts_service = MagicMock(spec=TTSService) mock_stt_service = MagicMock(spec=STTService) + mock_session_service = MagicMock(spec=SessionService) # Create a mock for the ServiceContainer and attach all the individual service mocks mock_services = MagicMock(spec=ServiceContainer) @@ -32,6 +34,7 @@ mock_services.document_service = mock_document_service mock_services.tts_service = mock_tts_service mock_services.stt_service = mock_stt_service + mock_services.session_service = mock_session_service # Mock the database session mock_db_session = MagicMock(spec=Session) diff --git a/ai-hub/tests/api/routes/test_sessions.py b/ai-hub/tests/api/routes/test_sessions.py index ad23a3a..a8a7626 100644 --- a/ai-hub/tests/api/routes/test_sessions.py +++ b/ai-hub/tests/api/routes/test_sessions.py @@ -11,13 +11,13 @@ mock_session.provider_name = "gemini" mock_session.title = "New Chat" mock_session.created_at = datetime.now() - mock_services.rag_service.create_session.return_value = mock_session + mock_services.session_service.create_session.return_value = mock_session response = test_client.post("/sessions", json={"user_id": "test_user", "provider_name": "gemini"}) assert response.status_code == 200 assert response.json()["id"] == 1 - mock_services.rag_service.create_session.assert_called_once() + mock_services.session_service.create_session.assert_called_once() def test_chat_in_session_success(client): """ diff --git a/ai-hub/tests/api/test_dependencies.py b/ai-hub/tests/api/test_dependencies.py index 16c0621..454a2fd 100644 --- a/ai-hub/tests/api/test_dependencies.py +++ b/ai-hub/tests/api/test_dependencies.py @@ -27,6 +27,16 @@ mock = MagicMock(spec=Session) yield mock +@pytest.fixture +def mock_faiss_vector_store(): + """ + Fixture that provides a mock FaissVectorStore with an embedder attribute. + """ + mock = MagicMock(spec=FaissVectorStore) + # The DocumentService.__init__ method tries to access this. + mock.embedder = MagicMock() + return mock + # --- Tests for get_db dependency --- @@ -93,45 +103,81 @@ # --- Tests for ServiceContainer class --- -def test_service_container_initialization(): +def test_service_container_with_document_service(mock_faiss_vector_store): """ - Tests that ServiceContainer initializes DocumentService, RAGService, and other services - with the correct dependencies. + Tests that the ServiceContainer correctly creates a DocumentService instance + using the with_document_service method. + """ + # Act + container = ServiceContainer().with_document_service(vector_store=mock_faiss_vector_store) + + # Assert + assert isinstance(container.document_service, DocumentService) + assert container.document_service.vector_store == mock_faiss_vector_store + +def test_service_container_with_rag_service(): + """ + Tests that the ServiceContainer correctly creates a RAGService instance + using the with_rag_service method. """ # Arrange: Create mock dependencies - mock_vector_store = MagicMock(spec=FaissVectorStore) - mock_vector_store.embedder = MagicMock() - mock_retrievers = [MagicMock(spec=Retriever), MagicMock(spec=Retriever)] - mock_tts_service = MagicMock(spec=TTSService) - mock_stt_service = MagicMock(spec=STTService) - mock_workspace_service = MagicMock(spec=WorkspaceService) - + # Act - container = ServiceContainer( - vector_store=mock_vector_store, - retrievers=mock_retrievers, - tts_service=mock_tts_service, - stt_service=mock_stt_service, - workspace_service=mock_workspace_service - ) + container = ServiceContainer().with_rag_service(retrievers=mock_retrievers) - # Assert: DocumentService - assert isinstance(container.document_service, DocumentService) - assert container.document_service.vector_store == mock_vector_store - - # Assert: RAGService + # Assert assert isinstance(container.rag_service, RAGService) assert container.rag_service.retrievers == mock_retrievers - # Assert: TTSService - assert isinstance(container.tts_service, TTSService) - assert container.tts_service == mock_tts_service +def test_service_container_with_service(): + """ + Tests that the ServiceContainer can add a service using the generic with_service method + and that it is accessible as an attribute. + """ + # Arrange + mock_tts_service = MagicMock(spec=TTSService) + mock_stt_service = MagicMock(spec=STTService) + + # Act + container = ServiceContainer().with_service("tts_service", mock_tts_service) \ + .with_service("stt_service", mock_stt_service) - # Assert: STTService - assert isinstance(container.stt_service, STTService) + # Assert + assert hasattr(container, "tts_service") + assert container.tts_service == mock_tts_service + assert hasattr(container, "stt_service") assert container.stt_service == mock_stt_service - # Assert: WorkspaceService - assert isinstance(container.workspace_service, WorkspaceService) - assert container.workspace_service == mock_workspace_service +def test_service_container_attribute_error(): + """ + Tests that accessing a non-existent service raises an AttributeError. + """ + # Arrange + container = ServiceContainer() + + # Act / Assert + with pytest.raises(AttributeError) as excinfo: + _ = container.non_existent_service + + assert "object has no service named 'non_existent_service'" in str(excinfo.value) + +def test_service_container_chaining(mock_faiss_vector_store): + """ + Tests that the with_* methods can be chained together. + """ + # Arrange + # mock_vector_store is now a fixture + mock_retrievers = [MagicMock(spec=Retriever)] + mock_tts_service = MagicMock(spec=TTSService) + + # Act + container = ServiceContainer() \ + .with_document_service(vector_store=mock_faiss_vector_store) \ + .with_rag_service(retrievers=mock_retrievers) \ + .with_service("tts_service", mock_tts_service) + + # Assert + assert isinstance(container.document_service, DocumentService) + assert isinstance(container.rag_service, RAGService) + assert container.tts_service == mock_tts_service \ No newline at end of file diff --git a/ai-hub/tests/core/pipelines/test_dspy_rag.py b/ai-hub/tests/core/pipelines/test_dspy_rag.py index be4a13d..4a5580b 100644 --- a/ai-hub/tests/core/pipelines/test_dspy_rag.py +++ b/ai-hub/tests/core/pipelines/test_dspy_rag.py @@ -68,7 +68,7 @@ question = "What is the capital of France?" history = [models.Message(sender="user", content="Hello there."), models.Message(sender="assistant", content="Hi.")] - response = await pipeline.forward(question, history, mock_db) + response = await pipeline(question, history, mock_db) expected_context = "Context 1.\n\nContext 2." expected_history = "Human: Hello there.\nAssistant: Hi." @@ -104,7 +104,7 @@ question = "Custom question?" history = [models.Message(sender="user", content="User message.")] - response = await pipeline.forward(question, history, mock_db) + response = await pipeline(question, history, mock_db) mock_dspy_predict_instance.aforward.assert_called_once_with( context="CUSTOM_CONTEXT: Context A | Context B", @@ -122,7 +122,7 @@ question = "No context question." history = [] - response = await pipeline.forward(question, history, mock_db) + response = await pipeline(question, history, mock_db) mock_dspy_predict_instance.aforward.assert_called_once_with( context="No context provided.", diff --git a/ai-hub/tests/core/services/test_rag.py b/ai-hub/tests/core/services/test_rag.py index 84ee258..7f87239 100644 --- a/ai-hub/tests/core/services/test_rag.py +++ b/ai-hub/tests/core/services/test_rag.py @@ -35,17 +35,17 @@ # --- Session Management Tests --- -def test_create_session(rag_service: RAGService): - """Tests that the create_session method correctly creates a new session.""" - mock_db = MagicMock(spec=Session) +# def test_create_session(rag_service: RAGService): +# """Tests that the create_session method correctly creates a new session.""" +# mock_db = MagicMock(spec=Session) - rag_service.create_session(db=mock_db, user_id="test_user", provider_name="gemini") +# # rag_service.create_session(db=mock_db, user_id="test_user", provider_name="gemini") - mock_db.add.assert_called_once() - added_object = mock_db.add.call_args[0][0] - assert isinstance(added_object, models.Session) - assert added_object.user_id == "test_user" - assert added_object.provider_name == "gemini" +# mock_db.add.assert_called_once() +# added_object = mock_db.add.call_args[0][0] +# assert isinstance(added_object, models.Session) +# assert added_object.user_id == "test_user" +# assert added_object.provider_name == "gemini" @patch('app.core.services.rag.get_llm_provider') @patch('app.core.services.rag.DspyRagPipeline') diff --git a/ai-hub/tests/test_app.py b/ai-hub/tests/test_app.py index 1870907..96e9973 100644 --- a/ai-hub/tests/test_app.py +++ b/ai-hub/tests/test_app.py @@ -69,7 +69,7 @@ mock_services = MagicMock() mock_service_container.return_value = mock_services - # Configure the mock rag_service to return a mocked session object + # Configure the mock to return a mocked session object mock_session_obj = models.Session( id=1, user_id="test_user", @@ -77,7 +77,7 @@ title="New Chat Session", created_at=datetime.now() ) - mock_services.rag_service.create_session.return_value = mock_session_obj + mock_services.session_service.create_session.return_value = mock_session_obj app = create_app() app.dependency_overrides[get_db] = override_get_db @@ -91,7 +91,7 @@ response_data = response.json() assert response_data["id"] == 1 assert response_data["user_id"] == "test_user" - mock_services.rag_service.create_session.assert_called_once_with( + mock_services.session_service.create_session.assert_called_once_with( db=mock_db, user_id="test_user", provider_name="gemini" ) diff --git a/ui/client-app/src/hooks/tempCodeRunnerFile.js b/ui/client-app/src/hooks/tempCodeRunnerFile.js new file mode 100644 index 0000000..a61fcae --- /dev/null +++ b/ui/client-app/src/hooks/tempCodeRunnerFile.js @@ -0,0 +1 @@ +createSession \ No newline at end of file