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
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ repos:
language: system
types: [python]
pass_filenames: false
stages: [pre-push]
stages: [pre-commit]
112 changes: 112 additions & 0 deletions app/api/dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
from functools import lru_cache

from fastapi import Depends

from app.core.config import Settings, get_settings
from app.services.cache_service import CacheService
from app.services.category_manager import CategoryManager
from app.services.chat_memory_service import ChatMemoryService
from app.services.guardrails_service import GuardrailsService
from app.services.lead_service import LeadService
from app.services.openai_service import OpenAIService
from app.services.price_comparator import PriceComparator
from app.services.rag_engine import RAGEngine
from app.services.scraper_service import ScraperService
from app.services.telegram_service import TelegramService
from app.services.vector_service import VectorService
from app.services.woo_service import WooService


@lru_cache(maxsize=1)
def get_openai_service() -> OpenAIService:
"""Provide a singleton instance of OpenAIService."""
return OpenAIService(get_settings())


@lru_cache(maxsize=1)
def get_vector_service() -> VectorService:
"""Provide a singleton instance of VectorService."""
return VectorService(get_settings())


@lru_cache(maxsize=1)
def get_woo_service() -> WooService:
"""Provide a singleton instance of WooService."""
return WooService(get_settings())


@lru_cache(maxsize=1)
def get_scraper_service() -> ScraperService:
"""Provide a singleton instance of ScraperService."""
return ScraperService(get_settings())


@lru_cache(maxsize=1)
def get_cache_service() -> CacheService:
"""Provide a singleton instance of CacheService."""
return CacheService(get_settings())


@lru_cache(maxsize=1)
def get_telegram_service() -> TelegramService:
"""Provide a singleton instance of TelegramService."""
return TelegramService(get_settings())


@lru_cache(maxsize=1)
def get_category_manager() -> CategoryManager:
"""Provide a singleton instance of CategoryManager."""
return CategoryManager()


@lru_cache(maxsize=1)
def get_guardrails_service() -> GuardrailsService:
"""Provide a singleton instance of GuardrailsService."""
return GuardrailsService()


@lru_cache(maxsize=1)
def get_chat_memory_service() -> ChatMemoryService:
"""Provide a singleton instance of ChatMemoryService."""
return ChatMemoryService()


def get_lead_service(
telegram_service: TelegramService = Depends(get_telegram_service),
chat_memory_service: ChatMemoryService = Depends(get_chat_memory_service),
) -> LeadService:
"""Provide a LeadService instance."""
return LeadService(telegram_service, chat_memory_service)


def get_price_comparator(
woo_service: WooService = Depends(get_woo_service),
scraper_service: ScraperService = Depends(get_scraper_service),
cache_service: CacheService = Depends(get_cache_service),
settings: Settings = Depends(get_settings),
) -> PriceComparator:
"""Provide a PriceComparator instance constructed from its dependencies."""
return PriceComparator(woo_service, scraper_service, cache_service, settings)


def get_rag_engine(
openai_service: OpenAIService = Depends(get_openai_service),
vector_service: VectorService = Depends(get_vector_service),
price_comparator: PriceComparator = Depends(get_price_comparator),
telegram_service: TelegramService = Depends(get_telegram_service),
category_manager: CategoryManager = Depends(get_category_manager),
guardrails_service: GuardrailsService = Depends(get_guardrails_service),
chat_memory_service: ChatMemoryService = Depends(get_chat_memory_service),
settings: Settings = Depends(get_settings),
) -> RAGEngine:
"""Dependency injection for RAGEngine."""
return RAGEngine(
openai_service=openai_service,
vector_service=vector_service,
price_comparator=price_comparator,
telegram_service=telegram_service,
category_manager=category_manager,
guardrails_service=guardrails_service,
chat_memory_service=chat_memory_service,
settings=settings,
)
47 changes: 1 addition & 46 deletions app/api/v1/endpoints/chat.py
Original file line number Diff line number Diff line change
@@ -1,68 +1,23 @@
import uuid
from collections.abc import AsyncGenerator
from functools import lru_cache

from fastapi import APIRouter, Depends, Request
from fastapi.responses import StreamingResponse

from app.core.config import get_settings
from app.api.dependencies import get_rag_engine
from app.core.constants import MSG_STREAM_CHAT_FAILED, MSG_SYNC_CHAT_FAILED
from app.core.logging_config import get_logger
from app.core.metrics import bot_messages_total
from app.core.security import verify_api_key
from app.middleware.rate_limiter import limiter
from app.schemas.chat import ChatRequest, ChatResponse
from app.services.cache_service import CacheService
from app.services.category_manager import CategoryManager
from app.services.chat_memory_service import ChatMemoryService
from app.services.guardrails_service import GuardrailsService
from app.services.openai_service import OpenAIService
from app.services.price_comparator import PriceComparator
from app.services.rag_engine import RAGEngine
from app.services.scraper_service import ScraperService
from app.services.telegram_service import TelegramService
from app.services.vector_service import VectorService
from app.services.woo_service import WooService
from app.utils.network import get_client_ip

logger = get_logger(__name__)
chat_router = APIRouter()


@lru_cache
def get_cached_rag_engine() -> RAGEngine:
"""Get cached instance of RAG engine."""
settings = get_settings()
openai_service = OpenAIService(settings)
vector_service = VectorService(settings)

