Kevin Li commited on
Commit
551ef8f
·
unverified ·
2 Parent(s): 306043c 64566ca

Merge pull request #1 from CUNYTechPrep/origin/alt/LanguageModelServices

Browse files
.dockerignore CHANGED
@@ -59,11 +59,11 @@ venv.bak/
59
  # PyCharm
60
  .idea/
61
 
62
- # Jupyter notebooks
63
- notebooks/
64
-
65
  # Documentation
66
  docs/
67
 
68
  # MacOS
69
  .DS_Store
 
 
 
 
59
  # PyCharm
60
  .idea/
61
 
 
 
 
62
  # Documentation
63
  docs/
64
 
65
  # MacOS
66
  .DS_Store
67
+
68
+ # Application logs
69
+ /logs
.env.template CHANGED
@@ -1,25 +1,41 @@
1
  # Copy this file and modify. Do not save or commit the secrets!
2
 
 
 
 
 
 
 
 
 
 
 
3
  # API Configuration
4
  API_HOST=0.0.0.0
5
  API_PORT=8000
6
- DEBUG=false
7
-
8
- # MongoDB Configuration
9
- MONGODB_URI=mongodb+srv://username:[email protected]/database?retryWrites=true&w=majority
10
- MONGODB_DB_NAME=ctp_slack_bot
11
 
12
  # Slack Configuration
13
  SLACK_BOT_TOKEN=🪙
14
  SLACK_SIGNING_SECRET=🔏
15
  SLACK_APP_TOKEN=🦥
16
 
 
 
 
 
 
 
 
 
 
 
 
17
  # Hugging Face Configuration
18
  HF_API_TOKEN=🤗
19
 
20
- # Logging Configuration
21
- LOG_LEVEL=INFO
22
- LOG_FORMAT=json
23
-
24
- # APScheduler Configuration
25
- SCHEDULER_TIMEZONE=UTC
 
1
  # Copy this file and modify. Do not save or commit the secrets!
2
 
3
+ # Application Configuration
4
+ DEBUG=false
5
+
6
+ # Logging Configuration
7
+ LOG_LEVEL=INFO
8
+ LOG_FORMAT=text
9
+
10
+ # APScheduler Configuration
11
+ SCHEDULER_TIMEZONE=UTC
12
+
13
  # API Configuration
14
  API_HOST=0.0.0.0
15
  API_PORT=8000
 
 
 
 
 
16
 
17
  # Slack Configuration
18
  SLACK_BOT_TOKEN=🪙
19
  SLACK_SIGNING_SECRET=🔏
20
  SLACK_APP_TOKEN=🦥
21
 
22
+ # Vectorization Configuration
23
+ EMBEDDING_MODEL=🌮
24
+ VECTOR_DIMENSION=9001
25
+ CHUNK_SIZE=42
26
+ CHUNK_OVERLAP=37
27
+ TOP_K_MATCHES=1
28
+
29
+ # MongoDB Configuration
30
+ MONGODB_URI=mongodb+srv://username:[email protected]/database?retryWrites=true&w=majority
31
+ MONGODB_NAME=ctp_slack_bot
32
+
33
  # Hugging Face Configuration
34
  HF_API_TOKEN=🤗
35
 
36
+ # OpenAI Configuration
37
+ OPENAI_API_KEY=😐
38
+ CHAT_MODEL=🙊
39
+ MAX_TOKENS=42
40
+ TEMPERATURE=0.5
41
+ SYSTEM_PROMPT="You are a helpful teaching assistant for a data science class.\nBased on the students question, you will be given context retreived from class transcripts and materials to answer their question.\nYour responses should be:\n\n1. Accurate and based on the class content\n2. Clear and educational\n3. Concise but complete\nIf you're unsure about something, acknowledge it and suggest asking the professor."
.gitignore CHANGED
@@ -91,8 +91,8 @@ dmypy.json
91
  # PyCharm
92
  .idea/
93
 
