Spaces:
Runtime error
Runtime error
First working Docker build and run
Browse files- .dockerignore +1 -1
- Dockerfile +6 -6
- pyproject.toml +2 -0
- src/ctp_slack_bot/api/app.py +0 -0
- src/ctp_slack_bot/api/main.py +60 -0
- src/ctp_slack_bot/api/routes.py +61 -0
- src/ctp_slack_bot/api/routes/__init__.py +0 -0
- src/ctp_slack_bot/core/config.py +58 -0
- src/ctp_slack_bot/core/logging.py +102 -0
- src/ctp_slack_bot/tasks/scheduler.py +64 -0
.dockerignore
CHANGED
@@ -46,7 +46,7 @@ coverage.xml
|
|
46 |
|
47 |
# Environments
|
48 |
.env
|
49 |
-
.venv
|
50 |
env/
|
51 |
venv/
|
52 |
ENV/
|
|
|
46 |
|
47 |
# Environments
|
48 |
.env
|
49 |
+
.venv/
|
50 |
env/
|
51 |
venv/
|
52 |
ENV/
|
Dockerfile
CHANGED
@@ -13,13 +13,13 @@ RUN apt-get update \
|
|
13 |
&& apt-get clean \
|
14 |
&& rm -rf /var/lib/apt/lists/*
|
15 |
|
16 |
-
# Copy project files.
|
17 |
-
COPY pyproject.toml README.md ./
|
18 |
-
COPY src/ ./src/
|
19 |
-
|
20 |
# Install Python dependencies.
|
21 |
-
|
22 |
-
|
|
|
|
|
|
|
|
|
23 |
|
24 |
# Create a non-root user and switch to it.
|
25 |
RUN useradd -m appuser
|
|
|
13 |
&& apt-get clean \
|
14 |
&& rm -rf /var/lib/apt/lists/*
|
15 |
|
|
|
|
|
|
|
|
|
16 |
# Install Python dependencies.
|
17 |
+
COPY pyproject.toml ./
|
18 |
+
RUN pip install --no-cache-dir --upgrade pip
|
19 |
+
|
20 |
+
# Copy project files and build.
|
21 |
+
COPY src/ ./src/
|
22 |
+
RUN pip install --no-cache-dir .
|
23 |
|
24 |
# Create a non-root user and switch to it.
|
25 |
RUN useradd -m appuser
|
pyproject.toml
CHANGED
@@ -20,6 +20,7 @@ classifiers = [
|
|
20 |
]
|
21 |
dependencies = [
|
22 |
"pydantic>=2.0.0",
|
|
|
23 |
"fastapi>=0.100.0",
|
24 |
"uvicorn>=0.22.0",
|
25 |
"loguru>=0.7.0",
|
@@ -27,6 +28,7 @@ dependencies = [
|
|
27 |
"httpx>=0.24.1",
|
28 |
"tenacity>=8.2.2",
|
29 |
"pybreaker>=1.0.2",
|
|
|
30 |
"apscheduler>=3.10.1",
|
31 |
"slack-sdk>=3.21.3",
|
32 |
"pymongo>=4.4.1",
|
|
|
20 |
]
|
21 |
dependencies = [
|
22 |
"pydantic>=2.0.0",
|
23 |
+
"pydantic-settings>=2.0.0",
|
24 |
"fastapi>=0.100.0",
|
25 |
"uvicorn>=0.22.0",
|
26 |
"loguru>=0.7.0",
|
|
|
28 |
"httpx>=0.24.1",
|
29 |
"tenacity>=8.2.2",
|
30 |
"pybreaker>=1.0.2",
|
31 |
+
"pytz>=2025.2",
|
32 |
"apscheduler>=3.10.1",
|
33 |
"slack-sdk>=3.21.3",
|
34 |
"pymongo>=4.4.1",
|
src/ctp_slack_bot/api/app.py
DELETED
File without changes
|
src/ctp_slack_bot/api/main.py
ADDED
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
24 |
+
#scheduler = start_scheduler()
|
25 |
+
#logger.info("Started scheduler")
|
26 |
+
|
27 |
+
yield
|
28 |
+
|
29 |
+
# Shutdown
|
30 |
+
logger.info("Shutting down application")
|
31 |
+
#stop_scheduler(scheduler)
|
32 |
+
#logger.info("Stopped scheduler")
|
33 |
+
|
34 |
+
|
35 |
+
app = FastAPI(
|
36 |
+
title="CTP Slack Bot",
|
37 |
+
description="A Slack bot for processing and analyzing Zoom transcripts using AI",
|
38 |
+
version="0.1.0",
|
39 |
+
lifespan=lifespan,
|
40 |
+
)
|
41 |
+
|
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__":
|
53 |
+
import uvicorn
|
54 |
+
|
55 |
+
uvicorn.run(
|
56 |
+
"main:app",
|
57 |
+
host="localhost", #settings.API_HOST,
|
58 |
+
port="8000" #settings.API_PORT,
|
59 |
+
#reload=settings.DEBUG,
|
60 |
+
)
|
src/ctp_slack_bot/api/routes.py
ADDED
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter, Depends, HTTPException, status
|
2 |
+
from loguru import logger
|
3 |
+
|
4 |
+
#from ctp_slack_bot.api.dependencies import get_slack_service, get_transcript_service
|
5 |
+
#from ctp_slack_bot.models.transcript import TranscriptRequest, TranscriptResponse
|
6 |
+
#from ctp_slack_bot.services.slack_service import SlackService
|
7 |
+
#from ctp_slack_bot.services.transcript_service import TranscriptService
|
8 |
+
|
9 |
+
router = APIRouter(prefix="/api/v1")
|
10 |
+
|
11 |
+
|
12 |
+
# @router.post("/transcripts/analyze", response_model=TranscriptResponse)
|
13 |
+
# async def analyze_transcript(
|
14 |
+
# request: TranscriptRequest,
|
15 |
+
# transcript_service: TranscriptService = Depends(get_transcript_service),
|
16 |
+
# ):
|
17 |
+
# """
|
18 |
+
# Analyze a Zoom transcript and return insights.
|
19 |
+
# """
|
20 |
+
# logger.info(f"Analyzing transcript: {request.transcript_id}")
|
21 |
+
# try:
|
22 |
+
# result = await transcript_service.analyze_transcript(request)
|
23 |
+
# return result
|
24 |
+
# except Exception as e:
|
25 |
+
# logger.error(f"Error analyzing transcript: {e}")
|
26 |
+
# raise HTTPException(
|
27 |
+
# status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
28 |
+
# detail="Failed to analyze transcript",
|
29 |
+
# )
|
30 |
+
|
31 |
+
|
32 |
+
# @router.post("/slack/message")
|
33 |
+
# async def send_slack_message(
|
34 |
+
# channel: str,
|
35 |
+
# message: str,
|
36 |
+
# slack_service: SlackService = Depends(get_slack_service),
|
37 |
+
# ):
|
38 |
+
# """
|
39 |
+
# Send a message to a Slack channel.
|
40 |
+
# """
|
41 |
+
# logger.info(f"Sending message to Slack channel: {channel}")
|
42 |
+
# try:
|
43 |
+
# result = await slack_service.send_message(channel, message)
|
44 |
+
# return {"status": "success", "message_ts": result.get("ts")}
|
45 |
+
# except Exception as e:
|
46 |
+
# logger.error(f"Error sending Slack message: {e}")
|
47 |
+
# raise HTTPException(
|
48 |
+
# status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
49 |
+
# detail="Failed to send Slack message",
|
50 |
+
# )
|
51 |
+
|
52 |
+
|
53 |
+
# @router.post("/slack/webhook", include_in_schema=False)
|
54 |
+
# async def slack_webhook(
|
55 |
+
# slack_service: SlackService = Depends(get_slack_service),
|
56 |
+
# ):
|
57 |
+
# """
|
58 |
+
# Webhook endpoint for Slack events.
|
59 |
+
# """
|
60 |
+
# # This would typically handle Slack verification and event processing
|
61 |
+
# return {"challenge": "challenge_token"}
|
src/ctp_slack_bot/api/routes/__init__.py
DELETED
File without changes
|
src/ctp_slack_bot/core/config.py
ADDED
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
# MongoDB Configuration
|
18 |
+
MONGODB_URI: Optional[SecretStr] = None # TODO: Remove optionality
|
19 |
+
MONGODB_DB_NAME: str = "ctp_slack_bot"
|
20 |
+
|
21 |
+
# Slack Configuration
|
22 |
+
SLACK_BOT_TOKEN: Optional[SecretStr] = None # TODO: Remove optionality
|
23 |
+
SLACK_SIGNING_SECRET: Optional[SecretStr] = None # TODO: Remove optionality
|
24 |
+
SLACK_APP_TOKEN: Optional[SecretStr] = None
|
25 |
+
|
26 |
+
# Hugging Face Configuration
|
27 |
+
HF_API_TOKEN: Optional[SecretStr] = None
|
28 |
+
|
29 |
+
# Logging Configuration
|
30 |
+
LOG_LEVEL: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO"
|
31 |
+
LOG_FORMAT: Literal["text", "json"] = "json"
|
32 |
+
|
33 |
+
# APScheduler Configuration
|
34 |
+
SCHEDULER_TIMEZONE: str = "UTC"
|
35 |
+
|
36 |
+
@validator("MONGODB_URI")
|
37 |
+
def validate_mongodb_uri(cls, v):
|
38 |
+
"""Validate MongoDB URI format"""
|
39 |
+
#if not v.get_secret_value().startswith("mongodb"):
|
40 |
+
# raise ValueError("MONGODB_URI must be a valid MongoDB connection string")
|
41 |
+
return v
|
42 |
+
|
43 |
+
model_config = SettingsConfigDict(
|
44 |
+
env_file=".env",
|
45 |
+
env_file_encoding="utf-8",
|
46 |
+
case_sensitive=True,
|
47 |
+
)
|
48 |
+
|
49 |
+
|
50 |
+
@lru_cache
|
51 |
+
def get_settings() -> Settings:
|
52 |
+
"""
|
53 |
+
Get cached settings instance.
|
54 |
+
"""
|
55 |
+
return Settings()
|
56 |
+
|
57 |
+
|
58 |
+
settings = get_settings()
|
src/ctp_slack_bot/core/logging.py
ADDED
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
import sys
|
3 |
+
from typing import Dict, Union
|
4 |
+
|
5 |
+
from loguru import logger
|
6 |
+
|
7 |
+
from ctp_slack_bot.core.config import settings
|
8 |
+
|
9 |
+
|
10 |
+
class InterceptHandler(logging.Handler):
|
11 |
+
"""
|
12 |
+
Intercept standard logging messages toward Loguru.
|
13 |
+
|
14 |
+
This handler intercepts all standard logging messages and redirects them
|
15 |
+
to Loguru, allowing unified logging across the application.
|
16 |
+
"""
|
17 |
+
|
18 |
+
def emit(self, record: logging.LogRecord) -> None:
|
19 |
+
# Get corresponding Loguru level if it exists
|
20 |
+
try:
|
21 |
+
level = logger.level(record.levelname).name
|
22 |
+
except ValueError:
|
23 |
+
level = record.levelno
|
24 |
+
|
25 |
+
# Find caller from where the logged message originated
|
26 |
+
frame, depth = logging.currentframe(), 2
|
27 |
+
while frame and frame.f_code.co_filename == logging.__file__:
|
28 |
+
frame = frame.f_back
|
29 |
+
depth += 1
|
30 |
+
|
31 |
+
logger.opt(depth=depth, exception=record.exc_info).log(
|
32 |
+
level, record.getMessage()
|
33 |
+
)
|
34 |
+
|
35 |
+
|
36 |
+
def setup_logging() -> None:
|
37 |
+
"""
|
38 |
+
Configure logging with Loguru.
|
39 |
+
|
40 |
+
This function sets up Loguru as the main logging provider,
|
41 |
+
configures the log format based on settings, and intercepts
|
42 |
+
standard logging messages.
|
43 |
+
"""
|
44 |
+
# Remove default loguru handler
|
45 |
+
logger.remove()
|
46 |
+
|
47 |
+
# Determine log format
|
48 |
+
if settings.LOG_FORMAT == "json":
|
49 |
+
log_format = {
|
50 |
+
"time": "{time:YYYY-MM-DD HH:mm:ss.SSS}",
|
51 |
+
"level": "{level}",
|
52 |
+
"message": "{message}",
|
53 |
+
"module": "{module}",
|
54 |
+
"function": "{function}",
|
55 |
+
"line": "{line}",
|
56 |
+
}
|
57 |
+
format_string = lambda record: record["message"]
|
58 |
+
else:
|
59 |
+
format_string = (
|
60 |
+
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
|
61 |
+
"<level>{level: <8}</level> | "
|
62 |
+
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - "
|
63 |
+
"<level>{message}</level>"
|
64 |
+
)
|
65 |
+
|
66 |
+
# Add console handler
|
67 |
+
logger.add(
|
68 |
+
sys.stderr,
|
69 |
+
format=format_string,
|
70 |
+
level=settings.LOG_LEVEL,
|
71 |
+
serialize=(settings.LOG_FORMAT == "json"),
|
72 |
+
backtrace=True,
|
73 |
+
diagnose=True,
|
74 |
+
)
|
75 |
+
|
76 |
+
# Add file handler for non-DEBUG environments
|
77 |
+
if settings.LOG_LEVEL != "DEBUG":
|
78 |
+
logger.add(
|
79 |
+
"logs/app.log",
|
80 |
+
rotation="10 MB",
|
81 |
+
retention="1 week",
|
82 |
+
compression="zip",
|
83 |
+
format=format_string,
|
84 |
+
level=settings.LOG_LEVEL,
|
85 |
+
serialize=(settings.LOG_FORMAT == "json"),
|
86 |
+
)
|
87 |
+
|
88 |
+
# Intercept standard logging messages
|
89 |
+
logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True)
|
90 |
+
|
91 |
+
# Update logging levels for some noisy libraries
|
92 |
+
for logger_name in [
|
93 |
+
"uvicorn",
|
94 |
+
"uvicorn.error",
|
95 |
+
"fastapi",
|
96 |
+
"httpx",
|
97 |
+
"apscheduler",
|
98 |
+
"pymongo",
|
99 |
+
]:
|
100 |
+
logging.getLogger(logger_name).setLevel(logging.INFO)
|
101 |
+
|
102 |
+
logger.info(f"Logging configured with level {settings.LOG_LEVEL}")
|
src/ctp_slack_bot/tasks/scheduler.py
ADDED
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from datetime import datetime
|
2 |
+
from typing import Optional
|
3 |
+
|
4 |
+
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
5 |
+
from apscheduler.triggers.cron import CronTrigger
|
6 |
+
from loguru import logger
|
7 |
+
from pytz import timezone
|
8 |
+
|
9 |
+
from ctp_slack_bot.core.config import settings
|
10 |
+
#from ctp_slack_bot.tasks.error_report import send_error_report
|
11 |
+
#from ctp_slack_bot.tasks.transcript_cleanup import cleanup_old_transcripts
|
12 |
+
|
13 |
+
|
14 |
+
def start_scheduler() -> AsyncIOScheduler:
|
15 |
+
"""
|
16 |
+
Start and configure the APScheduler instance.
|
17 |
+
|
18 |
+
Returns:
|
19 |
+
AsyncIOScheduler: Configured scheduler instance
|
20 |
+
"""
|
21 |
+
scheduler = AsyncIOScheduler(timezone=timezone(settings.SCHEDULER_TIMEZONE))
|
22 |
+
|
23 |
+
# Add jobs to the scheduler
|
24 |
+
|
25 |
+
# Daily error report at 7 AM
|
26 |
+
# scheduler.add_job(
|
27 |
+
# send_error_report,
|
28 |
+
# CronTrigger(hour=7, minute=0),
|
29 |
+
# id="daily_error_report",
|
30 |
+
# name="Daily Error Report",
|
31 |
+
# replace_existing=True,
|
32 |
+
# )
|
33 |
+
|
34 |
+
# Weekly transcript cleanup on Sundays at 1 AM
|
35 |
+
# scheduler.add_job(
|
36 |
+
# cleanup_old_transcripts,
|
37 |
+
# CronTrigger(day_of_week="sun", hour=1, minute=0),
|
38 |
+
# id="weekly_transcript_cleanup",
|
39 |
+
# name="Weekly Transcript Cleanup",
|
40 |
+
# replace_existing=True,
|
41 |
+
# )
|
42 |
+
|
43 |
+
# Start the scheduler
|
44 |
+
scheduler.start()
|
45 |
+
logger.info("Scheduler started with timezone: {}", settings.SCHEDULER_TIMEZONE)
|
46 |
+
logger.info("Next run for error report: {}",
|
47 |
+
scheduler.get_job("daily_error_report").next_run_time)
|
48 |
+
logger.info("Next run for transcript cleanup: {}",
|
49 |
+
scheduler.get_job("weekly_transcript_cleanup").next_run_time)
|
50 |
+
|
51 |
+
return scheduler
|
52 |
+
|
53 |
+
|
54 |
+
def stop_scheduler(scheduler: Optional[AsyncIOScheduler] = None) -> None:
|
55 |
+
"""
|
56 |
+
Shutdown the scheduler gracefully.
|
57 |
+
|
58 |
+
Args:
|
59 |
+
scheduler: The scheduler instance to shut down
|
60 |
+
"""
|
61 |
+
if scheduler is not None and scheduler.running:
|
62 |
+
logger.info("Shutting down scheduler")
|
63 |
+
scheduler.shutdown(wait=False)
|
64 |
+
logger.info("Scheduler shutdown complete")
|