Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,12 @@ async def handle_stdio(config: Config, log_level: str):

# Initialize components
search_engine = SearchEngine()
query_processor = QueryProcessor(config.openai)
query_processor = QueryProcessor(config.openai_query)
mcp_handler = MCPHandler(search_engine, query_processor)

# Initialize search engine
try:
await search_engine.initialize(config.qdrant, config.openai)
await search_engine.initialize(config.qdrant, config.openai_embedding)
if not disable_console_logging:
logger.info("Search engine initialized successfully")
except Exception as e:
Expand Down Expand Up @@ -303,7 +303,6 @@ def cli(log_level: str = "INFO", config: Path | None = None) -> None:
# Create and set the event loop
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)

# Set up signal handlers
for sig in (signal.SIGTERM, signal.SIGINT):
loop.add_signal_handler(sig, lambda: asyncio.create_task(shutdown(loop)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from dotenv import load_dotenv
from pydantic import BaseModel
from abc import ABC

# Load environment variables from .env file
load_dotenv()
Expand All @@ -23,6 +24,7 @@ class QdrantConfig(BaseModel):
url: str = "http://localhost:6333"
api_key: str | None = None
collection_name: str = "documents"
vector_size: int = 4096

def __init__(self, **data):
"""Initialize with environment variables if not provided."""
Expand All @@ -32,23 +34,52 @@ def __init__(self, **data):
data["api_key"] = os.getenv("QDRANT_API_KEY")
if "collection_name" not in data:
data["collection_name"] = os.getenv("QDRANT_COLLECTION_NAME", "documents")
if "vector_size" not in data:
data["vector_size"] = int(os.getenv("QDRANT_VECTOR_SIZE", "4096"))
super().__init__(**data)


class OpenAIConfig(BaseModel):
"""OpenAI configuration settings."""
class OpenAIConfig(BaseModel, ABC):
"""Abstract base class for OpenAI configuration settings."""

api_key: str
model: str = "text-embedding-3-small"
chat_model: str = "gpt-3.5-turbo"
api_key: str = ""
model: str = ""
base_url: str = "" # if None, the OpenAI package defaults to "https://api.openai.com/v1"

class OpenAIEmbeddingConfig(OpenAIConfig):
"""OpenAI configuration for embedding."""

def __init__(self, **data):
"""Initialize with environment variables if not provided"""
if "api_key" not in data:
data["api_key"] = os.getenv("OPENAI_EMBEDDING_API_KEY", "")
if "model" not in data:
data["model"] = os.getenv("OPENAI_EMBEDDING_MODEL", "text-embedding-3-small")
if "base_url" not in data:
data["base_url"] = os.getenv("OPENAI_EMBEDDING_BASE_URL", "http://localhost:1234/v1")
super().__init__(**data)

class OpenAIQueryConfig(OpenAIConfig):
"""OpenAI configuration for queries."""

def __init__(self, **data):
"""Initialize with environment variables if not provided"""
if "api_key" not in data:
data["api_key"] = os.getenv("OPENAI_QUERY_API_KEY", "")
if "model" not in data:
data["model"] = os.getenv("OPENAI_QUERY_MODEL", "gpt-3.5-turbo")
if "base_url" not in data:
data["base_url"] = os.getenv("OPENAI_QUERY_BASE_URL", "http://localhost:1234/v1")
super().__init__(**data)


class Config(BaseModel):
"""Main configuration class."""

server: ServerConfig
qdrant: QdrantConfig
openai: OpenAIConfig
openai_query: OpenAIQueryConfig
openai_embedding: OpenAIEmbeddingConfig

def __init__(self, **data):
"""Initialize configuration with environment variables."""
Expand All @@ -57,6 +88,8 @@ def __init__(self, **data):
data["server"] = ServerConfig()
if "qdrant" not in data:
data["qdrant"] = QdrantConfig()
if "openai" not in data:
data["openai"] = {"api_key": os.getenv("OPENAI_API_KEY")}
if "openai_query" not in data:
data["openai_query"] = OpenAIQueryConfig()
if "openai_embedding" not in data:
data["openai_embedding"] = OpenAIEmbeddingConfig()
super().__init__(**data)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from openai import AsyncOpenAI
from qdrant_loader_mcp_server.config import OpenAIConfig

class PatchedAsyncOpenAI(AsyncOpenAI):

model: str

def __init__(self, *args, model: str = "", **kwargs):
super().__init__(*args, **kwargs)
self.model = model

@classmethod
def from_config(cls, config: OpenAIConfig):
return cls(
api_key=config.api_key,
base_url=config.base_url,
model = config.model
)
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"""Search engine implementation for the MCP server."""


from openai import AsyncOpenAI
from ..patch.openai import PatchedAsyncOpenAI
from qdrant_client import QdrantClient
from qdrant_client.http import models

from ..config import OpenAIConfig, QdrantConfig
from ..config import OpenAIEmbeddingConfig, QdrantConfig
from ..utils.logging import LoggingConfig
from .hybrid_search import HybridSearchEngine
from .models import SearchResult
Expand All @@ -20,18 +20,18 @@ def __init__(self):
"""Initialize the search engine."""
self.client: QdrantClient | None = None
self.config: QdrantConfig | None = None
self.openai_client: AsyncOpenAI | None = None
self.openai_client: PatchedAsyncOpenAI | None = None
self.hybrid_search: HybridSearchEngine | None = None
self.logger = LoggingConfig.get_logger(__name__)

async def initialize(
self, config: QdrantConfig, openai_config: OpenAIConfig
self, config: QdrantConfig, openai_config: OpenAIEmbeddingConfig
) -> None:
"""Initialize the search engine with configuration."""
self.config = config
try:
self.client = QdrantClient(url=config.url, api_key=config.api_key)
self.openai_client = AsyncOpenAI(api_key=openai_config.api_key)
self.openai_client = PatchedAsyncOpenAI.from_config(openai_config)

# Ensure collection exists
if self.client is None:
Expand All @@ -42,7 +42,7 @@ async def initialize(
self.client.create_collection(
collection_name=config.collection_name,
vectors_config=models.VectorParams(
size=1536, # Default size for OpenAI embeddings
size=config.vector_size,
distance=models.Distance.COSINE,
),
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"""Hybrid search implementation combining vector and keyword search."""

import re
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import Any

import numpy as np
from openai import AsyncOpenAI
from ..patch.openai import PatchedAsyncOpenAI
from qdrant_client import QdrantClient
from qdrant_client.http import models
from rank_bm25 import BM25Okapi
Expand Down Expand Up @@ -74,17 +74,17 @@ class HybridSearchResult:
paragraph_count: int | None = None

# 🔥 NEW: Semantic analysis (NLP results)
entities: list[dict | str] = None
topics: list[dict | str] = None
key_phrases: list[dict | str] = None
pos_tags: list[dict] = None
entities: list[dict | str] = field(default_factory=list)
topics: list[dict | str] = field(default_factory=list)
key_phrases: list[dict | str] = field(default_factory=list)
pos_tags: list[dict] = field(default_factory=list)

# 🔥 NEW: Navigation context
previous_section: str | None = None
next_section: str | None = None
sibling_sections: list[str] = None
subsections: list[str] = None
document_hierarchy: list[str] = None
sibling_sections: list[str] = field(default_factory=list)
subsections: list[str] = field(default_factory=list)
document_hierarchy: list[str] = field(default_factory=list)

# 🔥 NEW: Chunking context
chunk_index: int | None = None
Expand All @@ -98,7 +98,7 @@ class HybridSearchResult:
is_converted: bool = False

# 🔥 NEW: Cross-references and enhanced context
cross_references: list[dict] = None
cross_references: list[dict] = field(default_factory=list)
topic_analysis: dict | None = None
content_type_context: str | None = None # Human-readable content description

Expand Down Expand Up @@ -128,7 +128,7 @@ class HybridSearchEngine:
def __init__(
self,
qdrant_client: QdrantClient,
openai_client: AsyncOpenAI,
openai_client: PatchedAsyncOpenAI,
collection_name: str,
vector_weight: float = 0.6,
keyword_weight: float = 0.3,
Expand Down Expand Up @@ -208,7 +208,7 @@ async def _get_embedding(self, text: str) -> list[float]:
"""Get embedding for text using OpenAI."""
try:
response = await self.openai_client.embeddings.create(
model="text-embedding-3-small",
model=self.openai_client.model,
input=text,
)
return response.data[0].embedding
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,18 @@
import re
from typing import Any

from openai import AsyncOpenAI
from ..patch.openai import PatchedAsyncOpenAI

from ..config import OpenAIConfig
from ..config import OpenAIQueryConfig
from ..utils.logging import LoggingConfig


class QueryProcessor:
"""Query processor for handling search queries."""

def __init__(self, openai_config: OpenAIConfig):
def __init__(self, openai_config: OpenAIQueryConfig):
"""Initialize the query processor."""
self.openai_client: AsyncOpenAI | None = AsyncOpenAI(
api_key=openai_config.api_key
)
self.openai_client: PatchedAsyncOpenAI | None = PatchedAsyncOpenAI.from_config(openai_config)
self.logger = LoggingConfig.get_logger(__name__)

async def process_query(self, query: str) -> dict[str, Any]:
Expand Down Expand Up @@ -90,7 +88,7 @@ async def _infer_intent(self, query: str) -> tuple[str, bool]:
raise RuntimeError("OpenAI client not initialized")

response = await self.openai_client.chat.completions.create(
model="gpt-3.5-turbo",
model=self.openai_client.model,
messages=[
{
"role": "system",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def filter(self, record):
or record.name == "__main__" # Allow logs from main module
or record.name == "asyncio" # Allow logs from asyncio
or record.name == "main" # Allow logs when started as a script
or record.name == "qdrant_loader_mcp_server" # Allow logs from the package
or record.name.startswith("qdrant_loader_mcp_server") # Allow logs from the package
)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ async def integration_handler():
search_engine = SearchEngine()

# Create OpenAI config for query processor
from qdrant_loader_mcp_server.config import OpenAIConfig
from qdrant_loader_mcp_server.config import OpenAIQueryConfig

openai_config = OpenAIConfig(api_key="test_key")
openai_config = OpenAIQueryConfig(api_key="test_key")
query_processor = QueryProcessor(openai_config)

# Patch external dependencies
Expand All @@ -60,20 +60,23 @@ async def integration_handler():
return_value=mock_qdrant_client,
),
patch(
"qdrant_loader_mcp_server.search.engine.AsyncOpenAI",
"qdrant_loader_mcp_server.search.engine.PatchedAsyncOpenAI",
return_value=mock_openai_client,
),
) as PatchedOpenAIMock,
patch(
"qdrant_loader_mcp_server.search.processor.AsyncOpenAI",
"qdrant_loader_mcp_server.search.processor.PatchedAsyncOpenAI",
return_value=mock_openai_client,
),
):

# Override named constructor
PatchedOpenAIMock.from_config.return_value = mock_openai_client

# Initialize components
from qdrant_loader_mcp_server.config import OpenAIConfig, QdrantConfig
from qdrant_loader_mcp_server.config import OpenAIEmbeddingConfig, QdrantConfig

qdrant_config = QdrantConfig(api_key="test_key")
openai_config = OpenAIConfig(api_key="test_key")
openai_config = OpenAIEmbeddingConfig(api_key="test_key")

await search_engine.initialize(qdrant_config, openai_config)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import numpy as np
import pytest
from openai import AsyncOpenAI
from qdrant_loader_mcp_server.patch.openai import PatchedAsyncOpenAI
from qdrant_loader_mcp_server.search.hybrid_search import (
HybridSearchEngine,
)
Expand Down Expand Up @@ -81,7 +81,8 @@ def mock_qdrant_client():
@pytest.fixture
def mock_openai_client():
"""Create a mock OpenAI client."""
client = AsyncMock(spec=AsyncOpenAI)
client = AsyncMock(spec=PatchedAsyncOpenAI)
client.model = "test-model"

# Mock embeddings response
embedding_response = MagicMock()
Expand Down Expand Up @@ -565,5 +566,5 @@ async def test_get_embedding_success(hybrid_search, mock_openai_client):

# Verify OpenAI API was called correctly
mock_openai_client.embeddings.create.assert_called_once_with(
model="text-embedding-3-small", input="test text"
model=mock_openai_client.model, input="test text"
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from openai import AsyncOpenAI
from qdrant_loader_mcp_server.patch.openai import PatchedAsyncOpenAI
from qdrant_client import QdrantClient
from qdrant_loader_mcp_server.search.engine import SearchEngine
from qdrant_loader_mcp_server.search.hybrid_search import (
Expand Down Expand Up @@ -56,7 +56,7 @@ def mock_qdrant_client():
@pytest.fixture
def mock_openai_client():
"""Create a mock OpenAI client."""
client = AsyncMock(spec=AsyncOpenAI)
client = AsyncMock(spec=PatchedAsyncOpenAI)

# Mock embedding response
mock_response = MagicMock()
Expand Down
Loading
Loading