94
- # Jupyter notebooks
95
- notebooks/
96
-
97
  # MacOS
98
  .DS_Store
 
 
 
 
91
  # PyCharm
92
  .idea/
93
 
 
 
 
94
  # MacOS
95
  .DS_Store
96
+
97
+ # Application logs
98
+ /logs
README.MD CHANGED
@@ -14,6 +14,7 @@
14
  * `src/`
15
  * `ctp_slack_bot/`
16
  * `api/`: FastAPI application structure
 
17
  * `core/`: fundamental components like configuration (using pydantic), logging setup (loguru), and custom exceptions
18
  * `db/`: database connection
19
  * `repositories/`: repository pattern implementation
@@ -23,7 +24,9 @@
23
  * `utils/`: reusable utilities
24
  * `tests/`: unit tests
25
  * `scripts/`: utility scripts for development, deployment, etc.
 
26
  * `notebooks/`: Jupyter notebooks for exploration and model development
 
27
 
28
  ## How to Run the Application
29
 
@@ -41,6 +44,8 @@ First, make sure you are set up with a Python virtual environment created by the
41
  pip3 install -e .
42
  ```
43
 
 
 
44
  If `localhost` port `8000` is free, running the following will make the application available on that port:
45
 
46
  ```sh
@@ -54,4 +59,6 @@ $ curl http://localhost:8000/health
54
  {"status":"healthy"}
55
  ```
56
 
 
 
57
  Uvicorn will restart the application automatically when any source files are changed.
 
14
  * `src/`
15
  * `ctp_slack_bot/`
16
  * `api/`: FastAPI application structure
17
+ * `routes.py`: API endpoint definitions
18
  * `core/`: fundamental components like configuration (using pydantic), logging setup (loguru), and custom exceptions
19
  * `db/`: database connection
20
  * `repositories/`: repository pattern implementation
 
24
  * `utils/`: reusable utilities
25
  * `tests/`: unit tests
26
  * `scripts/`: utility scripts for development, deployment, etc.
27
+ * `run-dev.sh`: script to run the application locally
28
  * `notebooks/`: Jupyter notebooks for exploration and model development
29
+ * `.env`: local environment variables for development purposes
30
 
31
  ## How to Run the Application
32
 
 
44
  pip3 install -e .
45
  ```
46
 
47
+ Make a copy of `.env.template` as `.env` and define the environment variables. (You can also define them by other means, but this has the least friction.) This file should not be committed and is excluded by `.gitignore`!
48
+
49
  If `localhost` port `8000` is free, running the following will make the application available on that port:
50
 
51
  ```sh
 
59
  {"status":"healthy"}
60
  ```
61
 
