diff --git a/.gitignore b/.gitignore
index 3e0b2c9..95f61e0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,5 +2,6 @@
.env
**/.env
**/*.egg-info
-faiss_index.bin
-ai_hub.db
\ No newline at end of file
+**/faiss_index.bin
+**/ai_hub.db
+.pytest_cache/
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 3e0b2c9..95f61e0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,5 +2,6 @@
.env
**/.env
**/*.egg-info
-faiss_index.bin
-ai_hub.db
\ No newline at end of file
+**/faiss_index.bin
+**/ai_hub.db
+.pytest_cache/
\ No newline at end of file
diff --git a/ai-hub/app/__init__.py b/ai-hub/app/__init__.py
index e69de29..3fbb1fd 100644
--- a/ai-hub/app/__init__.py
+++ b/ai-hub/app/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/.gitignore b/.gitignore
index 3e0b2c9..95f61e0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,5 +2,6 @@
.env
**/.env
**/*.egg-info
-faiss_index.bin
-ai_hub.db
\ No newline at end of file
+**/faiss_index.bin
+**/ai_hub.db
+.pytest_cache/
\ No newline at end of file
diff --git a/ai-hub/app/__init__.py b/ai-hub/app/__init__.py
index e69de29..3fbb1fd 100644
--- a/ai-hub/app/__init__.py
+++ b/ai-hub/app/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/app.py b/ai-hub/app/app.py
index 04f9d7b..0718c32 100644
--- a/ai-hub/app/app.py
+++ b/ai-hub/app/app.py
@@ -24,7 +24,7 @@
"""
# Initialize core services for RAG
# CORRECTED: Now passing the required arguments to FaissVectorStore
- vector_store = FaissVectorStore(index_file_path="faiss_index.bin", dimension=768)
+ vector_store = FaissVectorStore(index_file_path="data/faiss_index.bin", dimension=768)
retrievers: List[Retriever] = [
FaissDBRetriever(vector_store=vector_store),
]
diff --git a/.gitignore b/.gitignore
index 3e0b2c9..95f61e0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,5 +2,6 @@
.env
**/.env
**/*.egg-info
-faiss_index.bin
-ai_hub.db
\ No newline at end of file
+**/faiss_index.bin
+**/ai_hub.db
+.pytest_cache/
\ No newline at end of file
diff --git a/ai-hub/app/__init__.py b/ai-hub/app/__init__.py
index e69de29..3fbb1fd 100644
--- a/ai-hub/app/__init__.py
+++ b/ai-hub/app/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/app.py b/ai-hub/app/app.py
index 04f9d7b..0718c32 100644
--- a/ai-hub/app/app.py
+++ b/ai-hub/app/app.py
@@ -24,7 +24,7 @@
"""
# Initialize core services for RAG
# CORRECTED: Now passing the required arguments to FaissVectorStore
- vector_store = FaissVectorStore(index_file_path="faiss_index.bin", dimension=768)
+ vector_store = FaissVectorStore(index_file_path="data/faiss_index.bin", dimension=768)
retrievers: List[Retriever] = [
FaissDBRetriever(vector_store=vector_store),
]
diff --git a/ai-hub/app/core/__init__.py b/ai-hub/app/core/__init__.py
index e69de29..3fbb1fd 100644
--- a/ai-hub/app/core/__init__.py
+++ b/ai-hub/app/core/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/.gitignore b/.gitignore
index 3e0b2c9..95f61e0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,5 +2,6 @@
.env
**/.env
**/*.egg-info
-faiss_index.bin
-ai_hub.db
\ No newline at end of file
+**/faiss_index.bin
+**/ai_hub.db
+.pytest_cache/
\ No newline at end of file
diff --git a/ai-hub/app/__init__.py b/ai-hub/app/__init__.py
index e69de29..3fbb1fd 100644
--- a/ai-hub/app/__init__.py
+++ b/ai-hub/app/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/app.py b/ai-hub/app/app.py
index 04f9d7b..0718c32 100644
--- a/ai-hub/app/app.py
+++ b/ai-hub/app/app.py
@@ -24,7 +24,7 @@
"""
# Initialize core services for RAG
# CORRECTED: Now passing the required arguments to FaissVectorStore
- vector_store = FaissVectorStore(index_file_path="faiss_index.bin", dimension=768)
+ vector_store = FaissVectorStore(index_file_path="data/faiss_index.bin", dimension=768)
retrievers: List[Retriever] = [
FaissDBRetriever(vector_store=vector_store),
]
diff --git a/ai-hub/app/core/__init__.py b/ai-hub/app/core/__init__.py
index e69de29..3fbb1fd 100644
--- a/ai-hub/app/core/__init__.py
+++ b/ai-hub/app/core/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/core/llm_providers.py b/ai-hub/app/core/llm_providers.py
index f31a701..de36a55 100644
--- a/ai-hub/app/core/llm_providers.py
+++ b/ai-hub/app/core/llm_providers.py
@@ -45,7 +45,7 @@
async def generate_response(self, prompt: str) -> str:
# Construct the request payload
messages_payload = [
- # {"role": "system", "content": "You are a helpful assistant."},
+ {"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": prompt},
]
diff --git a/.gitignore b/.gitignore
index 3e0b2c9..95f61e0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,5 +2,6 @@
.env
**/.env
**/*.egg-info
-faiss_index.bin
-ai_hub.db
\ No newline at end of file
+**/faiss_index.bin
+**/ai_hub.db
+.pytest_cache/
\ No newline at end of file
diff --git a/ai-hub/app/__init__.py b/ai-hub/app/__init__.py
index e69de29..3fbb1fd 100644
--- a/ai-hub/app/__init__.py
+++ b/ai-hub/app/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/app.py b/ai-hub/app/app.py
index 04f9d7b..0718c32 100644
--- a/ai-hub/app/app.py
+++ b/ai-hub/app/app.py
@@ -24,7 +24,7 @@
"""
# Initialize core services for RAG
# CORRECTED: Now passing the required arguments to FaissVectorStore
- vector_store = FaissVectorStore(index_file_path="faiss_index.bin", dimension=768)
+ vector_store = FaissVectorStore(index_file_path="data/faiss_index.bin", dimension=768)
retrievers: List[Retriever] = [
FaissDBRetriever(vector_store=vector_store),
]
diff --git a/ai-hub/app/core/__init__.py b/ai-hub/app/core/__init__.py
index e69de29..3fbb1fd 100644
--- a/ai-hub/app/core/__init__.py
+++ b/ai-hub/app/core/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/core/llm_providers.py b/ai-hub/app/core/llm_providers.py
index f31a701..de36a55 100644
--- a/ai-hub/app/core/llm_providers.py
+++ b/ai-hub/app/core/llm_providers.py
@@ -45,7 +45,7 @@
async def generate_response(self, prompt: str) -> str:
# Construct the request payload
messages_payload = [
- # {"role": "system", "content": "You are a helpful assistant."},
+ {"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": prompt},
]
diff --git a/ai-hub/app/core/pipelines/__init__.py b/ai-hub/app/core/pipelines/__init__.py
new file mode 100644
index 0000000..3fbb1fd
--- /dev/null
+++ b/ai-hub/app/core/pipelines/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/.gitignore b/.gitignore
index 3e0b2c9..95f61e0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,5 +2,6 @@
.env
**/.env
**/*.egg-info
-faiss_index.bin
-ai_hub.db
\ No newline at end of file
+**/faiss_index.bin
+**/ai_hub.db
+.pytest_cache/
\ No newline at end of file
diff --git a/ai-hub/app/__init__.py b/ai-hub/app/__init__.py
index e69de29..3fbb1fd 100644
--- a/ai-hub/app/__init__.py
+++ b/ai-hub/app/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/app.py b/ai-hub/app/app.py
index 04f9d7b..0718c32 100644
--- a/ai-hub/app/app.py
+++ b/ai-hub/app/app.py
@@ -24,7 +24,7 @@
"""
# Initialize core services for RAG
# CORRECTED: Now passing the required arguments to FaissVectorStore
- vector_store = FaissVectorStore(index_file_path="faiss_index.bin", dimension=768)
+ vector_store = FaissVectorStore(index_file_path="data/faiss_index.bin", dimension=768)
retrievers: List[Retriever] = [
FaissDBRetriever(vector_store=vector_store),
]
diff --git a/ai-hub/app/core/__init__.py b/ai-hub/app/core/__init__.py
index e69de29..3fbb1fd 100644
--- a/ai-hub/app/core/__init__.py
+++ b/ai-hub/app/core/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/core/llm_providers.py b/ai-hub/app/core/llm_providers.py
index f31a701..de36a55 100644
--- a/ai-hub/app/core/llm_providers.py
+++ b/ai-hub/app/core/llm_providers.py
@@ -45,7 +45,7 @@
async def generate_response(self, prompt: str) -> str:
# Construct the request payload
messages_payload = [
- # {"role": "system", "content": "You are a helpful assistant."},
+ {"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": prompt},
]
diff --git a/ai-hub/app/core/pipelines/__init__.py b/ai-hub/app/core/pipelines/__init__.py
new file mode 100644
index 0000000..3fbb1fd
--- /dev/null
+++ b/ai-hub/app/core/pipelines/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/core/pipelines/dspy_rag.py b/ai-hub/app/core/pipelines/dspy_rag.py
new file mode 100644
index 0000000..322c01b
--- /dev/null
+++ b/ai-hub/app/core/pipelines/dspy_rag.py
@@ -0,0 +1,87 @@
+# In app/core/pipelines/dspy_rag.py
+
+import dspy
+import logging
+from typing import List
+from types import SimpleNamespace
+from sqlalchemy.orm import Session
+
+from app.core.retrievers import Retriever
+from app.core.llm_providers import LLMProvider
+
+class DSPyLLMProvider(dspy.BaseLM):
+ """
+ A custom wrapper for the LLMProvider to make it compatible with DSPy.
+ """
+ def __init__(self, provider: LLMProvider, model_name: str, **kwargs):
+ super().__init__(model=model_name)
+ self.provider = provider
+ self.kwargs.update(kwargs)
+ print(f"DSPyLLMProvider initialized for model: {self.model}")
+
+ async def aforward(self, prompt: str, **kwargs):
+ """
+ The required asynchronous forward pass for the language model.
+ """
+ logging.info(f"[DSPyLLMProvider.aforward] Received prompt of length: {len(prompt) if prompt else 0}")
+ if not prompt or not prompt.strip():
+ logging.error("[DSPyLLMProvider.aforward] Received a null or empty prompt!")
+ return SimpleNamespace(choices=[SimpleNamespace(message=SimpleNamespace(content="Error: Received an empty prompt."))])
+
+ response_text = await self.provider.generate_response(prompt)
+
+ mock_choice = SimpleNamespace(message=SimpleNamespace(content=response_text, tool_calls=None))
+ return SimpleNamespace(choices=[mock_choice], usage=SimpleNamespace(prompt_tokens=0, completion_tokens=0, total_tokens=0), model=self.model)
+
+class AnswerWithContext(dspy.Signature):
+ """Given the context, answer the user's question."""
+ context = dspy.InputField(desc="Relevant document snippets from the knowledge base.")
+ question = dspy.InputField()
+ answer = dspy.OutputField()
+
+class DspyRagPipeline(dspy.Module):
+ """
+ A simple RAG pipeline that retrieves context and then generates an answer using DSPy.
+ """
+ def __init__(self, retrievers: List[Retriever]):
+ super().__init__()
+ self.retrievers = retrievers
+ # We still define the predictor to access its signature easily.
+ self.generate_answer = dspy.Predict(AnswerWithContext)
+
+ async def forward(self, question: str, db: Session) -> str:
+ """
+ Executes the RAG pipeline asynchronously.
+ """
+ logging.info(f"[DspyRagPipeline.forward] Received question: '{question}'")
+ retrieved_contexts = []
+ for retriever in self.retrievers:
+ context = retriever.retrieve_context(question, db)
+ retrieved_contexts.extend(context)
+
+ context_text = "\n\n".join(retrieved_contexts)
+ if not context_text:
+ print("⚠️ No context retrieved. Falling back to direct QA.")
+ context_text = "No context provided."
+
+ lm = dspy.settings.lm
+ if lm is None:
+ raise RuntimeError("DSPy LM has not been configured. Call dspy.configure(lm=...) first.")
+
+ # --- FIX: Revert to manual prompt construction ---
+ # Get the instruction from the signature's docstring.
+ instruction = self.generate_answer.signature.__doc__
+
+ # Build the full prompt exactly as DSPy would.
+ full_prompt = (
+ f"{instruction}\n\n"
+ f"---\n\n"
+ f"Context: {context_text}\n\n"
+ f"Question: {question}\n\n"
+ f"Answer:"
+ )
+
+ # Call the language model's aforward method directly with the complete prompt.
+ response_obj = await lm.aforward(prompt=full_prompt)
+
+ return response_obj.choices[0].message.content
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 3e0b2c9..95f61e0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,5 +2,6 @@
.env
**/.env
**/*.egg-info
-faiss_index.bin
-ai_hub.db
\ No newline at end of file
+**/faiss_index.bin
+**/ai_hub.db
+.pytest_cache/
\ No newline at end of file
diff --git a/ai-hub/app/__init__.py b/ai-hub/app/__init__.py
index e69de29..3fbb1fd 100644
--- a/ai-hub/app/__init__.py
+++ b/ai-hub/app/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/app.py b/ai-hub/app/app.py
index 04f9d7b..0718c32 100644
--- a/ai-hub/app/app.py
+++ b/ai-hub/app/app.py
@@ -24,7 +24,7 @@
"""
# Initialize core services for RAG
# CORRECTED: Now passing the required arguments to FaissVectorStore
- vector_store = FaissVectorStore(index_file_path="faiss_index.bin", dimension=768)
+ vector_store = FaissVectorStore(index_file_path="data/faiss_index.bin", dimension=768)
retrievers: List[Retriever] = [
FaissDBRetriever(vector_store=vector_store),
]
diff --git a/ai-hub/app/core/__init__.py b/ai-hub/app/core/__init__.py
index e69de29..3fbb1fd 100644
--- a/ai-hub/app/core/__init__.py
+++ b/ai-hub/app/core/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/core/llm_providers.py b/ai-hub/app/core/llm_providers.py
index f31a701..de36a55 100644
--- a/ai-hub/app/core/llm_providers.py
+++ b/ai-hub/app/core/llm_providers.py
@@ -45,7 +45,7 @@
async def generate_response(self, prompt: str) -> str:
# Construct the request payload
messages_payload = [
- # {"role": "system", "content": "You are a helpful assistant."},
+ {"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": prompt},
]
diff --git a/ai-hub/app/core/pipelines/__init__.py b/ai-hub/app/core/pipelines/__init__.py
new file mode 100644
index 0000000..3fbb1fd
--- /dev/null
+++ b/ai-hub/app/core/pipelines/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/core/pipelines/dspy_rag.py b/ai-hub/app/core/pipelines/dspy_rag.py
new file mode 100644
index 0000000..322c01b
--- /dev/null
+++ b/ai-hub/app/core/pipelines/dspy_rag.py
@@ -0,0 +1,87 @@
+# In app/core/pipelines/dspy_rag.py
+
+import dspy
+import logging
+from typing import List
+from types import SimpleNamespace
+from sqlalchemy.orm import Session
+
+from app.core.retrievers import Retriever
+from app.core.llm_providers import LLMProvider
+
+class DSPyLLMProvider(dspy.BaseLM):
+ """
+ A custom wrapper for the LLMProvider to make it compatible with DSPy.
+ """
+ def __init__(self, provider: LLMProvider, model_name: str, **kwargs):
+ super().__init__(model=model_name)
+ self.provider = provider
+ self.kwargs.update(kwargs)
+ print(f"DSPyLLMProvider initialized for model: {self.model}")
+
+ async def aforward(self, prompt: str, **kwargs):
+ """
+ The required asynchronous forward pass for the language model.
+ """
+ logging.info(f"[DSPyLLMProvider.aforward] Received prompt of length: {len(prompt) if prompt else 0}")
+ if not prompt or not prompt.strip():
+ logging.error("[DSPyLLMProvider.aforward] Received a null or empty prompt!")
+ return SimpleNamespace(choices=[SimpleNamespace(message=SimpleNamespace(content="Error: Received an empty prompt."))])
+
+ response_text = await self.provider.generate_response(prompt)
+
+ mock_choice = SimpleNamespace(message=SimpleNamespace(content=response_text, tool_calls=None))
+ return SimpleNamespace(choices=[mock_choice], usage=SimpleNamespace(prompt_tokens=0, completion_tokens=0, total_tokens=0), model=self.model)
+
+class AnswerWithContext(dspy.Signature):
+ """Given the context, answer the user's question."""
+ context = dspy.InputField(desc="Relevant document snippets from the knowledge base.")
+ question = dspy.InputField()
+ answer = dspy.OutputField()
+
+class DspyRagPipeline(dspy.Module):
+ """
+ A simple RAG pipeline that retrieves context and then generates an answer using DSPy.
+ """
+ def __init__(self, retrievers: List[Retriever]):
+ super().__init__()
+ self.retrievers = retrievers
+ # We still define the predictor to access its signature easily.
+ self.generate_answer = dspy.Predict(AnswerWithContext)
+
+ async def forward(self, question: str, db: Session) -> str:
+ """
+ Executes the RAG pipeline asynchronously.
+ """
+ logging.info(f"[DspyRagPipeline.forward] Received question: '{question}'")
+ retrieved_contexts = []
+ for retriever in self.retrievers:
+ context = retriever.retrieve_context(question, db)
+ retrieved_contexts.extend(context)
+
+ context_text = "\n\n".join(retrieved_contexts)
+ if not context_text:
+ print("⚠️ No context retrieved. Falling back to direct QA.")
+ context_text = "No context provided."
+
+ lm = dspy.settings.lm
+ if lm is None:
+ raise RuntimeError("DSPy LM has not been configured. Call dspy.configure(lm=...) first.")
+
+ # --- FIX: Revert to manual prompt construction ---
+ # Get the instruction from the signature's docstring.
+ instruction = self.generate_answer.signature.__doc__
+
+ # Build the full prompt exactly as DSPy would.
+ full_prompt = (
+ f"{instruction}\n\n"
+ f"---\n\n"
+ f"Context: {context_text}\n\n"
+ f"Question: {question}\n\n"
+ f"Answer:"
+ )
+
+ # Call the language model's aforward method directly with the complete prompt.
+ response_obj = await lm.aforward(prompt=full_prompt)
+
+ return response_obj.choices[0].message.content
\ No newline at end of file
diff --git a/ai-hub/app/core/rag_service.py b/ai-hub/app/core/rag_service.py
index 7cb57e9..93b4bb9 100644
--- a/ai-hub/app/core/rag_service.py
+++ b/ai-hub/app/core/rag_service.py
@@ -1,120 +1,28 @@
-import asyncio
from typing import List, Dict, Any
-from types import SimpleNamespace
from sqlalchemy.orm import Session
from sqlalchemy.exc import SQLAlchemyError
import dspy
-import logging
from app.core.vector_store import FaissVectorStore
from app.db import models
from app.core.retrievers import Retriever
-from app.core.llm_providers import LLMProvider, get_llm_provider
-
-# --- DSPy Components for RAG ---
-
-class DSPyLLMProvider(dspy.BaseLM):
- """
- A custom wrapper for the LLMProvider to make it compatible with DSPy.
- """
- def __init__(self, provider: LLMProvider, model_name: str, **kwargs):
- super().__init__(model=model_name)
- self.provider = provider
- self.kwargs.update(kwargs)
- print(f"DSPyLLMProvider initialized for model: {self.model}")
-
- async def aforward(self, prompt: str, **kwargs):
- """
- The required asynchronous forward pass for the language model.
- """
- logging.info(f"[DSPyLLMProvider.aforward] Received prompt of length: {len(prompt) if prompt else 0}")
-
- # --- CRITICAL FIX: Ensure prompt is not None or empty ---
- if not prompt or not prompt.strip():
- logging.error("[DSPyLLMProvider.aforward] Received a null or empty prompt!")
- # Return a default, safe response instead of calling the API with null.
- return SimpleNamespace(choices=[SimpleNamespace(message=SimpleNamespace(content="Error: Received an empty prompt."))])
-
- # Call the async provider directly using the existing event loop
- response_text = await self.provider.generate_response(prompt)
-
- # Create a mock response object that mimics the OpenAI API structure
- mock_choice = SimpleNamespace(
- message=SimpleNamespace(content=response_text, tool_calls=None)
- )
- mock_response = SimpleNamespace(
- choices=[mock_choice],
- usage=SimpleNamespace(prompt_tokens=0, completion_tokens=0, total_tokens=0),
- model=self.model
- )
- return mock_response
-
-class AnswerWithContext(dspy.Signature):
- """
- Signature for our RAG task: input is a context and question, output is an answer.
- """
- context = dspy.InputField(desc="Relevant document snippets from the knowledge base.")
- question = dspy.InputField()
- answer = dspy.OutputField()
-
-class RAGPipeline(dspy.Module):
- """
- A simple RAG pipeline that retrieves context and then generates an answer.
- """
- def __init__(self, retrievers: List[Retriever]):
- super().__init__()
- self.retrievers = retrievers
- # We only need the signature here to generate the prompt text.
- self.generate_answer = dspy.Predict(AnswerWithContext)
-
- async def forward(self, question: str, db: Session) -> str:
- """
- Executes the RAG pipeline asynchronously.
- """
- logging.info(f"[RAGPipeline.forward] Received question: '{question}'")
- retrieved_contexts = []
- for retriever in self.retrievers:
- context = retriever.retrieve_context(question, db)
- retrieved_contexts.extend(context)
-
- context_text = "\n\n".join(retrieved_contexts)
- if not context_text:
- print("⚠️ No context retrieved. Falling back to direct QA.")
- context_text = "No context provided."
-
- # --- REVISED LOGIC ---
- # 1. Manually create the full prompt using the signature's template.
- # The `dspy.Predict` object can be called with the inputs to get the compiled prompt.
- # We access the last generated prompt from the LM's history.
- # Since we haven't called the LM yet, we temporarily configure a basic LM.
-
- # Get the configured language model from dspy settings
- lm = dspy.settings.lm
- if lm is None:
- raise RuntimeError("DSPy LM has not been configured. Call dspy.configure(lm=...) first.")
-
- # 2. Use the signature to create a dspy.Example, which generates the prompt.
- # The dspy.Predict module will format this into a prompt string.
- example = dspy.Example(context=context_text, question=question, signatures=self.generate_answer.signature)
-
- # 3. Call the language model directly with the full prompt string.
- # The `example.signatures` contains the logic to render the prompt.
- # In modern DSPy, `dspy.predict` is a simpler way to do this.
- # We will call the LM's aforward method directly for clarity.
- full_prompt = self.generate_answer.signature.instructions.format(context=context_text, question=question) + "\nAnswer:"
-
- response_obj = await lm.aforward(prompt=full_prompt)
-
- return response_obj.choices[0].message.content
+from app.core.llm_providers import get_llm_provider
+from app.core.pipelines.dspy_rag import DspyRagPipeline, DSPyLLMProvider
-# --- Main RAG Service Class --- (This class remains unchanged)
class RAGService:
+ """
+ Service class for managing the RAG (Retrieval-Augmented Generation) pipeline.
+ This class acts as a high-level orchestrator.
+ """
def __init__(self, vector_store: FaissVectorStore, retrievers: List[Retriever]):
self.vector_store = vector_store
self.retrievers = retrievers
def add_document(self, db: Session, doc_data: Dict[str, Any]) -> int:
+ """
+ Adds a document to both the database and the vector store.
+ """
try:
document_db = models.Document(
title=doc_data["title"],
@@ -124,7 +32,9 @@
db.add(document_db)
db.commit()
db.refresh(document_db)
+
faiss_index = self.vector_store.add_document(document_db.text)
+
vector_metadata = models.VectorMetadata(
document_id=document_db.id,
faiss_index=faiss_index,
@@ -144,16 +54,24 @@
raise
async def chat_with_rag(self, db: Session, prompt: str, model: str) -> str:
+ """
+ Generates a response to a user prompt by orchestrating the RAG pipeline.
+ """
print(f"Received Prompt: {prompt}")
if not prompt or not prompt.strip():
raise ValueError("The prompt cannot be null, empty, or contain only whitespace.")
+ # 1. Get the underlying LLM provider (e.g., Gemini, DeepSeek)
llm_provider_instance = get_llm_provider(model)
+
+ # 2. Wrap it in our custom DSPy-compatible provider
dspy_llm_provider = DSPyLLMProvider(provider=llm_provider_instance, model_name=model)
- # Configure dspy's global settings with our custom LM
+ # 3. Configure DSPy's global settings to use our custom LM
dspy.configure(lm=dspy_llm_provider)
- rag_pipeline = RAGPipeline(retrievers=self.retrievers)
+ # 4. Initialize and execute the RAG pipeline
+ rag_pipeline = DspyRagPipeline(retrievers=self.retrievers)
answer = await rag_pipeline.forward(question=prompt, db=db)
+
return answer
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 3e0b2c9..95f61e0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,5 +2,6 @@
.env
**/.env
**/*.egg-info
-faiss_index.bin
-ai_hub.db
\ No newline at end of file
+**/faiss_index.bin
+**/ai_hub.db
+.pytest_cache/
\ No newline at end of file
diff --git a/ai-hub/app/__init__.py b/ai-hub/app/__init__.py
index e69de29..3fbb1fd 100644
--- a/ai-hub/app/__init__.py
+++ b/ai-hub/app/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/app.py b/ai-hub/app/app.py
index 04f9d7b..0718c32 100644
--- a/ai-hub/app/app.py
+++ b/ai-hub/app/app.py
@@ -24,7 +24,7 @@
"""
# Initialize core services for RAG
# CORRECTED: Now passing the required arguments to FaissVectorStore
- vector_store = FaissVectorStore(index_file_path="faiss_index.bin", dimension=768)
+ vector_store = FaissVectorStore(index_file_path="data/faiss_index.bin", dimension=768)
retrievers: List[Retriever] = [
FaissDBRetriever(vector_store=vector_store),
]
diff --git a/ai-hub/app/core/__init__.py b/ai-hub/app/core/__init__.py
index e69de29..3fbb1fd 100644
--- a/ai-hub/app/core/__init__.py
+++ b/ai-hub/app/core/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/core/llm_providers.py b/ai-hub/app/core/llm_providers.py
index f31a701..de36a55 100644
--- a/ai-hub/app/core/llm_providers.py
+++ b/ai-hub/app/core/llm_providers.py
@@ -45,7 +45,7 @@
async def generate_response(self, prompt: str) -> str:
# Construct the request payload
messages_payload = [
- # {"role": "system", "content": "You are a helpful assistant."},
+ {"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": prompt},
]
diff --git a/ai-hub/app/core/pipelines/__init__.py b/ai-hub/app/core/pipelines/__init__.py
new file mode 100644
index 0000000..3fbb1fd
--- /dev/null
+++ b/ai-hub/app/core/pipelines/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/core/pipelines/dspy_rag.py b/ai-hub/app/core/pipelines/dspy_rag.py
new file mode 100644
index 0000000..322c01b
--- /dev/null
+++ b/ai-hub/app/core/pipelines/dspy_rag.py
@@ -0,0 +1,87 @@
+# In app/core/pipelines/dspy_rag.py
+
+import dspy
+import logging
+from typing import List
+from types import SimpleNamespace
+from sqlalchemy.orm import Session
+
+from app.core.retrievers import Retriever
+from app.core.llm_providers import LLMProvider
+
+class DSPyLLMProvider(dspy.BaseLM):
+ """
+ A custom wrapper for the LLMProvider to make it compatible with DSPy.
+ """
+ def __init__(self, provider: LLMProvider, model_name: str, **kwargs):
+ super().__init__(model=model_name)
+ self.provider = provider
+ self.kwargs.update(kwargs)
+ print(f"DSPyLLMProvider initialized for model: {self.model}")
+
+ async def aforward(self, prompt: str, **kwargs):
+ """
+ The required asynchronous forward pass for the language model.
+ """
+ logging.info(f"[DSPyLLMProvider.aforward] Received prompt of length: {len(prompt) if prompt else 0}")
+ if not prompt or not prompt.strip():
+ logging.error("[DSPyLLMProvider.aforward] Received a null or empty prompt!")
+ return SimpleNamespace(choices=[SimpleNamespace(message=SimpleNamespace(content="Error: Received an empty prompt."))])
+
+ response_text = await self.provider.generate_response(prompt)
+
+ mock_choice = SimpleNamespace(message=SimpleNamespace(content=response_text, tool_calls=None))
+ return SimpleNamespace(choices=[mock_choice], usage=SimpleNamespace(prompt_tokens=0, completion_tokens=0, total_tokens=0), model=self.model)
+
+class AnswerWithContext(dspy.Signature):
+ """Given the context, answer the user's question."""
+ context = dspy.InputField(desc="Relevant document snippets from the knowledge base.")
+ question = dspy.InputField()
+ answer = dspy.OutputField()
+
+class DspyRagPipeline(dspy.Module):
+ """
+ A simple RAG pipeline that retrieves context and then generates an answer using DSPy.
+ """
+ def __init__(self, retrievers: List[Retriever]):
+ super().__init__()
+ self.retrievers = retrievers
+ # We still define the predictor to access its signature easily.
+ self.generate_answer = dspy.Predict(AnswerWithContext)
+
+ async def forward(self, question: str, db: Session) -> str:
+ """
+ Executes the RAG pipeline asynchronously.
+ """
+ logging.info(f"[DspyRagPipeline.forward] Received question: '{question}'")
+ retrieved_contexts = []
+ for retriever in self.retrievers:
+ context = retriever.retrieve_context(question, db)
+ retrieved_contexts.extend(context)
+
+ context_text = "\n\n".join(retrieved_contexts)
+ if not context_text:
+ print("⚠️ No context retrieved. Falling back to direct QA.")
+ context_text = "No context provided."
+
+ lm = dspy.settings.lm
+ if lm is None:
+ raise RuntimeError("DSPy LM has not been configured. Call dspy.configure(lm=...) first.")
+
+ # --- FIX: Revert to manual prompt construction ---
+ # Get the instruction from the signature's docstring.
+ instruction = self.generate_answer.signature.__doc__
+
+ # Build the full prompt exactly as DSPy would.
+ full_prompt = (
+ f"{instruction}\n\n"
+ f"---\n\n"
+ f"Context: {context_text}\n\n"
+ f"Question: {question}\n\n"
+ f"Answer:"
+ )
+
+ # Call the language model's aforward method directly with the complete prompt.
+ response_obj = await lm.aforward(prompt=full_prompt)
+
+ return response_obj.choices[0].message.content
\ No newline at end of file
diff --git a/ai-hub/app/core/rag_service.py b/ai-hub/app/core/rag_service.py
index 7cb57e9..93b4bb9 100644
--- a/ai-hub/app/core/rag_service.py
+++ b/ai-hub/app/core/rag_service.py
@@ -1,120 +1,28 @@
-import asyncio
from typing import List, Dict, Any
-from types import SimpleNamespace
from sqlalchemy.orm import Session
from sqlalchemy.exc import SQLAlchemyError
import dspy
-import logging
from app.core.vector_store import FaissVectorStore
from app.db import models
from app.core.retrievers import Retriever
-from app.core.llm_providers import LLMProvider, get_llm_provider
-
-# --- DSPy Components for RAG ---
-
-class DSPyLLMProvider(dspy.BaseLM):
- """
- A custom wrapper for the LLMProvider to make it compatible with DSPy.
- """
- def __init__(self, provider: LLMProvider, model_name: str, **kwargs):
- super().__init__(model=model_name)
- self.provider = provider
- self.kwargs.update(kwargs)
- print(f"DSPyLLMProvider initialized for model: {self.model}")
-
- async def aforward(self, prompt: str, **kwargs):
- """
- The required asynchronous forward pass for the language model.
- """
- logging.info(f"[DSPyLLMProvider.aforward] Received prompt of length: {len(prompt) if prompt else 0}")
-
- # --- CRITICAL FIX: Ensure prompt is not None or empty ---
- if not prompt or not prompt.strip():
- logging.error("[DSPyLLMProvider.aforward] Received a null or empty prompt!")
- # Return a default, safe response instead of calling the API with null.
- return SimpleNamespace(choices=[SimpleNamespace(message=SimpleNamespace(content="Error: Received an empty prompt."))])
-
- # Call the async provider directly using the existing event loop
- response_text = await self.provider.generate_response(prompt)
-
- # Create a mock response object that mimics the OpenAI API structure
- mock_choice = SimpleNamespace(
- message=SimpleNamespace(content=response_text, tool_calls=None)
- )
- mock_response = SimpleNamespace(
- choices=[mock_choice],
- usage=SimpleNamespace(prompt_tokens=0, completion_tokens=0, total_tokens=0),
- model=self.model
- )
- return mock_response
-
-class AnswerWithContext(dspy.Signature):
- """
- Signature for our RAG task: input is a context and question, output is an answer.
- """
- context = dspy.InputField(desc="Relevant document snippets from the knowledge base.")
- question = dspy.InputField()
- answer = dspy.OutputField()
-
-class RAGPipeline(dspy.Module):
- """
- A simple RAG pipeline that retrieves context and then generates an answer.
- """
- def __init__(self, retrievers: List[Retriever]):
- super().__init__()
- self.retrievers = retrievers
- # We only need the signature here to generate the prompt text.
- self.generate_answer = dspy.Predict(AnswerWithContext)
-
- async def forward(self, question: str, db: Session) -> str:
- """
- Executes the RAG pipeline asynchronously.
- """
- logging.info(f"[RAGPipeline.forward] Received question: '{question}'")
- retrieved_contexts = []
- for retriever in self.retrievers:
- context = retriever.retrieve_context(question, db)
- retrieved_contexts.extend(context)
-
- context_text = "\n\n".join(retrieved_contexts)
- if not context_text:
- print("⚠️ No context retrieved. Falling back to direct QA.")
- context_text = "No context provided."
-
- # --- REVISED LOGIC ---
- # 1. Manually create the full prompt using the signature's template.
- # The `dspy.Predict` object can be called with the inputs to get the compiled prompt.
- # We access the last generated prompt from the LM's history.
- # Since we haven't called the LM yet, we temporarily configure a basic LM.
-
- # Get the configured language model from dspy settings
- lm = dspy.settings.lm
- if lm is None:
- raise RuntimeError("DSPy LM has not been configured. Call dspy.configure(lm=...) first.")
-
- # 2. Use the signature to create a dspy.Example, which generates the prompt.
- # The dspy.Predict module will format this into a prompt string.
- example = dspy.Example(context=context_text, question=question, signatures=self.generate_answer.signature)
-
- # 3. Call the language model directly with the full prompt string.
- # The `example.signatures` contains the logic to render the prompt.
- # In modern DSPy, `dspy.predict` is a simpler way to do this.
- # We will call the LM's aforward method directly for clarity.
- full_prompt = self.generate_answer.signature.instructions.format(context=context_text, question=question) + "\nAnswer:"
-
- response_obj = await lm.aforward(prompt=full_prompt)
-
- return response_obj.choices[0].message.content
+from app.core.llm_providers import get_llm_provider
+from app.core.pipelines.dspy_rag import DspyRagPipeline, DSPyLLMProvider
-# --- Main RAG Service Class --- (This class remains unchanged)
class RAGService:
+ """
+ Service class for managing the RAG (Retrieval-Augmented Generation) pipeline.
+ This class acts as a high-level orchestrator.
+ """
def __init__(self, vector_store: FaissVectorStore, retrievers: List[Retriever]):
self.vector_store = vector_store
self.retrievers = retrievers
def add_document(self, db: Session, doc_data: Dict[str, Any]) -> int:
+ """
+ Adds a document to both the database and the vector store.
+ """
try:
document_db = models.Document(
title=doc_data["title"],
@@ -124,7 +32,9 @@
db.add(document_db)
db.commit()
db.refresh(document_db)
+
faiss_index = self.vector_store.add_document(document_db.text)
+
vector_metadata = models.VectorMetadata(
document_id=document_db.id,
faiss_index=faiss_index,
@@ -144,16 +54,24 @@
raise
async def chat_with_rag(self, db: Session, prompt: str, model: str) -> str:
+ """
+ Generates a response to a user prompt by orchestrating the RAG pipeline.
+ """
print(f"Received Prompt: {prompt}")
if not prompt or not prompt.strip():
raise ValueError("The prompt cannot be null, empty, or contain only whitespace.")
+ # 1. Get the underlying LLM provider (e.g., Gemini, DeepSeek)
llm_provider_instance = get_llm_provider(model)
+
+ # 2. Wrap it in our custom DSPy-compatible provider
dspy_llm_provider = DSPyLLMProvider(provider=llm_provider_instance, model_name=model)
- # Configure dspy's global settings with our custom LM
+ # 3. Configure DSPy's global settings to use our custom LM
dspy.configure(lm=dspy_llm_provider)
- rag_pipeline = RAGPipeline(retrievers=self.retrievers)
+ # 4. Initialize and execute the RAG pipeline
+ rag_pipeline = DspyRagPipeline(retrievers=self.retrievers)
answer = await rag_pipeline.forward(question=prompt, db=db)
+
return answer
\ No newline at end of file
diff --git a/ai-hub/app/db/database.py b/ai-hub/app/db/database.py
index 2fc97fb..cbb6b13 100644
--- a/ai-hub/app/db/database.py
+++ b/ai-hub/app/db/database.py
@@ -9,7 +9,7 @@
# Default database URLs
POSTGRES_DEFAULT_URL = "postgresql://user:password@localhost/ai_hub_db"
-SQLITE_DEFAULT_URL = "sqlite:///./ai_hub.db"
+SQLITE_DEFAULT_URL = "sqlite:///./data/ai_hub.db"
DATABASE_URL = ""
engine_args = {}
diff --git a/.gitignore b/.gitignore
index 3e0b2c9..95f61e0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,5 +2,6 @@
.env
**/.env
**/*.egg-info
-faiss_index.bin
-ai_hub.db
\ No newline at end of file
+**/faiss_index.bin
+**/ai_hub.db
+.pytest_cache/
\ No newline at end of file
diff --git a/ai-hub/app/__init__.py b/ai-hub/app/__init__.py
index e69de29..3fbb1fd 100644
--- a/ai-hub/app/__init__.py
+++ b/ai-hub/app/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/app.py b/ai-hub/app/app.py
index 04f9d7b..0718c32 100644
--- a/ai-hub/app/app.py
+++ b/ai-hub/app/app.py
@@ -24,7 +24,7 @@
"""
# Initialize core services for RAG
# CORRECTED: Now passing the required arguments to FaissVectorStore
- vector_store = FaissVectorStore(index_file_path="faiss_index.bin", dimension=768)
+ vector_store = FaissVectorStore(index_file_path="data/faiss_index.bin", dimension=768)
retrievers: List[Retriever] = [
FaissDBRetriever(vector_store=vector_store),
]
diff --git a/ai-hub/app/core/__init__.py b/ai-hub/app/core/__init__.py
index e69de29..3fbb1fd 100644
--- a/ai-hub/app/core/__init__.py
+++ b/ai-hub/app/core/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/core/llm_providers.py b/ai-hub/app/core/llm_providers.py
index f31a701..de36a55 100644
--- a/ai-hub/app/core/llm_providers.py
+++ b/ai-hub/app/core/llm_providers.py
@@ -45,7 +45,7 @@
async def generate_response(self, prompt: str) -> str:
# Construct the request payload
messages_payload = [
- # {"role": "system", "content": "You are a helpful assistant."},
+ {"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": prompt},
]
diff --git a/ai-hub/app/core/pipelines/__init__.py b/ai-hub/app/core/pipelines/__init__.py
new file mode 100644
index 0000000..3fbb1fd
--- /dev/null
+++ b/ai-hub/app/core/pipelines/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/core/pipelines/dspy_rag.py b/ai-hub/app/core/pipelines/dspy_rag.py
new file mode 100644
index 0000000..322c01b
--- /dev/null
+++ b/ai-hub/app/core/pipelines/dspy_rag.py
@@ -0,0 +1,87 @@
+# In app/core/pipelines/dspy_rag.py
+
+import dspy
+import logging
+from typing import List
+from types import SimpleNamespace
+from sqlalchemy.orm import Session
+
+from app.core.retrievers import Retriever
+from app.core.llm_providers import LLMProvider
+
+class DSPyLLMProvider(dspy.BaseLM):
+ """
+ A custom wrapper for the LLMProvider to make it compatible with DSPy.
+ """
+ def __init__(self, provider: LLMProvider, model_name: str, **kwargs):
+ super().__init__(model=model_name)
+ self.provider = provider
+ self.kwargs.update(kwargs)
+ print(f"DSPyLLMProvider initialized for model: {self.model}")
+
+ async def aforward(self, prompt: str, **kwargs):
+ """
+ The required asynchronous forward pass for the language model.
+ """
+ logging.info(f"[DSPyLLMProvider.aforward] Received prompt of length: {len(prompt) if prompt else 0}")
+ if not prompt or not prompt.strip():
+ logging.error("[DSPyLLMProvider.aforward] Received a null or empty prompt!")
+ return SimpleNamespace(choices=[SimpleNamespace(message=SimpleNamespace(content="Error: Received an empty prompt."))])
+
+ response_text = await self.provider.generate_response(prompt)
+
+ mock_choice = SimpleNamespace(message=SimpleNamespace(content=response_text, tool_calls=None))
+ return SimpleNamespace(choices=[mock_choice], usage=SimpleNamespace(prompt_tokens=0, completion_tokens=0, total_tokens=0), model=self.model)
+
+class AnswerWithContext(dspy.Signature):
+ """Given the context, answer the user's question."""
+ context = dspy.InputField(desc="Relevant document snippets from the knowledge base.")
+ question = dspy.InputField()
+ answer = dspy.OutputField()
+
+class DspyRagPipeline(dspy.Module):
+ """
+ A simple RAG pipeline that retrieves context and then generates an answer using DSPy.
+ """
+ def __init__(self, retrievers: List[Retriever]):
+ super().__init__()
+ self.retrievers = retrievers
+ # We still define the predictor to access its signature easily.
+ self.generate_answer = dspy.Predict(AnswerWithContext)
+
+ async def forward(self, question: str, db: Session) -> str:
+ """
+ Executes the RAG pipeline asynchronously.
+ """
+ logging.info(f"[DspyRagPipeline.forward] Received question: '{question}'")
+ retrieved_contexts = []
+ for retriever in self.retrievers:
+ context = retriever.retrieve_context(question, db)
+ retrieved_contexts.extend(context)
+
+ context_text = "\n\n".join(retrieved_contexts)
+ if not context_text:
+ print("⚠️ No context retrieved. Falling back to direct QA.")
+ context_text = "No context provided."
+
+ lm = dspy.settings.lm
+ if lm is None:
+ raise RuntimeError("DSPy LM has not been configured. Call dspy.configure(lm=...) first.")
+
+ # --- FIX: Revert to manual prompt construction ---
+ # Get the instruction from the signature's docstring.
+ instruction = self.generate_answer.signature.__doc__
+
+ # Build the full prompt exactly as DSPy would.
+ full_prompt = (
+ f"{instruction}\n\n"
+ f"---\n\n"
+ f"Context: {context_text}\n\n"
+ f"Question: {question}\n\n"
+ f"Answer:"
+ )
+
+ # Call the language model's aforward method directly with the complete prompt.
+ response_obj = await lm.aforward(prompt=full_prompt)
+
+ return response_obj.choices[0].message.content
\ No newline at end of file
diff --git a/ai-hub/app/core/rag_service.py b/ai-hub/app/core/rag_service.py
index 7cb57e9..93b4bb9 100644
--- a/ai-hub/app/core/rag_service.py
+++ b/ai-hub/app/core/rag_service.py
@@ -1,120 +1,28 @@
-import asyncio
from typing import List, Dict, Any
-from types import SimpleNamespace
from sqlalchemy.orm import Session
from sqlalchemy.exc import SQLAlchemyError
import dspy
-import logging
from app.core.vector_store import FaissVectorStore
from app.db import models
from app.core.retrievers import Retriever
-from app.core.llm_providers import LLMProvider, get_llm_provider
-
-# --- DSPy Components for RAG ---
-
-class DSPyLLMProvider(dspy.BaseLM):
- """
- A custom wrapper for the LLMProvider to make it compatible with DSPy.
- """
- def __init__(self, provider: LLMProvider, model_name: str, **kwargs):
- super().__init__(model=model_name)
- self.provider = provider
- self.kwargs.update(kwargs)
- print(f"DSPyLLMProvider initialized for model: {self.model}")
-
- async def aforward(self, prompt: str, **kwargs):
- """
- The required asynchronous forward pass for the language model.
- """
- logging.info(f"[DSPyLLMProvider.aforward] Received prompt of length: {len(prompt) if prompt else 0}")
-
- # --- CRITICAL FIX: Ensure prompt is not None or empty ---
- if not prompt or not prompt.strip():
- logging.error("[DSPyLLMProvider.aforward] Received a null or empty prompt!")
- # Return a default, safe response instead of calling the API with null.
- return SimpleNamespace(choices=[SimpleNamespace(message=SimpleNamespace(content="Error: Received an empty prompt."))])
-
- # Call the async provider directly using the existing event loop
- response_text = await self.provider.generate_response(prompt)
-
- # Create a mock response object that mimics the OpenAI API structure
- mock_choice = SimpleNamespace(
- message=SimpleNamespace(content=response_text, tool_calls=None)
- )
- mock_response = SimpleNamespace(
- choices=[mock_choice],
- usage=SimpleNamespace(prompt_tokens=0, completion_tokens=0, total_tokens=0),
- model=self.model
- )
- return mock_response
-
-class AnswerWithContext(dspy.Signature):
- """
- Signature for our RAG task: input is a context and question, output is an answer.
- """
- context = dspy.InputField(desc="Relevant document snippets from the knowledge base.")
- question = dspy.InputField()
- answer = dspy.OutputField()
-
-class RAGPipeline(dspy.Module):
- """
- A simple RAG pipeline that retrieves context and then generates an answer.
- """
- def __init__(self, retrievers: List[Retriever]):
- super().__init__()
- self.retrievers = retrievers
- # We only need the signature here to generate the prompt text.
- self.generate_answer = dspy.Predict(AnswerWithContext)
-
- async def forward(self, question: str, db: Session) -> str:
- """
- Executes the RAG pipeline asynchronously.
- """
- logging.info(f"[RAGPipeline.forward] Received question: '{question}'")
- retrieved_contexts = []
- for retriever in self.retrievers:
- context = retriever.retrieve_context(question, db)
- retrieved_contexts.extend(context)
-
- context_text = "\n\n".join(retrieved_contexts)
- if not context_text:
- print("⚠️ No context retrieved. Falling back to direct QA.")
- context_text = "No context provided."
-
- # --- REVISED LOGIC ---
- # 1. Manually create the full prompt using the signature's template.
- # The `dspy.Predict` object can be called with the inputs to get the compiled prompt.
- # We access the last generated prompt from the LM's history.
- # Since we haven't called the LM yet, we temporarily configure a basic LM.
-
- # Get the configured language model from dspy settings
- lm = dspy.settings.lm
- if lm is None:
- raise RuntimeError("DSPy LM has not been configured. Call dspy.configure(lm=...) first.")
-
- # 2. Use the signature to create a dspy.Example, which generates the prompt.
- # The dspy.Predict module will format this into a prompt string.
- example = dspy.Example(context=context_text, question=question, signatures=self.generate_answer.signature)
-
- # 3. Call the language model directly with the full prompt string.
- # The `example.signatures` contains the logic to render the prompt.
- # In modern DSPy, `dspy.predict` is a simpler way to do this.
- # We will call the LM's aforward method directly for clarity.
- full_prompt = self.generate_answer.signature.instructions.format(context=context_text, question=question) + "\nAnswer:"
-
- response_obj = await lm.aforward(prompt=full_prompt)
-
- return response_obj.choices[0].message.content
+from app.core.llm_providers import get_llm_provider
+from app.core.pipelines.dspy_rag import DspyRagPipeline, DSPyLLMProvider
-# --- Main RAG Service Class --- (This class remains unchanged)
class RAGService:
+ """
+ Service class for managing the RAG (Retrieval-Augmented Generation) pipeline.
+ This class acts as a high-level orchestrator.
+ """
def __init__(self, vector_store: FaissVectorStore, retrievers: List[Retriever]):
self.vector_store = vector_store
self.retrievers = retrievers
def add_document(self, db: Session, doc_data: Dict[str, Any]) -> int:
+ """
+ Adds a document to both the database and the vector store.
+ """
try:
document_db = models.Document(
title=doc_data["title"],
@@ -124,7 +32,9 @@
db.add(document_db)
db.commit()
db.refresh(document_db)
+
faiss_index = self.vector_store.add_document(document_db.text)
+
vector_metadata = models.VectorMetadata(
document_id=document_db.id,
faiss_index=faiss_index,
@@ -144,16 +54,24 @@
raise
async def chat_with_rag(self, db: Session, prompt: str, model: str) -> str:
+ """
+ Generates a response to a user prompt by orchestrating the RAG pipeline.
+ """
print(f"Received Prompt: {prompt}")
if not prompt or not prompt.strip():
raise ValueError("The prompt cannot be null, empty, or contain only whitespace.")
+ # 1. Get the underlying LLM provider (e.g., Gemini, DeepSeek)
llm_provider_instance = get_llm_provider(model)
+
+ # 2. Wrap it in our custom DSPy-compatible provider
dspy_llm_provider = DSPyLLMProvider(provider=llm_provider_instance, model_name=model)
- # Configure dspy's global settings with our custom LM
+ # 3. Configure DSPy's global settings to use our custom LM
dspy.configure(lm=dspy_llm_provider)
- rag_pipeline = RAGPipeline(retrievers=self.retrievers)
+ # 4. Initialize and execute the RAG pipeline
+ rag_pipeline = DspyRagPipeline(retrievers=self.retrievers)
answer = await rag_pipeline.forward(question=prompt, db=db)
+
return answer
\ No newline at end of file
diff --git a/ai-hub/app/db/database.py b/ai-hub/app/db/database.py
index 2fc97fb..cbb6b13 100644
--- a/ai-hub/app/db/database.py
+++ b/ai-hub/app/db/database.py
@@ -9,7 +9,7 @@
# Default database URLs
POSTGRES_DEFAULT_URL = "postgresql://user:password@localhost/ai_hub_db"
-SQLITE_DEFAULT_URL = "sqlite:///./ai_hub.db"
+SQLITE_DEFAULT_URL = "sqlite:///./data/ai_hub.db"
DATABASE_URL = ""
engine_args = {}
diff --git a/ai-hub/app/db/guide.md b/ai-hub/app/db/guide.md
index a3185c7..e8986e2 100644
--- a/ai-hub/app/db/guide.md
+++ b/ai-hub/app/db/guide.md
@@ -18,7 +18,7 @@
| Variable | Description | Default Value | Supported Values |
| -------------- | ------------------------------------- | ----------------------------------------------------------------------------------------------- | -------------------- |
| `DB_MODE` | Specifies the type of database to use | `"postgres"` | `postgres`, `sqlite` |
-| `DATABASE_URL` | Connection string for the database | PostgreSQL: `postgresql://user:password@localhost/ai_hub_db`
SQLite: `sqlite:///./ai_hub.db` | Any SQLAlchemy URI |
+| `DATABASE_URL` | Connection string for the database | PostgreSQL: `postgresql://user:password@localhost/ai_hub_db`
SQLite: `sqlite:///./data/ai_hub.db` | Any SQLAlchemy URI |
### 💡 Example: Switch to SQLite
diff --git a/.gitignore b/.gitignore
index 3e0b2c9..95f61e0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,5 +2,6 @@
.env
**/.env
**/*.egg-info
-faiss_index.bin
-ai_hub.db
\ No newline at end of file
+**/faiss_index.bin
+**/ai_hub.db
+.pytest_cache/
\ No newline at end of file
diff --git a/ai-hub/app/__init__.py b/ai-hub/app/__init__.py
index e69de29..3fbb1fd 100644
--- a/ai-hub/app/__init__.py
+++ b/ai-hub/app/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/app.py b/ai-hub/app/app.py
index 04f9d7b..0718c32 100644
--- a/ai-hub/app/app.py
+++ b/ai-hub/app/app.py
@@ -24,7 +24,7 @@
"""
# Initialize core services for RAG
# CORRECTED: Now passing the required arguments to FaissVectorStore
- vector_store = FaissVectorStore(index_file_path="faiss_index.bin", dimension=768)
+ vector_store = FaissVectorStore(index_file_path="data/faiss_index.bin", dimension=768)
retrievers: List[Retriever] = [
FaissDBRetriever(vector_store=vector_store),
]
diff --git a/ai-hub/app/core/__init__.py b/ai-hub/app/core/__init__.py
index e69de29..3fbb1fd 100644
--- a/ai-hub/app/core/__init__.py
+++ b/ai-hub/app/core/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/core/llm_providers.py b/ai-hub/app/core/llm_providers.py
index f31a701..de36a55 100644
--- a/ai-hub/app/core/llm_providers.py
+++ b/ai-hub/app/core/llm_providers.py
@@ -45,7 +45,7 @@
async def generate_response(self, prompt: str) -> str:
# Construct the request payload
messages_payload = [
- # {"role": "system", "content": "You are a helpful assistant."},
+ {"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": prompt},
]
diff --git a/ai-hub/app/core/pipelines/__init__.py b/ai-hub/app/core/pipelines/__init__.py
new file mode 100644
index 0000000..3fbb1fd
--- /dev/null
+++ b/ai-hub/app/core/pipelines/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/core/pipelines/dspy_rag.py b/ai-hub/app/core/pipelines/dspy_rag.py
new file mode 100644
index 0000000..322c01b
--- /dev/null
+++ b/ai-hub/app/core/pipelines/dspy_rag.py
@@ -0,0 +1,87 @@
+# In app/core/pipelines/dspy_rag.py
+
+import dspy
+import logging
+from typing import List
+from types import SimpleNamespace
+from sqlalchemy.orm import Session
+
+from app.core.retrievers import Retriever
+from app.core.llm_providers import LLMProvider
+
+class DSPyLLMProvider(dspy.BaseLM):
+ """
+ A custom wrapper for the LLMProvider to make it compatible with DSPy.
+ """
+ def __init__(self, provider: LLMProvider, model_name: str, **kwargs):
+ super().__init__(model=model_name)
+ self.provider = provider
+ self.kwargs.update(kwargs)
+ print(f"DSPyLLMProvider initialized for model: {self.model}")
+
+ async def aforward(self, prompt: str, **kwargs):
+ """
+ The required asynchronous forward pass for the language model.
+ """
+ logging.info(f"[DSPyLLMProvider.aforward] Received prompt of length: {len(prompt) if prompt else 0}")
+ if not prompt or not prompt.strip():
+ logging.error("[DSPyLLMProvider.aforward] Received a null or empty prompt!")
+ return SimpleNamespace(choices=[SimpleNamespace(message=SimpleNamespace(content="Error: Received an empty prompt."))])
+
+ response_text = await self.provider.generate_response(prompt)
+
+ mock_choice = SimpleNamespace(message=SimpleNamespace(content=response_text, tool_calls=None))
+ return SimpleNamespace(choices=[mock_choice], usage=SimpleNamespace(prompt_tokens=0, completion_tokens=0, total_tokens=0), model=self.model)
+
+class AnswerWithContext(dspy.Signature):
+ """Given the context, answer the user's question."""
+ context = dspy.InputField(desc="Relevant document snippets from the knowledge base.")
+ question = dspy.InputField()
+ answer = dspy.OutputField()
+
+class DspyRagPipeline(dspy.Module):
+ """
+ A simple RAG pipeline that retrieves context and then generates an answer using DSPy.
+ """
+ def __init__(self, retrievers: List[Retriever]):
+ super().__init__()
+ self.retrievers = retrievers
+ # We still define the predictor to access its signature easily.
+ self.generate_answer = dspy.Predict(AnswerWithContext)
+
+ async def forward(self, question: str, db: Session) -> str:
+ """
+ Executes the RAG pipeline asynchronously.
+ """
+ logging.info(f"[DspyRagPipeline.forward] Received question: '{question}'")
+ retrieved_contexts = []
+ for retriever in self.retrievers:
+ context = retriever.retrieve_context(question, db)
+ retrieved_contexts.extend(context)
+
+ context_text = "\n\n".join(retrieved_contexts)
+ if not context_text:
+ print("⚠️ No context retrieved. Falling back to direct QA.")
+ context_text = "No context provided."
+
+ lm = dspy.settings.lm
+ if lm is None:
+ raise RuntimeError("DSPy LM has not been configured. Call dspy.configure(lm=...) first.")
+
+ # --- FIX: Revert to manual prompt construction ---
+ # Get the instruction from the signature's docstring.
+ instruction = self.generate_answer.signature.__doc__
+
+ # Build the full prompt exactly as DSPy would.
+ full_prompt = (
+ f"{instruction}\n\n"
+ f"---\n\n"
+ f"Context: {context_text}\n\n"
+ f"Question: {question}\n\n"
+ f"Answer:"
+ )
+
+ # Call the language model's aforward method directly with the complete prompt.
+ response_obj = await lm.aforward(prompt=full_prompt)
+
+ return response_obj.choices[0].message.content
\ No newline at end of file
diff --git a/ai-hub/app/core/rag_service.py b/ai-hub/app/core/rag_service.py
index 7cb57e9..93b4bb9 100644
--- a/ai-hub/app/core/rag_service.py
+++ b/ai-hub/app/core/rag_service.py
@@ -1,120 +1,28 @@
-import asyncio
from typing import List, Dict, Any
-from types import SimpleNamespace
from sqlalchemy.orm import Session
from sqlalchemy.exc import SQLAlchemyError
import dspy
-import logging
from app.core.vector_store import FaissVectorStore
from app.db import models
from app.core.retrievers import Retriever
-from app.core.llm_providers import LLMProvider, get_llm_provider
-
-# --- DSPy Components for RAG ---
-
-class DSPyLLMProvider(dspy.BaseLM):
- """
- A custom wrapper for the LLMProvider to make it compatible with DSPy.
- """
- def __init__(self, provider: LLMProvider, model_name: str, **kwargs):
- super().__init__(model=model_name)
- self.provider = provider
- self.kwargs.update(kwargs)
- print(f"DSPyLLMProvider initialized for model: {self.model}")
-
- async def aforward(self, prompt: str, **kwargs):
- """
- The required asynchronous forward pass for the language model.
- """
- logging.info(f"[DSPyLLMProvider.aforward] Received prompt of length: {len(prompt) if prompt else 0}")
-
- # --- CRITICAL FIX: Ensure prompt is not None or empty ---
- if not prompt or not prompt.strip():
- logging.error("[DSPyLLMProvider.aforward] Received a null or empty prompt!")
- # Return a default, safe response instead of calling the API with null.
- return SimpleNamespace(choices=[SimpleNamespace(message=SimpleNamespace(content="Error: Received an empty prompt."))])
-
- # Call the async provider directly using the existing event loop
- response_text = await self.provider.generate_response(prompt)
-
- # Create a mock response object that mimics the OpenAI API structure
- mock_choice = SimpleNamespace(
- message=SimpleNamespace(content=response_text, tool_calls=None)
- )
- mock_response = SimpleNamespace(
- choices=[mock_choice],
- usage=SimpleNamespace(prompt_tokens=0, completion_tokens=0, total_tokens=0),
- model=self.model
- )
- return mock_response
-
-class AnswerWithContext(dspy.Signature):
- """
- Signature for our RAG task: input is a context and question, output is an answer.
- """
- context = dspy.InputField(desc="Relevant document snippets from the knowledge base.")
- question = dspy.InputField()
- answer = dspy.OutputField()
-
-class RAGPipeline(dspy.Module):
- """
- A simple RAG pipeline that retrieves context and then generates an answer.
- """
- def __init__(self, retrievers: List[Retriever]):
- super().__init__()
- self.retrievers = retrievers
- # We only need the signature here to generate the prompt text.
- self.generate_answer = dspy.Predict(AnswerWithContext)
-
- async def forward(self, question: str, db: Session) -> str:
- """
- Executes the RAG pipeline asynchronously.
- """
- logging.info(f"[RAGPipeline.forward] Received question: '{question}'")
- retrieved_contexts = []
- for retriever in self.retrievers:
- context = retriever.retrieve_context(question, db)
- retrieved_contexts.extend(context)
-
- context_text = "\n\n".join(retrieved_contexts)
- if not context_text:
- print("⚠️ No context retrieved. Falling back to direct QA.")
- context_text = "No context provided."
-
- # --- REVISED LOGIC ---
- # 1. Manually create the full prompt using the signature's template.
- # The `dspy.Predict` object can be called with the inputs to get the compiled prompt.
- # We access the last generated prompt from the LM's history.
- # Since we haven't called the LM yet, we temporarily configure a basic LM.
-
- # Get the configured language model from dspy settings
- lm = dspy.settings.lm
- if lm is None:
- raise RuntimeError("DSPy LM has not been configured. Call dspy.configure(lm=...) first.")
-
- # 2. Use the signature to create a dspy.Example, which generates the prompt.
- # The dspy.Predict module will format this into a prompt string.
- example = dspy.Example(context=context_text, question=question, signatures=self.generate_answer.signature)
-
- # 3. Call the language model directly with the full prompt string.
- # The `example.signatures` contains the logic to render the prompt.
- # In modern DSPy, `dspy.predict` is a simpler way to do this.
- # We will call the LM's aforward method directly for clarity.
- full_prompt = self.generate_answer.signature.instructions.format(context=context_text, question=question) + "\nAnswer:"
-
- response_obj = await lm.aforward(prompt=full_prompt)
-
- return response_obj.choices[0].message.content
+from app.core.llm_providers import get_llm_provider
+from app.core.pipelines.dspy_rag import DspyRagPipeline, DSPyLLMProvider
-# --- Main RAG Service Class --- (This class remains unchanged)
class RAGService:
+ """
+ Service class for managing the RAG (Retrieval-Augmented Generation) pipeline.
+ This class acts as a high-level orchestrator.
+ """
def __init__(self, vector_store: FaissVectorStore, retrievers: List[Retriever]):
self.vector_store = vector_store
self.retrievers = retrievers
def add_document(self, db: Session, doc_data: Dict[str, Any]) -> int:
+ """
+ Adds a document to both the database and the vector store.
+ """
try:
document_db = models.Document(
title=doc_data["title"],
@@ -124,7 +32,9 @@
db.add(document_db)
db.commit()
db.refresh(document_db)
+
faiss_index = self.vector_store.add_document(document_db.text)
+
vector_metadata = models.VectorMetadata(
document_id=document_db.id,
faiss_index=faiss_index,
@@ -144,16 +54,24 @@
raise
async def chat_with_rag(self, db: Session, prompt: str, model: str) -> str:
+ """
+ Generates a response to a user prompt by orchestrating the RAG pipeline.
+ """
print(f"Received Prompt: {prompt}")
if not prompt or not prompt.strip():
raise ValueError("The prompt cannot be null, empty, or contain only whitespace.")
+ # 1. Get the underlying LLM provider (e.g., Gemini, DeepSeek)
llm_provider_instance = get_llm_provider(model)
+
+ # 2. Wrap it in our custom DSPy-compatible provider
dspy_llm_provider = DSPyLLMProvider(provider=llm_provider_instance, model_name=model)
- # Configure dspy's global settings with our custom LM
+ # 3. Configure DSPy's global settings to use our custom LM
dspy.configure(lm=dspy_llm_provider)
- rag_pipeline = RAGPipeline(retrievers=self.retrievers)
+ # 4. Initialize and execute the RAG pipeline
+ rag_pipeline = DspyRagPipeline(retrievers=self.retrievers)
answer = await rag_pipeline.forward(question=prompt, db=db)
+
return answer
\ No newline at end of file
diff --git a/ai-hub/app/db/database.py b/ai-hub/app/db/database.py
index 2fc97fb..cbb6b13 100644
--- a/ai-hub/app/db/database.py
+++ b/ai-hub/app/db/database.py
@@ -9,7 +9,7 @@
# Default database URLs
POSTGRES_DEFAULT_URL = "postgresql://user:password@localhost/ai_hub_db"
-SQLITE_DEFAULT_URL = "sqlite:///./ai_hub.db"
+SQLITE_DEFAULT_URL = "sqlite:///./data/ai_hub.db"
DATABASE_URL = ""
engine_args = {}
diff --git a/ai-hub/app/db/guide.md b/ai-hub/app/db/guide.md
index a3185c7..e8986e2 100644
--- a/ai-hub/app/db/guide.md
+++ b/ai-hub/app/db/guide.md
@@ -18,7 +18,7 @@
| Variable | Description | Default Value | Supported Values |
| -------------- | ------------------------------------- | ----------------------------------------------------------------------------------------------- | -------------------- |
| `DB_MODE` | Specifies the type of database to use | `"postgres"` | `postgres`, `sqlite` |
-| `DATABASE_URL` | Connection string for the database | PostgreSQL: `postgresql://user:password@localhost/ai_hub_db`
SQLite: `sqlite:///./ai_hub.db` | Any SQLAlchemy URI |
+| `DATABASE_URL` | Connection string for the database | PostgreSQL: `postgresql://user:password@localhost/ai_hub_db`
SQLite: `sqlite:///./data/ai_hub.db` | Any SQLAlchemy URI |
### 💡 Example: Switch to SQLite
diff --git a/ai-hub/app/db_setup.py b/ai-hub/app/db_setup.py
index 739ebd2..63a64a6 100644
--- a/ai-hub/app/db_setup.py
+++ b/ai-hub/app/db_setup.py
@@ -11,7 +11,7 @@
# This configuration allows for easy switching between SQLite and PostgreSQL.
DB_MODE = os.getenv("DB_MODE", "sqlite")
if DB_MODE == "sqlite":
- DATABASE_URL = "sqlite:///./ai_hub.db"
+ DATABASE_URL = "sqlite:///./data/ai_hub.db"
# The connect_args are needed for SQLite to work with FastAPI's multiple threads
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
else:
diff --git a/.gitignore b/.gitignore
index 3e0b2c9..95f61e0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,5 +2,6 @@
.env
**/.env
**/*.egg-info
-faiss_index.bin
-ai_hub.db
\ No newline at end of file
+**/faiss_index.bin
+**/ai_hub.db
+.pytest_cache/
\ No newline at end of file
diff --git a/ai-hub/app/__init__.py b/ai-hub/app/__init__.py
index e69de29..3fbb1fd 100644
--- a/ai-hub/app/__init__.py
+++ b/ai-hub/app/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/app.py b/ai-hub/app/app.py
index 04f9d7b..0718c32 100644
--- a/ai-hub/app/app.py
+++ b/ai-hub/app/app.py
@@ -24,7 +24,7 @@
"""
# Initialize core services for RAG
# CORRECTED: Now passing the required arguments to FaissVectorStore
- vector_store = FaissVectorStore(index_file_path="faiss_index.bin", dimension=768)
+ vector_store = FaissVectorStore(index_file_path="data/faiss_index.bin", dimension=768)
retrievers: List[Retriever] = [
FaissDBRetriever(vector_store=vector_store),
]
diff --git a/ai-hub/app/core/__init__.py b/ai-hub/app/core/__init__.py
index e69de29..3fbb1fd 100644
--- a/ai-hub/app/core/__init__.py
+++ b/ai-hub/app/core/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/core/llm_providers.py b/ai-hub/app/core/llm_providers.py
index f31a701..de36a55 100644
--- a/ai-hub/app/core/llm_providers.py
+++ b/ai-hub/app/core/llm_providers.py
@@ -45,7 +45,7 @@
async def generate_response(self, prompt: str) -> str:
# Construct the request payload
messages_payload = [
- # {"role": "system", "content": "You are a helpful assistant."},
+ {"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": prompt},
]
diff --git a/ai-hub/app/core/pipelines/__init__.py b/ai-hub/app/core/pipelines/__init__.py
new file mode 100644
index 0000000..3fbb1fd
--- /dev/null
+++ b/ai-hub/app/core/pipelines/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/core/pipelines/dspy_rag.py b/ai-hub/app/core/pipelines/dspy_rag.py
new file mode 100644
index 0000000..322c01b
--- /dev/null
+++ b/ai-hub/app/core/pipelines/dspy_rag.py
@@ -0,0 +1,87 @@
+# In app/core/pipelines/dspy_rag.py
+
+import dspy
+import logging
+from typing import List
+from types import SimpleNamespace
+from sqlalchemy.orm import Session
+
+from app.core.retrievers import Retriever
+from app.core.llm_providers import LLMProvider
+
+class DSPyLLMProvider(dspy.BaseLM):
+ """
+ A custom wrapper for the LLMProvider to make it compatible with DSPy.
+ """
+ def __init__(self, provider: LLMProvider, model_name: str, **kwargs):
+ super().__init__(model=model_name)
+ self.provider = provider
+ self.kwargs.update(kwargs)
+ print(f"DSPyLLMProvider initialized for model: {self.model}")
+
+ async def aforward(self, prompt: str, **kwargs):
+ """
+ The required asynchronous forward pass for the language model.
+ """
+ logging.info(f"[DSPyLLMProvider.aforward] Received prompt of length: {len(prompt) if prompt else 0}")
+ if not prompt or not prompt.strip():
+ logging.error("[DSPyLLMProvider.aforward] Received a null or empty prompt!")
+ return SimpleNamespace(choices=[SimpleNamespace(message=SimpleNamespace(content="Error: Received an empty prompt."))])
+
+ response_text = await self.provider.generate_response(prompt)
+
+ mock_choice = SimpleNamespace(message=SimpleNamespace(content=response_text, tool_calls=None))
+ return SimpleNamespace(choices=[mock_choice], usage=SimpleNamespace(prompt_tokens=0, completion_tokens=0, total_tokens=0), model=self.model)
+
+class AnswerWithContext(dspy.Signature):
+ """Given the context, answer the user's question."""
+ context = dspy.InputField(desc="Relevant document snippets from the knowledge base.")
+ question = dspy.InputField()
+ answer = dspy.OutputField()
+
+class DspyRagPipeline(dspy.Module):
+ """
+ A simple RAG pipeline that retrieves context and then generates an answer using DSPy.
+ """
+ def __init__(self, retrievers: List[Retriever]):
+ super().__init__()
+ self.retrievers = retrievers
+ # We still define the predictor to access its signature easily.
+ self.generate_answer = dspy.Predict(AnswerWithContext)
+
+ async def forward(self, question: str, db: Session) -> str:
+ """
+ Executes the RAG pipeline asynchronously.
+ """
+ logging.info(f"[DspyRagPipeline.forward] Received question: '{question}'")
+ retrieved_contexts = []
+ for retriever in self.retrievers:
+ context = retriever.retrieve_context(question, db)
+ retrieved_contexts.extend(context)
+
+ context_text = "\n\n".join(retrieved_contexts)
+ if not context_text:
+ print("⚠️ No context retrieved. Falling back to direct QA.")
+ context_text = "No context provided."
+
+ lm = dspy.settings.lm
+ if lm is None:
+ raise RuntimeError("DSPy LM has not been configured. Call dspy.configure(lm=...) first.")
+
+ # --- FIX: Revert to manual prompt construction ---
+ # Get the instruction from the signature's docstring.
+ instruction = self.generate_answer.signature.__doc__
+
+ # Build the full prompt exactly as DSPy would.
+ full_prompt = (
+ f"{instruction}\n\n"
+ f"---\n\n"
+ f"Context: {context_text}\n\n"
+ f"Question: {question}\n\n"
+ f"Answer:"
+ )
+
+ # Call the language model's aforward method directly with the complete prompt.
+ response_obj = await lm.aforward(prompt=full_prompt)
+
+ return response_obj.choices[0].message.content
\ No newline at end of file
diff --git a/ai-hub/app/core/rag_service.py b/ai-hub/app/core/rag_service.py
index 7cb57e9..93b4bb9 100644
--- a/ai-hub/app/core/rag_service.py
+++ b/ai-hub/app/core/rag_service.py
@@ -1,120 +1,28 @@
-import asyncio
from typing import List, Dict, Any
-from types import SimpleNamespace
from sqlalchemy.orm import Session
from sqlalchemy.exc import SQLAlchemyError
import dspy
-import logging
from app.core.vector_store import FaissVectorStore
from app.db import models
from app.core.retrievers import Retriever
-from app.core.llm_providers import LLMProvider, get_llm_provider
-
-# --- DSPy Components for RAG ---
-
-class DSPyLLMProvider(dspy.BaseLM):
- """
- A custom wrapper for the LLMProvider to make it compatible with DSPy.
- """
- def __init__(self, provider: LLMProvider, model_name: str, **kwargs):
- super().__init__(model=model_name)
- self.provider = provider
- self.kwargs.update(kwargs)
- print(f"DSPyLLMProvider initialized for model: {self.model}")
-
- async def aforward(self, prompt: str, **kwargs):
- """
- The required asynchronous forward pass for the language model.
- """
- logging.info(f"[DSPyLLMProvider.aforward] Received prompt of length: {len(prompt) if prompt else 0}")
-
- # --- CRITICAL FIX: Ensure prompt is not None or empty ---
- if not prompt or not prompt.strip():
- logging.error("[DSPyLLMProvider.aforward] Received a null or empty prompt!")
- # Return a default, safe response instead of calling the API with null.
- return SimpleNamespace(choices=[SimpleNamespace(message=SimpleNamespace(content="Error: Received an empty prompt."))])
-
- # Call the async provider directly using the existing event loop
- response_text = await self.provider.generate_response(prompt)
-
- # Create a mock response object that mimics the OpenAI API structure
- mock_choice = SimpleNamespace(
- message=SimpleNamespace(content=response_text, tool_calls=None)
- )
- mock_response = SimpleNamespace(
- choices=[mock_choice],
- usage=SimpleNamespace(prompt_tokens=0, completion_tokens=0, total_tokens=0),
- model=self.model
- )
- return mock_response
-
-class AnswerWithContext(dspy.Signature):
- """
- Signature for our RAG task: input is a context and question, output is an answer.
- """
- context = dspy.InputField(desc="Relevant document snippets from the knowledge base.")
- question = dspy.InputField()
- answer = dspy.OutputField()
-
-class RAGPipeline(dspy.Module):
- """
- A simple RAG pipeline that retrieves context and then generates an answer.
- """
- def __init__(self, retrievers: List[Retriever]):
- super().__init__()
- self.retrievers = retrievers
- # We only need the signature here to generate the prompt text.
- self.generate_answer = dspy.Predict(AnswerWithContext)
-
- async def forward(self, question: str, db: Session) -> str:
- """
- Executes the RAG pipeline asynchronously.
- """
- logging.info(f"[RAGPipeline.forward] Received question: '{question}'")
- retrieved_contexts = []
- for retriever in self.retrievers:
- context = retriever.retrieve_context(question, db)
- retrieved_contexts.extend(context)
-
- context_text = "\n\n".join(retrieved_contexts)
- if not context_text:
- print("⚠️ No context retrieved. Falling back to direct QA.")
- context_text = "No context provided."
-
- # --- REVISED LOGIC ---
- # 1. Manually create the full prompt using the signature's template.
- # The `dspy.Predict` object can be called with the inputs to get the compiled prompt.
- # We access the last generated prompt from the LM's history.
- # Since we haven't called the LM yet, we temporarily configure a basic LM.
-
- # Get the configured language model from dspy settings
- lm = dspy.settings.lm
- if lm is None:
- raise RuntimeError("DSPy LM has not been configured. Call dspy.configure(lm=...) first.")
-
- # 2. Use the signature to create a dspy.Example, which generates the prompt.
- # The dspy.Predict module will format this into a prompt string.
- example = dspy.Example(context=context_text, question=question, signatures=self.generate_answer.signature)
-
- # 3. Call the language model directly with the full prompt string.
- # The `example.signatures` contains the logic to render the prompt.
- # In modern DSPy, `dspy.predict` is a simpler way to do this.
- # We will call the LM's aforward method directly for clarity.
- full_prompt = self.generate_answer.signature.instructions.format(context=context_text, question=question) + "\nAnswer:"
-
- response_obj = await lm.aforward(prompt=full_prompt)
-
- return response_obj.choices[0].message.content
+from app.core.llm_providers import get_llm_provider
+from app.core.pipelines.dspy_rag import DspyRagPipeline, DSPyLLMProvider
-# --- Main RAG Service Class --- (This class remains unchanged)
class RAGService:
+ """
+ Service class for managing the RAG (Retrieval-Augmented Generation) pipeline.
+ This class acts as a high-level orchestrator.
+ """
def __init__(self, vector_store: FaissVectorStore, retrievers: List[Retriever]):
self.vector_store = vector_store
self.retrievers = retrievers
def add_document(self, db: Session, doc_data: Dict[str, Any]) -> int:
+ """
+ Adds a document to both the database and the vector store.
+ """
try:
document_db = models.Document(
title=doc_data["title"],
@@ -124,7 +32,9 @@
db.add(document_db)
db.commit()
db.refresh(document_db)
+
faiss_index = self.vector_store.add_document(document_db.text)
+
vector_metadata = models.VectorMetadata(
document_id=document_db.id,
faiss_index=faiss_index,
@@ -144,16 +54,24 @@
raise
async def chat_with_rag(self, db: Session, prompt: str, model: str) -> str:
+ """
+ Generates a response to a user prompt by orchestrating the RAG pipeline.
+ """
print(f"Received Prompt: {prompt}")
if not prompt or not prompt.strip():
raise ValueError("The prompt cannot be null, empty, or contain only whitespace.")
+ # 1. Get the underlying LLM provider (e.g., Gemini, DeepSeek)
llm_provider_instance = get_llm_provider(model)
+
+ # 2. Wrap it in our custom DSPy-compatible provider
dspy_llm_provider = DSPyLLMProvider(provider=llm_provider_instance, model_name=model)
- # Configure dspy's global settings with our custom LM
+ # 3. Configure DSPy's global settings to use our custom LM
dspy.configure(lm=dspy_llm_provider)
- rag_pipeline = RAGPipeline(retrievers=self.retrievers)
+ # 4. Initialize and execute the RAG pipeline
+ rag_pipeline = DspyRagPipeline(retrievers=self.retrievers)
answer = await rag_pipeline.forward(question=prompt, db=db)
+
return answer
\ No newline at end of file
diff --git a/ai-hub/app/db/database.py b/ai-hub/app/db/database.py
index 2fc97fb..cbb6b13 100644
--- a/ai-hub/app/db/database.py
+++ b/ai-hub/app/db/database.py
@@ -9,7 +9,7 @@
# Default database URLs
POSTGRES_DEFAULT_URL = "postgresql://user:password@localhost/ai_hub_db"
-SQLITE_DEFAULT_URL = "sqlite:///./ai_hub.db"
+SQLITE_DEFAULT_URL = "sqlite:///./data/ai_hub.db"
DATABASE_URL = ""
engine_args = {}
diff --git a/ai-hub/app/db/guide.md b/ai-hub/app/db/guide.md
index a3185c7..e8986e2 100644
--- a/ai-hub/app/db/guide.md
+++ b/ai-hub/app/db/guide.md
@@ -18,7 +18,7 @@
| Variable | Description | Default Value | Supported Values |
| -------------- | ------------------------------------- | ----------------------------------------------------------------------------------------------- | -------------------- |
| `DB_MODE` | Specifies the type of database to use | `"postgres"` | `postgres`, `sqlite` |
-| `DATABASE_URL` | Connection string for the database | PostgreSQL: `postgresql://user:password@localhost/ai_hub_db`
SQLite: `sqlite:///./ai_hub.db` | Any SQLAlchemy URI |
+| `DATABASE_URL` | Connection string for the database | PostgreSQL: `postgresql://user:password@localhost/ai_hub_db`
SQLite: `sqlite:///./data/ai_hub.db` | Any SQLAlchemy URI |
### 💡 Example: Switch to SQLite
diff --git a/ai-hub/app/db_setup.py b/ai-hub/app/db_setup.py
index 739ebd2..63a64a6 100644
--- a/ai-hub/app/db_setup.py
+++ b/ai-hub/app/db_setup.py
@@ -11,7 +11,7 @@
# This configuration allows for easy switching between SQLite and PostgreSQL.
DB_MODE = os.getenv("DB_MODE", "sqlite")
if DB_MODE == "sqlite":
- DATABASE_URL = "sqlite:///./ai_hub.db"
+ DATABASE_URL = "sqlite:///./data/ai_hub.db"
# The connect_args are needed for SQLite to work with FastAPI's multiple threads
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
else:
diff --git a/ai-hub/tests/core/__init__.py b/ai-hub/tests/core/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/ai-hub/tests/core/__init__.py
diff --git a/.gitignore b/.gitignore
index 3e0b2c9..95f61e0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,5 +2,6 @@
.env
**/.env
**/*.egg-info
-faiss_index.bin
-ai_hub.db
\ No newline at end of file
+**/faiss_index.bin
+**/ai_hub.db
+.pytest_cache/
\ No newline at end of file
diff --git a/ai-hub/app/__init__.py b/ai-hub/app/__init__.py
index e69de29..3fbb1fd 100644
--- a/ai-hub/app/__init__.py
+++ b/ai-hub/app/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/app.py b/ai-hub/app/app.py
index 04f9d7b..0718c32 100644
--- a/ai-hub/app/app.py
+++ b/ai-hub/app/app.py
@@ -24,7 +24,7 @@
"""
# Initialize core services for RAG
# CORRECTED: Now passing the required arguments to FaissVectorStore
- vector_store = FaissVectorStore(index_file_path="faiss_index.bin", dimension=768)
+ vector_store = FaissVectorStore(index_file_path="data/faiss_index.bin", dimension=768)
retrievers: List[Retriever] = [
FaissDBRetriever(vector_store=vector_store),
]
diff --git a/ai-hub/app/core/__init__.py b/ai-hub/app/core/__init__.py
index e69de29..3fbb1fd 100644
--- a/ai-hub/app/core/__init__.py
+++ b/ai-hub/app/core/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/core/llm_providers.py b/ai-hub/app/core/llm_providers.py
index f31a701..de36a55 100644
--- a/ai-hub/app/core/llm_providers.py
+++ b/ai-hub/app/core/llm_providers.py
@@ -45,7 +45,7 @@
async def generate_response(self, prompt: str) -> str:
# Construct the request payload
messages_payload = [
- # {"role": "system", "content": "You are a helpful assistant."},
+ {"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": prompt},
]
diff --git a/ai-hub/app/core/pipelines/__init__.py b/ai-hub/app/core/pipelines/__init__.py
new file mode 100644
index 0000000..3fbb1fd
--- /dev/null
+++ b/ai-hub/app/core/pipelines/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/core/pipelines/dspy_rag.py b/ai-hub/app/core/pipelines/dspy_rag.py
new file mode 100644
index 0000000..322c01b
--- /dev/null
+++ b/ai-hub/app/core/pipelines/dspy_rag.py
@@ -0,0 +1,87 @@
+# In app/core/pipelines/dspy_rag.py
+
+import dspy
+import logging
+from typing import List
+from types import SimpleNamespace
+from sqlalchemy.orm import Session
+
+from app.core.retrievers import Retriever
+from app.core.llm_providers import LLMProvider
+
+class DSPyLLMProvider(dspy.BaseLM):
+ """
+ A custom wrapper for the LLMProvider to make it compatible with DSPy.
+ """
+ def __init__(self, provider: LLMProvider, model_name: str, **kwargs):
+ super().__init__(model=model_name)
+ self.provider = provider
+ self.kwargs.update(kwargs)
+ print(f"DSPyLLMProvider initialized for model: {self.model}")
+
+ async def aforward(self, prompt: str, **kwargs):
+ """
+ The required asynchronous forward pass for the language model.
+ """
+ logging.info(f"[DSPyLLMProvider.aforward] Received prompt of length: {len(prompt) if prompt else 0}")
+ if not prompt or not prompt.strip():
+ logging.error("[DSPyLLMProvider.aforward] Received a null or empty prompt!")
+ return SimpleNamespace(choices=[SimpleNamespace(message=SimpleNamespace(content="Error: Received an empty prompt."))])
+
+ response_text = await self.provider.generate_response(prompt)
+
+ mock_choice = SimpleNamespace(message=SimpleNamespace(content=response_text, tool_calls=None))
+ return SimpleNamespace(choices=[mock_choice], usage=SimpleNamespace(prompt_tokens=0, completion_tokens=0, total_tokens=0), model=self.model)
+
+class AnswerWithContext(dspy.Signature):
+ """Given the context, answer the user's question."""
+ context = dspy.InputField(desc="Relevant document snippets from the knowledge base.")
+ question = dspy.InputField()
+ answer = dspy.OutputField()
+
+class DspyRagPipeline(dspy.Module):
+ """
+ A simple RAG pipeline that retrieves context and then generates an answer using DSPy.
+ """
+ def __init__(self, retrievers: List[Retriever]):
+ super().__init__()
+ self.retrievers = retrievers
+ # We still define the predictor to access its signature easily.
+ self.generate_answer = dspy.Predict(AnswerWithContext)
+
+ async def forward(self, question: str, db: Session) -> str:
+ """
+ Executes the RAG pipeline asynchronously.
+ """
+ logging.info(f"[DspyRagPipeline.forward] Received question: '{question}'")
+ retrieved_contexts = []
+ for retriever in self.retrievers:
+ context = retriever.retrieve_context(question, db)
+ retrieved_contexts.extend(context)
+
+ context_text = "\n\n".join(retrieved_contexts)
+ if not context_text:
+ print("⚠️ No context retrieved. Falling back to direct QA.")
+ context_text = "No context provided."
+
+ lm = dspy.settings.lm
+ if lm is None:
+ raise RuntimeError("DSPy LM has not been configured. Call dspy.configure(lm=...) first.")
+
+ # --- FIX: Revert to manual prompt construction ---
+ # Get the instruction from the signature's docstring.
+ instruction = self.generate_answer.signature.__doc__
+
+ # Build the full prompt exactly as DSPy would.
+ full_prompt = (
+ f"{instruction}\n\n"
+ f"---\n\n"
+ f"Context: {context_text}\n\n"
+ f"Question: {question}\n\n"
+ f"Answer:"
+ )
+
+ # Call the language model's aforward method directly with the complete prompt.
+ response_obj = await lm.aforward(prompt=full_prompt)
+
+ return response_obj.choices[0].message.content
\ No newline at end of file
diff --git a/ai-hub/app/core/rag_service.py b/ai-hub/app/core/rag_service.py
index 7cb57e9..93b4bb9 100644
--- a/ai-hub/app/core/rag_service.py
+++ b/ai-hub/app/core/rag_service.py
@@ -1,120 +1,28 @@
-import asyncio
from typing import List, Dict, Any
-from types import SimpleNamespace
from sqlalchemy.orm import Session
from sqlalchemy.exc import SQLAlchemyError
import dspy
-import logging
from app.core.vector_store import FaissVectorStore
from app.db import models
from app.core.retrievers import Retriever
-from app.core.llm_providers import LLMProvider, get_llm_provider
-
-# --- DSPy Components for RAG ---
-
-class DSPyLLMProvider(dspy.BaseLM):
- """
- A custom wrapper for the LLMProvider to make it compatible with DSPy.
- """
- def __init__(self, provider: LLMProvider, model_name: str, **kwargs):
- super().__init__(model=model_name)
- self.provider = provider
- self.kwargs.update(kwargs)
- print(f"DSPyLLMProvider initialized for model: {self.model}")
-
- async def aforward(self, prompt: str, **kwargs):
- """
- The required asynchronous forward pass for the language model.
- """
- logging.info(f"[DSPyLLMProvider.aforward] Received prompt of length: {len(prompt) if prompt else 0}")
-
- # --- CRITICAL FIX: Ensure prompt is not None or empty ---
- if not prompt or not prompt.strip():
- logging.error("[DSPyLLMProvider.aforward] Received a null or empty prompt!")
- # Return a default, safe response instead of calling the API with null.
- return SimpleNamespace(choices=[SimpleNamespace(message=SimpleNamespace(content="Error: Received an empty prompt."))])
-
- # Call the async provider directly using the existing event loop
- response_text = await self.provider.generate_response(prompt)
-
- # Create a mock response object that mimics the OpenAI API structure
- mock_choice = SimpleNamespace(
- message=SimpleNamespace(content=response_text, tool_calls=None)
- )
- mock_response = SimpleNamespace(
- choices=[mock_choice],
- usage=SimpleNamespace(prompt_tokens=0, completion_tokens=0, total_tokens=0),
- model=self.model
- )
- return mock_response
-
-class AnswerWithContext(dspy.Signature):
- """
- Signature for our RAG task: input is a context and question, output is an answer.
- """
- context = dspy.InputField(desc="Relevant document snippets from the knowledge base.")
- question = dspy.InputField()
- answer = dspy.OutputField()
-
-class RAGPipeline(dspy.Module):
- """
- A simple RAG pipeline that retrieves context and then generates an answer.
- """
- def __init__(self, retrievers: List[Retriever]):
- super().__init__()
- self.retrievers = retrievers
- # We only need the signature here to generate the prompt text.
- self.generate_answer = dspy.Predict(AnswerWithContext)
-
- async def forward(self, question: str, db: Session) -> str:
- """
- Executes the RAG pipeline asynchronously.
- """
- logging.info(f"[RAGPipeline.forward] Received question: '{question}'")
- retrieved_contexts = []
- for retriever in self.retrievers:
- context = retriever.retrieve_context(question, db)
- retrieved_contexts.extend(context)
-
- context_text = "\n\n".join(retrieved_contexts)
- if not context_text:
- print("⚠️ No context retrieved. Falling back to direct QA.")
- context_text = "No context provided."
-
- # --- REVISED LOGIC ---
- # 1. Manually create the full prompt using the signature's template.
- # The `dspy.Predict` object can be called with the inputs to get the compiled prompt.
- # We access the last generated prompt from the LM's history.
- # Since we haven't called the LM yet, we temporarily configure a basic LM.
-
- # Get the configured language model from dspy settings
- lm = dspy.settings.lm
- if lm is None:
- raise RuntimeError("DSPy LM has not been configured. Call dspy.configure(lm=...) first.")
-
- # 2. Use the signature to create a dspy.Example, which generates the prompt.
- # The dspy.Predict module will format this into a prompt string.
- example = dspy.Example(context=context_text, question=question, signatures=self.generate_answer.signature)
-
- # 3. Call the language model directly with the full prompt string.
- # The `example.signatures` contains the logic to render the prompt.
- # In modern DSPy, `dspy.predict` is a simpler way to do this.
- # We will call the LM's aforward method directly for clarity.
- full_prompt = self.generate_answer.signature.instructions.format(context=context_text, question=question) + "\nAnswer:"
-
- response_obj = await lm.aforward(prompt=full_prompt)
-
- return response_obj.choices[0].message.content
+from app.core.llm_providers import get_llm_provider
+from app.core.pipelines.dspy_rag import DspyRagPipeline, DSPyLLMProvider
-# --- Main RAG Service Class --- (This class remains unchanged)
class RAGService:
+ """
+ Service class for managing the RAG (Retrieval-Augmented Generation) pipeline.
+ This class acts as a high-level orchestrator.
+ """
def __init__(self, vector_store: FaissVectorStore, retrievers: List[Retriever]):
self.vector_store = vector_store
self.retrievers = retrievers
def add_document(self, db: Session, doc_data: Dict[str, Any]) -> int:
+ """
+ Adds a document to both the database and the vector store.
+ """
try:
document_db = models.Document(
title=doc_data["title"],
@@ -124,7 +32,9 @@
db.add(document_db)
db.commit()
db.refresh(document_db)
+
faiss_index = self.vector_store.add_document(document_db.text)
+
vector_metadata = models.VectorMetadata(
document_id=document_db.id,
faiss_index=faiss_index,
@@ -144,16 +54,24 @@
raise
async def chat_with_rag(self, db: Session, prompt: str, model: str) -> str:
+ """
+ Generates a response to a user prompt by orchestrating the RAG pipeline.
+ """
print(f"Received Prompt: {prompt}")
if not prompt or not prompt.strip():
raise ValueError("The prompt cannot be null, empty, or contain only whitespace.")
+ # 1. Get the underlying LLM provider (e.g., Gemini, DeepSeek)
llm_provider_instance = get_llm_provider(model)
+
+ # 2. Wrap it in our custom DSPy-compatible provider
dspy_llm_provider = DSPyLLMProvider(provider=llm_provider_instance, model_name=model)
- # Configure dspy's global settings with our custom LM
+ # 3. Configure DSPy's global settings to use our custom LM
dspy.configure(lm=dspy_llm_provider)
- rag_pipeline = RAGPipeline(retrievers=self.retrievers)
+ # 4. Initialize and execute the RAG pipeline
+ rag_pipeline = DspyRagPipeline(retrievers=self.retrievers)
answer = await rag_pipeline.forward(question=prompt, db=db)
+
return answer
\ No newline at end of file
diff --git a/ai-hub/app/db/database.py b/ai-hub/app/db/database.py
index 2fc97fb..cbb6b13 100644
--- a/ai-hub/app/db/database.py
+++ b/ai-hub/app/db/database.py
@@ -9,7 +9,7 @@
# Default database URLs
POSTGRES_DEFAULT_URL = "postgresql://user:password@localhost/ai_hub_db"
-SQLITE_DEFAULT_URL = "sqlite:///./ai_hub.db"
+SQLITE_DEFAULT_URL = "sqlite:///./data/ai_hub.db"
DATABASE_URL = ""
engine_args = {}
diff --git a/ai-hub/app/db/guide.md b/ai-hub/app/db/guide.md
index a3185c7..e8986e2 100644
--- a/ai-hub/app/db/guide.md
+++ b/ai-hub/app/db/guide.md
@@ -18,7 +18,7 @@
| Variable | Description | Default Value | Supported Values |
| -------------- | ------------------------------------- | ----------------------------------------------------------------------------------------------- | -------------------- |
| `DB_MODE` | Specifies the type of database to use | `"postgres"` | `postgres`, `sqlite` |
-| `DATABASE_URL` | Connection string for the database | PostgreSQL: `postgresql://user:password@localhost/ai_hub_db`
SQLite: `sqlite:///./ai_hub.db` | Any SQLAlchemy URI |
+| `DATABASE_URL` | Connection string for the database | PostgreSQL: `postgresql://user:password@localhost/ai_hub_db`
SQLite: `sqlite:///./data/ai_hub.db` | Any SQLAlchemy URI |
### 💡 Example: Switch to SQLite
diff --git a/ai-hub/app/db_setup.py b/ai-hub/app/db_setup.py
index 739ebd2..63a64a6 100644
--- a/ai-hub/app/db_setup.py
+++ b/ai-hub/app/db_setup.py
@@ -11,7 +11,7 @@
# This configuration allows for easy switching between SQLite and PostgreSQL.
DB_MODE = os.getenv("DB_MODE", "sqlite")
if DB_MODE == "sqlite":
- DATABASE_URL = "sqlite:///./ai_hub.db"
+ DATABASE_URL = "sqlite:///./data/ai_hub.db"
# The connect_args are needed for SQLite to work with FastAPI's multiple threads
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
else:
diff --git a/ai-hub/tests/core/__init__.py b/ai-hub/tests/core/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/ai-hub/tests/core/__init__.py
diff --git a/ai-hub/tests/core/pipelines/__init__.py b/ai-hub/tests/core/pipelines/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/ai-hub/tests/core/pipelines/__init__.py
diff --git a/.gitignore b/.gitignore
index 3e0b2c9..95f61e0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,5 +2,6 @@
.env
**/.env
**/*.egg-info
-faiss_index.bin
-ai_hub.db
\ No newline at end of file
+**/faiss_index.bin
+**/ai_hub.db
+.pytest_cache/
\ No newline at end of file
diff --git a/ai-hub/app/__init__.py b/ai-hub/app/__init__.py
index e69de29..3fbb1fd 100644
--- a/ai-hub/app/__init__.py
+++ b/ai-hub/app/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/app.py b/ai-hub/app/app.py
index 04f9d7b..0718c32 100644
--- a/ai-hub/app/app.py
+++ b/ai-hub/app/app.py
@@ -24,7 +24,7 @@
"""
# Initialize core services for RAG
# CORRECTED: Now passing the required arguments to FaissVectorStore
- vector_store = FaissVectorStore(index_file_path="faiss_index.bin", dimension=768)
+ vector_store = FaissVectorStore(index_file_path="data/faiss_index.bin", dimension=768)
retrievers: List[Retriever] = [
FaissDBRetriever(vector_store=vector_store),
]
diff --git a/ai-hub/app/core/__init__.py b/ai-hub/app/core/__init__.py
index e69de29..3fbb1fd 100644
--- a/ai-hub/app/core/__init__.py
+++ b/ai-hub/app/core/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/core/llm_providers.py b/ai-hub/app/core/llm_providers.py
index f31a701..de36a55 100644
--- a/ai-hub/app/core/llm_providers.py
+++ b/ai-hub/app/core/llm_providers.py
@@ -45,7 +45,7 @@
async def generate_response(self, prompt: str) -> str:
# Construct the request payload
messages_payload = [
- # {"role": "system", "content": "You are a helpful assistant."},
+ {"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": prompt},
]
diff --git a/ai-hub/app/core/pipelines/__init__.py b/ai-hub/app/core/pipelines/__init__.py
new file mode 100644
index 0000000..3fbb1fd
--- /dev/null
+++ b/ai-hub/app/core/pipelines/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/core/pipelines/dspy_rag.py b/ai-hub/app/core/pipelines/dspy_rag.py
new file mode 100644
index 0000000..322c01b
--- /dev/null
+++ b/ai-hub/app/core/pipelines/dspy_rag.py
@@ -0,0 +1,87 @@
+# In app/core/pipelines/dspy_rag.py
+
+import dspy
+import logging
+from typing import List
+from types import SimpleNamespace
+from sqlalchemy.orm import Session
+
+from app.core.retrievers import Retriever
+from app.core.llm_providers import LLMProvider
+
+class DSPyLLMProvider(dspy.BaseLM):
+ """
+ A custom wrapper for the LLMProvider to make it compatible with DSPy.
+ """
+ def __init__(self, provider: LLMProvider, model_name: str, **kwargs):
+ super().__init__(model=model_name)
+ self.provider = provider
+ self.kwargs.update(kwargs)
+ print(f"DSPyLLMProvider initialized for model: {self.model}")
+
+ async def aforward(self, prompt: str, **kwargs):
+ """
+ The required asynchronous forward pass for the language model.
+ """
+ logging.info(f"[DSPyLLMProvider.aforward] Received prompt of length: {len(prompt) if prompt else 0}")
+ if not prompt or not prompt.strip():
+ logging.error("[DSPyLLMProvider.aforward] Received a null or empty prompt!")
+ return SimpleNamespace(choices=[SimpleNamespace(message=SimpleNamespace(content="Error: Received an empty prompt."))])
+
+ response_text = await self.provider.generate_response(prompt)
+
+ mock_choice = SimpleNamespace(message=SimpleNamespace(content=response_text, tool_calls=None))
+ return SimpleNamespace(choices=[mock_choice], usage=SimpleNamespace(prompt_tokens=0, completion_tokens=0, total_tokens=0), model=self.model)
+
+class AnswerWithContext(dspy.Signature):
+ """Given the context, answer the user's question."""
+ context = dspy.InputField(desc="Relevant document snippets from the knowledge base.")
+ question = dspy.InputField()
+ answer = dspy.OutputField()
+
+class DspyRagPipeline(dspy.Module):
+ """
+ A simple RAG pipeline that retrieves context and then generates an answer using DSPy.
+ """
+ def __init__(self, retrievers: List[Retriever]):
+ super().__init__()
+ self.retrievers = retrievers
+ # We still define the predictor to access its signature easily.
+ self.generate_answer = dspy.Predict(AnswerWithContext)
+
+ async def forward(self, question: str, db: Session) -> str:
+ """
+ Executes the RAG pipeline asynchronously.
+ """
+ logging.info(f"[DspyRagPipeline.forward] Received question: '{question}'")
+ retrieved_contexts = []
+ for retriever in self.retrievers:
+ context = retriever.retrieve_context(question, db)
+ retrieved_contexts.extend(context)
+
+ context_text = "\n\n".join(retrieved_contexts)
+ if not context_text:
+ print("⚠️ No context retrieved. Falling back to direct QA.")
+ context_text = "No context provided."
+
+ lm = dspy.settings.lm
+ if lm is None:
+ raise RuntimeError("DSPy LM has not been configured. Call dspy.configure(lm=...) first.")
+
+ # --- FIX: Revert to manual prompt construction ---
+ # Get the instruction from the signature's docstring.
+ instruction = self.generate_answer.signature.__doc__
+
+ # Build the full prompt exactly as DSPy would.
+ full_prompt = (
+ f"{instruction}\n\n"
+ f"---\n\n"
+ f"Context: {context_text}\n\n"
+ f"Question: {question}\n\n"
+ f"Answer:"
+ )
+
+ # Call the language model's aforward method directly with the complete prompt.
+ response_obj = await lm.aforward(prompt=full_prompt)
+
+ return response_obj.choices[0].message.content
\ No newline at end of file
diff --git a/ai-hub/app/core/rag_service.py b/ai-hub/app/core/rag_service.py
index 7cb57e9..93b4bb9 100644
--- a/ai-hub/app/core/rag_service.py
+++ b/ai-hub/app/core/rag_service.py
@@ -1,120 +1,28 @@
-import asyncio
from typing import List, Dict, Any
-from types import SimpleNamespace
from sqlalchemy.orm import Session
from sqlalchemy.exc import SQLAlchemyError
import dspy
-import logging
from app.core.vector_store import FaissVectorStore
from app.db import models
from app.core.retrievers import Retriever
-from app.core.llm_providers import LLMProvider, get_llm_provider
-
-# --- DSPy Components for RAG ---
-
-class DSPyLLMProvider(dspy.BaseLM):
- """
- A custom wrapper for the LLMProvider to make it compatible with DSPy.
- """
- def __init__(self, provider: LLMProvider, model_name: str, **kwargs):
- super().__init__(model=model_name)
- self.provider = provider
- self.kwargs.update(kwargs)
- print(f"DSPyLLMProvider initialized for model: {self.model}")
-
- async def aforward(self, prompt: str, **kwargs):
- """
- The required asynchronous forward pass for the language model.
- """
- logging.info(f"[DSPyLLMProvider.aforward] Received prompt of length: {len(prompt) if prompt else 0}")
-
- # --- CRITICAL FIX: Ensure prompt is not None or empty ---
- if not prompt or not prompt.strip():
- logging.error("[DSPyLLMProvider.aforward] Received a null or empty prompt!")
- # Return a default, safe response instead of calling the API with null.
- return SimpleNamespace(choices=[SimpleNamespace(message=SimpleNamespace(content="Error: Received an empty prompt."))])
-
- # Call the async provider directly using the existing event loop
- response_text = await self.provider.generate_response(prompt)
-
- # Create a mock response object that mimics the OpenAI API structure
- mock_choice = SimpleNamespace(
- message=SimpleNamespace(content=response_text, tool_calls=None)
- )
- mock_response = SimpleNamespace(
- choices=[mock_choice],
- usage=SimpleNamespace(prompt_tokens=0, completion_tokens=0, total_tokens=0),
- model=self.model
- )
- return mock_response
-
-class AnswerWithContext(dspy.Signature):
- """
- Signature for our RAG task: input is a context and question, output is an answer.
- """
- context = dspy.InputField(desc="Relevant document snippets from the knowledge base.")
- question = dspy.InputField()
- answer = dspy.OutputField()
-
-class RAGPipeline(dspy.Module):
- """
- A simple RAG pipeline that retrieves context and then generates an answer.
- """
- def __init__(self, retrievers: List[Retriever]):
- super().__init__()
- self.retrievers = retrievers
- # We only need the signature here to generate the prompt text.
- self.generate_answer = dspy.Predict(AnswerWithContext)
-
- async def forward(self, question: str, db: Session) -> str:
- """
- Executes the RAG pipeline asynchronously.
- """
- logging.info(f"[RAGPipeline.forward] Received question: '{question}'")
- retrieved_contexts = []
- for retriever in self.retrievers:
- context = retriever.retrieve_context(question, db)
- retrieved_contexts.extend(context)
-
- context_text = "\n\n".join(retrieved_contexts)
- if not context_text:
- print("⚠️ No context retrieved. Falling back to direct QA.")
- context_text = "No context provided."
-
- # --- REVISED LOGIC ---
- # 1. Manually create the full prompt using the signature's template.
- # The `dspy.Predict` object can be called with the inputs to get the compiled prompt.
- # We access the last generated prompt from the LM's history.
- # Since we haven't called the LM yet, we temporarily configure a basic LM.
-
- # Get the configured language model from dspy settings
- lm = dspy.settings.lm
- if lm is None:
- raise RuntimeError("DSPy LM has not been configured. Call dspy.configure(lm=...) first.")
-
- # 2. Use the signature to create a dspy.Example, which generates the prompt.
- # The dspy.Predict module will format this into a prompt string.
- example = dspy.Example(context=context_text, question=question, signatures=self.generate_answer.signature)
-
- # 3. Call the language model directly with the full prompt string.
- # The `example.signatures` contains the logic to render the prompt.
- # In modern DSPy, `dspy.predict` is a simpler way to do this.
- # We will call the LM's aforward method directly for clarity.
- full_prompt = self.generate_answer.signature.instructions.format(context=context_text, question=question) + "\nAnswer:"
-
- response_obj = await lm.aforward(prompt=full_prompt)
-
- return response_obj.choices[0].message.content
+from app.core.llm_providers import get_llm_provider
+from app.core.pipelines.dspy_rag import DspyRagPipeline, DSPyLLMProvider
-# --- Main RAG Service Class --- (This class remains unchanged)
class RAGService:
+ """
+ Service class for managing the RAG (Retrieval-Augmented Generation) pipeline.
+ This class acts as a high-level orchestrator.
+ """
def __init__(self, vector_store: FaissVectorStore, retrievers: List[Retriever]):
self.vector_store = vector_store
self.retrievers = retrievers
def add_document(self, db: Session, doc_data: Dict[str, Any]) -> int:
+ """
+ Adds a document to both the database and the vector store.
+ """
try:
document_db = models.Document(
title=doc_data["title"],
@@ -124,7 +32,9 @@
db.add(document_db)
db.commit()
db.refresh(document_db)
+
faiss_index = self.vector_store.add_document(document_db.text)
+
vector_metadata = models.VectorMetadata(
document_id=document_db.id,
faiss_index=faiss_index,
@@ -144,16 +54,24 @@
raise
async def chat_with_rag(self, db: Session, prompt: str, model: str) -> str:
+ """
+ Generates a response to a user prompt by orchestrating the RAG pipeline.
+ """
print(f"Received Prompt: {prompt}")
if not prompt or not prompt.strip():
raise ValueError("The prompt cannot be null, empty, or contain only whitespace.")
+ # 1. Get the underlying LLM provider (e.g., Gemini, DeepSeek)
llm_provider_instance = get_llm_provider(model)
+
+ # 2. Wrap it in our custom DSPy-compatible provider
dspy_llm_provider = DSPyLLMProvider(provider=llm_provider_instance, model_name=model)
- # Configure dspy's global settings with our custom LM
+ # 3. Configure DSPy's global settings to use our custom LM
dspy.configure(lm=dspy_llm_provider)
- rag_pipeline = RAGPipeline(retrievers=self.retrievers)
+ # 4. Initialize and execute the RAG pipeline
+ rag_pipeline = DspyRagPipeline(retrievers=self.retrievers)
answer = await rag_pipeline.forward(question=prompt, db=db)
+
return answer
\ No newline at end of file
diff --git a/ai-hub/app/db/database.py b/ai-hub/app/db/database.py
index 2fc97fb..cbb6b13 100644
--- a/ai-hub/app/db/database.py
+++ b/ai-hub/app/db/database.py
@@ -9,7 +9,7 @@
# Default database URLs
POSTGRES_DEFAULT_URL = "postgresql://user:password@localhost/ai_hub_db"
-SQLITE_DEFAULT_URL = "sqlite:///./ai_hub.db"
+SQLITE_DEFAULT_URL = "sqlite:///./data/ai_hub.db"
DATABASE_URL = ""
engine_args = {}
diff --git a/ai-hub/app/db/guide.md b/ai-hub/app/db/guide.md
index a3185c7..e8986e2 100644
--- a/ai-hub/app/db/guide.md
+++ b/ai-hub/app/db/guide.md
@@ -18,7 +18,7 @@
| Variable | Description | Default Value | Supported Values |
| -------------- | ------------------------------------- | ----------------------------------------------------------------------------------------------- | -------------------- |
| `DB_MODE` | Specifies the type of database to use | `"postgres"` | `postgres`, `sqlite` |
-| `DATABASE_URL` | Connection string for the database | PostgreSQL: `postgresql://user:password@localhost/ai_hub_db`
SQLite: `sqlite:///./ai_hub.db` | Any SQLAlchemy URI |
+| `DATABASE_URL` | Connection string for the database | PostgreSQL: `postgresql://user:password@localhost/ai_hub_db`
SQLite: `sqlite:///./data/ai_hub.db` | Any SQLAlchemy URI |
### 💡 Example: Switch to SQLite
diff --git a/ai-hub/app/db_setup.py b/ai-hub/app/db_setup.py
index 739ebd2..63a64a6 100644
--- a/ai-hub/app/db_setup.py
+++ b/ai-hub/app/db_setup.py
@@ -11,7 +11,7 @@
# This configuration allows for easy switching between SQLite and PostgreSQL.
DB_MODE = os.getenv("DB_MODE", "sqlite")
if DB_MODE == "sqlite":
- DATABASE_URL = "sqlite:///./ai_hub.db"
+ DATABASE_URL = "sqlite:///./data/ai_hub.db"
# The connect_args are needed for SQLite to work with FastAPI's multiple threads
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
else:
diff --git a/ai-hub/tests/core/__init__.py b/ai-hub/tests/core/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/ai-hub/tests/core/__init__.py
diff --git a/ai-hub/tests/core/pipelines/__init__.py b/ai-hub/tests/core/pipelines/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/ai-hub/tests/core/pipelines/__init__.py
diff --git a/ai-hub/tests/core/pipelines/test_dspy_rag.py b/ai-hub/tests/core/pipelines/test_dspy_rag.py
new file mode 100644
index 0000000..5e4aa3d
--- /dev/null
+++ b/ai-hub/tests/core/pipelines/test_dspy_rag.py
@@ -0,0 +1,111 @@
+# tests/core/pipelines/test_dspy_rag.py
+
+import asyncio
+from unittest.mock import MagicMock, AsyncMock
+from sqlalchemy.orm import Session
+import dspy
+import pytest
+
+# Import the pipeline being tested
+from app.core.pipelines.dspy_rag import DspyRagPipeline, AnswerWithContext
+
+# Import its dependencies for mocking
+from app.core.retrievers import Retriever
+
+@pytest.fixture
+def mock_lm_configured():
+ """
+ A pytest fixture to mock the dspy language model and configure it globally
+ for the duration of a test.
+ """
+ # 1. Create the mock LM object
+ mock_lm_instance = MagicMock()
+ # 2. Mock its async `aforward` method to return a dspy-compatible object
+ mock_lm_instance.aforward = AsyncMock(
+ return_value=MagicMock(
+ choices=[MagicMock(message=MagicMock(content="Mocked LLM answer"))]
+ )
+ )
+
+ # 3. Store the original LM (if any) to restore it after the test
+ original_lm = dspy.settings.lm
+
+ # 4. CRITICAL FIX: Configure dspy to use our mock LM
+ dspy.configure(lm=mock_lm_instance)
+
+ # 5. The test runs here, with the mock configured
+ yield mock_lm_instance
+
+ # 6. After the test, restore the original LM configuration
+ dspy.configure(lm=original_lm)
+
+
+def test_dspy_rag_pipeline_with_context(mock_lm_configured):
+ """
+ Tests that DspyRagPipeline correctly processes a question when context is found.
+ """
+ # --- Arrange ---
+ mock_retriever = MagicMock(spec=Retriever)
+ mock_retriever.retrieve_context.return_value = ["Context chunk 1.", "Context chunk 2."]
+ mock_db = MagicMock(spec=Session)
+
+ pipeline = DspyRagPipeline(retrievers=[mock_retriever])
+ question = "What is the question?"
+
+ # --- Act ---
+ response = asyncio.run(pipeline.forward(question=question, db=mock_db))
+
+ # --- Assert ---
+ # Assert the retriever was called correctly
+ mock_retriever.retrieve_context.assert_called_once_with(question, mock_db)
+
+ # Assert the language model was called with the correctly constructed prompt
+ expected_context = "Context chunk 1.\n\nContext chunk 2."
+ instruction = AnswerWithContext.__doc__
+ expected_prompt = (
+ f"{instruction}\n\n"
+ f"---\n\n"
+ f"Context: {expected_context}\n\n"
+ f"Question: {question}\n\n"
+ f"Answer:"
+ )
+ # The fixture provides the mock_lm_configured object, which is the mock LM
+ mock_lm_configured.aforward.assert_called_once_with(prompt=expected_prompt)
+
+ # Assert the final answer from the mock is returned
+ assert response == "Mocked LLM answer"
+
+
+def test_dspy_rag_pipeline_without_context(mock_lm_configured):
+ """
+ Tests that DspyRagPipeline correctly handles the case where no context is found.
+ """
+ # --- Arrange ---
+ mock_retriever = MagicMock(spec=Retriever)
+ mock_retriever.retrieve_context.return_value = [] # No context found
+ mock_db = MagicMock(spec=Session)
+
+ pipeline = DspyRagPipeline(retrievers=[mock_retriever])
+ question = "What is the question?"
+
+ # --- Act ---
+ response = asyncio.run(pipeline.forward(question=question, db=mock_db))
+
+ # --- Assert ---
+ # Assert the retriever was called
+ mock_retriever.retrieve_context.assert_called_once_with(question, mock_db)
+
+ # Assert the LM was called with the placeholder context
+ expected_context = "No context provided."
+ instruction = AnswerWithContext.__doc__
+ expected_prompt = (
+ f"{instruction}\n\n"
+ f"---\n\n"
+ f"Context: {expected_context}\n\n"
+ f"Question: {question}\n\n"
+ f"Answer:"
+ )
+ mock_lm_configured.aforward.assert_called_once_with(prompt=expected_prompt)
+
+ # Assert the final answer from the mock is returned
+ assert response == "Mocked LLM answer"
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 3e0b2c9..95f61e0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,5 +2,6 @@
.env
**/.env
**/*.egg-info
-faiss_index.bin
-ai_hub.db
\ No newline at end of file
+**/faiss_index.bin
+**/ai_hub.db
+.pytest_cache/
\ No newline at end of file
diff --git a/ai-hub/app/__init__.py b/ai-hub/app/__init__.py
index e69de29..3fbb1fd 100644
--- a/ai-hub/app/__init__.py
+++ b/ai-hub/app/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/app.py b/ai-hub/app/app.py
index 04f9d7b..0718c32 100644
--- a/ai-hub/app/app.py
+++ b/ai-hub/app/app.py
@@ -24,7 +24,7 @@
"""
# Initialize core services for RAG
# CORRECTED: Now passing the required arguments to FaissVectorStore
- vector_store = FaissVectorStore(index_file_path="faiss_index.bin", dimension=768)
+ vector_store = FaissVectorStore(index_file_path="data/faiss_index.bin", dimension=768)
retrievers: List[Retriever] = [
FaissDBRetriever(vector_store=vector_store),
]
diff --git a/ai-hub/app/core/__init__.py b/ai-hub/app/core/__init__.py
index e69de29..3fbb1fd 100644
--- a/ai-hub/app/core/__init__.py
+++ b/ai-hub/app/core/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/core/llm_providers.py b/ai-hub/app/core/llm_providers.py
index f31a701..de36a55 100644
--- a/ai-hub/app/core/llm_providers.py
+++ b/ai-hub/app/core/llm_providers.py
@@ -45,7 +45,7 @@
async def generate_response(self, prompt: str) -> str:
# Construct the request payload
messages_payload = [
- # {"role": "system", "content": "You are a helpful assistant."},
+ {"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": prompt},
]
diff --git a/ai-hub/app/core/pipelines/__init__.py b/ai-hub/app/core/pipelines/__init__.py
new file mode 100644
index 0000000..3fbb1fd
--- /dev/null
+++ b/ai-hub/app/core/pipelines/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/core/pipelines/dspy_rag.py b/ai-hub/app/core/pipelines/dspy_rag.py
new file mode 100644
index 0000000..322c01b
--- /dev/null
+++ b/ai-hub/app/core/pipelines/dspy_rag.py
@@ -0,0 +1,87 @@
+# In app/core/pipelines/dspy_rag.py
+
+import dspy
+import logging
+from typing import List
+from types import SimpleNamespace
+from sqlalchemy.orm import Session
+
+from app.core.retrievers import Retriever
+from app.core.llm_providers import LLMProvider
+
+class DSPyLLMProvider(dspy.BaseLM):
+ """
+ A custom wrapper for the LLMProvider to make it compatible with DSPy.
+ """
+ def __init__(self, provider: LLMProvider, model_name: str, **kwargs):
+ super().__init__(model=model_name)
+ self.provider = provider
+ self.kwargs.update(kwargs)
+ print(f"DSPyLLMProvider initialized for model: {self.model}")
+
+ async def aforward(self, prompt: str, **kwargs):
+ """
+ The required asynchronous forward pass for the language model.
+ """
+ logging.info(f"[DSPyLLMProvider.aforward] Received prompt of length: {len(prompt) if prompt else 0}")
+ if not prompt or not prompt.strip():
+ logging.error("[DSPyLLMProvider.aforward] Received a null or empty prompt!")
+ return SimpleNamespace(choices=[SimpleNamespace(message=SimpleNamespace(content="Error: Received an empty prompt."))])
+
+ response_text = await self.provider.generate_response(prompt)
+
+ mock_choice = SimpleNamespace(message=SimpleNamespace(content=response_text, tool_calls=None))
+ return SimpleNamespace(choices=[mock_choice], usage=SimpleNamespace(prompt_tokens=0, completion_tokens=0, total_tokens=0), model=self.model)
+
+class AnswerWithContext(dspy.Signature):
+ """Given the context, answer the user's question."""
+ context = dspy.InputField(desc="Relevant document snippets from the knowledge base.")
+ question = dspy.InputField()
+ answer = dspy.OutputField()
+
+class DspyRagPipeline(dspy.Module):
+ """
+ A simple RAG pipeline that retrieves context and then generates an answer using DSPy.
+ """
+ def __init__(self, retrievers: List[Retriever]):
+ super().__init__()
+ self.retrievers = retrievers
+ # We still define the predictor to access its signature easily.
+ self.generate_answer = dspy.Predict(AnswerWithContext)
+
+ async def forward(self, question: str, db: Session) -> str:
+ """
+ Executes the RAG pipeline asynchronously.
+ """
+ logging.info(f"[DspyRagPipeline.forward] Received question: '{question}'")
+ retrieved_contexts = []
+ for retriever in self.retrievers:
+ context = retriever.retrieve_context(question, db)
+ retrieved_contexts.extend(context)
+
+ context_text = "\n\n".join(retrieved_contexts)
+ if not context_text:
+ print("⚠️ No context retrieved. Falling back to direct QA.")
+ context_text = "No context provided."
+
+ lm = dspy.settings.lm
+ if lm is None:
+ raise RuntimeError("DSPy LM has not been configured. Call dspy.configure(lm=...) first.")
+
+ # --- FIX: Revert to manual prompt construction ---
+ # Get the instruction from the signature's docstring.
+ instruction = self.generate_answer.signature.__doc__
+
+ # Build the full prompt exactly as DSPy would.
+ full_prompt = (
+ f"{instruction}\n\n"
+ f"---\n\n"
+ f"Context: {context_text}\n\n"
+ f"Question: {question}\n\n"
+ f"Answer:"
+ )
+
+ # Call the language model's aforward method directly with the complete prompt.
+ response_obj = await lm.aforward(prompt=full_prompt)
+
+ return response_obj.choices[0].message.content
\ No newline at end of file
diff --git a/ai-hub/app/core/rag_service.py b/ai-hub/app/core/rag_service.py
index 7cb57e9..93b4bb9 100644
--- a/ai-hub/app/core/rag_service.py
+++ b/ai-hub/app/core/rag_service.py
@@ -1,120 +1,28 @@
-import asyncio
from typing import List, Dict, Any
-from types import SimpleNamespace
from sqlalchemy.orm import Session
from sqlalchemy.exc import SQLAlchemyError
import dspy
-import logging
from app.core.vector_store import FaissVectorStore
from app.db import models
from app.core.retrievers import Retriever
-from app.core.llm_providers import LLMProvider, get_llm_provider
-
-# --- DSPy Components for RAG ---
-
-class DSPyLLMProvider(dspy.BaseLM):
- """
- A custom wrapper for the LLMProvider to make it compatible with DSPy.
- """
- def __init__(self, provider: LLMProvider, model_name: str, **kwargs):
- super().__init__(model=model_name)
- self.provider = provider
- self.kwargs.update(kwargs)
- print(f"DSPyLLMProvider initialized for model: {self.model}")
-
- async def aforward(self, prompt: str, **kwargs):
- """
- The required asynchronous forward pass for the language model.
- """
- logging.info(f"[DSPyLLMProvider.aforward] Received prompt of length: {len(prompt) if prompt else 0}")
-
- # --- CRITICAL FIX: Ensure prompt is not None or empty ---
- if not prompt or not prompt.strip():
- logging.error("[DSPyLLMProvider.aforward] Received a null or empty prompt!")
- # Return a default, safe response instead of calling the API with null.
- return SimpleNamespace(choices=[SimpleNamespace(message=SimpleNamespace(content="Error: Received an empty prompt."))])
-
- # Call the async provider directly using the existing event loop
- response_text = await self.provider.generate_response(prompt)
-
- # Create a mock response object that mimics the OpenAI API structure
- mock_choice = SimpleNamespace(
- message=SimpleNamespace(content=response_text, tool_calls=None)
- )
- mock_response = SimpleNamespace(
- choices=[mock_choice],
- usage=SimpleNamespace(prompt_tokens=0, completion_tokens=0, total_tokens=0),
- model=self.model
- )
- return mock_response
-
-class AnswerWithContext(dspy.Signature):
- """
- Signature for our RAG task: input is a context and question, output is an answer.
- """
- context = dspy.InputField(desc="Relevant document snippets from the knowledge base.")
- question = dspy.InputField()
- answer = dspy.OutputField()
-
-class RAGPipeline(dspy.Module):
- """
- A simple RAG pipeline that retrieves context and then generates an answer.
- """
- def __init__(self, retrievers: List[Retriever]):
- super().__init__()
- self.retrievers = retrievers
- # We only need the signature here to generate the prompt text.
- self.generate_answer = dspy.Predict(AnswerWithContext)
-
- async def forward(self, question: str, db: Session) -> str:
- """
- Executes the RAG pipeline asynchronously.
- """
- logging.info(f"[RAGPipeline.forward] Received question: '{question}'")
- retrieved_contexts = []
- for retriever in self.retrievers:
- context = retriever.retrieve_context(question, db)
- retrieved_contexts.extend(context)
-
- context_text = "\n\n".join(retrieved_contexts)
- if not context_text:
- print("⚠️ No context retrieved. Falling back to direct QA.")
- context_text = "No context provided."
-
- # --- REVISED LOGIC ---
- # 1. Manually create the full prompt using the signature's template.
- # The `dspy.Predict` object can be called with the inputs to get the compiled prompt.
- # We access the last generated prompt from the LM's history.
- # Since we haven't called the LM yet, we temporarily configure a basic LM.
-
- # Get the configured language model from dspy settings
- lm = dspy.settings.lm
- if lm is None:
- raise RuntimeError("DSPy LM has not been configured. Call dspy.configure(lm=...) first.")
-
- # 2. Use the signature to create a dspy.Example, which generates the prompt.
- # The dspy.Predict module will format this into a prompt string.
- example = dspy.Example(context=context_text, question=question, signatures=self.generate_answer.signature)
-
- # 3. Call the language model directly with the full prompt string.
- # The `example.signatures` contains the logic to render the prompt.
- # In modern DSPy, `dspy.predict` is a simpler way to do this.
- # We will call the LM's aforward method directly for clarity.
- full_prompt = self.generate_answer.signature.instructions.format(context=context_text, question=question) + "\nAnswer:"
-
- response_obj = await lm.aforward(prompt=full_prompt)
-
- return response_obj.choices[0].message.content
+from app.core.llm_providers import get_llm_provider
+from app.core.pipelines.dspy_rag import DspyRagPipeline, DSPyLLMProvider
-# --- Main RAG Service Class --- (This class remains unchanged)
class RAGService:
+ """
+ Service class for managing the RAG (Retrieval-Augmented Generation) pipeline.
+ This class acts as a high-level orchestrator.
+ """
def __init__(self, vector_store: FaissVectorStore, retrievers: List[Retriever]):
self.vector_store = vector_store
self.retrievers = retrievers
def add_document(self, db: Session, doc_data: Dict[str, Any]) -> int:
+ """
+ Adds a document to both the database and the vector store.
+ """
try:
document_db = models.Document(
title=doc_data["title"],
@@ -124,7 +32,9 @@
db.add(document_db)
db.commit()
db.refresh(document_db)
+
faiss_index = self.vector_store.add_document(document_db.text)
+
vector_metadata = models.VectorMetadata(
document_id=document_db.id,
faiss_index=faiss_index,
@@ -144,16 +54,24 @@
raise
async def chat_with_rag(self, db: Session, prompt: str, model: str) -> str:
+ """
+ Generates a response to a user prompt by orchestrating the RAG pipeline.
+ """
print(f"Received Prompt: {prompt}")
if not prompt or not prompt.strip():
raise ValueError("The prompt cannot be null, empty, or contain only whitespace.")
+ # 1. Get the underlying LLM provider (e.g., Gemini, DeepSeek)
llm_provider_instance = get_llm_provider(model)
+
+ # 2. Wrap it in our custom DSPy-compatible provider
dspy_llm_provider = DSPyLLMProvider(provider=llm_provider_instance, model_name=model)
- # Configure dspy's global settings with our custom LM
+ # 3. Configure DSPy's global settings to use our custom LM
dspy.configure(lm=dspy_llm_provider)
- rag_pipeline = RAGPipeline(retrievers=self.retrievers)
+ # 4. Initialize and execute the RAG pipeline
+ rag_pipeline = DspyRagPipeline(retrievers=self.retrievers)
answer = await rag_pipeline.forward(question=prompt, db=db)
+
return answer
\ No newline at end of file
diff --git a/ai-hub/app/db/database.py b/ai-hub/app/db/database.py
index 2fc97fb..cbb6b13 100644
--- a/ai-hub/app/db/database.py
+++ b/ai-hub/app/db/database.py
@@ -9,7 +9,7 @@
# Default database URLs
POSTGRES_DEFAULT_URL = "postgresql://user:password@localhost/ai_hub_db"
-SQLITE_DEFAULT_URL = "sqlite:///./ai_hub.db"
+SQLITE_DEFAULT_URL = "sqlite:///./data/ai_hub.db"
DATABASE_URL = ""
engine_args = {}
diff --git a/ai-hub/app/db/guide.md b/ai-hub/app/db/guide.md
index a3185c7..e8986e2 100644
--- a/ai-hub/app/db/guide.md
+++ b/ai-hub/app/db/guide.md
@@ -18,7 +18,7 @@
| Variable | Description | Default Value | Supported Values |
| -------------- | ------------------------------------- | ----------------------------------------------------------------------------------------------- | -------------------- |
| `DB_MODE` | Specifies the type of database to use | `"postgres"` | `postgres`, `sqlite` |
-| `DATABASE_URL` | Connection string for the database | PostgreSQL: `postgresql://user:password@localhost/ai_hub_db`
SQLite: `sqlite:///./ai_hub.db` | Any SQLAlchemy URI |
+| `DATABASE_URL` | Connection string for the database | PostgreSQL: `postgresql://user:password@localhost/ai_hub_db`
SQLite: `sqlite:///./data/ai_hub.db` | Any SQLAlchemy URI |
### 💡 Example: Switch to SQLite
diff --git a/ai-hub/app/db_setup.py b/ai-hub/app/db_setup.py
index 739ebd2..63a64a6 100644
--- a/ai-hub/app/db_setup.py
+++ b/ai-hub/app/db_setup.py
@@ -11,7 +11,7 @@
# This configuration allows for easy switching between SQLite and PostgreSQL.
DB_MODE = os.getenv("DB_MODE", "sqlite")
if DB_MODE == "sqlite":
- DATABASE_URL = "sqlite:///./ai_hub.db"
+ DATABASE_URL = "sqlite:///./data/ai_hub.db"
# The connect_args are needed for SQLite to work with FastAPI's multiple threads
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
else:
diff --git a/ai-hub/tests/core/__init__.py b/ai-hub/tests/core/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/ai-hub/tests/core/__init__.py
diff --git a/ai-hub/tests/core/pipelines/__init__.py b/ai-hub/tests/core/pipelines/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/ai-hub/tests/core/pipelines/__init__.py
diff --git a/ai-hub/tests/core/pipelines/test_dspy_rag.py b/ai-hub/tests/core/pipelines/test_dspy_rag.py
new file mode 100644
index 0000000..5e4aa3d
--- /dev/null
+++ b/ai-hub/tests/core/pipelines/test_dspy_rag.py
@@ -0,0 +1,111 @@
+# tests/core/pipelines/test_dspy_rag.py
+
+import asyncio
+from unittest.mock import MagicMock, AsyncMock
+from sqlalchemy.orm import Session
+import dspy
+import pytest
+
+# Import the pipeline being tested
+from app.core.pipelines.dspy_rag import DspyRagPipeline, AnswerWithContext
+
+# Import its dependencies for mocking
+from app.core.retrievers import Retriever
+
+@pytest.fixture
+def mock_lm_configured():
+ """
+ A pytest fixture to mock the dspy language model and configure it globally
+ for the duration of a test.
+ """
+ # 1. Create the mock LM object
+ mock_lm_instance = MagicMock()
+ # 2. Mock its async `aforward` method to return a dspy-compatible object
+ mock_lm_instance.aforward = AsyncMock(
+ return_value=MagicMock(
+ choices=[MagicMock(message=MagicMock(content="Mocked LLM answer"))]
+ )
+ )
+
+ # 3. Store the original LM (if any) to restore it after the test
+ original_lm = dspy.settings.lm
+
+ # 4. CRITICAL FIX: Configure dspy to use our mock LM
+ dspy.configure(lm=mock_lm_instance)
+
+ # 5. The test runs here, with the mock configured
+ yield mock_lm_instance
+
+ # 6. After the test, restore the original LM configuration
+ dspy.configure(lm=original_lm)
+
+
+def test_dspy_rag_pipeline_with_context(mock_lm_configured):
+ """
+ Tests that DspyRagPipeline correctly processes a question when context is found.
+ """
+ # --- Arrange ---
+ mock_retriever = MagicMock(spec=Retriever)
+ mock_retriever.retrieve_context.return_value = ["Context chunk 1.", "Context chunk 2."]
+ mock_db = MagicMock(spec=Session)
+
+ pipeline = DspyRagPipeline(retrievers=[mock_retriever])
+ question = "What is the question?"
+
+ # --- Act ---
+ response = asyncio.run(pipeline.forward(question=question, db=mock_db))
+
+ # --- Assert ---
+ # Assert the retriever was called correctly
+ mock_retriever.retrieve_context.assert_called_once_with(question, mock_db)
+
+ # Assert the language model was called with the correctly constructed prompt
+ expected_context = "Context chunk 1.\n\nContext chunk 2."
+ instruction = AnswerWithContext.__doc__
+ expected_prompt = (
+ f"{instruction}\n\n"
+ f"---\n\n"
+ f"Context: {expected_context}\n\n"
+ f"Question: {question}\n\n"
+ f"Answer:"
+ )
+ # The fixture provides the mock_lm_configured object, which is the mock LM
+ mock_lm_configured.aforward.assert_called_once_with(prompt=expected_prompt)
+
+ # Assert the final answer from the mock is returned
+ assert response == "Mocked LLM answer"
+
+
+def test_dspy_rag_pipeline_without_context(mock_lm_configured):
+ """
+ Tests that DspyRagPipeline correctly handles the case where no context is found.
+ """
+ # --- Arrange ---
+ mock_retriever = MagicMock(spec=Retriever)
+ mock_retriever.retrieve_context.return_value = [] # No context found
+ mock_db = MagicMock(spec=Session)
+
+ pipeline = DspyRagPipeline(retrievers=[mock_retriever])
+ question = "What is the question?"
+
+ # --- Act ---
+ response = asyncio.run(pipeline.forward(question=question, db=mock_db))
+
+ # --- Assert ---
+ # Assert the retriever was called
+ mock_retriever.retrieve_context.assert_called_once_with(question, mock_db)
+
+ # Assert the LM was called with the placeholder context
+ expected_context = "No context provided."
+ instruction = AnswerWithContext.__doc__
+ expected_prompt = (
+ f"{instruction}\n\n"
+ f"---\n\n"
+ f"Context: {expected_context}\n\n"
+ f"Question: {question}\n\n"
+ f"Answer:"
+ )
+ mock_lm_configured.aforward.assert_called_once_with(prompt=expected_prompt)
+
+ # Assert the final answer from the mock is returned
+ assert response == "Mocked LLM answer"
\ No newline at end of file
diff --git a/ai-hub/tests/core/test_rag_service.py b/ai-hub/tests/core/test_rag_service.py
index e0ca1a9..bdd4d17 100644
--- a/ai-hub/tests/core/test_rag_service.py
+++ b/ai-hub/tests/core/test_rag_service.py
@@ -1,96 +1,60 @@
import asyncio
-from unittest.mock import patch, MagicMock, AsyncMock, call
+from unittest.mock import patch, MagicMock, AsyncMock
from sqlalchemy.orm import Session
-import dspy
-# Import what you are testing
-from app.core.rag_service import RAGService, RAGPipeline, DSPyLLMProvider
-# Import dependencies that need to be referenced
+# Import the service being tested
+from app.core.rag_service import RAGService
+
+# Import dependencies that need to be referenced in mocks
from app.core.retrievers import Retriever
-from app.core.llm_providers import LLMProvider # For type checks if needed
+from app.core.pipelines.dspy_rag import DspyRagPipeline, DSPyLLMProvider
+from app.core.llm_providers import LLMProvider
-# --- RAGService Unit Tests ---
-# ... (Your successful add_document tests are fine and don't need changes) ...
-
-# NOTE: The patch target for get_llm_provider has been corrected.
@patch('app.core.rag_service.get_llm_provider')
-@patch('app.core.rag_service.RAGPipeline')
+@patch('app.core.rag_service.DspyRagPipeline') # Patched the new class name
@patch('dspy.configure')
-def test_rag_service_chat_with_rag_with_context(mock_configure, mock_rag_pipeline, mock_get_llm_provider):
+def test_rag_service_orchestration(mock_configure, mock_dspy_pipeline, mock_get_llm_provider):
"""
- Test the RAGService.chat_with_rag method when context is retrieved.
+ Tests that RAGService.chat_with_rag correctly orchestrates its dependencies.
+ It should:
+ 1. Get the correct LLM provider.
+ 2. Configure DSPy with a wrapped provider.
+ 3. Instantiate and call the pipeline with the correct arguments.
"""
# --- Arrange ---
+ # Mock the dependencies that RAGService uses
mock_llm_provider = MagicMock(spec=LLMProvider)
mock_get_llm_provider.return_value = mock_llm_provider
mock_db = MagicMock(spec=Session)
-
mock_retriever = MagicMock(spec=Retriever)
- mock_retriever.retrieve_context.return_value = ["Context text 1.", "Context text 2."]
- mock_rag_pipeline_instance = MagicMock(spec=RAGPipeline)
- mock_rag_pipeline_instance.forward = AsyncMock(return_value="LLM response with context")
- mock_rag_pipeline.return_value = mock_rag_pipeline_instance
+ # Mock the pipeline instance and its return value
+ mock_pipeline_instance = MagicMock(spec=DspyRagPipeline)
+ mock_pipeline_instance.forward = AsyncMock(return_value="Final RAG response")
+ mock_dspy_pipeline.return_value = mock_pipeline_instance
+ # Instantiate the service class we are testing
rag_service = RAGService(vector_store=MagicMock(), retrievers=[mock_retriever])
prompt = "Test prompt."
+ model = "deepseek"
# --- Act ---
- response_text = asyncio.run(rag_service.chat_with_rag(db=mock_db, prompt=prompt, model="deepseek"))
+ response_text = asyncio.run(rag_service.chat_with_rag(db=mock_db, prompt=prompt, model=model))
# --- Assert ---
- mock_get_llm_provider.assert_called_once_with("deepseek")
-
+ # 1. Assert that the correct LLM provider was requested
+ mock_get_llm_provider.assert_called_once_with(model)
+
+ # 2. Assert that dspy was configured with a correctly wrapped provider
mock_configure.assert_called_once()
lm_instance = mock_configure.call_args.kwargs['lm']
+ assert isinstance(lm_instance, DSPyLLMProvider)
+ assert lm_instance.provider == mock_llm_provider
+
+ # 3. Assert that the pipeline was instantiated and called correctly
+ mock_dspy_pipeline.assert_called_once_with(retrievers=[mock_retriever])
+ mock_pipeline_instance.forward.assert_called_once_with(question=prompt, db=mock_db)
- # FIX 1: Assert it's an instance of the correct wrapper class.
- assert isinstance(lm_instance, DSPyLLMProvider)
- # This assertion will now pass because the patch target is correct.
- assert lm_instance.provider == mock_llm_provider
-
- mock_rag_pipeline.assert_called_once_with(retrievers=[mock_retriever])
- mock_rag_pipeline_instance.forward.assert_called_once_with(question=prompt, db=mock_db)
- assert response_text == "LLM response with context"
-
-
-# NOTE: The patch target for get_llm_provider has been corrected.
-@patch('app.core.rag_service.get_llm_provider')
-@patch('app.core.rag_service.RAGPipeline')
-@patch('dspy.configure')
-def test_rag_service_chat_with_rag_without_context(mock_configure, mock_rag_pipeline, mock_get_llm_provider):
- """
- Test the RAGService.chat_with_rag method when no context is retrieved.
- """
- # --- Arrange ---
- mock_db = MagicMock(spec=Session)
- mock_llm_provider = MagicMock(spec=LLMProvider)
- mock_get_llm_provider.return_value = mock_llm_provider
-
- mock_retriever = MagicMock(spec=Retriever)
- mock_retriever.retrieve_context.return_value = []
-
- mock_rag_pipeline_instance = MagicMock(spec=RAGPipeline)
- mock_rag_pipeline_instance.forward = AsyncMock(return_value="LLM response without context")
- mock_rag_pipeline.return_value = mock_rag_pipeline_instance
-
- rag_service = RAGService(vector_store=MagicMock(), retrievers=[mock_retriever])
- prompt = "Test prompt without context."
-
- # --- Act ---
- response_text = asyncio.run(rag_service.chat_with_rag(db=mock_db, prompt=prompt, model="deepseek"))
-
- # --- Assert ---
- mock_get_llm_provider.assert_called_once_with("deepseek")
-
- mock_configure.assert_called_once()
- lm_instance = mock_configure.call_args.kwargs['lm']
-
- assert isinstance(lm_instance, DSPyLLMProvider)
- # This assertion will now pass because the patch target is correct.
- assert lm_instance.provider == mock_llm_provider
-
- mock_rag_pipeline.assert_called_once_with(retrievers=[mock_retriever])
- mock_rag_pipeline_instance.forward.assert_called_once_with(question=prompt, db=mock_db)
- assert response_text == "LLM response without context"
\ No newline at end of file
+ # 4. Assert the final response is returned
+ assert response_text == "Final RAG response"
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 3e0b2c9..95f61e0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,5 +2,6 @@
.env
**/.env
**/*.egg-info
-faiss_index.bin
-ai_hub.db
\ No newline at end of file
+**/faiss_index.bin
+**/ai_hub.db
+.pytest_cache/
\ No newline at end of file
diff --git a/ai-hub/app/__init__.py b/ai-hub/app/__init__.py
index e69de29..3fbb1fd 100644
--- a/ai-hub/app/__init__.py
+++ b/ai-hub/app/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/app.py b/ai-hub/app/app.py
index 04f9d7b..0718c32 100644
--- a/ai-hub/app/app.py
+++ b/ai-hub/app/app.py
@@ -24,7 +24,7 @@
"""
# Initialize core services for RAG
# CORRECTED: Now passing the required arguments to FaissVectorStore
- vector_store = FaissVectorStore(index_file_path="faiss_index.bin", dimension=768)
+ vector_store = FaissVectorStore(index_file_path="data/faiss_index.bin", dimension=768)
retrievers: List[Retriever] = [
FaissDBRetriever(vector_store=vector_store),
]
diff --git a/ai-hub/app/core/__init__.py b/ai-hub/app/core/__init__.py
index e69de29..3fbb1fd 100644
--- a/ai-hub/app/core/__init__.py
+++ b/ai-hub/app/core/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/core/llm_providers.py b/ai-hub/app/core/llm_providers.py
index f31a701..de36a55 100644
--- a/ai-hub/app/core/llm_providers.py
+++ b/ai-hub/app/core/llm_providers.py
@@ -45,7 +45,7 @@
async def generate_response(self, prompt: str) -> str:
# Construct the request payload
messages_payload = [
- # {"role": "system", "content": "You are a helpful assistant."},
+ {"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": prompt},
]
diff --git a/ai-hub/app/core/pipelines/__init__.py b/ai-hub/app/core/pipelines/__init__.py
new file mode 100644
index 0000000..3fbb1fd
--- /dev/null
+++ b/ai-hub/app/core/pipelines/__init__.py
@@ -0,0 +1 @@
+# This file can be left empty.
diff --git a/ai-hub/app/core/pipelines/dspy_rag.py b/ai-hub/app/core/pipelines/dspy_rag.py
new file mode 100644
index 0000000..322c01b
--- /dev/null
+++ b/ai-hub/app/core/pipelines/dspy_rag.py
@@ -0,0 +1,87 @@
+# In app/core/pipelines/dspy_rag.py
+
+import dspy
+import logging
+from typing import List
+from types import SimpleNamespace
+from sqlalchemy.orm import Session
+
+from app.core.retrievers import Retriever
+from app.core.llm_providers import LLMProvider
+
+class DSPyLLMProvider(dspy.BaseLM):
+ """
+ A custom wrapper for the LLMProvider to make it compatible with DSPy.
+ """
+ def __init__(self, provider: LLMProvider, model_name: str, **kwargs):
+ super().__init__(model=model_name)
+ self.provider = provider
+ self.kwargs.update(kwargs)
+ print(f"DSPyLLMProvider initialized for model: {self.model}")
+
+ async def aforward(self, prompt: str, **kwargs):
+ """
+ The required asynchronous forward pass for the language model.
+ """
+ logging.info(f"[DSPyLLMProvider.aforward] Received prompt of length: {len(prompt) if prompt else 0}")
+ if not prompt or not prompt.strip():
+ logging.error("[DSPyLLMProvider.aforward] Received a null or empty prompt!")
+ return SimpleNamespace(choices=[SimpleNamespace(message=SimpleNamespace(content="Error: Received an empty prompt."))])
+
+ response_text = await self.provider.generate_response(prompt)
+
+ mock_choice = SimpleNamespace(message=SimpleNamespace(content=response_text, tool_calls=None))
+ return SimpleNamespace(choices=[mock_choice], usage=SimpleNamespace(prompt_tokens=0, completion_tokens=0, total_tokens=0), model=self.model)
+
+class AnswerWithContext(dspy.Signature):
+ """Given the context, answer the user's question."""
+ context = dspy.InputField(desc="Relevant document snippets from the knowledge base.")
+ question = dspy.InputField()
+ answer = dspy.OutputField()
+
+class DspyRagPipeline(dspy.Module):
+ """
+ A simple RAG pipeline that retrieves context and then generates an answer using DSPy.
+ """
+ def __init__(self, retrievers: List[Retriever]):
+ super().__init__()
+ self.retrievers = retrievers
+ # We still define the predictor to access its signature easily.
+ self.generate_answer = dspy.Predict(AnswerWithContext)
+
+ async def forward(self, question: str, db: Session) -> str:
+ """
+ Executes the RAG pipeline asynchronously.
+ """
+ logging.info(f"[DspyRagPipeline.forward] Received question: '{question}'")
+ retrieved_contexts = []
+ for retriever in self.retrievers:
+ context = retriever.retrieve_context(question, db)
+ retrieved_contexts.extend(context)
+
+ context_text = "\n\n".join(retrieved_contexts)
+ if not context_text:
+ print("⚠️ No context retrieved. Falling back to direct QA.")
+ context_text = "No context provided."
+
+ lm = dspy.settings.lm
+ if lm is None:
+ raise RuntimeError("DSPy LM has not been configured. Call dspy.configure(lm=...) first.")
+
+ # --- FIX: Revert to manual prompt construction ---
+ # Get the instruction from the signature's docstring.
+ instruction = self.generate_answer.signature.__doc__
+
+ # Build the full prompt exactly as DSPy would.
+ full_prompt = (
+ f"{instruction}\n\n"
+ f"---\n\n"
+ f"Context: {context_text}\n\n"
+ f"Question: {question}\n\n"
+ f"Answer:"
+ )
+
+ # Call the language model's aforward method directly with the complete prompt.
+ response_obj = await lm.aforward(prompt=full_prompt)
+
+ return response_obj.choices[0].message.content
\ No newline at end of file
diff --git a/ai-hub/app/core/rag_service.py b/ai-hub/app/core/rag_service.py
index 7cb57e9..93b4bb9 100644
--- a/ai-hub/app/core/rag_service.py
+++ b/ai-hub/app/core/rag_service.py
@@ -1,120 +1,28 @@
-import asyncio
from typing import List, Dict, Any
-from types import SimpleNamespace
from sqlalchemy.orm import Session
from sqlalchemy.exc import SQLAlchemyError
import dspy
-import logging
from app.core.vector_store import FaissVectorStore
from app.db import models
from app.core.retrievers import Retriever
-from app.core.llm_providers import LLMProvider, get_llm_provider
-
-# --- DSPy Components for RAG ---
-
-class DSPyLLMProvider(dspy.BaseLM):
- """
- A custom wrapper for the LLMProvider to make it compatible with DSPy.
- """
- def __init__(self, provider: LLMProvider, model_name: str, **kwargs):
- super().__init__(model=model_name)
- self.provider = provider
- self.kwargs.update(kwargs)
- print(f"DSPyLLMProvider initialized for model: {self.model}")
-
- async def aforward(self, prompt: str, **kwargs):
- """
- The required asynchronous forward pass for the language model.
- """
- logging.info(f"[DSPyLLMProvider.aforward] Received prompt of length: {len(prompt) if prompt else 0}")
-
- # --- CRITICAL FIX: Ensure prompt is not None or empty ---
- if not prompt or not prompt.strip():
- logging.error("[DSPyLLMProvider.aforward] Received a null or empty prompt!")
- # Return a default, safe response instead of calling the API with null.
- return SimpleNamespace(choices=[SimpleNamespace(message=SimpleNamespace(content="Error: Received an empty prompt."))])
-
- # Call the async provider directly using the existing event loop
- response_text = await self.provider.generate_response(prompt)
-
- # Create a mock response object that mimics the OpenAI API structure
- mock_choice = SimpleNamespace(
- message=SimpleNamespace(content=response_text, tool_calls=None)
- )
- mock_response = SimpleNamespace(
- choices=[mock_choice],
- usage=SimpleNamespace(prompt_tokens=0, completion_tokens=0, total_tokens=0),
- model=self.model
- )
- return mock_response
-
-class AnswerWithContext(dspy.Signature):
- """
- Signature for our RAG task: input is a context and question, output is an answer.
- """
- context = dspy.InputField(desc="Relevant document snippets from the knowledge base.")
- question = dspy.InputField()
- answer = dspy.OutputField()
-
-class RAGPipeline(dspy.Module):
- """
- A simple RAG pipeline that retrieves context and then generates an answer.
- """
- def __init__(self, retrievers: List[Retriever]):
- super().__init__()
- self.retrievers = retrievers
- # We only need the signature here to generate the prompt text.
- self.generate_answer = dspy.Predict(AnswerWithContext)
-
- async def forward(self, question: str, db: Session) -> str:
- """
- Executes the RAG pipeline asynchronously.
- """
- logging.info(f"[RAGPipeline.forward] Received question: '{question}'")
- retrieved_contexts = []
- for retriever in self.retrievers:
- context = retriever.retrieve_context(question, db)
- retrieved_contexts.extend(context)
-
- context_text = "\n\n".join(retrieved_contexts)
- if not context_text:
- print("⚠️ No context retrieved. Falling back to direct QA.")
- context_text = "No context provided."
-
- # --- REVISED LOGIC ---
- # 1. Manually create the full prompt using the signature's template.
- # The `dspy.Predict` object can be called with the inputs to get the compiled prompt.
- # We access the last generated prompt from the LM's history.
- # Since we haven't called the LM yet, we temporarily configure a basic LM.
-
- # Get the configured language model from dspy settings
- lm = dspy.settings.lm
- if lm is None:
- raise RuntimeError("DSPy LM has not been configured. Call dspy.configure(lm=...) first.")
-
- # 2. Use the signature to create a dspy.Example, which generates the prompt.
- # The dspy.Predict module will format this into a prompt string.
- example = dspy.Example(context=context_text, question=question, signatures=self.generate_answer.signature)
-
- # 3. Call the language model directly with the full prompt string.
- # The `example.signatures` contains the logic to render the prompt.
- # In modern DSPy, `dspy.predict` is a simpler way to do this.
- # We will call the LM's aforward method directly for clarity.
- full_prompt = self.generate_answer.signature.instructions.format(context=context_text, question=question) + "\nAnswer:"
-
- response_obj = await lm.aforward(prompt=full_prompt)
-
- return response_obj.choices[0].message.content
+from app.core.llm_providers import get_llm_provider
+from app.core.pipelines.dspy_rag import DspyRagPipeline, DSPyLLMProvider
-# --- Main RAG Service Class --- (This class remains unchanged)
class RAGService:
+ """
+ Service class for managing the RAG (Retrieval-Augmented Generation) pipeline.
+ This class acts as a high-level orchestrator.
+ """
def __init__(self, vector_store: FaissVectorStore, retrievers: List[Retriever]):
self.vector_store = vector_store
self.retrievers = retrievers
def add_document(self, db: Session, doc_data: Dict[str, Any]) -> int:
+ """
+ Adds a document to both the database and the vector store.
+ """
try:
document_db = models.Document(
title=doc_data["title"],
@@ -124,7 +32,9 @@
db.add(document_db)
db.commit()
db.refresh(document_db)
+
faiss_index = self.vector_store.add_document(document_db.text)
+
vector_metadata = models.VectorMetadata(
document_id=document_db.id,
faiss_index=faiss_index,
@@ -144,16 +54,24 @@
raise
async def chat_with_rag(self, db: Session, prompt: str, model: str) -> str:
+ """
+ Generates a response to a user prompt by orchestrating the RAG pipeline.
+ """
print(f"Received Prompt: {prompt}")
if not prompt or not prompt.strip():
raise ValueError("The prompt cannot be null, empty, or contain only whitespace.")
+ # 1. Get the underlying LLM provider (e.g., Gemini, DeepSeek)
llm_provider_instance = get_llm_provider(model)
+
+ # 2. Wrap it in our custom DSPy-compatible provider
dspy_llm_provider = DSPyLLMProvider(provider=llm_provider_instance, model_name=model)
- # Configure dspy's global settings with our custom LM
+ # 3. Configure DSPy's global settings to use our custom LM
dspy.configure(lm=dspy_llm_provider)
- rag_pipeline = RAGPipeline(retrievers=self.retrievers)
+ # 4. Initialize and execute the RAG pipeline
+ rag_pipeline = DspyRagPipeline(retrievers=self.retrievers)
answer = await rag_pipeline.forward(question=prompt, db=db)
+
return answer
\ No newline at end of file
diff --git a/ai-hub/app/db/database.py b/ai-hub/app/db/database.py
index 2fc97fb..cbb6b13 100644
--- a/ai-hub/app/db/database.py
+++ b/ai-hub/app/db/database.py
@@ -9,7 +9,7 @@
# Default database URLs
POSTGRES_DEFAULT_URL = "postgresql://user:password@localhost/ai_hub_db"
-SQLITE_DEFAULT_URL = "sqlite:///./ai_hub.db"
+SQLITE_DEFAULT_URL = "sqlite:///./data/ai_hub.db"
DATABASE_URL = ""
engine_args = {}
diff --git a/ai-hub/app/db/guide.md b/ai-hub/app/db/guide.md
index a3185c7..e8986e2 100644
--- a/ai-hub/app/db/guide.md
+++ b/ai-hub/app/db/guide.md
@@ -18,7 +18,7 @@
| Variable | Description | Default Value | Supported Values |
| -------------- | ------------------------------------- | ----------------------------------------------------------------------------------------------- | -------------------- |
| `DB_MODE` | Specifies the type of database to use | `"postgres"` | `postgres`, `sqlite` |
-| `DATABASE_URL` | Connection string for the database | PostgreSQL: `postgresql://user:password@localhost/ai_hub_db`
SQLite: `sqlite:///./ai_hub.db` | Any SQLAlchemy URI |
+| `DATABASE_URL` | Connection string for the database | PostgreSQL: `postgresql://user:password@localhost/ai_hub_db`
SQLite: `sqlite:///./data/ai_hub.db` | Any SQLAlchemy URI |
### 💡 Example: Switch to SQLite
diff --git a/ai-hub/app/db_setup.py b/ai-hub/app/db_setup.py
index 739ebd2..63a64a6 100644
--- a/ai-hub/app/db_setup.py
+++ b/ai-hub/app/db_setup.py
@@ -11,7 +11,7 @@
# This configuration allows for easy switching between SQLite and PostgreSQL.
DB_MODE = os.getenv("DB_MODE", "sqlite")
if DB_MODE == "sqlite":
- DATABASE_URL = "sqlite:///./ai_hub.db"
+ DATABASE_URL = "sqlite:///./data/ai_hub.db"
# The connect_args are needed for SQLite to work with FastAPI's multiple threads
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
else:
diff --git a/ai-hub/tests/core/__init__.py b/ai-hub/tests/core/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/ai-hub/tests/core/__init__.py
diff --git a/ai-hub/tests/core/pipelines/__init__.py b/ai-hub/tests/core/pipelines/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/ai-hub/tests/core/pipelines/__init__.py
diff --git a/ai-hub/tests/core/pipelines/test_dspy_rag.py b/ai-hub/tests/core/pipelines/test_dspy_rag.py
new file mode 100644
index 0000000..5e4aa3d
--- /dev/null
+++ b/ai-hub/tests/core/pipelines/test_dspy_rag.py
@@ -0,0 +1,111 @@
+# tests/core/pipelines/test_dspy_rag.py
+
+import asyncio
+from unittest.mock import MagicMock, AsyncMock
+from sqlalchemy.orm import Session
+import dspy
+import pytest
+
+# Import the pipeline being tested
+from app.core.pipelines.dspy_rag import DspyRagPipeline, AnswerWithContext
+
+# Import its dependencies for mocking
+from app.core.retrievers import Retriever
+
+@pytest.fixture
+def mock_lm_configured():
+ """
+ A pytest fixture to mock the dspy language model and configure it globally
+ for the duration of a test.
+ """
+ # 1. Create the mock LM object
+ mock_lm_instance = MagicMock()
+ # 2. Mock its async `aforward` method to return a dspy-compatible object
+ mock_lm_instance.aforward = AsyncMock(
+ return_value=MagicMock(
+ choices=[MagicMock(message=MagicMock(content="Mocked LLM answer"))]
+ )
+ )
+
+ # 3. Store the original LM (if any) to restore it after the test
+ original_lm = dspy.settings.lm
+
+ # 4. CRITICAL FIX: Configure dspy to use our mock LM
+ dspy.configure(lm=mock_lm_instance)
+
+ # 5. The test runs here, with the mock configured
+ yield mock_lm_instance
+
+ # 6. After the test, restore the original LM configuration
+ dspy.configure(lm=original_lm)
+
+
+def test_dspy_rag_pipeline_with_context(mock_lm_configured):
+ """
+ Tests that DspyRagPipeline correctly processes a question when context is found.
+ """
+ # --- Arrange ---
+ mock_retriever = MagicMock(spec=Retriever)
+ mock_retriever.retrieve_context.return_value = ["Context chunk 1.", "Context chunk 2."]
+ mock_db = MagicMock(spec=Session)
+
+ pipeline = DspyRagPipeline(retrievers=[mock_retriever])
+ question = "What is the question?"
+
+ # --- Act ---
+ response = asyncio.run(pipeline.forward(question=question, db=mock_db))
+
+ # --- Assert ---
+ # Assert the retriever was called correctly
+ mock_retriever.retrieve_context.assert_called_once_with(question, mock_db)
+
+ # Assert the language model was called with the correctly constructed prompt
+ expected_context = "Context chunk 1.\n\nContext chunk 2."
+ instruction = AnswerWithContext.__doc__
+ expected_prompt = (
+ f"{instruction}\n\n"
+ f"---\n\n"
+ f"Context: {expected_context}\n\n"
+ f"Question: {question}\n\n"
+ f"Answer:"
+ )
+ # The fixture provides the mock_lm_configured object, which is the mock LM
+ mock_lm_configured.aforward.assert_called_once_with(prompt=expected_prompt)
+
+ # Assert the final answer from the mock is returned
+ assert response == "Mocked LLM answer"
+
+
+def test_dspy_rag_pipeline_without_context(mock_lm_configured):
+ """
+ Tests that DspyRagPipeline correctly handles the case where no context is found.
+ """
+ # --- Arrange ---
+ mock_retriever = MagicMock(spec=Retriever)
+ mock_retriever.retrieve_context.return_value = [] # No context found
+ mock_db = MagicMock(spec=Session)
+
+ pipeline = DspyRagPipeline(retrievers=[mock_retriever])
+ question = "What is the question?"
+
+ # --- Act ---
+ response = asyncio.run(pipeline.forward(question=question, db=mock_db))
+
+ # --- Assert ---
+ # Assert the retriever was called
+ mock_retriever.retrieve_context.assert_called_once_with(question, mock_db)
+
+ # Assert the LM was called with the placeholder context
+ expected_context = "No context provided."
+ instruction = AnswerWithContext.__doc__
+ expected_prompt = (
+ f"{instruction}\n\n"
+ f"---\n\n"
+ f"Context: {expected_context}\n\n"
+ f"Question: {question}\n\n"
+ f"Answer:"
+ )
+ mock_lm_configured.aforward.assert_called_once_with(prompt=expected_prompt)
+
+ # Assert the final answer from the mock is returned
+ assert response == "Mocked LLM answer"
\ No newline at end of file
diff --git a/ai-hub/tests/core/test_rag_service.py b/ai-hub/tests/core/test_rag_service.py
index e0ca1a9..bdd4d17 100644
--- a/ai-hub/tests/core/test_rag_service.py
+++ b/ai-hub/tests/core/test_rag_service.py
@@ -1,96 +1,60 @@
import asyncio
-from unittest.mock import patch, MagicMock, AsyncMock, call
+from unittest.mock import patch, MagicMock, AsyncMock
from sqlalchemy.orm import Session
-import dspy
-# Import what you are testing
-from app.core.rag_service import RAGService, RAGPipeline, DSPyLLMProvider
-# Import dependencies that need to be referenced
+# Import the service being tested
+from app.core.rag_service import RAGService
+
+# Import dependencies that need to be referenced in mocks
from app.core.retrievers import Retriever
-from app.core.llm_providers import LLMProvider # For type checks if needed
+from app.core.pipelines.dspy_rag import DspyRagPipeline, DSPyLLMProvider
+from app.core.llm_providers import LLMProvider
-# --- RAGService Unit Tests ---
-# ... (Your successful add_document tests are fine and don't need changes) ...
-
-# NOTE: The patch target for get_llm_provider has been corrected.
@patch('app.core.rag_service.get_llm_provider')
-@patch('app.core.rag_service.RAGPipeline')
+@patch('app.core.rag_service.DspyRagPipeline') # Patched the new class name
@patch('dspy.configure')
-def test_rag_service_chat_with_rag_with_context(mock_configure, mock_rag_pipeline, mock_get_llm_provider):
+def test_rag_service_orchestration(mock_configure, mock_dspy_pipeline, mock_get_llm_provider):
"""
- Test the RAGService.chat_with_rag method when context is retrieved.
+ Tests that RAGService.chat_with_rag correctly orchestrates its dependencies.
+ It should:
+ 1. Get the correct LLM provider.
+ 2. Configure DSPy with a wrapped provider.
+ 3. Instantiate and call the pipeline with the correct arguments.
"""
# --- Arrange ---
+ # Mock the dependencies that RAGService uses
mock_llm_provider = MagicMock(spec=LLMProvider)
mock_get_llm_provider.return_value = mock_llm_provider
mock_db = MagicMock(spec=Session)
-
mock_retriever = MagicMock(spec=Retriever)
- mock_retriever.retrieve_context.return_value = ["Context text 1.", "Context text 2."]
- mock_rag_pipeline_instance = MagicMock(spec=RAGPipeline)
- mock_rag_pipeline_instance.forward = AsyncMock(return_value="LLM response with context")
- mock_rag_pipeline.return_value = mock_rag_pipeline_instance
+ # Mock the pipeline instance and its return value
+ mock_pipeline_instance = MagicMock(spec=DspyRagPipeline)
+ mock_pipeline_instance.forward = AsyncMock(return_value="Final RAG response")
+ mock_dspy_pipeline.return_value = mock_pipeline_instance
+ # Instantiate the service class we are testing
rag_service = RAGService(vector_store=MagicMock(), retrievers=[mock_retriever])
prompt = "Test prompt."
+ model = "deepseek"
# --- Act ---
- response_text = asyncio.run(rag_service.chat_with_rag(db=mock_db, prompt=prompt, model="deepseek"))
+ response_text = asyncio.run(rag_service.chat_with_rag(db=mock_db, prompt=prompt, model=model))
# --- Assert ---
- mock_get_llm_provider.assert_called_once_with("deepseek")
-
+ # 1. Assert that the correct LLM provider was requested
+ mock_get_llm_provider.assert_called_once_with(model)
+
+ # 2. Assert that dspy was configured with a correctly wrapped provider
mock_configure.assert_called_once()
lm_instance = mock_configure.call_args.kwargs['lm']
+ assert isinstance(lm_instance, DSPyLLMProvider)
+ assert lm_instance.provider == mock_llm_provider
+
+ # 3. Assert that the pipeline was instantiated and called correctly
+ mock_dspy_pipeline.assert_called_once_with(retrievers=[mock_retriever])
+ mock_pipeline_instance.forward.assert_called_once_with(question=prompt, db=mock_db)
- # FIX 1: Assert it's an instance of the correct wrapper class.
- assert isinstance(lm_instance, DSPyLLMProvider)
- # This assertion will now pass because the patch target is correct.
- assert lm_instance.provider == mock_llm_provider
-
- mock_rag_pipeline.assert_called_once_with(retrievers=[mock_retriever])
- mock_rag_pipeline_instance.forward.assert_called_once_with(question=prompt, db=mock_db)
- assert response_text == "LLM response with context"
-
-
-# NOTE: The patch target for get_llm_provider has been corrected.
-@patch('app.core.rag_service.get_llm_provider')
-@patch('app.core.rag_service.RAGPipeline')
-@patch('dspy.configure')
-def test_rag_service_chat_with_rag_without_context(mock_configure, mock_rag_pipeline, mock_get_llm_provider):
- """
- Test the RAGService.chat_with_rag method when no context is retrieved.
- """
- # --- Arrange ---
- mock_db = MagicMock(spec=Session)
- mock_llm_provider = MagicMock(spec=LLMProvider)
- mock_get_llm_provider.return_value = mock_llm_provider
-
- mock_retriever = MagicMock(spec=Retriever)
- mock_retriever.retrieve_context.return_value = []
-
- mock_rag_pipeline_instance = MagicMock(spec=RAGPipeline)
- mock_rag_pipeline_instance.forward = AsyncMock(return_value="LLM response without context")
- mock_rag_pipeline.return_value = mock_rag_pipeline_instance
-
- rag_service = RAGService(vector_store=MagicMock(), retrievers=[mock_retriever])
- prompt = "Test prompt without context."
-
- # --- Act ---
- response_text = asyncio.run(rag_service.chat_with_rag(db=mock_db, prompt=prompt, model="deepseek"))
-
- # --- Assert ---
- mock_get_llm_provider.assert_called_once_with("deepseek")
-
- mock_configure.assert_called_once()
- lm_instance = mock_configure.call_args.kwargs['lm']
-
- assert isinstance(lm_instance, DSPyLLMProvider)
- # This assertion will now pass because the patch target is correct.
- assert lm_instance.provider == mock_llm_provider
-
- mock_rag_pipeline.assert_called_once_with(retrievers=[mock_retriever])
- mock_rag_pipeline_instance.forward.assert_called_once_with(question=prompt, db=mock_db)
- assert response_text == "LLM response without context"
\ No newline at end of file
+ # 4. Assert the final response is returned
+ assert response_text == "Final RAG response"
\ No newline at end of file
diff --git a/ai-hub/tests/db/test_database.py b/ai-hub/tests/db/test_database.py
index 4f8921a..8287067 100644
--- a/ai-hub/tests/db/test_database.py
+++ b/ai-hub/tests/db/test_database.py
@@ -19,7 +19,7 @@
# Assert: Check if the configuration is correct for SQLite
assert database.DB_MODE == "sqlite"
- assert "sqlite:///./ai_hub.db" in database.DATABASE_URL
+ assert "sqlite:///./data/ai_hub.db" in database.DATABASE_URL
assert "connect_args" in database.engine_args
assert database.engine_args["connect_args"] == {"check_same_thread": False}