woo_service = WooService(settings)
scraper_service = ScraperService(settings)
cache_service = CacheService(settings)

price_comparator = PriceComparator(woo_service, scraper_service, cache_service, settings)
telegram_service = TelegramService(settings)
category_manager = CategoryManager()
guardrails_service = GuardrailsService()
chat_memory_service = ChatMemoryService()

return RAGEngine(
openai_service=openai_service,
vector_service=vector_service,
price_comparator=price_comparator,
telegram_service=telegram_service,
category_manager=category_manager,
guardrails_service=guardrails_service,
chat_memory_service=chat_memory_service,
settings=settings,
)


def get_rag_engine() -> RAGEngine:
"""Dependency injection for RAGEngine."""
return get_cached_rag_engine()


@chat_router.post("", response_model=ChatResponse)
@limiter.limit("20/minute") # type: ignore
async def chat_completion(
Expand Down
26 changes: 12 additions & 14 deletions app/api/v1/endpoints/health.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from fastapi import APIRouter, HTTPException, status
from openai import AsyncOpenAI
from pinecone import Pinecone # type: ignore[reportMissingTypeStubs]
import asyncio

from app.core.config import get_settings
from fastapi import APIRouter, Depends, HTTPException, status

from app.api.dependencies import get_openai_service, get_vector_service
from app.core.logging_config import get_logger
from app.services.openai_service import OpenAIService
from app.services.vector_service import VectorService

logger = get_logger(__name__)
health_router = APIRouter()
Expand All @@ -16,31 +18,27 @@ async def health_check() -> dict[str, str]:


@health_router.get("/ready", status_code=status.HTTP_200_OK)
async def readiness_check() -> dict[str, str]:
async def readiness_check(
openai_service: OpenAIService = Depends(get_openai_service),
vector_service: VectorService = Depends(get_vector_service),
) -> dict[str, str]:
"""Check availability of dependencies: Pinecone and OpenAI (Readiness probe)."""
settings = get_settings()
components_status = {"status": "ready", "pinecone": "ok", "openai": "ok"}
errors: list[str] = []

# Check OpenAI
try:
openai_client = AsyncOpenAI(
api_key=settings.openai_api_key.get_secret_value(),
timeout=5.0,
max_retries=0,
)
# Call model listing as lightweight check
await openai_client.models.list()
await openai_service.client.models.list()
except Exception as e:
logger.warning("OpenAI readiness check failed", error=str(e))
components_status["openai"] = "failed"
errors.append(f"OpenAI: {e!s}")

# Check Pinecone
try:
pinecone_client = Pinecone(api_key=settings.pinecone_api_key.get_secret_value())
# Check service access (listing indices)
pinecone_client.list_indexes()
await asyncio.to_thread(vector_service.pc.list_indexes)
except Exception as e:
logger.warning("Pinecone readiness check failed", error=str(e))
components_status["pinecone"] = "failed"
Expand Down
16 changes: 5 additions & 11 deletions app/api/v1/endpoints/ingest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile, status

from app.core.config import Settings, get_settings
from app.api.dependencies import get_openai_service, get_vector_service
from app.core.logging_config import get_logger
from app.core.security import verify_api_key
from app.middleware.rate_limiter import limiter
Expand All @@ -18,14 +18,6 @@
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10 MB


def get_openai_service(settings: Settings = Depends(get_settings)) -> OpenAIService:
return OpenAIService(settings)


def get_vector_service(settings: Settings = Depends(get_settings)) -> VectorService:
return VectorService(settings)


@ingest_router.post("/document", response_model=IngestResponse)
@limiter.limit("5/minute") # type: ignore[reportUntypedFunctionDecorator, reportUnknownMemberType]
async def upload_document(
Expand All @@ -35,6 +27,7 @@ async def upload_document(
openai_service: OpenAIService = Depends(get_openai_service),
vector_service: VectorService = Depends(get_vector_service),
) -> IngestResponse:
"""Upload a document for ingestion into the RAG vector store."""
filename = file.filename or "unknown"
logger.info("Starting document ingestion", filename=filename)

Expand Down Expand Up @@ -72,7 +65,7 @@ async def upload_document(
return IngestResponse(document_id=document_id, chunks_count=len(chunks), status="indexed")

except Exception as e:
logger.error("Ingestion pipeline failed", error=str(e), document_id=document_id)
logger.exception("Ingestion pipeline failed", document_id=document_id)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to process document"
) from e
Expand All @@ -87,6 +80,7 @@ async def upload_text(
openai_service: OpenAIService = Depends(get_openai_service),
vector_service: VectorService = Depends(get_vector_service),
) -> IngestResponse:
"""Ingest raw text into the RAG vector store."""
logger.info("Starting text ingestion")
document_id = generate_document_id("raw_text")
chunks = chunk_text(payload.text)
Expand All @@ -113,7 +107,7 @@ async def upload_text(
return IngestResponse(document_id=document_id, chunks_count=len(chunks), status="indexed")

except Exception as e:
logger.error("Text ingestion failed", error=str(e), document_id=document_id)
logger.exception("Text ingestion failed", document_id=document_id)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to process text"
) from e
Loading