62
+ In debug mode (`DEBUG=true`), [http://localhost:8000/env](http://localhost:8000/env) will pretty-print the non-sensitive environment variables as JSON.
63
+
64
  Uvicorn will restart the application automatically when any source files are changed.
pyproject.toml CHANGED
@@ -43,6 +43,7 @@ dev = [
43
  "pytest>=7.3.1",
44
  "pytest-cov>=4.1.0",
45
  "mypy>=1.3.0",
 
46
  "black>=23.3.0",
47
  "isort>=5.12.0",
48
  "ruff>=0.0.270",
 
43
  "pytest>=7.3.1",
44
  "pytest-cov>=4.1.0",
45
  "mypy>=1.3.0",
46
+ "types-pytz>=2025.2",
47
  "black>=23.3.0",
48
  "isort>=5.12.0",
49
  "ruff>=0.0.270",
src/ctp_slack_bot/api/main.py CHANGED
@@ -1,23 +1,23 @@
1
- import logging
2
  from contextlib import asynccontextmanager
3
-
4
- from fastapi import FastAPI
5
  from loguru import logger
 
6
 
7
  from ctp_slack_bot.api.routes import router
8
- from ctp_slack_bot.core.config import settings
9
  from ctp_slack_bot.core.logging import setup_logging
 
10
  from ctp_slack_bot.tasks.scheduler import start_scheduler, stop_scheduler
11
 
12
 
13
  @asynccontextmanager
14
- async def lifespan(app: FastAPI):
15
  """
16
  Lifespan context manager for FastAPI application.
17
  Handles startup and shutdown events.
18
  """
19
  # Setup logging
20
- #setup_logging()
21
  logger.info("Starting application")
22
 
23
  # Start scheduler
@@ -42,11 +42,19 @@ app = FastAPI(
42
  # Include routers
43
  app.include_router(router)
44
 
45
-
46
  @app.get("/health")
47
- async def health_check():
48
- """Health check endpoint"""
49
- return {"status": "healthy"}
 
 
 
 
 
 
 
 
 
50
 
51
 
52
  if __name__ == "__main__":
@@ -54,7 +62,7 @@ if __name__ == "__main__":
54
 
55
  uvicorn.run(
56
  "main:app",
57
- host="localhost", #settings.API_HOST,
58
- port=8000, #settings.API_PORT,
59
- reload=True #settings.DEBUG,
60
  )
 
 
1
  from contextlib import asynccontextmanager
2
+ from fastapi import FastAPI, HTTPException
 
3
  from loguru import logger
4
+ from typing import AsyncGenerator, Never
5
 
6
  from ctp_slack_bot.api.routes import router
7
+ from ctp_slack_bot.core.config import Settings, settings
8
  from ctp_slack_bot.core.logging import setup_logging
9
+ from ctp_slack_bot.core.response_rendering import PrettyJSONResponse
10
  from ctp_slack_bot.tasks.scheduler import start_scheduler, stop_scheduler
11
 
12
 
13
  @asynccontextmanager
14
+ async def lifespan(app: FastAPI) -> AsyncGenerator:
15
  """
16
  Lifespan context manager for FastAPI application.
17
  Handles startup and shutdown events.
18
  """
19
  # Setup logging
20
+ setup_logging()
21
  logger.info("Starting application")
22
 
23
  # Start scheduler
 
42
  # Include routers
43
  app.include_router(router)
44
 
 
45
  @app.get("/health")
46
+ async def health() -> dict[str, str]:
47
+ """Health check"""
48
+ return {
49
+ "status": "healthy"
50
+ }
51
+
52
+ @app.get("/env", response_class=PrettyJSONResponse)
53
+ async def env() -> Settings:
54
+ """Server-internal environment variables"""
55
+ if not settings.DEBUG:
56
+ raise HTTPException(status_code=404)
57
+ return settings
58
 
59
 
60
  if __name__ == "__main__":
 
62
 
63
  uvicorn.run(
64
  "main:app",
65
+ host=settings.API_HOST,
66
+ port=settings.API_PORT,
67
+ reload=settings.DEBUG
68
  )
src/ctp_slack_bot/core/config.py CHANGED
@@ -1,57 +1,54 @@
1
  from functools import lru_cache
2
  from typing import Literal, Optional
3
 
4
- from pydantic import Field, SecretStr, validator
5
  from pydantic_settings import BaseSettings, SettingsConfigDict
6
 
7
 
8
- class Settings(BaseSettings):
9
  """
10
  Application settings loaded from environment variables.
11
  """
12
- # API Configuration
13
- API_HOST: str = "0.0.0.0"
14
- API_PORT: int = 8000
15
  DEBUG: bool = False
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  # Vectorization Configuration
18
- EMBEDDING_MODEL: str = "text-embedding-3-small"
19
- VECTOR_DIMENSION: int = 1536
20
- CHUNK_SIZE: int = 1000
21
- CHUNK_OVERLAP: int = 200
22
- TOP_K_MATCHES: int = 5
23
 
24
  # MongoDB Configuration
25
- MONGODB_URI: Optional[SecretStr] = None # TODO: Remove optionality
26
- MONGODB_DB_NAME: str = "ctp_slack_bot"
27
-
28
- # Slack Configuration
29
- SLACK_BOT_TOKEN: Optional[SecretStr] = None # TODO: Remove optionality
30
- SLACK_SIGNING_SECRET: Optional[SecretStr] = None # TODO: Remove optionality
31
- SLACK_APP_TOKEN: Optional[SecretStr] = None
32
-
33
  # Hugging Face Configuration
34
  HF_API_TOKEN: Optional[SecretStr] = None
35
 
36
  # OpenAI Configuration
37
  OPENAI_API_KEY: Optional[SecretStr] = None
 
 
 
 
38
 
39
-
40
-
41
- # Logging Configuration
42
- LOG_LEVEL: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO"
43
- LOG_FORMAT: Literal["text", "json"] = "json"
44
-
45
- # APScheduler Configuration
46
- SCHEDULER_TIMEZONE: str = "UTC"
47
-
48
- @validator("MONGODB_URI")
49
- def validate_mongodb_uri(cls, v):
50
- """Validate MongoDB URI format"""
51
- #if not v.get_secret_value().startswith("mongodb"):
52
- # raise ValueError("MONGODB_URI must be a valid MongoDB connection string")
53
- return v
54
-
55
  model_config = SettingsConfigDict(
56
  env_file=".env",
57
  env_file_encoding="utf-8",
@@ -64,7 +61,7 @@ def get_settings() -> Settings:
64
  """
65
  Get cached settings instance.
66
  """
67
- return Settings()
68
 
69
 
70
  settings = get_settings()
 
1
  from functools import lru_cache
2
  from typing import Literal, Optional
3
 
4
+ from pydantic import Field, MongoDsn, NonNegativeFloat, NonNegativeInt, PositiveInt, SecretStr
5
  from pydantic_settings import BaseSettings, SettingsConfigDict
6
 
7
 
8
+ class Settings(BaseSettings): # TODO: Strong guarantees of validity, because garbage in = garbage out, and settings flow into all the nooks and crannies
9
  """
10
  Application settings loaded from environment variables.
11
  """
12
+ # Application Configuration
 
 
13
  DEBUG: bool = False
14
 
15
+ # Logging Configuration
16
+ LOG_LEVEL: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = Field(default_factory=lambda data: "DEBUG" if data.get("DEBUG", False) else "INFO")
17
+ LOG_FORMAT: Literal["text", "json"] = "json"
18
+
19
+ # APScheduler Configuration
20
+ SCHEDULER_TIMEZONE: str = "UTC"
21
+
22
+ # API Configuration
23
+ API_HOST: str
24
+ API_PORT: PositiveInt
25
+
26
+ # Slack Configuration
27
+ SLACK_BOT_TOKEN: SecretStr
28
+ SLACK_SIGNING_SECRET: SecretStr
29
+ SLACK_APP_TOKEN: SecretStr
30
+
31
  # Vectorization Configuration
32
+ EMBEDDING_MODEL: str
33
+ VECTOR_DIMENSION: PositiveInt
34
+ CHUNK_SIZE: PositiveInt
35
+ CHUNK_OVERLAP: NonNegativeInt
36
+ TOP_K_MATCHES: PositiveInt
37
 
38
  # MongoDB Configuration
39
+ MONGODB_URI: SecretStr # TODO: Contemplate switching to MongoDsn type for the main URL, and separate out the credentials to SecretStr variables.
40
+ MONGODB_NAME: str
41
+
 
 
 
 
 
42
  # Hugging Face Configuration
43
  HF_API_TOKEN: Optional[SecretStr] = None
44
 
45
  # OpenAI Configuration
46
  OPENAI_API_KEY: Optional[SecretStr] = None
47
+ CHAT_MODEL: str
48
+ MAX_TOKENS: PositiveInt
49
+ TEMPERATURE: NonNegativeFloat
50
+ SYSTEM_PROMPT: str
51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  model_config = SettingsConfigDict(
53
  env_file=".env",
54
  env_file_encoding="utf-8",
 
61
  """
62
  Get cached settings instance.
63
  """
64
+ return Settings() # type: ignore
65
 
66
 
67
  settings = get_settings()
src/ctp_slack_bot/core/response_rendering.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from json import dumps
2
+ from starlette.responses import JSONResponse
3
+ from typing import Any, Self
4
+
5
+ class PrettyJSONResponse(JSONResponse):
6
+ def render(self: Self, content: Any) -> bytes:
7
+ return dumps(
8
+ content,
9
+ ensure_ascii=False,
10
+ allow_nan=False,
11
+ indent=4,
12
+ separators=(", ", ": "),
13
+ ).encode("utf-8")
src/ctp_slack_bot/db/MongoDB.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from motor.motor_asyncio import AsyncIOMotorClient
2
+ from pymongo import IndexModel, ASCENDING
3
+ import logging
4
+ from typing import Optional
5
+
6
+ from ctp_slack_bot.core.config import settings
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ class MongoDB:
11
+ """
12
+ MongoDB connection and initialization class.
13
+ Handles connection to MongoDB, database selection, and index creation.
14
+ """
15
+ def __init__(self):
16
+ self.client: Optional[AsyncIOMotorClient] = None
17
+ self.db = None
18
+ self.vector_collection = None
19
+ self.initialized = False
20
+
21
+ async def connect(self):
22
+ """
23
+ Connect to MongoDB using connection string from settings.
24
+ """
25
+ if self.client is not None:
26
+ return
27
+
28
+ if not settings.MONGODB_URI:
29
+ raise ValueError("MONGODB_URI is not set in environment variables")
30
+
31
+ try:
32
+ # Create MongoDB connection
33
+ self.client = AsyncIOMotorClient(settings.MONGODB_URI.get_secret_value())
34
+ self.db = self.client[settings.MONGODB_DB_NAME]
35
+ self.vector_collection = self.db["vector_store"]
36
+ logger.info(f"Connected to MongoDB: {settings.MONGODB_DB_NAME}")
37
+ except Exception as e:
38
+ logger.error(f"Error connecting to MongoDB: {str(e)}")
39
+ raise
40
+
41
+ async def initialize(self):
42
+ """
43
+ Initialize MongoDB with required collections and indexes.
44
+ """
45
+ if self.initialized:
46
+ return
47
+
48
+ if not self.client:
49
+ await self.connect()
50
+
51
+ try:
52
+ # Create vector index for similarity search
53
+ await self.create_vector_index()
54
+ self.initialized = True
55
+ logger.info("MongoDB initialized successfully")
56
+ except Exception as e:
57
+ logger.error(f"Error initializing MongoDB: {str(e)}")
58
+ raise
59
+
60
+ async def create_vector_index(self):
61
+ """
62
+ Create vector index for similarity search using MongoDB Atlas Vector Search.
63
+ """
64
+ try:
65
+ # Check if index already exists
66
+ existing_indexes = await self.vector_collection.list_indexes().to_list(length=None)
67
+ index_names = [index.get('name') for index in existing_indexes]
68
+
69
+ if "vector_index" not in index_names:
70
+ # Create vector search index
71
+ index_definition = {
72
+ "mappings": {
73
+ "dynamic": True,
74
+ "fields": {
75
+ "embedding": {
76
+ "dimensions": settings.VECTOR_DIMENSION,
77
+ "similarity": "cosine",
78
+ "type": "knnVector"
79
+ }
80
+ }
81
+ }
82
+ }
83
+
84
+ # Create the index
85
+ await self.db.command({
86
+ "createIndexes": self.vector_collection.name,
87
+ "indexes": [
88
+ {
89
+ "name": "vector_index",
90
+ "key": {"embedding": "vector"},
91
+ "weights": {"embedding": 1},
92
+ "vectorSearchOptions": index_definition
93
+ }
94
+ ]
95
+ })
96
+
97
+ # Create additional metadata indexes for filtering
98
+ await self.vector_collection.create_index([("metadata.source", ASCENDING)])
99
+ await self.vector_collection.create_index([("metadata.timestamp", ASCENDING)])
100
+
101
+ logger.info("Vector search index created")
102
+ else:
103
+ logger.info("Vector search index already exists")
104
+
105
+ except Exception as e:
106
+ logger.error(f"Error creating vector index: {str(e)}")
107
+ raise
108
+
109
+ async def close(self):
110
+ """
111
+ Close MongoDB connection.
112
+ """
113
+ if self.client:
114
+ self.client.close()
115
+ self.client = None
116
+ self.db = None
117
+ self.vector_collection = None
118
+ self.initialized = False
119
+ logger.info("MongoDB connection closed")
120
+
121
+ # Create a singleton instance
122
+ mongodb = MongoDB()
src/ctp_slack_bot/models/VectorQuery.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field, validator
2
+ from typing import Optional, List, Dict, Any
3
+ from ctp_slack_bot.core.config import settings
4
+
5
+ class VectorQuery(BaseModel):
6
+ """Model for vector database similarity search queries.
7
+
8
+ Attributes:
9
+ query_text: The text to be vectorized and used for similarity search
10
+ k: Number of similar documents to retrieve
11
+ score_threshold: Minimum similarity score threshold for inclusion in results
12
+ filter_metadata: Optional filters for metadata fields
13
+ """
14
+ query_text: str
15
+ k: int = Field(default=settings.TOP_K_MATCHES)
16
+ score_threshold: float = Field(default=0.7)
17
+ filter_metadata: Optional[Dict[str, Any]] = None
src/ctp_slack_bot/models/content.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from typing import Optional, List, Dict, Any
3
+ from ctp_slack_bot.models.slack import SlackMessage
4
+
5
+ class RetreivedContext(BaseModel):
6
+ """Represents a the context of a question from Slack returned from the Vector Store Database.
7
+
8
+ contextual_text: The text that is relevant to the question.
9
+ metadata_source: The source of the contextual text.
10
+ similarity_score: The similarity score of the contextual text to the question.
11
+
12
+ in_reation_to_question: OPTINAL: The question that the contextual text is related to.
13
+ """
14
+ contextual_text: str
15
+ metadata_source: str
16
+ similarity_score: float
17
+
18
+ said_by: str = Optional[None]
19
+ in_reation_to_question: str = Optional[None]
src/ctp_slack_bot/services/AnswerQuestionService.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, validator
2
+ from typing import List, Optional, Tuple
3
+ from ctp_slack_bot.core.config import settings
4
+ import numpy as np
5
+ from openai import OpenAI
6
+ from ctp_slack_bot.models.slack import SlackMessage
7
+ from ctp_slack_bot.models.content import RetreivedContext
8
+
9
+ class GenerateAnswer():
10
+ """
11
+ Service for language model operations.
12
+ """
13
+ def __init__(self):
14
+ self.client = OpenAI(api_key=settings.OPENAI_API_KEY)
15
+
16
+ def generate_answer(self, question: SlackMessage, context: List[RetreivedContext]) -> str:
17
+ """Generate a response using OpenAI's API with retrieved context.
18
+
19
+ Args:
20
+ question (str): The user's question
21
+ context (List[RetreivedContext]): List of RetreivedContext
22
+
23
+ Returns:
24
+ str: Generated answer
25
+ """
26
+ # Prepare context string from retrieved chunks
27
+ context_str = ""
28
+ for c in context:
29
+ context_str += f"{c.contextual_text}\n"
30
+
31
+
32
+ # logger.info(f"Generating response for question: {question}")
33
+ # logger.info(f"Using {len(context)} context chunks")
34
+
35
+ # Create messages for the chat completion
36
+ messages = [
37
+ {"role": "system", "content": settings.SYSTEM_PROMPT},
38
+ {"role": "user", "content":
39
+ f"""Student Auestion: {question.text}
40
+ Context from class materials and transcripts: {context_str}
41
+ Please answer the Student Auestion based on the Context from class materials and transcripts. If the context doesn't contain relevant information, acknowledge that and suggest asking the professor."""}
42
+ ]
43
+
44
+ # Generate response
45
+ response = self.client.chat.completions.create(
46
+ model=settings.CHAT_MODEL,
47
+ messages=messages,
48
+ max_tokens=settings.MAX_TOKENS,
49
+ temperature=settings.TEMPERATURE
50
+ )
51
+
52
+ return response.choices[0].message.content
53
+
54
+
55
+
56
+ ### REMOVE BELOW, PUT SOMEWHERE IN TESTS BUT IDK WHERE YET
57
+ # sm = SlackMessage(text="What is the capital of France?", channel_id="123", user_id="456", timestamp="789")
58
+ # context = [RetreivedContext(contextual_text="The capital of France is Paris", metadata_source="class materials", similarity_score=0.95)]
59
+ # a = GenerateAnswer()
60
+ # a.generate_answer(sm, context)
src/ctp_slack_bot/services/ContextRetrievalService.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from typing import List, Dict, Any, Optional
3
+
4
+ from ctp_slack_bot.models.slack import SlackMessage
5
+ from ctp_slack_bot.models.content import RetreivedContext
6
+ from ctp_slack_bot.models.VectorQuery import VectorQuery
7
+ from ctp_slack_bot.services.VectorizationService import VectorizationService
8
+ from ctp_slack_bot.services.VectorDatabaseService import VectorDatabaseService
9
+ from ctp_slack_bot.core.config import settings
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class ContextRetrievalService:
14
+ """
15
+ Service for retrieving relevant context from the vector database based on user questions.
16
+ """
17
+
18
+ def __init__(self):
19
+ self.vectorization_service = VectorizationService()
20
+ self.vector_db_service = VectorDatabaseService()
21
+
22
+ async def initialize(self):
23
+ """
24
+ Initialize the required services.
25
+ """
26
+ await self.vector_db_service.initialize()
27
+
28
+ async def get_context(self, message: SlackMessage) -> List[RetreivedContext]:
29
+ """
30
+ Retrieve relevant context for a given Slack message.
31
+
32
+ This function:
33
+ 1. Extracts the question text from the message
34
+ 2. Vectorizes the question using VectorizationService
35
+ 3. Queries VectorDatabaseService for similar context
36
+ 4. Returns the relevant context as a list of RetreivedContext objects
37
+
38
+ Args:
39
+ message: The SlackMessage containing the user's question
40
+
41
+ Returns:
42
+ List[RetreivedContext]: List of retrieved context items with similarity scores
43
+ """
44
+ if not message.is_question:
45
+ logger.debug(f"Message {message.key} is not a question, skipping context retrieval")
46
+ return []
47
+
48
+ try:
49
+ # Vectorize the message text
50
+ embeddings = self.vectorization_service.get_embeddings([message.text])
51
+ if embeddings is None or len(embeddings) == 0:
52
+ logger.error(f"Failed to generate embedding for message: {message.key}")
53
+ return []
54
+
55
+ query_embedding = embeddings[0].tolist()
56
+
57
+ # Create vector query
58
+ vector_query = VectorQuery(
59
+ query_text=message.text,
60
+ k=settings.TOP_K_MATCHES,
61
+ score_threshold=0.7 # Minimum similarity threshold
62
+ )
63
+
64
+ # Search for similar content in vector database
65
+ context_results = await self.vector_db_service.search_by_similarity(
66
+ query=vector_query,
67
+ query_embedding=query_embedding
68
+ )
69
+
70
+ logger.info(f"Retrieved {len(context_results)} context items for message: {message.key}")
71
+ return context_results
72
+
73
+ except Exception as e:
74
+ logger.error(f"Error retrieving context for message {message.key}: {str(e)}")
75
+ return []
76
+
src/ctp_slack_bot/services/VectorDatabaseService.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from typing import List, Dict, Any, Optional
3
+ # import numpy as np
4
+
5
+ from ctp_slack_bot.db.MongoDB import mongodb
6
+ from ctp_slack_bot.models.VectorQuery import VectorQuery
7
+ from ctp_slack_bot.models.content import RetreivedContext
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ class VectorDatabaseService:
12
+ """
13
+ Service for storing and retrieving vector embeddings from MongoDB.
14
+ """
15
+
16
+ async def initialize(self):
17
+ """
18
+ Initialize the database connection.
19
+ """
20
+ await mongodb.initialize()
21
+
22
+ async def store(self, text: str, embedding: List[float], metadata: Dict[str, Any]) -> str:
23
+ """
24
+ Store text and its embedding vector in the database.
25
+
26
+ Args:
27
+ text: The text content to store
28
+ embedding: The vector embedding of the text
29
+ metadata: Additional metadata about the text (source, timestamp, etc.)
30
+
31
+ Returns:
32
+ str: The ID of the stored document
33
+ """
34
+ if not mongodb.initialized:
35
+ await mongodb.initialize()
36
+
37
+ try:
38
+ # Create document to store
39
+ document = {
40
+ "text": text,
41
+ "embedding": embedding,
42
+ "metadata": metadata
43
+ }
44
+
45
+ # Insert into collection
46
+ result = await mongodb.vector_collection.insert_one(document)
47
+ logger.debug(f"Stored document with ID: {result.inserted_id}")
48
+
49
+ return str(result.inserted_id)
50
+ except Exception as e:
51
+ logger.error(f"Error storing embedding: {str(e)}")
52
+ raise
53
+
54
+ async def search_by_similarity(self, query: VectorQuery, query_embedding: List[float]) -> List[RetreivedContext]:
55
+ """
56
+ Query the vector database for similar documents.
57
+
58
+ Args:
59
+ query: VectorQuery object with search parameters
60
+ query_embedding: The vector embedding of the query text
61
+
62
+ Returns:
63
+ List[RetreivedContext]: List of similar documents with similarity scores
64
+ """
65
+ if not mongodb.initialized:
66
+ await mongodb.initialize()
67
+
68
+ try:
69
+ # Build aggregation pipeline for vector search
70
+ pipeline = [
71
+ {
72
+ "$search": {
73
+ "index": "vector_index",
74
+ "knnBeta": {
75
+ "vector": query_embedding,
76
+ "path": "embedding",
77
+ "k": query.k
78
+ }
79
+ }
80
+ },
81
+ {
82
+ "$project": {
83
+ "_id": 0,
84
+ "text": 1,
85
+ "metadata": 1,
86
+ "score": {"$meta": "searchScore"}
87
+ }
88
+ }
89
+ ]
90
+
91
+ # Add metadata filters if provided
92
+ if query.filter_metadata:
93
+ metadata_filter = {f"metadata.{k}": v for k, v in query.filter_metadata.items()}
94
+ pipeline.insert(1, {"$match": metadata_filter})
95
+
96
+ # Execute the pipeline
97
+ results = await mongodb.vector_collection.aggregate(pipeline).to_list(length=query.k)
98
+
99
+ # Convert to RetreivedContext objects directly
100
+ context_results = []
101
+ for result in results:
102
+ # Normalize score to [0,1] range
103
+ normalized_score = result.get("score", 0)
104
+
105
+ # Skip if below threshold
106
+ if normalized_score < query.score_threshold:
107
+ continue
108
+
109
+ context_results.append(
110
+ RetreivedContext(
111
+ contextual_text=result["text"],
112
+ metadata_source=result["metadata"].get("source", "unknown"),
113
+ similarity_score=normalized_score,
114
+ said_by=result["metadata"].get("speaker", None),
115
+ in_reation_to_question=result["metadata"].get("related_question", None)
116
+ )
117
+ )
118
+
119
+ logger.debug(f"Found {len(context_results)} similar documents")
120
+ return context_results
121
+
122
+ except Exception as e:
123
+ logger.error(f"Error in similarity search: {str(e)}")
124
+ raise