Spaces:
Runtime error
Runtime error
Move more components into dependency injection container
Browse files- .env.template +0 -4
- Dockerfile +5 -2
- pyproject.toml +10 -14
- scripts/run-dev.sh +1 -1
- src/ctp_slack_bot/api/__init__.py +0 -0
- src/ctp_slack_bot/api/main.py +0 -63
- src/ctp_slack_bot/app.py +34 -0
- src/ctp_slack_bot/containers.py +11 -11
- src/ctp_slack_bot/core/config.py +9 -8
- src/ctp_slack_bot/core/logging.py +21 -22
- src/ctp_slack_bot/db/mongo_db.py +13 -9
- src/ctp_slack_bot/services/answer_retrieval_service.py +6 -4
- src/ctp_slack_bot/services/content_ingestion_service.py +7 -5
- src/ctp_slack_bot/services/context_retrieval_service.py +6 -5
- src/ctp_slack_bot/services/embeddings_model_service.py +4 -5
- src/ctp_slack_bot/services/event_brokerage_service.py +6 -4
- src/ctp_slack_bot/services/language_model_service.py +22 -22
- src/ctp_slack_bot/services/question_dispatch_service.py +7 -5
- src/ctp_slack_bot/services/schedule_service.py +57 -0
- src/ctp_slack_bot/services/slack_service.py +27 -7
- src/ctp_slack_bot/services/vector_database_service.py +7 -6
- src/ctp_slack_bot/services/vectorization_service.py +6 -4
- src/ctp_slack_bot/tasks/__init__.py +0 -1
- src/ctp_slack_bot/tasks/scheduler.py +0 -60
.env.template
CHANGED
@@ -3,10 +3,6 @@
|
|
3 |
# Application Configuration
|
4 |
DEBUG=TRUE
|
5 |
|
6 |
-
# Logging Configuration
|
7 |
-
LOG_LEVEL=INFO
|
8 |
-
LOG_FORMAT=text
|
9 |
-
|
10 |
# APScheduler Configuration
|
11 |
SCHEDULER_TIMEZONE=UTC
|
12 |
|
|
|
3 |
# Application Configuration
|
4 |
DEBUG=TRUE
|
5 |
|
|
|
|
|
|
|
|
|
6 |
# APScheduler Configuration
|
7 |
SCHEDULER_TIMEZONE=UTC
|
8 |
|
Dockerfile
CHANGED
@@ -5,7 +5,7 @@ WORKDIR /app
|
|
5 |
# Set environment variables.
|
6 |
ENV PYTHONDONTWRITEBYTECODE=1 \
|
7 |
PYTHONUNBUFFERED=1 \
|
8 |
-
PYTHONPATH=/app
|
9 |
|
10 |
# Install system dependencies.
|
11 |
RUN apt-get update \
|
@@ -25,5 +25,8 @@ RUN pip install --no-cache-dir .
|
|
25 |
RUN useradd -m appuser
|
26 |
USER appuser
|
27 |
|
|
|
|
|
|
|
28 |
# Run the application.
|
29 |
-
CMD ["python", "-m", "
|
|
|
5 |
# Set environment variables.
|
6 |
ENV PYTHONDONTWRITEBYTECODE=1 \
|
7 |
PYTHONUNBUFFERED=1 \
|
8 |
+
PYTHONPATH=/app/src
|
9 |
|
10 |
# Install system dependencies.
|
11 |
RUN apt-get update \
|
|
|
25 |
RUN useradd -m appuser
|
26 |
USER appuser
|
27 |
|
28 |
+
# Expose a volume mount for logs
|
29 |
+
VOLUME ./logs
|
30 |
+
|
31 |
# Run the application.
|
32 |
+
CMD ["python", "-m", "ctp_slack_bot.app"]
|
pyproject.toml
CHANGED
@@ -19,28 +19,24 @@ classifiers = [
|
|
19 |
"Operating System :: OS Independent",
|
20 |
]
|
21 |
dependencies = [
|
22 |
-
"more-itertools>=10.6.0",
|
23 |
-
"dependency-injector>=4.46.0",
|
24 |
"pydantic>=2.11.2",
|
25 |
"pydantic-settings>=2.8.1",
|
26 |
-
|
27 |
-
# "uvicorn>=0.34.0",
|
28 |
-
"loguru>=0.7.3",
|
29 |
"python-dotenv>=1.1.0",
|
30 |
-
"
|
31 |
-
"
|
32 |
-
"
|
33 |
"pytz>=2025.2",
|
34 |
"apscheduler>=3.11.0",
|
|
|
|
|
35 |
"aiohttp>=3.11.16",
|
|
|
36 |
"slack-sdk>=3.35.0",
|
37 |
"slack_bolt>=1.23.0",
|
38 |
-
"
|
39 |
-
"
|
40 |
-
|
41 |
-
# "langchain>=0.3.23",
|
42 |
-
# "transformers>=4.51.0",
|
43 |
-
# "torch>=2.6.0",
|
44 |
]
|
45 |
|
46 |
[project.optional-dependencies]
|
|
|
19 |
"Operating System :: OS Independent",
|
20 |
]
|
21 |
dependencies = [
|
|
|
|
|
22 |
"pydantic>=2.11.2",
|
23 |
"pydantic-settings>=2.8.1",
|
24 |
+
"more-itertools>=10.6.0",
|
|
|
|
|
25 |
"python-dotenv>=1.1.0",
|
26 |
+
"loguru>=0.7.3",
|
27 |
+
"fastapi>=0.115.12",
|
28 |
+
"dependency-injector>=4.46.0",
|
29 |
"pytz>=2025.2",
|
30 |
"apscheduler>=3.11.0",
|
31 |
+
# "tenacity>=9.1.2",
|
32 |
+
# "pybreaker>=1.3.0",
|
33 |
"aiohttp>=3.11.16",
|
34 |
+
"webvtt-py>=0.5.1",
|
35 |
"slack-sdk>=3.35.0",
|
36 |
"slack_bolt>=1.23.0",
|
37 |
+
"motor>=3.7.0",
|
38 |
+
"openai>=1.70.0"
|
39 |
+
|
|
|
|
|
|
|
40 |
]
|
41 |
|
42 |
[project.optional-dependencies]
|
scripts/run-dev.sh
CHANGED
@@ -2,4 +2,4 @@
|
|
2 |
|
3 |
parent_path=$(cd "$(dirname "${BASH_SOURCE[0]}")"; pwd -P)
|
4 |
|
5 |
-
python3 "${parent_path}/../src/ctp_slack_bot/
|
|
|
2 |
|
3 |
parent_path=$(cd "$(dirname "${BASH_SOURCE[0]}")"; pwd -P)
|
4 |
|
5 |
+
LOG_LEVEL=DEBUG python3 "${parent_path}/../src/ctp_slack_bot/app.py"
|
src/ctp_slack_bot/api/__init__.py
DELETED
File without changes
|
src/ctp_slack_bot/api/main.py
DELETED
@@ -1,63 +0,0 @@
|
|
1 |
-
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
2 |
-
from asyncio import run as run_async
|
3 |
-
from contextlib import asynccontextmanager
|
4 |
-
from dependency_injector.wiring import inject, Provide
|
5 |
-
from fastapi import FastAPI, HTTPException, Depends
|
6 |
-
from loguru import logger
|
7 |
-
from typing import Any, AsyncGenerator
|
8 |
-
from slack_bolt.async_app import AsyncApp
|
9 |
-
from slack_bolt.adapter.socket_mode.async_handler import AsyncSocketModeHandler
|
10 |
-
from starlette.requests import Request
|
11 |
-
from starlette.responses import Response
|
12 |
-
from threading import Thread
|
13 |
-
from typing import Any, Dict, Self
|
14 |
-
|
15 |
-
from ctp_slack_bot.containers import Container
|
16 |
-
from ctp_slack_bot.core.config import Settings
|
17 |
-
from ctp_slack_bot.core.logging import setup_logging
|
18 |
-
from ctp_slack_bot.core.response_rendering import PrettyJSONResponse
|
19 |
-
from ctp_slack_bot.tasks import start_scheduler, stop_scheduler
|
20 |
-
|
21 |
-
async def main() -> None:
|
22 |
-
container = Container()
|
23 |
-
container.wire(packages=['ctp_slack_bot'])
|
24 |
-
|
25 |
-
# Setup logging.
|
26 |
-
setup_logging(container)
|
27 |
-
logger.info("Starting application")
|
28 |
-
|
29 |
-
# Start the scheduler.
|
30 |
-
scheduler = start_scheduler(container)
|
31 |
-
logger.info("Started scheduler")
|
32 |
-
|
33 |
-
# Initialize primordial dependencies in container.
|
34 |
-
container.primordial_services()
|
35 |
-
|
36 |
-
# Start Slack socket mode in a background thread and set up an event handler for the Bolt app.
|
37 |
-
bolt_app = container.slack_bolt_app()
|
38 |
-
slack_service = container.slack_service()
|
39 |
-
@bolt_app.event("message")
|
40 |
-
async def handle_message(body: Dict[str, Any]) -> None:
|
41 |
-
logger.debug("Ignored regular message: {}", body.get("event").get("text"))
|
42 |
-
#await slack_service.process_message(body)
|
43 |
-
@bolt_app.event("app_mention")
|
44 |
-
async def handle_app_mention(body: Dict[str, Any]) -> None:
|
45 |
-
#logger.debug("Ignored app mention: {}", body.get("event").get("text"))
|
46 |
-
await slack_service.process_message(body)
|
47 |
-
|
48 |
-
# Start Socket Mode handler in a background thread
|
49 |
-
socket_mode_handler = AsyncSocketModeHandler(
|
50 |
-
app=bolt_app,
|
51 |
-
app_token=container.settings().SLACK_APP_TOKEN.get_secret_value()
|
52 |
-
)
|
53 |
-
logger.info("Starting Slack Socket Mode handler…")
|
54 |
-
await socket_mode_handler.start_async()
|
55 |
-
|
56 |
-
# Shutdown.
|
57 |
-
logger.info("Shutting down application")
|
58 |
-
stop_scheduler(scheduler)
|
59 |
-
logger.info("Stopped scheduler")
|
60 |
-
|
61 |
-
if __name__ == "__main__":
|
62 |
-
# run()
|
63 |
-
run_async(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/ctp_slack_bot/app.py
ADDED
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from asyncio import run
|
2 |
+
from loguru import logger
|
3 |
+
|
4 |
+
from ctp_slack_bot.containers import Container
|
5 |
+
from ctp_slack_bot.core.logging import setup_logging
|
6 |
+
|
7 |
+
async def main() -> None:
|
8 |
+
# Setup logging.
|
9 |
+
setup_logging()
|
10 |
+
logger.info("Starting application…")
|
11 |
+
|
12 |
+
# Set up dependency injection container.
|
13 |
+
container = Container()
|
14 |
+
container.wire(packages=['ctp_slack_bot'])
|
15 |
+
|
16 |
+
# Kick off services which should be active from the start.
|
17 |
+
container.content_ingestion_service()
|
18 |
+
container.question_dispatch_service()
|
19 |
+
|
20 |
+
# Start the scheduler.
|
21 |
+
schedule_service = container.schedule_service()
|
22 |
+
schedule_service.start()
|
23 |
+
|
24 |
+
# Start the Slack socket mode handler in a background thread.
|
25 |
+
socket_mode_handler = container.socket_mode_handler()
|
26 |
+
logger.info("Starting Slack Socket Mode handler…")
|
27 |
+
await socket_mode_handler.start_async()
|
28 |
+
|
29 |
+
# Shutdown. (This will never execute, because the socket mode handler never returns.)
|
30 |
+
logger.info("Shutting down application…")
|
31 |
+
schedule_service.stop()
|
32 |
+
|
33 |
+
if __name__ == "__main__":
|
34 |
+
run(main())
|
src/ctp_slack_bot/containers.py
CHANGED
@@ -1,9 +1,10 @@
|
|
1 |
from dependency_injector.containers import DeclarativeContainer
|
2 |
-
from dependency_injector.providers import
|
|
|
3 |
from slack_bolt.async_app import AsyncApp
|
4 |
|
5 |
from ctp_slack_bot.core.config import Settings
|
6 |
-
from ctp_slack_bot.db.mongo_db import
|
7 |
from ctp_slack_bot.db.repositories import MongoVectorizedChunkRepository
|
8 |
from ctp_slack_bot.services.answer_retrieval_service import AnswerRetrievalService
|
9 |
from ctp_slack_bot.services.content_ingestion_service import ContentIngestionService
|
@@ -12,7 +13,8 @@ from ctp_slack_bot.services.embeddings_model_service import EmbeddingsModelServi
|
|
12 |
from ctp_slack_bot.services.event_brokerage_service import EventBrokerageService
|
13 |
from ctp_slack_bot.services.language_model_service import LanguageModelService
|
14 |
from ctp_slack_bot.services.question_dispatch_service import QuestionDispatchService
|
15 |
-
from ctp_slack_bot.services.
|
|
|
16 |
from ctp_slack_bot.services.vector_database_service import VectorDatabaseService
|
17 |
from ctp_slack_bot.services.vectorization_service import VectorizationService
|
18 |
|
@@ -20,12 +22,9 @@ from ctp_slack_bot.services.vectorization_service import VectorizationService
|
|
20 |
class Container(DeclarativeContainer):
|
21 |
settings = Singleton(Settings)
|
22 |
event_brokerage_service = Singleton(EventBrokerageService)
|
23 |
-
|
24 |
-
mongo_db =
|
25 |
-
vectorized_chunk_repository = Singleton(
|
26 |
-
MongoVectorizedChunkRepository,
|
27 |
-
mongo_db=mongo_db
|
28 |
-
)
|
29 |
vector_database_service = Singleton(VectorDatabaseService, settings=settings, mongo_db=mongo_db)
|
30 |
embeddings_model_service = Singleton(EmbeddingsModelService, settings=settings)
|
31 |
vectorization_service = Singleton(VectorizationService, settings=settings, embeddings_model_service=embeddings_model_service)
|
@@ -34,5 +33,6 @@ class Container(DeclarativeContainer):
|
|
34 |
language_model_service = Singleton(LanguageModelService, settings=settings)
|
35 |
answer_retrieval_service = Singleton(AnswerRetrievalService, settings=settings, event_brokerage_service=event_brokerage_service, language_model_service=language_model_service)
|
36 |
question_dispatch_service = Singleton(QuestionDispatchService, settings=settings, event_brokerage_service=event_brokerage_service, content_ingestion_service=content_ingestion_service, context_retrieval_service=context_retrieval_service, answer_retrieval_service=answer_retrieval_service)
|
37 |
-
|
38 |
-
|
|
|
|
1 |
from dependency_injector.containers import DeclarativeContainer
|
2 |
+
from dependency_injector.providers import Resource, Singleton
|
3 |
+
from slack_bolt.adapter.socket_mode.async_handler import AsyncSocketModeHandler
|
4 |
from slack_bolt.async_app import AsyncApp
|
5 |
|
6 |
from ctp_slack_bot.core.config import Settings
|
7 |
+
from ctp_slack_bot.db.mongo_db import MongoDBResource
|
8 |
from ctp_slack_bot.db.repositories import MongoVectorizedChunkRepository
|
9 |
from ctp_slack_bot.services.answer_retrieval_service import AnswerRetrievalService
|
10 |
from ctp_slack_bot.services.content_ingestion_service import ContentIngestionService
|
|
|
13 |
from ctp_slack_bot.services.event_brokerage_service import EventBrokerageService
|
14 |
from ctp_slack_bot.services.language_model_service import LanguageModelService
|
15 |
from ctp_slack_bot.services.question_dispatch_service import QuestionDispatchService
|
16 |
+
from ctp_slack_bot.services.schedule_service import ScheduleService
|
17 |
+
from ctp_slack_bot.services.slack_service import SlackServiceResource
|
18 |
from ctp_slack_bot.services.vector_database_service import VectorDatabaseService
|
19 |
from ctp_slack_bot.services.vectorization_service import VectorizationService
|
20 |
|
|
|
22 |
class Container(DeclarativeContainer):
|
23 |
settings = Singleton(Settings)
|
24 |
event_brokerage_service = Singleton(EventBrokerageService)
|
25 |
+
schedule_service = Singleton(ScheduleService, settings=settings)
|
26 |
+
mongo_db = Resource(MongoDBResource, settings=settings) # TODO: generalize to any database.
|
27 |
+
vectorized_chunk_repository = Singleton(MongoVectorizedChunkRepository, mongo_db=mongo_db)
|
|
|
|
|
|
|
28 |
vector_database_service = Singleton(VectorDatabaseService, settings=settings, mongo_db=mongo_db)
|
29 |
embeddings_model_service = Singleton(EmbeddingsModelService, settings=settings)
|
30 |
vectorization_service = Singleton(VectorizationService, settings=settings, embeddings_model_service=embeddings_model_service)
|
|
|
33 |
language_model_service = Singleton(LanguageModelService, settings=settings)
|
34 |
answer_retrieval_service = Singleton(AnswerRetrievalService, settings=settings, event_brokerage_service=event_brokerage_service, language_model_service=language_model_service)
|
35 |
question_dispatch_service = Singleton(QuestionDispatchService, settings=settings, event_brokerage_service=event_brokerage_service, content_ingestion_service=content_ingestion_service, context_retrieval_service=context_retrieval_service, answer_retrieval_service=answer_retrieval_service)
|
36 |
+
slack_bolt_app = Singleton(AsyncApp, token=settings.provided.SLACK_BOT_TOKEN().get_secret_value())
|
37 |
+
slack_service = Resource(SlackServiceResource, event_brokerage_service=event_brokerage_service, slack_bolt_app=slack_bolt_app)
|
38 |
+
socket_mode_handler = Singleton(lambda _, app, app_token: AsyncSocketModeHandler(app, app_token), slack_service, slack_bolt_app, settings.provided.SLACK_APP_TOKEN().get_secret_value())
|
src/ctp_slack_bot/core/config.py
CHANGED
@@ -1,11 +1,13 @@
|
|
1 |
from pydantic import Field, MongoDsn, NonNegativeFloat, NonNegativeInt, PositiveInt, SecretStr
|
2 |
from pydantic_settings import BaseSettings, SettingsConfigDict
|
3 |
-
from
|
|
|
4 |
|
5 |
class Settings(BaseSettings): # TODO: Strong guarantees of validity, because garbage in = garbage out, and settings flow into all the nooks and crannies
|
6 |
"""
|
7 |
Application settings loaded from environment variables.
|
8 |
"""
|
|
|
9 |
# Application Configuration
|
10 |
DEBUG: bool = False
|
11 |
|
@@ -13,17 +15,11 @@ class Settings(BaseSettings): # TODO: Strong guarantees of validity, because gar
|
|
13 |
LOG_LEVEL: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = Field(default_factory=lambda data: "DEBUG" if data.get("DEBUG", False) else "INFO")
|
14 |
LOG_FORMAT: Literal["text", "json"] = "json"
|
15 |
|
16 |
-
# API Configuration
|
17 |
-
API_HOST: str = "0.0.0.0"
|
18 |
-
API_PORT: int = 8000
|
19 |
-
|
20 |
# APScheduler Configuration
|
21 |
SCHEDULER_TIMEZONE: Optional[str] = "UTC"
|
22 |
|
23 |
# Slack Configuration
|
24 |
-
SLACK_USER_TOKEN: Optional[SecretStr] = None
|
25 |
SLACK_BOT_TOKEN: SecretStr
|
26 |
-
SLACK_SIGNING_SECRET: Optional[SecretStr] = None
|
27 |
SLACK_APP_TOKEN: SecretStr
|
28 |
|
29 |
# Vectorization Configuration
|
@@ -32,7 +28,7 @@ class Settings(BaseSettings): # TODO: Strong guarantees of validity, because gar
|
|
32 |
CHUNK_SIZE: PositiveInt
|
33 |
CHUNK_OVERLAP: NonNegativeInt
|
34 |
TOP_K_MATCHES: PositiveInt
|
35 |
-
|
36 |
# MongoDB Configuration
|
37 |
MONGODB_URI: SecretStr # TODO: Contemplate switching to MongoDsn type for the main URL, and separate out the credentials to SecretStr variables.
|
38 |
MONGODB_NAME: str
|
@@ -52,4 +48,9 @@ class Settings(BaseSettings): # TODO: Strong guarantees of validity, because gar
|
|
52 |
env_file=".env",
|
53 |
env_file_encoding="utf-8",
|
54 |
case_sensitive=True,
|
|
|
|
|
55 |
)
|
|
|
|
|
|
|
|
1 |
from pydantic import Field, MongoDsn, NonNegativeFloat, NonNegativeInt, PositiveInt, SecretStr
|
2 |
from pydantic_settings import BaseSettings, SettingsConfigDict
|
3 |
+
from types import MappingProxyType
|
4 |
+
from typing import Literal, Mapping, Optional, Self
|
5 |
|
6 |
class Settings(BaseSettings): # TODO: Strong guarantees of validity, because garbage in = garbage out, and settings flow into all the nooks and crannies
|
7 |
"""
|
8 |
Application settings loaded from environment variables.
|
9 |
"""
|
10 |
+
|
11 |
# Application Configuration
|
12 |
DEBUG: bool = False
|
13 |
|
|
|
15 |
LOG_LEVEL: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = Field(default_factory=lambda data: "DEBUG" if data.get("DEBUG", False) else "INFO")
|
16 |
LOG_FORMAT: Literal["text", "json"] = "json"
|
17 |
|
|
|
|
|
|
|
|
|
18 |
# APScheduler Configuration
|
19 |
SCHEDULER_TIMEZONE: Optional[str] = "UTC"
|
20 |
|
21 |
# Slack Configuration
|
|
|
22 |
SLACK_BOT_TOKEN: SecretStr
|
|
|
23 |
SLACK_APP_TOKEN: SecretStr
|
24 |
|
25 |
# Vectorization Configuration
|
|
|
28 |
CHUNK_SIZE: PositiveInt
|
29 |
CHUNK_OVERLAP: NonNegativeInt
|
30 |
TOP_K_MATCHES: PositiveInt
|
31 |
+
|
32 |
# MongoDB Configuration
|
33 |
MONGODB_URI: SecretStr # TODO: Contemplate switching to MongoDsn type for the main URL, and separate out the credentials to SecretStr variables.
|
34 |
MONGODB_NAME: str
|
|
|
48 |
env_file=".env",
|
49 |
env_file_encoding="utf-8",
|
50 |
case_sensitive=True,
|
51 |
+
extra="allow",
|
52 |
+
frozen=True
|
53 |
)
|
54 |
+
|
55 |
+
def get_extra_environment_variables(self: Self) -> Mapping[str, str]:
|
56 |
+
return MappingProxyType(self.__pydantic_extra__)
|
src/ctp_slack_bot/core/logging.py
CHANGED
@@ -1,11 +1,9 @@
|
|
1 |
-
from dependency_injector.wiring import Provide
|
2 |
from logging import __file__ as logging_file, basicConfig, currentframe, getLogger, Handler, INFO, LogRecord
|
3 |
from loguru import logger
|
|
|
4 |
from sys import stderr
|
5 |
from typing import Self
|
6 |
|
7 |
-
from ctp_slack_bot.containers import Container
|
8 |
-
|
9 |
class InterceptHandler(Handler):
|
10 |
"""
|
11 |
Intercept standard logging messages toward Loguru.
|
@@ -32,22 +30,23 @@ class InterceptHandler(Handler):
|
|
32 |
)
|
33 |
|
34 |
|
35 |
-
def setup_logging(
|
36 |
"""
|
37 |
Configure logging with Loguru.
|
38 |
|
39 |
-
This function sets up Loguru as the main logging provider,
|
40 |
-
|
41 |
-
standard logging messages.
|
42 |
"""
|
43 |
-
from ctp_slack_bot.containers import Container
|
44 |
-
settings = container.settings() if container else Provide[Container.settings]
|
45 |
|
46 |
-
#
|
|
|
|
|
|
|
|
|
47 |
logger.remove()
|
48 |
|
49 |
-
# Determine log format
|
50 |
-
if
|
51 |
log_format = {
|
52 |
"time": "{time:YYYY-MM-DD HH:mm:ss.SSS}",
|
53 |
"level": "{level}",
|
@@ -65,33 +64,33 @@ def setup_logging(container: "Container") -> None: # TODO: Perhaps get rid of th
|
|
65 |
"<level>{message}</level>"
|
66 |
)
|
67 |
|
68 |
-
# Add console handler
|
69 |
logger.add(
|
70 |
stderr,
|
71 |
format=format_string,
|
72 |
-
level=
|
73 |
-
serialize=(
|
74 |
backtrace=True,
|
75 |
diagnose=True,
|
76 |
)
|
77 |
|
78 |
-
# Add file handler for non-DEBUG environments
|
79 |
-
if
|
80 |
logger.add(
|
81 |
"logs/app.log",
|
82 |
rotation="10 MB",
|
83 |
retention="1 week",
|
84 |
compression="zip",
|
85 |
format=format_string,
|
86 |
-
level=
|
87 |
-
serialize=(
|
88 |
)
|
89 |
|
90 |
-
# Intercept standard logging messages
|
91 |
basicConfig(handlers=[InterceptHandler()], level=0, force=True)
|
92 |
|
93 |
-
# Update logging levels for some noisy libraries
|
94 |
for logger_name in ("uvicorn", "uvicorn.error", "fastapi", "httpx", "apscheduler", "pymongo"):
|
95 |
getLogger(logger_name).setLevel(INFO)
|
96 |
|
97 |
-
logger.info(f"Logging configured with level {
|
|
|
|
|
1 |
from logging import __file__ as logging_file, basicConfig, currentframe, getLogger, Handler, INFO, LogRecord
|
2 |
from loguru import logger
|
3 |
+
from os import getenv
|
4 |
from sys import stderr
|
5 |
from typing import Self
|
6 |
|
|
|
|
|
7 |
class InterceptHandler(Handler):
|
8 |
"""
|
9 |
Intercept standard logging messages toward Loguru.
|
|
|
30 |
)
|
31 |
|
32 |
|
33 |
+
def setup_logging() -> None:
|
34 |
"""
|
35 |
Configure logging with Loguru.
|
36 |
|
37 |
+
This function sets up Loguru as the main logging provider, configures the log format based on environment variables,
|
38 |
+
and intercepts standard logging messages.
|
|
|
39 |
"""
|
|
|
|
|
40 |
|
41 |
+
# Get logger configuration from environment variables.
|
42 |
+
log_level = getenv("LOG_LEVEL", "INFO")
|
43 |
+
log_format = getenv("LOG_FORMAT", "text")
|
44 |
+
|
45 |
+
# Remove default loguru handler.
|
46 |
logger.remove()
|
47 |
|
48 |
+
# Determine log format.
|
49 |
+
if log_format == "json":
|
50 |
log_format = {
|
51 |
"time": "{time:YYYY-MM-DD HH:mm:ss.SSS}",
|
52 |
"level": "{level}",
|
|
|
64 |
"<level>{message}</level>"
|
65 |
)
|
66 |
|
67 |
+
# Add console handler.
|
68 |
logger.add(
|
69 |
stderr,
|
70 |
format=format_string,
|
71 |
+
level=log_level,
|
72 |
+
serialize=(log_format == "json"),
|
73 |
backtrace=True,
|
74 |
diagnose=True,
|
75 |
)
|
76 |
|
77 |
+
# Add file handler for non-DEBUG environments.
|
78 |
+
if log_level != "DEBUG":
|
79 |
logger.add(
|
80 |
"logs/app.log",
|
81 |
rotation="10 MB",
|
82 |
retention="1 week",
|
83 |
compression="zip",
|
84 |
format=format_string,
|
85 |
+
level=log_level,
|
86 |
+
serialize=(log_format == "json"),
|
87 |
)
|
88 |
|
89 |
+
# Intercept standard logging messages.
|
90 |
basicConfig(handlers=[InterceptHandler()], level=0, force=True)
|
91 |
|
92 |
+
# Update logging levels for some noisy libraries.
|
93 |
for logger_name in ("uvicorn", "uvicorn.error", "fastapi", "httpx", "apscheduler", "pymongo"):
|
94 |
getLogger(logger_name).setLevel(INFO)
|
95 |
|
96 |
+
logger.info(f"Logging configured with level {log_level}")
|
src/ctp_slack_bot/db/mongo_db.py
CHANGED
@@ -1,7 +1,8 @@
|
|
|
|
1 |
from motor.motor_asyncio import AsyncIOMotorClient
|
2 |
from pymongo.errors import ConnectionFailure, ServerSelectionTimeoutError
|
3 |
from loguru import logger
|
4 |
-
from pydantic import BaseModel,
|
5 |
from typing import Any, Dict, Optional, Self
|
6 |
import asyncio
|
7 |
|
@@ -18,14 +19,11 @@ class MongoDB(BaseModel):
|
|
18 |
class Config:
|
19 |
arbitrary_types_allowed = True
|
20 |
|
21 |
-
|
22 |
-
|
23 |
-
"""Initialize MongoDB connection after model creation."""
|
24 |
-
self._initialize_client()
|
25 |
logger.debug("Created {}", self.__class__.__name__)
|
26 |
-
return self
|
27 |
|
28 |
-
def
|
29 |
"""Initialize MongoDB client with settings."""
|
30 |
try:
|
31 |
connection_string = self.settings.MONGODB_URI.get_secret_value()
|
@@ -58,7 +56,7 @@ class MongoDB(BaseModel):
|
|
58 |
"""Get the MongoDB client instance."""
|
59 |
if self._client is None:
|
60 |
logger.warning("MongoDB client not initialized. Attempting to initialize.")
|
61 |
-
self.
|
62 |
if self._client is None:
|
63 |
raise ConnectionError("Failed to initialize MongoDB client")
|
64 |
return self._client
|
@@ -68,7 +66,7 @@ class MongoDB(BaseModel):
|
|
68 |
"""Get the MongoDB database instance."""
|
69 |
if self._db is None:
|
70 |
logger.warning("MongoDB database not initialized. Attempting to initialize client.")
|
71 |
-
self.
|
72 |
if self._db is None:
|
73 |
raise ConnectionError("Failed to initialize MongoDB database")
|
74 |
return self._db
|
@@ -146,3 +144,9 @@ class MongoDB(BaseModel):
|
|
146 |
logger.info("MongoDB connection closed")
|
147 |
self._client = None
|
148 |
self._db = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from dependency_injector.resources import Resource
|
2 |
from motor.motor_asyncio import AsyncIOMotorClient
|
3 |
from pymongo.errors import ConnectionFailure, ServerSelectionTimeoutError
|
4 |
from loguru import logger
|
5 |
+
from pydantic import BaseModel, PrivateAttr
|
6 |
from typing import Any, Dict, Optional, Self
|
7 |
import asyncio
|
8 |
|
|
|
19 |
class Config:
|
20 |
arbitrary_types_allowed = True
|
21 |
|
22 |
+
def __init__(self: Self, **data: Dict[str, Any]) -> None:
|
23 |
+
super().__init__(**data)
|
|
|
|
|
24 |
logger.debug("Created {}", self.__class__.__name__)
|
|
|
25 |
|
26 |
+
def connect(self: Self) -> None:
|
27 |
"""Initialize MongoDB client with settings."""
|
28 |
try:
|
29 |
connection_string = self.settings.MONGODB_URI.get_secret_value()
|
|
|
56 |
"""Get the MongoDB client instance."""
|
57 |
if self._client is None:
|
58 |
logger.warning("MongoDB client not initialized. Attempting to initialize.")
|
59 |
+
self.connect()
|
60 |
if self._client is None:
|
61 |
raise ConnectionError("Failed to initialize MongoDB client")
|
62 |
return self._client
|
|
|
66 |
"""Get the MongoDB database instance."""
|
67 |
if self._db is None:
|
68 |
logger.warning("MongoDB database not initialized. Attempting to initialize client.")
|
69 |
+
self.connect()
|
70 |
if self._db is None:
|
71 |
raise ConnectionError("Failed to initialize MongoDB database")
|
72 |
return self._db
|
|
|
144 |
logger.info("MongoDB connection closed")
|
145 |
self._client = None
|
146 |
self._db = None
|
147 |
+
|
148 |
+
class MongoDBResource(Resource):
|
149 |
+
def init(self: Self, settings: Settings) -> MongoDB:
|
150 |
+
mongo_db = MongoDB(settings=settings)
|
151 |
+
mongo_db.connect()
|
152 |
+
return mongo_db
|
src/ctp_slack_bot/services/answer_retrieval_service.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
from loguru import logger
|
2 |
-
from pydantic import BaseModel
|
3 |
from typing import Collection, Self
|
4 |
|
5 |
from ctp_slack_bot.core import Settings
|
@@ -18,10 +18,12 @@ class AnswerRetrievalService(BaseModel):
|
|
18 |
event_brokerage_service: EventBrokerageService
|
19 |
language_model_service: LanguageModelService
|
20 |
|
21 |
-
|
22 |
-
|
|
|
|
|
|
|
23 |
logger.debug("Created {}", self.__class__.__name__)
|
24 |
-
return self
|
25 |
|
26 |
async def push(self: Self, question: SlackMessage, context: Collection[Chunk]) -> None:
|
27 |
channel_to_respond_to = question.channel
|
|
|
1 |
from loguru import logger
|
2 |
+
from pydantic import BaseModel
|
3 |
from typing import Collection, Self
|
4 |
|
5 |
from ctp_slack_bot.core import Settings
|
|
|
18 |
event_brokerage_service: EventBrokerageService
|
19 |
language_model_service: LanguageModelService
|
20 |
|
21 |
+
class Config:
|
22 |
+
frozen=True
|
23 |
+
|
24 |
+
def __init__(self: Self, **data) -> None:
|
25 |
+
super().__init__(**data)
|
26 |
logger.debug("Created {}", self.__class__.__name__)
|
|
|
27 |
|
28 |
async def push(self: Self, question: SlackMessage, context: Collection[Chunk]) -> None:
|
29 |
channel_to_respond_to = question.channel
|
src/ctp_slack_bot/services/content_ingestion_service.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
from loguru import logger
|
2 |
-
from pydantic import BaseModel
|
3 |
from typing import Self, Sequence
|
4 |
|
5 |
from ctp_slack_bot.core import Settings
|
@@ -19,12 +19,14 @@ class ContentIngestionService(BaseModel):
|
|
19 |
vector_database_service: VectorDatabaseService
|
20 |
vectorization_service: VectorizationService
|
21 |
|
22 |
-
|
23 |
-
|
24 |
-
|
|
|
|
|
25 |
self.event_brokerage_service.subscribe(EventType.INCOMING_CONTENT, self.process_incoming_content)
|
26 |
self.event_brokerage_service.subscribe(EventType.INCOMING_SLACK_MESSAGE, self.process_incoming_slack_message)
|
27 |
-
|
28 |
|
29 |
async def process_incoming_content(self: Self, content: Content) -> None:
|
30 |
logger.debug("Content ingestion service received content with metadata: {}", content.get_metadata())
|
|
|
1 |
from loguru import logger
|
2 |
+
from pydantic import BaseModel
|
3 |
from typing import Self, Sequence
|
4 |
|
5 |
from ctp_slack_bot.core import Settings
|
|
|
19 |
vector_database_service: VectorDatabaseService
|
20 |
vectorization_service: VectorizationService
|
21 |
|
22 |
+
class Config:
|
23 |
+
frozen=True
|
24 |
+
|
25 |
+
def __init__(self: Self, **data) -> None:
|
26 |
+
super().__init__(**data)
|
27 |
self.event_brokerage_service.subscribe(EventType.INCOMING_CONTENT, self.process_incoming_content)
|
28 |
self.event_brokerage_service.subscribe(EventType.INCOMING_SLACK_MESSAGE, self.process_incoming_slack_message)
|
29 |
+
logger.debug("Created {}", self.__class__.__name__)
|
30 |
|
31 |
async def process_incoming_content(self: Self, content: Content) -> None:
|
32 |
logger.debug("Content ingestion service received content with metadata: {}", content.get_metadata())
|
src/ctp_slack_bot/services/context_retrieval_service.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
from loguru import logger
|
2 |
-
from pydantic import BaseModel
|
3 |
from typing import Self, Sequence
|
4 |
|
5 |
from ctp_slack_bot.core.config import Settings
|
@@ -16,11 +16,12 @@ class ContextRetrievalService(BaseModel):
|
|
16 |
vectorization_service: VectorizationService
|
17 |
vector_database_service: VectorDatabaseService
|
18 |
|
19 |
-
|
20 |
-
|
21 |
-
|
|
|
|
|
22 |
logger.debug("Created {}", self.__class__.__name__)
|
23 |
-
return self
|
24 |
|
25 |
async def get_context(self: Self, message: SlackMessage) -> Sequence[Chunk]:
|
26 |
"""
|
|
|
1 |
from loguru import logger
|
2 |
+
from pydantic import BaseModel
|
3 |
from typing import Self, Sequence
|
4 |
|
5 |
from ctp_slack_bot.core.config import Settings
|
|
|
16 |
vectorization_service: VectorizationService
|
17 |
vector_database_service: VectorDatabaseService
|
18 |
|
19 |
+
class Config:
|
20 |
+
frozen=True
|
21 |
+
|
22 |
+
def __init__(self: Self, **data) -> None:
|
23 |
+
super().__init__(**data)
|
24 |
logger.debug("Created {}", self.__class__.__name__)
|
|
|
25 |
|
26 |
async def get_context(self: Self, message: SlackMessage) -> Sequence[Chunk]:
|
27 |
"""
|
src/ctp_slack_bot/services/embeddings_model_service.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
from loguru import logger
|
2 |
from openai import OpenAI
|
3 |
-
from pydantic import BaseModel, PrivateAttr
|
4 |
from typing import Any, Dict, Sequence, Self
|
5 |
|
6 |
from ctp_slack_bot.core import Settings
|
@@ -13,14 +13,13 @@ class EmbeddingsModelService(BaseModel):
|
|
13 |
settings: Settings
|
14 |
_open_ai_client: PrivateAttr = PrivateAttr()
|
15 |
|
|
|
|
|
|
|
16 |
def __init__(self: Self, **data: Dict[str, Any]) -> None:
|
17 |
super().__init__(**data)
|
18 |
self._open_ai_client = OpenAI(api_key=self.settings.OPENAI_API_KEY.get_secret_value())
|
19 |
-
|
20 |
-
@model_validator(mode='after')
|
21 |
-
def post_init(self: Self) -> Self:
|
22 |
logger.debug("Created {}", self.__class__.__name__)
|
23 |
-
return self
|
24 |
|
25 |
def get_embeddings(self: Self, texts: Sequence[str]) -> Sequence[Sequence[float]]:
|
26 |
"""Get embeddings for a collection of texts using OpenAI’s API.
|
|
|
1 |
from loguru import logger
|
2 |
from openai import OpenAI
|
3 |
+
from pydantic import BaseModel, PrivateAttr
|
4 |
from typing import Any, Dict, Sequence, Self
|
5 |
|
6 |
from ctp_slack_bot.core import Settings
|
|
|
13 |
settings: Settings
|
14 |
_open_ai_client: PrivateAttr = PrivateAttr()
|
15 |
|
16 |
+
class Config:
|
17 |
+
frozen=True
|
18 |
+
|
19 |
def __init__(self: Self, **data: Dict[str, Any]) -> None:
|
20 |
super().__init__(**data)
|
21 |
self._open_ai_client = OpenAI(api_key=self.settings.OPENAI_API_KEY.get_secret_value())
|
|
|
|
|
|
|
22 |
logger.debug("Created {}", self.__class__.__name__)
|
|
|
23 |
|
24 |
def get_embeddings(self: Self, texts: Sequence[str]) -> Sequence[Sequence[float]]:
|
25 |
"""Get embeddings for a collection of texts using OpenAI’s API.
|
src/ctp_slack_bot/services/event_brokerage_service.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1 |
from asyncio import create_task, iscoroutinefunction, to_thread
|
2 |
from collections import defaultdict
|
3 |
from loguru import logger
|
4 |
-
from pydantic import BaseModel,
|
5 |
from typing import Any, Callable, Dict, List, Self
|
6 |
|
7 |
from ctp_slack_bot.enums import EventType
|
@@ -13,10 +13,12 @@ class EventBrokerageService(BaseModel):
|
|
13 |
|
14 |
_subscribers: PrivateAttr = PrivateAttr(default_factory=lambda: defaultdict(list))
|
15 |
|
16 |
-
|
17 |
-
|
|
|
|
|
|
|
18 |
logger.debug("Created {}", self.__class__.__name__)
|
19 |
-
return self
|
20 |
|
21 |
def subscribe(self: Self, type: EventType, callback: Callable) -> None:
|
22 |
"""Subscribe to an event type with a callback function."""
|
|
|
1 |
from asyncio import create_task, iscoroutinefunction, to_thread
|
2 |
from collections import defaultdict
|
3 |
from loguru import logger
|
4 |
+
from pydantic import BaseModel, PrivateAttr
|
5 |
from typing import Any, Callable, Dict, List, Self
|
6 |
|
7 |
from ctp_slack_bot.enums import EventType
|
|
|
13 |
|
14 |
_subscribers: PrivateAttr = PrivateAttr(default_factory=lambda: defaultdict(list))
|
15 |
|
16 |
+
class Config:
|
17 |
+
frozen=True
|
18 |
+
|
19 |
+
def __init__(self: Self, **data) -> None:
|
20 |
+
super().__init__(**data)
|
21 |
logger.debug("Created {}", self.__class__.__name__)
|
|
|
22 |
|
23 |
def subscribe(self: Self, type: EventType, callback: Callable) -> None:
|
24 |
"""Subscribe to an event type with a callback function."""
|
src/ctp_slack_bot/services/language_model_service.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1 |
from loguru import logger
|
2 |
from openai import OpenAI
|
3 |
from openai.types.chat import ChatCompletion
|
4 |
-
from pydantic import BaseModel, PrivateAttr
|
5 |
from typing import Collection, Self
|
6 |
|
7 |
from ctp_slack_bot.core import Settings
|
@@ -15,14 +15,13 @@ class LanguageModelService(BaseModel):
|
|
15 |
settings: Settings
|
16 |
_open_ai_client: PrivateAttr = PrivateAttr()
|
17 |
|
|
|
|
|
|
|
18 |
def __init__(self: Self, **data) -> None:
|
19 |
super().__init__(**data)
|
20 |
self._open_ai_client = OpenAI(api_key=self.settings.OPENAI_API_KEY.get_secret_value())
|
21 |
-
|
22 |
-
@model_validator(mode='after')
|
23 |
-
def post_init(self: Self) -> Self:
|
24 |
logger.debug("Created {}", self.__class__.__name__)
|
25 |
-
return self
|
26 |
|
27 |
def answer_question(self, question: str, context: Collection[Chunk]) -> str:
|
28 |
"""Generate a response using OpenAI’s API with retrieved context.
|
@@ -35,21 +34,22 @@ class LanguageModelService(BaseModel):
|
|
35 |
str: Generated answer
|
36 |
"""
|
37 |
logger.debug("Generating response for question “{}” using {} context chunks…", question, len(context))
|
38 |
-
messages = [
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
]
|
48 |
-
response: ChatCompletion = self._open_ai_client.chat.completions.create(
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
)
|
54 |
|
55 |
-
return response.choices[0].message.content
|
|
|
|
1 |
from loguru import logger
|
2 |
from openai import OpenAI
|
3 |
from openai.types.chat import ChatCompletion
|
4 |
+
from pydantic import BaseModel, PrivateAttr
|
5 |
from typing import Collection, Self
|
6 |
|
7 |
from ctp_slack_bot.core import Settings
|
|
|
15 |
settings: Settings
|
16 |
_open_ai_client: PrivateAttr = PrivateAttr()
|
17 |
|
18 |
+
class Config:
|
19 |
+
frozen=True
|
20 |
+
|
21 |
def __init__(self: Self, **data) -> None:
|
22 |
super().__init__(**data)
|
23 |
self._open_ai_client = OpenAI(api_key=self.settings.OPENAI_API_KEY.get_secret_value())
|
|
|
|
|
|
|
24 |
logger.debug("Created {}", self.__class__.__name__)
|
|
|
25 |
|
26 |
def answer_question(self, question: str, context: Collection[Chunk]) -> str:
|
27 |
"""Generate a response using OpenAI’s API with retrieved context.
|
|
|
34 |
str: Generated answer
|
35 |
"""
|
36 |
logger.debug("Generating response for question “{}” using {} context chunks…", question, len(context))
|
37 |
+
# messages = [
|
38 |
+
# {"role": "system", "content": self.settings.SYSTEM_PROMPT},
|
39 |
+
# {"role": "user", "content":
|
40 |
+
# f"""Student Question: {question}
|
41 |
+
|
42 |
+
# Context from class materials and transcripts:
|
43 |
+
# {'\n'.join(chunk.text for chunk in context)}
|
44 |
+
|
45 |
+
# Please answer the Student Question based on the Context from class materials and transcripts. If the context doesn’t contain relevant information, acknowledge that and suggest asking the professor."""}
|
46 |
+
# ]
|
47 |
+
# response: ChatCompletion = self._open_ai_client.chat.completions.create(
|
48 |
+
# model=self.settings.CHAT_MODEL,
|
49 |
+
# messages=messages,
|
50 |
+
# max_tokens=self.settings.MAX_TOKENS,
|
51 |
+
# temperature=self.settings.TEMPERATURE
|
52 |
+
# )
|
53 |
|
54 |
+
# return response.choices[0].message.content
|
55 |
+
return f"Mock response to “{question}”"
|
src/ctp_slack_bot/services/question_dispatch_service.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
# from asyncio import create_task
|
2 |
from loguru import logger
|
3 |
-
from pydantic import BaseModel
|
4 |
from typing import Self
|
5 |
|
6 |
from ctp_slack_bot.core import Settings
|
@@ -21,11 +21,13 @@ class QuestionDispatchService(BaseModel):
|
|
21 |
context_retrieval_service: ContextRetrievalService
|
22 |
answer_retrieval_service: AnswerRetrievalService
|
23 |
|
24 |
-
|
25 |
-
|
26 |
-
|
|
|
|
|
27 |
self.event_brokerage_service.subscribe(EventType.INCOMING_SLACK_MESSAGE, self.__process_incoming_slack_message)
|
28 |
-
|
29 |
|
30 |
async def __process_incoming_slack_message(self: Self, message: SlackMessage) -> None:
|
31 |
if message.subtype != 'bot_message':
|
|
|
1 |
# from asyncio import create_task
|
2 |
from loguru import logger
|
3 |
+
from pydantic import BaseModel
|
4 |
from typing import Self
|
5 |
|
6 |
from ctp_slack_bot.core import Settings
|
|
|
21 |
context_retrieval_service: ContextRetrievalService
|
22 |
answer_retrieval_service: AnswerRetrievalService
|
23 |
|
24 |
+
class Config:
|
25 |
+
frozen=True
|
26 |
+
|
27 |
+
def __init__(self: Self, **data) -> None:
|
28 |
+
super().__init__(**data)
|
29 |
self.event_brokerage_service.subscribe(EventType.INCOMING_SLACK_MESSAGE, self.__process_incoming_slack_message)
|
30 |
+
logger.debug("Created {}", self.__class__.__name__)
|
31 |
|
32 |
async def __process_incoming_slack_message(self: Self, message: SlackMessage) -> None:
|
33 |
if message.subtype != 'bot_message':
|
src/ctp_slack_bot/services/schedule_service.py
ADDED
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
2 |
+
from apscheduler.triggers.cron import CronTrigger
|
3 |
+
from asyncio import create_task, iscoroutinefunction, to_thread
|
4 |
+
from datetime import datetime
|
5 |
+
from loguru import logger
|
6 |
+
from pydantic import BaseModel, PrivateAttr
|
7 |
+
from pytz import timezone
|
8 |
+
from typing import Optional, Self
|
9 |
+
|
10 |
+
from ctp_slack_bot.core import Settings
|
11 |
+
|
12 |
+
class ScheduleService(BaseModel):
|
13 |
+
"""
|
14 |
+
Service for running scheduled tasks.
|
15 |
+
"""
|
16 |
+
|
17 |
+
settings: Settings
|
18 |
+
_scheduler: PrivateAttr
|
19 |
+
|
20 |
+
class Config:
|
21 |
+
frozen=True
|
22 |
+
|
23 |
+
def __init__(self: Self, **data) -> None:
|
24 |
+
super().__init__(**data)
|
25 |
+
zone = self.settings.SCHEDULER_TIMEZONE
|
26 |
+
self._configure_jobs()
|
27 |
+
self._scheduler = AsyncIOScheduler(timezone=timezone(zone))
|
28 |
+
logger.debug("Created {}", self.__class__.__name__)
|
29 |
+
|
30 |
+
def _configure_jobs(self: Self) -> None:
|
31 |
+
# Example jobs (uncomment and implement as needed)
|
32 |
+
# self._scheduler.add_job(
|
33 |
+
# send_error_report,
|
34 |
+
# CronTrigger(hour=7, minute=0),
|
35 |
+
# id="daily_error_report",
|
36 |
+
# name="Daily Error Report",
|
37 |
+
# replace_existing=True,
|
38 |
+
# )
|
39 |
+
# self._scheduler.add_job(
|
40 |
+
# cleanup_old_transcripts,
|
41 |
+
# CronTrigger(day_of_week="sun", hour=1, minute=0),
|
42 |
+
# id="weekly_transcript_cleanup",
|
43 |
+
# name="Weekly Transcript Cleanup",
|
44 |
+
# replace_existing=True,
|
45 |
+
# )
|
46 |
+
pass
|
47 |
+
|
48 |
+
def start(self: Self) -> None:
|
49 |
+
self._scheduler.start()
|
50 |
+
logger.info("Started scheduler.")
|
51 |
+
|
52 |
+
def stop(self: Self) -> None:
|
53 |
+
if self._scheduler.running:
|
54 |
+
self._scheduler.shutdown(wait=False)
|
55 |
+
logger.info("Shut down scheduler.")
|
56 |
+
else:
|
57 |
+
logger.debug("The scheduler is not running. There is no scheduler to shut down.")
|
src/ctp_slack_bot/services/slack_service.py
CHANGED
@@ -1,8 +1,9 @@
|
|
|
|
1 |
from loguru import logger
|
2 |
from openai import OpenAI
|
3 |
-
from pydantic import BaseModel
|
4 |
from slack_bolt.async_app import AsyncApp
|
5 |
-
from typing import Any,
|
6 |
|
7 |
from ctp_slack_bot.enums import EventType
|
8 |
from ctp_slack_bot.models import SlackMessage, SlackResponse
|
@@ -19,14 +20,14 @@ class SlackService(BaseModel):
|
|
19 |
|
20 |
class Config:
|
21 |
arbitrary_types_allowed = True
|
|
|
22 |
|
23 |
-
|
24 |
-
|
25 |
self.event_brokerage_service.subscribe(EventType.OUTGOING_SLACK_RESPONSE, self.send_message)
|
26 |
logger.debug("Created {}", self.__class__.__name__)
|
27 |
-
return self
|
28 |
|
29 |
-
def adapt_event_payload(self: Self, event:
|
30 |
return SlackMessage(
|
31 |
type=event.get("type"),
|
32 |
subtype=event.get("subtype"),
|
@@ -40,10 +41,29 @@ class SlackService(BaseModel):
|
|
40 |
event_ts=event.get("event_ts")
|
41 |
)
|
42 |
|
43 |
-
async def process_message(self: Self, event:
|
44 |
slack_message = self.adapt_event_payload(event.get("event", {}))
|
45 |
logger.debug("Received message from Slack: {}", slack_message)
|
46 |
await self.event_brokerage_service.publish(EventType.INCOMING_SLACK_MESSAGE, slack_message)
|
47 |
|
48 |
async def send_message(self: Self, message: SlackResponse) -> None:
|
49 |
await self.slack_bolt_app.client.chat_postMessage(channel=message.channel, text=message.text, thread_ts=message.thread_ts)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from dependency_injector.resources import Resource
|
2 |
from loguru import logger
|
3 |
from openai import OpenAI
|
4 |
+
from pydantic import BaseModel
|
5 |
from slack_bolt.async_app import AsyncApp
|
6 |
+
from typing import Any, Mapping, Self
|
7 |
|
8 |
from ctp_slack_bot.enums import EventType
|
9 |
from ctp_slack_bot.models import SlackMessage, SlackResponse
|
|
|
20 |
|
21 |
class Config:
|
22 |
arbitrary_types_allowed = True
|
23 |
+
frozen=True
|
24 |
|
25 |
+
def __init__(self: Self, **data) -> None:
|
26 |
+
super().__init__(**data)
|
27 |
self.event_brokerage_service.subscribe(EventType.OUTGOING_SLACK_RESPONSE, self.send_message)
|
28 |
logger.debug("Created {}", self.__class__.__name__)
|
|
|
29 |
|
30 |
+
def adapt_event_payload(self: Self, event: Mapping[str, Any]) -> SlackMessage:
|
31 |
return SlackMessage(
|
32 |
type=event.get("type"),
|
33 |
subtype=event.get("subtype"),
|
|
|
41 |
event_ts=event.get("event_ts")
|
42 |
)
|
43 |
|
44 |
+
async def process_message(self: Self, event: Mapping[str, Any]) -> None:
|
45 |
slack_message = self.adapt_event_payload(event.get("event", {}))
|
46 |
logger.debug("Received message from Slack: {}", slack_message)
|
47 |
await self.event_brokerage_service.publish(EventType.INCOMING_SLACK_MESSAGE, slack_message)
|
48 |
|
49 |
async def send_message(self: Self, message: SlackResponse) -> None:
|
50 |
await self.slack_bolt_app.client.chat_postMessage(channel=message.channel, text=message.text, thread_ts=message.thread_ts)
|
51 |
+
|
52 |
+
async def handle_message_event(self: Self, body: Mapping[str, Any]) -> None:
|
53 |
+
logger.debug("Ignored regular message: {}", body.get("event", {}).get("text"))
|
54 |
+
# await self.process_message(body)
|
55 |
+
|
56 |
+
async def handle_app_mention_event(self: Self, body: Mapping[str, Any]) -> None:
|
57 |
+
logger.debug("Received app mention for processing: {}", body.get("event", {}).get("text"))
|
58 |
+
await self.process_message(body)
|
59 |
+
|
60 |
+
def register(self: Self) -> None:
|
61 |
+
self.slack_bolt_app.event("message")(self.handle_message_event)
|
62 |
+
self.slack_bolt_app.event("app_mention")(self.handle_app_mention_event)
|
63 |
+
logger.debug("Registered 2 handlers for Slack Bolt message and app mention events.")
|
64 |
+
|
65 |
+
class SlackServiceResource(Resource):
|
66 |
+
def init(self: Self, event_brokerage_service: EventBrokerageService, slack_bolt_app: AsyncApp) -> SlackService:
|
67 |
+
slack_service = SlackService(event_brokerage_service=event_brokerage_service, slack_bolt_app=slack_bolt_app)
|
68 |
+
slack_service.register()
|
69 |
+
return slack_service
|
src/ctp_slack_bot/services/vector_database_service.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
from loguru import logger
|
2 |
-
from pydantic import BaseModel
|
3 |
from typing import Any, Collection, Dict, List, Optional, Self, Sequence
|
4 |
|
5 |
from ctp_slack_bot.core import Settings
|
@@ -13,12 +13,13 @@ class VectorDatabaseService(BaseModel): # TODO: this should not rely specificall
|
|
13 |
settings: Settings
|
14 |
mongo_db: MongoDB
|
15 |
|
16 |
-
|
17 |
-
|
|
|
|
|
|
|
18 |
logger.debug("Created {}", self.__class__.__name__)
|
19 |
-
|
20 |
-
|
21 |
-
# TODO: Weight cost of going all async.
|
22 |
async def store(self: Self, chunks: Collection[VectorizedChunk]) -> None:
|
23 |
"""
|
24 |
Stores vectorized chunks and their embedding vectors in the database.
|
|
|
1 |
from loguru import logger
|
2 |
+
from pydantic import BaseModel
|
3 |
from typing import Any, Collection, Dict, List, Optional, Self, Sequence
|
4 |
|
5 |
from ctp_slack_bot.core import Settings
|
|
|
13 |
settings: Settings
|
14 |
mongo_db: MongoDB
|
15 |
|
16 |
+
class Config:
|
17 |
+
frozen=True
|
18 |
+
|
19 |
+
def __init__(self: Self, **data) -> None:
|
20 |
+
super().__init__(**data)
|
21 |
logger.debug("Created {}", self.__class__.__name__)
|
22 |
+
|
|
|
|
|
23 |
async def store(self: Self, chunks: Collection[VectorizedChunk]) -> None:
|
24 |
"""
|
25 |
Stores vectorized chunks and their embedding vectors in the database.
|
src/ctp_slack_bot/services/vectorization_service.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
from loguru import logger
|
2 |
-
from pydantic import BaseModel
|
3 |
from typing import Self, Sequence
|
4 |
|
5 |
from ctp_slack_bot.core import Settings
|
@@ -14,10 +14,12 @@ class VectorizationService(BaseModel):
|
|
14 |
settings: Settings
|
15 |
embeddings_model_service: EmbeddingsModelService
|
16 |
|
17 |
-
|
18 |
-
|
|
|
|
|
|
|
19 |
logger.debug("Created {}", self.__class__.__name__)
|
20 |
-
return self
|
21 |
|
22 |
def vectorize(self: Self, chunks: Sequence[Chunk]) -> Sequence[VectorizedChunk]:
|
23 |
embeddings = self.embeddings_model_service.get_embeddings([chunk.text for chunk in chunks])
|
|
|
1 |
from loguru import logger
|
2 |
+
from pydantic import BaseModel
|
3 |
from typing import Self, Sequence
|
4 |
|
5 |
from ctp_slack_bot.core import Settings
|
|
|
14 |
settings: Settings
|
15 |
embeddings_model_service: EmbeddingsModelService
|
16 |
|
17 |
+
class Config:
|
18 |
+
frozen=True
|
19 |
+
|
20 |
+
def __init__(self: Self, **data) -> None:
|
21 |
+
super().__init__(**data)
|
22 |
logger.debug("Created {}", self.__class__.__name__)
|
|
|
23 |
|
24 |
def vectorize(self: Self, chunks: Sequence[Chunk]) -> Sequence[VectorizedChunk]:
|
25 |
embeddings = self.embeddings_model_service.get_embeddings([chunk.text for chunk in chunks])
|
src/ctp_slack_bot/tasks/__init__.py
CHANGED
@@ -1 +0,0 @@
|
|
1 |
-
from ctp_slack_bot.tasks.scheduler import start_scheduler, stop_scheduler
|
|
|
|
src/ctp_slack_bot/tasks/scheduler.py
DELETED
@@ -1,60 +0,0 @@
|
|
1 |
-
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
2 |
-
from apscheduler.triggers.cron import CronTrigger
|
3 |
-
from datetime import datetime
|
4 |
-
from dependency_injector.wiring import inject, Provide
|
5 |
-
from loguru import logger
|
6 |
-
from pytz import timezone
|
7 |
-
from typing import Optional
|
8 |
-
|
9 |
-
from ctp_slack_bot.containers import Container
|
10 |
-
|
11 |
-
@inject
|
12 |
-
def start_scheduler(container: Container) -> AsyncIOScheduler:
|
13 |
-
"""
|
14 |
-
Start and configure the APScheduler instance.
|
15 |
-
|
16 |
-
Returns:
|
17 |
-
AsyncIOScheduler: Configured scheduler instance
|
18 |
-
"""
|
19 |
-
settings = container.settings() if container else Provide[Container.settings]
|
20 |
-
zone = settings.SCHEDULER_TIMEZONE
|
21 |
-
scheduler = AsyncIOScheduler(timezone=timezone(zone))
|
22 |
-
|
23 |
-
# Add jobs to the scheduler.
|
24 |
-
# scheduler.add_job(
|
25 |
-
# send_error_report,
|
26 |
-
# CronTrigger(hour=7, minute=0),
|
27 |
-
# id="daily_error_report",
|
28 |
-
# name="Daily Error Report",
|
29 |
-
# replace_existing=True,
|
30 |
-
# )
|
31 |
-
# scheduler.add_job(
|
32 |
-
# cleanup_old_transcripts,
|
33 |
-
# CronTrigger(day_of_week="sun", hour=1, minute=0),
|
34 |
-
# id="weekly_transcript_cleanup",
|
35 |
-
# name="Weekly Transcript Cleanup",
|
36 |
-
# replace_existing=True,
|
37 |
-
# )
|
38 |
-
|
39 |
-
# Start the scheduler.
|
40 |
-
scheduler.start()
|
41 |
-
logger.info("Scheduler started with timezone: {}", settings.SCHEDULER_TIMEZONE)
|
42 |
-
# logger.info("Next run for error report: {}",
|
43 |
-
# scheduler.get_job("daily_error_report").next_run_time)
|
44 |
-
# logger.info("Next run for transcript cleanup: {}",
|
45 |
-
# scheduler.get_job("weekly_transcript_cleanup").next_run_time)
|
46 |
-
|
47 |
-
return scheduler
|
48 |
-
|
49 |
-
|
50 |
-
def stop_scheduler(scheduler: AsyncIOScheduler) -> None:
|
51 |
-
"""
|
52 |
-
Shutdown the scheduler gracefully.
|
53 |
-
|
54 |
-
Args:
|
55 |
-
scheduler: The scheduler instance to shut down
|
56 |
-
"""
|
57 |
-
if scheduler.running:
|
58 |
-
logger.info("Shutting down scheduler")
|
59 |
-
scheduler.shutdown(wait=False)
|
60 |
-
logger.info("Scheduler shutdown complete")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|