LiKenun commited on
Commit
c21d29c
·
1 Parent(s): 6f7eb7e

Move more components into dependency injection container

Browse files
.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", "src.ctp_slack_bot.api.main"]
 
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
- # "fastapi>=0.115.12",
27
- # "uvicorn>=0.34.0",
28
- "loguru>=0.7.3",
29
  "python-dotenv>=1.1.0",
30
- "httpx>=0.28.1",
31
- "tenacity>=9.1.2",
32
- "pybreaker>=1.3.0",
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
- "pymongo>=4.11.3 ",
39
- "webvtt-py>=0.5.1",
40
- "openai>=1.70.0",
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/api/main.py"
 
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 Factory, List, Singleton
 
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 MongoDB
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.slack_service import SlackService
 
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
- slack_bolt_app = Factory(AsyncApp, token=settings.provided.SLACK_BOT_TOKEN().get_secret_value())
24
- mongo_db = Singleton(MongoDB, settings=settings) # TODO: generalize to any database.
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
- slack_service = Singleton(SlackService, event_brokerage_service=event_brokerage_service, slack_bolt_app=slack_bolt_app)
38
- primordial_services = List(settings, event_brokerage_service, slack_bolt_app, slack_service, vectorized_chunk_repository, question_dispatch_service, content_ingestion_service)
 
 
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 typing import Literal, Optional
 
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(container: "Container") -> None: # TODO: Perhaps get rid of the container dependence since we only need two settings.
36
  """
37
  Configure logging with Loguru.
38
 
39
- This function sets up Loguru as the main logging provider,
40
- configures the log format based on settings, and intercepts
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
- # Remove default loguru handler
 
 
 
 
47
  logger.remove()
48
 
49
- # Determine log format
50
- if settings.LOG_FORMAT == "json":
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=settings.LOG_LEVEL,
73
- serialize=(settings.LOG_FORMAT == "json"),
74
  backtrace=True,
75
  diagnose=True,
76
  )
77
 
78
- # Add file handler for non-DEBUG environments
79
- if settings.LOG_LEVEL != "DEBUG":
80
  logger.add(
81
  "logs/app.log",
82
  rotation="10 MB",
83
  retention="1 week",
84
  compression="zip",
85
  format=format_string,
86
- level=settings.LOG_LEVEL,
87
- serialize=(settings.LOG_FORMAT == "json"),
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 {settings.LOG_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, model_validator, PrivateAttr
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
- @model_validator(mode='after')
22
- def post_init(self: Self) -> Self:
23
- """Initialize MongoDB connection after model creation."""
24
- self._initialize_client()
25
  logger.debug("Created {}", self.__class__.__name__)
26
- return self
27
 
28
- def _initialize_client(self: Self) -> None:
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._initialize_client()
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._initialize_client()
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, model_validator
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
- @model_validator(mode='after')
22
- def post_init(self: Self) -> Self:
 
 
 
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, model_validator
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
- @model_validator(mode='after')
23
- def post_init(self: Self) -> Self:
24
- logger.debug("Created {}", self.__class__.__name__)
 
 
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
- return self
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, model_validator
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
- # Should not allow initialization calls to bubble up all the way to the surface ― sequester in `post_init` or the class on which it depends.
20
- @model_validator(mode='after')
21
- def post_init(self: Self) -> Self:
 
 
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, model_validator
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, model_validator, PrivateAttr
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
- @model_validator(mode='after')
17
- def post_init(self: Self) -> Self:
 
 
 
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, model_validator
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
- {"role": "system", "content": self.settings.SYSTEM_PROMPT},
40
- {"role": "user", "content":
41
- f"""Student Question: {question}
42
-
43
- Context from class materials and transcripts:
44
- {'\n'.join(chunk.text for chunk in context)}
45
-
46
- 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."""}
47
- ]
48
- response: ChatCompletion = self._open_ai_client.chat.completions.create(
49
- model=self.settings.CHAT_MODEL,
50
- messages=messages,
51
- max_tokens=self.settings.MAX_TOKENS,
52
- temperature=self.settings.TEMPERATURE
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, model_validator
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
- @model_validator(mode='after')
25
- def post_init(self: Self) -> Self:
26
- logger.debug("Created {}", self.__class__.__name__)
 
 
27
  self.event_brokerage_service.subscribe(EventType.INCOMING_SLACK_MESSAGE, self.__process_incoming_slack_message)
28
- return self
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, model_validator
4
  from slack_bolt.async_app import AsyncApp
5
- from typing import Any, Dict, Self
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
- @model_validator(mode='after')
24
- def post_init(self: Self) -> Self:
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: Dict[str, Any]) -> SlackMessage:
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: Dict[str, Any]) -> None:
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, model_validator
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
- @model_validator(mode='after')
17
- def post_init(self: Self) -> Self:
 
 
 
18
  logger.debug("Created {}", self.__class__.__name__)
19
- return self
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, model_validator
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
- @model_validator(mode='after')
18
- def post_init(self: Self) -> Self:
 
 
 
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")