tonebeta commited on
Commit
6468574
·
1 Parent(s): c529e22

feat: Set up initial project structure for slack_url_bot including core application, dependencies, documentation, and build automation.

Browse files
Files changed (9) hide show
  1. .python-version +1 -0
  2. Dockerfile +62 -0
  3. Makefile +60 -0
  4. PRD.md +313 -0
  5. README.md +80 -11
  6. main.py +569 -0
  7. pyproject.toml +25 -0
  8. requirements.txt +9 -0
  9. uv.lock +0 -0
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.13
Dockerfile ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dockerfile for Slack URL Summarizer Bot - Hugging Face Spaces
2
+ # Hugging Face Spaces requires port 7860
3
+
4
+ # --- Stage 1: Builder ---
5
+ FROM python:3.12-slim AS builder
6
+
7
+ WORKDIR /app
8
+
9
+ # Install uv package manager
10
+ RUN pip install uv
11
+
12
+ # Create virtual environment
13
+ RUN uv venv /opt/venv
14
+
15
+ # Copy dependency files
16
+ COPY pyproject.toml uv.lock ./
17
+
18
+ # Install dependencies
19
+ RUN . /opt/venv/bin/activate && uv pip sync pyproject.toml --no-cache
20
+
21
+ # --- Stage 2: Final Image ---
22
+ FROM python:3.12-slim
23
+
24
+ # Install curl for healthcheck
25
+ RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
26
+
27
+ # Create non-privileged user
28
+ RUN useradd --create-home --shell /bin/bash appuser
29
+
30
+ WORKDIR /home/appuser/app
31
+
32
+ # Copy virtual environment from builder
33
+ COPY --from=builder --chown=appuser:appuser /opt/venv /opt/venv
34
+
35
+ # Copy application code
36
+ COPY --chown=appuser:appuser main.py .
37
+
38
+ # Switch to non-privileged user
39
+ USER appuser
40
+
41
+ # Set environment variables
42
+ # Hugging Face Spaces requires port 7860
43
+ ENV PATH="/opt/venv/bin:$PATH"
44
+ ENV PYTHONUNBUFFERED=1
45
+ ENV PORT=7860
46
+
47
+ # Expose Hugging Face Spaces required port
48
+ EXPOSE 7860
49
+
50
+ # --- Runtime Configuration ---
51
+ # Required environment variables (set in HF Space Secrets):
52
+ # - SLACK_BOT_TOKEN
53
+ # - SLACK_SIGNING_SECRET
54
+ # - AZURE_OPENAI_ENDPOINT
55
+ # - AZURE_OPENAI_API_KEY
56
+
57
+ # Healthcheck
58
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
59
+ CMD curl -f http://localhost:7860/health || exit 1
60
+
61
+ # Run the application on port 7860
62
+ CMD ["uvicorn", "main:api", "--host", "0.0.0.0", "--port", "7860"]
Makefile ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Makefile for managing the Slack URL Summarizer Bot Docker container
2
+
3
+ # --- Variables ---
4
+ IMAGE_NAME := slack-crawler-summary-bot
5
+ CONTAINER_NAME := slack-crawler-summary-bot_container
6
+ ENV_FILE := .env
7
+
8
+ # Hugging Face variables - **EDIT THESE**
9
+ HF_USERNAME := your-hf-username
10
+ HF_SPACE_NAME := your-hf-space-name
11
+ HF_IMAGE_NAME := registry.hf.space/$(HF_USERNAME)/$(HF_SPACE_NAME):latest
12
+
13
+ # --- Local Docker Management ---
14
+
15
+ # Build the Docker image
16
+ build:
17
+ docker build -t $(IMAGE_NAME) .
18
+
19
+ # Run the Docker container in detached mode
20
+ deploy:
21
+ docker run -d --name $(CONTAINER_NAME) --env-file $(ENV_FILE) -p 8000:8000 $(IMAGE_NAME)
22
+
23
+ # Stop the Docker container
24
+ stop:
25
+ docker stop $(CONTAINER_NAME)
26
+
27
+ # Remove the Docker container
28
+ rm:
29
+ docker rm $(CONTAINER_NAME)
30
+
31
+ # Restart the Docker container
32
+ restart: stop rm deploy
33
+
34
+ # Run the container in interactive mode for debugging
35
+ debug:
36
+ docker run -it --rm --name $(CONTAINER_NAME)-debug --env-file $(ENV_FILE) -p 8000:8000 $(IMAGE_NAME) /bin/bash
37
+
38
+ # Show logs of the running container
39
+ logs:
40
+ docker logs -f $(CONTAINER_NAME)
41
+
42
+ # --- Hugging Face Deployment ---
43
+
44
+ # Log in to Hugging Face Docker registry
45
+ hf-login:
46
+ @echo "You will be prompted for your Hugging Face username and a User Access Token with write permissions."
47
+ docker login registry.hf.space
48
+
49
+ # Tag the Docker image for Hugging Face
50
+ hf-tag:
51
+ docker tag $(IMAGE_NAME) $(HF_IMAGE_NAME)
52
+
53
+ # Push the Docker image to Hugging Face Spaces
54
+ hf-push:
55
+ docker push $(HF_IMAGE_NAME)
56
+
57
+ # Build, tag, and push to Hugging Face
58
+ hf-deploy: build hf-tag hf-push
59
+
60
+ .PHONY: build deploy stop rm restart debug logs hf-login hf-tag hf-push hf-deploy
PRD.md ADDED
@@ -0,0 +1,313 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Slack URL Summarizer Bot - Technical Specification
2
+
3
+ ## 1. Project Overview
4
+
5
+ ### 1.1 Purpose
6
+ Develop a Slack bot that automatically detects URLs in messages, extracts content from those URLs, generates summaries, translates them to Traditional Chinese, and posts the results back to the channel.
7
+
8
+ ### 1.2 Key Features
9
+ - Automatic URL detection in Slack messages
10
+ - Web content extraction and parsing
11
+ - Content summarization using AI
12
+ - Translation to Traditional Chinese
13
+ - Automated response posting to Slack channels
14
+
15
+ ## 2. Functional Requirements
16
+
17
+ ### 2.1 Core Functionality
18
+ | Feature | Description | Priority |
19
+ |---------|-------------|----------|
20
+ | URL Detection | Detect and extract URLs from Slack messages | High |
21
+ | Content Extraction | Extract main content from web pages | High |
22
+ | Content Summarization | Generate concise summaries of extracted content | High |
23
+ | Translation | Translate summaries to Traditional Chinese | High |
24
+ | Slack Integration | Post results back to originating channel | High |
25
+
26
+ ### 2.2 User Stories
27
+ - **As a Slack user**, I want to paste a URL and automatically receive a Chinese summary, so I can quickly understand the content without reading the full article
28
+ - **As a team member**, I want summaries posted in the same channel, so everyone can benefit from the content
29
+ - **As a user**, I want the bot to handle multiple URLs in one message, so I can share multiple resources efficiently
30
+
31
+ ## 3. Technical Architecture
32
+
33
+ ### 3.1 System Architecture
34
+ ```
35
+ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
36
+ │ Slack App │ │ Web Server │ │ AI Services │
37
+ │ │ │ (FastAPI) │ │ │
38
+ │ • Events API │◄──►│ • URL Extract │◄──►│ • OpenAI API │
39
+ │ • Bot Token │ │ • Content Parse │ │ • Summarization │
40
+ │ • Webhooks │ │ • Response Send │ │ • Translation │
41
+ └─────────────────┘ └─────────────────┘ └─────────────────┘
42
+ ```
43
+
44
+ ### 3.2 Technology Stack
45
+ - **Backend Framework**: Python + FastAPI
46
+ - **Slack Integration**: Slack Bolt SDK for Python
47
+ - **Content Extraction**: newspaper3k / readability-lxml
48
+ - **AI Services**: Azure OpenAI API
49
+ - **HTTP Client**: httpx
50
+ - **Deployment**: Docker + Cloud hosting (AWS/GCP/Azure)
51
+ - **Database**: Redis (for caching) - Optional
52
+
53
+ ## 4. Implementation Details
54
+
55
+ ### 4.1 Slack Bot Setup
56
+ #### Required Scopes
57
+ - `chat:write` - Post messages to channels
58
+ - `channels:read` - Read channel information
59
+ - `app_mentions:read` - Read mentions
60
+ - `channels:history` - Read channel messages
61
+
62
+ #### Event Subscriptions
63
+ - `message.channels` - Listen to channel messages
64
+ - `app_mention` - Listen to bot mentions
65
+
66
+ ### 4.2 Core Components
67
+
68
+ #### 4.2.1 URL Detection Module
69
+ ```python
70
+ import re
71
+
72
+ def extract_urls(text: str) -> List[str]:
73
+ """Extract all URLs from message text"""
74
+ pattern = r'https?://[^\s<>"{\[\]|\\^`]+'
75
+ return re.findall(pattern, text)
76
+ ```
77
+
78
+ #### 4.2.2 Content Extraction Module
79
+ ```python
80
+ from newspaper import Article
81
+
82
+ def extract_content(url: str) -> dict:
83
+ """Extract main content from URL"""
84
+ try:
85
+ article = Article(url)
86
+ article.download()
87
+ article.parse()
88
+ return {
89
+ 'title': article.title,
90
+ 'text': article.text,
91
+ 'authors': article.authors,
92
+ 'publish_date': article.publish_date
93
+ }
94
+ except Exception as e:
95
+ return {'error': str(e)}
96
+ ```
97
+
98
+ #### 4.2.3 AI Processing Module
99
+ ```python
100
+ import httpx
101
+ import os
102
+ from dotenv import load_dotenv
103
+
104
+ load_dotenv()
105
+
106
+ def summarize_and_translate(text: str) -> str:
107
+ """Summarize content and translate to Traditional Chinese using Azure OpenAI"""
108
+ url = f"{os.getenv('AZURE_OPENAI_ENDPOINT')}/openai/deployments/{os.getenv('AZURE_OPENAI_DEPLOYMENT_NAME')}/chat/completions?api-version={os.getenv('AZURE_OPENAI_API_VERSION')}"
109
+ headers = {
110
+ "Content-Type": "application/json",
111
+ "api-key": os.getenv("AZURE_OPENAI_API_KEY"),
112
+ }
113
+ body = {
114
+ "messages": [
115
+ {
116
+ "role": "user",
117
+ "content": f"請將以下文章摘要成 3–5 句,並翻譯為繁體中文:\n\n{text}"
118
+ }
119
+ ],
120
+ "temperature": 0.7
121
+ }
122
+
123
+ resp = httpx.post(url, headers=headers, json=body)
124
+ resp.raise_for_status()
125
+ return resp.json()["choices"][0]["message"]["content"].strip()
126
+ ```
127
+
128
+ ## 5. API Specifications
129
+
130
+ ### 5.1 Slack Event Handler
131
+ ```python
132
+ @app.event("message")
133
+ def handle_message(event, say):
134
+ """Handle incoming Slack messages"""
135
+ # Extract URLs from message
136
+ urls = extract_urls(event.get('text', ''))
137
+
138
+ if not urls:
139
+ return
140
+
141
+ # Process each URL
142
+ for url in urls:
143
+ process_url_async(url, event['channel'], say)
144
+ ```
145
+
146
+ ### 5.2 URL Processing Pipeline
147
+ ```python
148
+ async def process_url_async(url: str, channel: str, say):
149
+ """Asynchronous URL processing pipeline"""
150
+ try:
151
+ # Step 1: Extract content
152
+ content = extract_content(url)
153
+
154
+ # Step 2: Summarize and translate
155
+ summary = summarize_and_translate(content['text'])
156
+
157
+ # Step 3: Format and send response
158
+ response = format_response(url, content['title'], summary)
159
+ say(channel=channel, text=response)
160
+
161
+ except Exception as e:
162
+ error_message = f"❌ 處理網址時發生錯誤: {url}"
163
+ say(channel=channel, text=error_message)
164
+ ```
165
+
166
+ ## 6. Error Handling & Edge Cases
167
+
168
+ ### 6.1 URL Validation
169
+ - Invalid URLs (malformed, unreachable)
170
+ - Protected content (login required, paywalls)
171
+ - Unsupported content types (PDFs, images, videos)
172
+ - Rate limiting from target websites
173
+
174
+ ### 6.2 Content Processing
175
+ - Empty or insufficient content
176
+ - Non-text content (images, videos)
177
+ - Multiple languages in source content
178
+ - Extremely long articles (token limits)
179
+
180
+ ### 6.3 API Failures
181
+ - Azure OpenAI API rate limits
182
+ - Network timeouts
183
+ - Slack API failures
184
+ - Service unavailability
185
+
186
+ ## 7. Response Format
187
+
188
+ ### 7.1 Successful Response Template
189
+ ```
190
+ 🔗 **原始網址**: {url}
191
+ 📰 **標題**: {title}
192
+ 📝 **中文摘要**:
193
+ {summary}
194
+
195
+ ---
196
+ ⏰ 處理時間: {timestamp}
197
+ ```
198
+
199
+ ### 7.2 Error Response Template
200
+ ```
201
+ ❌ **處理失敗**: {url}
202
+ 🔍 **錯誤原因**: {error_message}
203
+ 💡 **建議**: 請檢查網址是否正確或稍後再試
204
+ ```
205
+
206
+ ## 8. Performance Requirements
207
+
208
+ ### 8.1 Response Time
209
+ - URL processing: < 30 seconds
210
+ - Simple pages: < 10 seconds
211
+ - Complex pages: < 20 seconds
212
+
213
+ ### 8.2 Throughput
214
+ - Support 100 concurrent requests
215
+ - Handle 1000 URLs per hour
216
+ - Rate limiting: 5 requests per user per minute
217
+
218
+ ## 9. Security Considerations
219
+
220
+ ### 9.1 Input Validation
221
+ - URL sanitization and validation
222
+ - Content length limits
223
+ - Malicious URL detection
224
+
225
+ ### 9.2 API Security
226
+ - Secure storage of API keys
227
+ - Rate limiting implementation
228
+ - Request logging and monitoring
229
+
230
+ ## 10. Deployment Strategy
231
+
232
+ ### 10.1 Environment Setup
233
+ - Development: Local Docker containers
234
+ - Staging: Cloud-based testing environment
235
+ - Production: Container orchestration (Kubernetes/ECS)
236
+
237
+ ### 10.2 Configuration Management
238
+ ```python
239
+ # Environment variables
240
+ SLACK_BOT_TOKEN = os.getenv('SLACK_BOT_TOKEN')
241
+ SLACK_SIGNING_SECRET = os.getenv('SLACK_SIGNING_SECRET')
242
+ AZURE_OPENAI_ENDPOINT = os.getenv('AZURE_OPENAI_ENDPOINT')
243
+ AZURE_OPENAI_API_KEY = os.getenv('AZURE_OPENAI_API_KEY')
244
+ AZURE_OPENAI_DEPLOYMENT_NAME = os.getenv('AZURE_OPENAI_DEPLOYMENT_NAME')
245
+ AZURE_OPENAI_API_VERSION = os.getenv('AZURE_OPENAI_API_VERSION')
246
+ ```
247
+
248
+ ## 11. Testing Strategy
249
+
250
+ ### 11.1 Unit Tests
251
+ - URL extraction logic
252
+ - Content parsing functions
253
+ - AI API integration
254
+ - Response formatting
255
+
256
+ ### 11.2 Integration Tests
257
+ - End-to-end Slack workflow
258
+ - External API interactions
259
+ - Error handling scenarios
260
+
261
+ ## 12. Monitoring & Logging
262
+
263
+ ### 12.1 Metrics to Track
264
+ - Response times
265
+ - Success/failure rates
266
+ - API usage costs
267
+ - User engagement
268
+
269
+ ### 12.2 Logging Requirements
270
+ - All URL processing attempts
271
+ - API call results
272
+ - Error occurrences
273
+ - Performance metrics
274
+
275
+ ## 13. Future Enhancements
276
+
277
+ ### 13.1 Phase 2 Features
278
+ - Multiple language support
279
+ - Custom summary length options
280
+ - Content caching for repeated URLs
281
+ - User preference settings
282
+
283
+ ### 13.2 Advanced Features
284
+ - Batch URL processing
285
+ - Scheduled summary delivery
286
+ - Content categorization
287
+ - Analytics dashboard
288
+
289
+ ## 14. Acceptance Criteria
290
+
291
+ ### 14.1 MVP Success Criteria
292
+ - [x] Bot responds to URLs in Slack messages
293
+ - [x] Successfully extracts content from common websites
294
+ - [x] Generates coherent Chinese summaries
295
+ - [x] Posts formatted responses to correct channels
296
+ - [x] Handles basic error scenarios gracefully
297
+
298
+ ### 14.2 Quality Gates
299
+ - 95% uptime requirement
300
+ - <20 second average response time
301
+ - <5% error rate for valid URLs
302
+ - Positive user feedback (>4.0/5.0)
303
+
304
+ ## 15. Timeline & Milestones
305
+
306
+ | Phase | Duration | Deliverables |
307
+ |-------|----------|-------------|
308
+ | Setup & Planning | 1 week | Project setup, Slack app creation |
309
+ | Core Development | 2 weeks | Basic URL processing pipeline |
310
+ | AI Integration | 1 week | Summarization and translation |
311
+ | Testing & Debugging | 1 week | Unit tests, integration tests |
312
+ | Deployment | 1 week | Production deployment, monitoring |
313
+ | **Total** | **6 weeks** | **Production-ready MVP** |
README.md CHANGED
@@ -1,11 +1,80 @@
1
- ---
2
- title: Slack Url Bot
3
- emoji: 🌍
4
- colorFrom: red
5
- colorTo: purple
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Slack URL Bot
3
+ emoji: 🔗
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: docker
7
+ pinned: false
8
+ license: mit
9
+ ---
10
+
11
+ # Slack URL Summarizer Bot
12
+
13
+ A Slack bot that automatically summarizes URLs shared in channels, translating content to Traditional Chinese using Azure OpenAI.
14
+
15
+ ## Features
16
+
17
+ - 🔗 **Automatic URL Detection**: Detects URLs in Slack messages
18
+ - 📰 **Content Extraction**: Extracts main content from web pages using newspaper3k
19
+ - 🤖 **AI Summarization**: Summarizes content using Azure OpenAI GPT-4
20
+ - 🌏 **Traditional Chinese**: All summaries are in Traditional Chinese
21
+ - ⚡ **Real-time Processing**: Async processing for fast responses
22
+
23
+ ## Configuration
24
+
25
+ This Space requires the following secrets to be set in your Hugging Face Space settings:
26
+
27
+ | Secret | Description |
28
+ |--------|-------------|
29
+ | `SLACK_BOT_TOKEN` | Your Slack Bot OAuth Token (xoxb-...) |
30
+ | `SLACK_SIGNING_SECRET` | Your Slack App Signing Secret |
31
+ | `AZURE_OPENAI_ENDPOINT` | Azure OpenAI endpoint URL |
32
+ | `AZURE_OPENAI_API_KEY` | Azure OpenAI API key |
33
+
34
+ ### Optional Environment Variables
35
+
36
+ | Variable | Default | Description |
37
+ |----------|---------|-------------|
38
+ | `AZURE_OPENAI_DEPLOYMENT_NAME` | gpt-4 | Azure OpenAI deployment name |
39
+ | `AZURE_OPENAI_API_VERSION` | 2025-01-01 | API version |
40
+
41
+ ## Slack App Setup
42
+
43
+ 1. Create a new Slack App at [api.slack.com/apps](https://api.slack.com/apps)
44
+ 2. Add Bot Token Scopes:
45
+ - `chat:write` - Send messages
46
+ - `channels:read` - Read channel info
47
+ - `channels:history` - Read message history
48
+ - `app_mentions:read` - Read mentions
49
+ 3. Enable Event Subscriptions with URL: `https://fongci-slack-url-bot.hf.space/slack/events`
50
+ 4. Subscribe to bot events:
51
+ - `message.channels`
52
+ - `app_mention`
53
+ 5. Install the app to your workspace
54
+
55
+ ## API Endpoints
56
+
57
+ - `GET /` - Health check
58
+ - `GET /health` - Detailed health status
59
+ - `POST /slack/events` - Slack events webhook
60
+
61
+ ## Local Development
62
+
63
+ ```bash
64
+ # Install dependencies
65
+ pip install uv
66
+ uv sync
67
+
68
+ # Set environment variables
69
+ export SLACK_BOT_TOKEN=xoxb-...
70
+ export SLACK_SIGNING_SECRET=...
71
+ export AZURE_OPENAI_ENDPOINT=...
72
+ export AZURE_OPENAI_API_KEY=...
73
+
74
+ # Run the app
75
+ python main.py
76
+ ```
77
+
78
+ ## License
79
+
80
+ MIT
main.py ADDED
@@ -0,0 +1,569 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import asyncio
4
+ import logging
5
+ from datetime import datetime
6
+ from typing import List, Dict, Optional
7
+ from dataclasses import dataclass
8
+ from contextlib import asynccontextmanager
9
+
10
+ import httpx
11
+ from fastapi import FastAPI, Request, HTTPException
12
+ from slack_bolt import App
13
+ from slack_bolt.adapter.fastapi import SlackRequestHandler
14
+ from newspaper import Article
15
+ import uvicorn
16
+ from dotenv import load_dotenv
17
+
18
+ # Load environment variables
19
+ load_dotenv()
20
+
21
+ # Configure logging
22
+ logging.basicConfig(
23
+ level=logging.INFO,
24
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
25
+ )
26
+ logger = logging.getLogger(__name__)
27
+
28
+ # Configuration
29
+ @dataclass
30
+ class Config:
31
+ slack_bot_token: str = os.getenv('SLACK_BOT_TOKEN', '')
32
+ slack_signing_secret: str = os.getenv('SLACK_SIGNING_SECRET', '')
33
+ azure_openai_endpoint: str = os.getenv('AZURE_OPENAI_ENDPOINT', '')
34
+ azure_openai_api_key: str = os.getenv('AZURE_OPENAI_API_KEY', '')
35
+ azure_openai_deployment_name: str = os.getenv('AZURE_OPENAI_DEPLOYMENT_NAME', 'gpt-4')
36
+ azure_openai_api_version: str = os.getenv('AZURE_OPENAI_API_VERSION', '2025-01-01')
37
+ max_content_length: int = 10000
38
+ processing_timeout: int = 30
39
+
40
+ config = Config()
41
+
42
+ @asynccontextmanager
43
+ async def lifespan(app: FastAPI):
44
+ """FastAPI lifespan event handler"""
45
+ # Startup
46
+ logger.info("Starting Slack URL Summarizer Bot")
47
+
48
+ # Validate configuration
49
+ required_vars = [
50
+ 'SLACK_BOT_TOKEN',
51
+ 'SLACK_SIGNING_SECRET',
52
+ 'AZURE_OPENAI_ENDPOINT',
53
+ 'AZURE_OPENAI_API_KEY'
54
+ ]
55
+
56
+ missing_vars = [var for var in required_vars if not os.getenv(var)]
57
+ if missing_vars:
58
+ logger.error(f"Missing required environment variables: {', '.join(missing_vars)}")
59
+ raise Exception(f"Missing required environment variables: {', '.join(missing_vars)}")
60
+
61
+ logger.info("Bot started successfully")
62
+
63
+ yield
64
+
65
+ # Shutdown
66
+ logger.info("Shutting down Slack URL Summarizer Bot")
67
+ if hasattr(processor, 'http_client'):
68
+ await processor.http_client.aclose()
69
+
70
+ # Initialize Slack app
71
+ slack_app = App(
72
+ token=config.slack_bot_token,
73
+ signing_secret=config.slack_signing_secret,
74
+ process_before_response=True,
75
+ # 暫時停用簽名驗證進行測試
76
+ request_verification_enabled=False
77
+ )
78
+
79
+ # Initialize FastAPI with lifespan
80
+ api = FastAPI(title="Slack URL Summarizer Bot", lifespan=lifespan)
81
+ handler = SlackRequestHandler(slack_app)
82
+
83
+ class URLProcessor:
84
+ """Core URL processing functionality"""
85
+
86
+ def __init__(self, config: Config):
87
+ self.config = config
88
+ self.http_client = httpx.AsyncClient(
89
+ timeout=httpx.Timeout(30.0),
90
+ follow_redirects=True
91
+ )
92
+
93
+ async def __aenter__(self):
94
+ return self
95
+
96
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
97
+ await self.http_client.aclose()
98
+
99
+ def extract_urls(self, text: str) -> List[str]:
100
+ """Extract all URLs from message text"""
101
+ pattern = r'https?://[^\s<>"{\[\]|\\^`]+'
102
+ urls = re.findall(pattern, text)
103
+ logger.info(f"Extracted {len(urls)} URLs from message")
104
+ return urls
105
+
106
+ async def extract_content(self, url: str) -> Dict:
107
+ """Extract main content from URL"""
108
+ try:
109
+ logger.info(f"Extracting content from: {url}")
110
+
111
+ # 設定更好的用戶代理來避免被阻擋
112
+ headers = {
113
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
114
+ }
115
+
116
+ # 先嘗試使用 httpx 直接獲取內容
117
+ try:
118
+ response = await self.http_client.get(url, headers=headers)
119
+ response.raise_for_status()
120
+
121
+ # 使用 newspaper3k 解析 HTML 內容
122
+ article = Article(url)
123
+ article.set_html(response.text)
124
+ article.parse()
125
+
126
+ except Exception as e:
127
+ # 如果 httpx 失敗,嘗試 newspaper3k 的原始方法
128
+ logger.warning(f"Direct HTTP request failed, trying newspaper3k: {str(e)}")
129
+
130
+ article = Article(url)
131
+ # 設定用戶代理
132
+ article.config.browser_user_agent = headers['User-Agent']
133
+ article.download()
134
+ article.parse()
135
+
136
+ # 驗證內容
137
+ if not article.text or len(article.text.strip()) < 50:
138
+ # 如果提取的內容太少,嘗試使用基本的網頁內容
139
+ if 'response' in locals() and response.text:
140
+ # 簡單的 HTML 解析
141
+ import re
142
+ from html import unescape
143
+
144
+ # 移除 HTML 標籤
145
+ text = re.sub(r'<[^>]+>', '', response.text)
146
+ text = unescape(text)
147
+ text = re.sub(r'\s+', ' ', text).strip()
148
+
149
+ if len(text) > 100:
150
+ # 取前 3000 字符作為內容
151
+ text = text[:3000] + "..." if len(text) > 3000 else text
152
+
153
+ result = {
154
+ 'title': url.split('/')[-1].replace('-', ' ').title(),
155
+ 'text': text,
156
+ 'authors': [],
157
+ 'publish_date': None,
158
+ 'url': url
159
+ }
160
+
161
+ logger.info(f"Successfully extracted content using fallback method from {url}")
162
+ return result
163
+
164
+ raise Exception("Insufficient content extracted")
165
+
166
+ # 截斷內容如果太長
167
+ text = article.text
168
+ if len(text) > self.config.max_content_length:
169
+ text = text[:self.config.max_content_length] + "..."
170
+
171
+ result = {
172
+ 'title': article.title or "No title available",
173
+ 'text': text,
174
+ 'authors': article.authors,
175
+ 'publish_date': article.publish_date,
176
+ 'url': url
177
+ }
178
+
179
+ logger.info(f"Successfully extracted content from {url}")
180
+ return result
181
+
182
+ except Exception as e:
183
+ logger.error(f"Error extracting content from {url}: {str(e)}")
184
+
185
+ # 最後的備用方案:返回基本信息讓 AI 處理
186
+ fallback_result = {
187
+ 'title': f"無法完全提取內容的網頁: {url}",
188
+ 'text': f"由於網站限制,無法提取完整內容。網址: {url}. 請嘗試直接訪問該網站查看內容。",
189
+ 'authors': [],
190
+ 'publish_date': None,
191
+ 'url': url
192
+ }
193
+
194
+ logger.info(f"Using fallback content for {url}")
195
+ return fallback_result
196
+
197
+ async def summarize_and_translate(self, content: Dict) -> str:
198
+ """Summarize content and translate to Traditional Chinese using Azure OpenAI"""
199
+ try:
200
+ logger.info(f"Summarizing content for: {content['url']}")
201
+
202
+ # 檢查是否為備用內容
203
+ if "無法完全提取內容" in content['title']:
204
+ prompt = f"""這個網址因為網站限制無法完全提取內容:{content['url']}
205
+
206
+ 請用繁體中文回覆一個友善的訊息,說明:
207
+ 1. 由於網站的保護機制,無法自動提取該網頁的完整內容
208
+ 2. 建議用戶直接點擊連結查看完整內容
209
+ 3. 如果是知名網站,可以簡單說明該網站的性質(如新聞、技術等)
210
+
211
+ 請保持簡潔友善的語調。"""
212
+
213
+ # 對於備用內容,使用簡化的處理
214
+ url = f"{self.config.azure_openai_endpoint}/openai/deployments/{self.config.azure_openai_deployment_name}/chat/completions?api-version={self.config.azure_openai_api_version}"
215
+
216
+ headers = {
217
+ "Content-Type": "application/json",
218
+ "api-key": self.config.azure_openai_api_key,
219
+ }
220
+
221
+ body = {
222
+ "messages": [
223
+ {
224
+ "role": "system",
225
+ "content": "你是一個友善的助手,會提供實用的建議。"
226
+ },
227
+ {
228
+ "role": "user",
229
+ "content": prompt
230
+ }
231
+ ],
232
+ "temperature": 0.3,
233
+ "max_tokens": 300
234
+ }
235
+
236
+ response = await self.http_client.post(url, headers=headers, json=body)
237
+ response.raise_for_status()
238
+
239
+ result = response.json()
240
+ summary = result["choices"][0]["message"]["content"].strip()
241
+
242
+ # 提取 token 使用量資訊
243
+ usage_info = result.get("usage", {})
244
+ token_stats = {
245
+ "prompt_tokens": usage_info.get("prompt_tokens", 0),
246
+ "completion_tokens": usage_info.get("completion_tokens", 0),
247
+ "total_tokens": usage_info.get("total_tokens", 0)
248
+ }
249
+
250
+ logger.info(f"Generated fallback response for: {content['url']}")
251
+ logger.info(f"Token usage - Prompt: {token_stats['prompt_tokens']}, Completion: {token_stats['completion_tokens']}, Total: {token_stats['total_tokens']}")
252
+
253
+ return summary, token_stats
254
+ else:
255
+ # 正常的摘要處理
256
+ prompt = f"""請將以下文章摘要成 3-5 句重點,並翻譯為繁體中文。請確保摘要簡潔明瞭且包含最重要的資訊:
257
+
258
+ 標題:{content['title']}
259
+ 內容:{content['text']}
260
+
261
+ 請用繁體中文回覆摘要。"""
262
+
263
+ # Azure OpenAI API call
264
+ url = f"{self.config.azure_openai_endpoint}/openai/deployments/{self.config.azure_openai_deployment_name}/chat/completions?api-version={self.config.azure_openai_api_version}"
265
+
266
+ headers = {
267
+ "Content-Type": "application/json",
268
+ "api-key": self.config.azure_openai_api_key,
269
+ }
270
+
271
+ body = {
272
+ "messages": [
273
+ {
274
+ "role": "system",
275
+ "content": "你是一個專業的技術文章摘要與翻譯專家,精通各種技術領域,能夠準確保留技術術語、專有名詞、數據細節,並將內容翻譯成自然流暢的繁體中文。你特別擅長處理科技、醫療、商業和學術文章,能夠識別並保留重要的技術細節。"
276
+ },
277
+ {
278
+ "role": "user",
279
+ "content": prompt
280
+ }
281
+ ],
282
+ "temperature": 0.3,
283
+ "max_tokens": 800
284
+ }
285
+
286
+ response = await self.http_client.post(url, headers=headers, json=body)
287
+ response.raise_for_status()
288
+
289
+ result = response.json()
290
+ summary = result["choices"][0]["message"]["content"].strip()
291
+
292
+ logger.info(f"Successfully generated summary for: {content['url']}")
293
+ return summary
294
+
295
+ except Exception as e:
296
+ logger.error(f"Error in summarization: {str(e)}")
297
+
298
+ # 回傳錯誤時也要保持 tuple 格式
299
+ error_summary = f"抱歉,AI 處理時發生錯誤。錯誤訊息:{str(e)}"
300
+ error_token_stats = {
301
+ "prompt_tokens": 0,
302
+ "completion_tokens": 0,
303
+ "total_tokens": 0
304
+ }
305
+
306
+ return error_summary, error_token_stats
307
+
308
+ def format_response(self, url: str, title: str, summary: str, token_stats: dict = None) -> str:
309
+ """Format the response message"""
310
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
311
+
312
+ response = f"""🔗 原始網址: {url}
313
+ 📰 標題: {title}
314
+
315
+ {summary}
316
+
317
+ ---
318
+ ⏰ 處理時間: {timestamp}"""
319
+
320
+ # 加入 token 使用統計
321
+ if token_stats:
322
+ response += f"""
323
+ 📊 Token 使用量: 輸入 {token_stats['prompt_tokens']} + 輸出 {token_stats['completion_tokens']} = 總計 {token_stats['total_tokens']} tokens"""
324
+
325
+ return response
326
+
327
+ def format_error_response(self, url: str, error_message: str) -> str:
328
+ """Format error response message"""
329
+ return f"""❌ 處理失敗: {url}
330
+ 🔍 錯誤原因: {error_message}
331
+ 💡 建議: 請檢查網址是否正確或稍後再試"""
332
+
333
+ # Global processor instance and deduplication cache
334
+ processor = URLProcessor(config)
335
+ processing_cache = set() # 用於去重的快取
336
+
337
+ async def process_url_async(url: str, channel: str, say):
338
+ """Asynchronous URL processing pipeline"""
339
+ # 建立唯一的處理 ID
340
+ process_id = f"{url}:{channel}:{int(datetime.now().timestamp())//60}" # 每分鐘重置
341
+
342
+ # 檢查是否已經在處理中
343
+ if process_id in processing_cache:
344
+ logger.info(f"URL {url} is already being processed, skipping duplicate")
345
+ return
346
+
347
+ # 添加到處理快取
348
+ processing_cache.add(process_id)
349
+
350
+ try:
351
+ logger.info(f"Starting to process URL: {url}")
352
+
353
+ async with URLProcessor(config) as proc:
354
+ # Step 1: Extract content
355
+ logger.info(f"Step 1: Extracting content from {url}")
356
+ content = await proc.extract_content(url)
357
+ logger.info(f"Content extracted successfully. Title: {content.get('title', 'N/A')}")
358
+
359
+ # Step 2: Summarize and translate
360
+ logger.info(f"Step 2: Summarizing and translating content for {url}")
361
+ try:
362
+ result = await proc.summarize_and_translate(content)
363
+
364
+ # 處理回傳值 - 可能是 tuple 或只是 string
365
+ if isinstance(result, tuple):
366
+ summary, token_stats = result
367
+ else:
368
+ summary = result
369
+ token_stats = None
370
+
371
+ logger.info(f"Summary generated successfully for {url}")
372
+ except Exception as e:
373
+ logger.error(f"Error in summarization, trying fallback: {str(e)}")
374
+ # 如果 AI 處理失敗,提供基本回應
375
+ summary = f"抱歉,由於技術問題無法生成摘要。請直接查看原始網址:{url}"
376
+ token_stats = None
377
+
378
+ # Step 3: Format and send response
379
+ logger.info(f"Step 3: Formatting and sending response for {url}")
380
+ response = proc.format_response(url, content['title'], summary, token_stats)
381
+
382
+ # Send to Slack (使用同步的 say 函數)
383
+ say(channel=channel, text=response)
384
+
385
+ logger.info(f"Successfully processed and sent response for: {url}")
386
+
387
+ except Exception as e:
388
+ logger.error(f"Error processing URL {url}: {str(e)}", exc_info=True)
389
+ error_message = processor.format_error_response(url, str(e))
390
+ say(channel=channel, text=error_message)
391
+ finally:
392
+ # 處理完成後從快取中移除(延遲5秒)
393
+ import threading
394
+ def remove_from_cache():
395
+ import time
396
+ time.sleep(5)
397
+ processing_cache.discard(process_id)
398
+
399
+ threading.Thread(target=remove_from_cache).start()
400
+
401
+ # Slack event handlers
402
+ @slack_app.event("message")
403
+ def handle_message(event, say, ack):
404
+ """Handle incoming Slack messages"""
405
+ ack() # 確認收到事件
406
+
407
+ try:
408
+ logger.info(f"Received message event: {event}")
409
+
410
+ # Skip bot messages
411
+ if event.get('bot_id'):
412
+ logger.info("Skipping bot message")
413
+ return
414
+
415
+ # Skip app_mention events (這些會由 handle_app_mention 處理)
416
+ if event.get('type') == 'app_mention':
417
+ logger.info("Skipping app_mention in message handler")
418
+ return
419
+
420
+ # Skip messages without text
421
+ if 'text' not in event:
422
+ logger.info("Skipping message without text")
423
+ return
424
+
425
+ message_text = event.get('text', '')
426
+ channel = event.get('channel')
427
+ user = event.get('user')
428
+
429
+ # 檢查是否為提及機器人的訊息 (避免重複處理)
430
+ if '<@U094J502LLC>' in message_text:
431
+ logger.info("Skipping mention message in message handler (will be handled by app_mention)")
432
+ return
433
+
434
+ logger.info(f"Processing message from user {user} in channel {channel}: {message_text}")
435
+
436
+ # Extract URLs from message
437
+ urls = processor.extract_urls(message_text)
438
+
439
+ if not urls:
440
+ logger.info("No URLs found in message")
441
+ return
442
+
443
+ logger.info(f"Found {len(urls)} URLs: {urls}")
444
+
445
+ # Send initial acknowledgment for multiple URLs
446
+ if len(urls) > 1:
447
+ say(
448
+ channel=channel,
449
+ text=f"🔄 正在處理 {len(urls)} 個網址,請稍候..."
450
+ )
451
+
452
+ # Process each URL asynchronously
453
+ import threading
454
+ for url in urls:
455
+ logger.info(f"Creating thread for URL: {url}")
456
+ thread = threading.Thread(
457
+ target=lambda u=url: asyncio.run(process_url_async(u, channel, say))
458
+ )
459
+ thread.start()
460
+
461
+ except Exception as e:
462
+ logger.error(f"Error in message handler: {str(e)}", exc_info=True)
463
+ say(
464
+ channel=event.get('channel'),
465
+ text="❌ 處理訊息時發生錯誤,請稍後再試"
466
+ )
467
+
468
+ @slack_app.event("app_mention")
469
+ def handle_app_mention(event, say, ack):
470
+ """Handle app mentions"""
471
+ ack() # 確認收到事件
472
+
473
+ logger.info(f"Received app mention: {event}")
474
+
475
+ # 檢查訊息中是否包含 URL
476
+ message_text = event.get('text', '')
477
+ urls = processor.extract_urls(message_text)
478
+
479
+ if urls:
480
+ # 如果有 URL,則處理 URL
481
+ logger.info(f"App mention contains URLs: {urls}")
482
+
483
+ # Send initial acknowledgment
484
+ say(
485
+ channel=event['channel'],
486
+ text=f"🔄 收到!正在處理 {len(urls)} 個網址..."
487
+ )
488
+
489
+ # Process URLs in threads
490
+ import threading
491
+ for url in urls:
492
+ logger.info(f"Creating thread for app mention URL: {url}")
493
+ thread = threading.Thread(
494
+ target=lambda u=url: asyncio.run(process_url_async(u, event['channel'], say))
495
+ )
496
+ thread.start()
497
+ else:
498
+ # 沒有 URL,回覆歡迎訊息
499
+ say(
500
+ channel=event["channel"],
501
+ text="👋 你好!我是網址摘要機器人。只要在頻道中貼上網址,我就會自動為你生成繁體中文摘要!"
502
+ )
503
+
504
+ # FastAPI routes
505
+ @api.get("/")
506
+ async def root():
507
+ """Health check endpoint"""
508
+ return {"status": "healthy", "service": "Slack URL Summarizer Bot"}
509
+
510
+ @api.get("/health")
511
+ async def health_check():
512
+ """Detailed health check"""
513
+ return {
514
+ "status": "healthy",
515
+ "timestamp": datetime.now().isoformat(),
516
+ "config": {
517
+ "slack_configured": bool(config.slack_bot_token),
518
+ "azure_openai_configured": bool(config.azure_openai_endpoint),
519
+ }
520
+ }
521
+
522
+ @api.get("/slack/events")
523
+ async def slack_events_get():
524
+ """Handle GET requests to slack events endpoint"""
525
+ return {"message": "Slack events endpoint is ready", "methods": ["POST"]}
526
+
527
+ @api.post("/slack/events")
528
+ async def slack_events(request: Request):
529
+ """Handle Slack events"""
530
+ try:
531
+ # Get the request body
532
+ body = await request.body()
533
+
534
+ # Parse JSON
535
+ import json
536
+ data = json.loads(body)
537
+
538
+ # Handle URL verification challenge
539
+ if data.get("type") == "url_verification":
540
+ challenge = data.get("challenge")
541
+ logger.info(f"Received URL verification challenge: {challenge}")
542
+ return {"challenge": challenge}
543
+
544
+ # Handle regular Slack events
545
+ logger.info(f"Received Slack event: {data.get('type')}")
546
+ return await handler.handle(request)
547
+
548
+ except json.JSONDecodeError:
549
+ logger.error("Invalid JSON in Slack request")
550
+ raise HTTPException(status_code=400, detail="Invalid JSON")
551
+ except Exception as e:
552
+ logger.error(f"Error handling Slack event: {str(e)}")
553
+ raise HTTPException(status_code=500, detail="Internal server error")
554
+
555
+ # Error handling middleware
556
+ @api.exception_handler(Exception)
557
+ async def global_exception_handler(request: Request, exc: Exception):
558
+ logger.error(f"Unhandled exception: {str(exc)}")
559
+ raise HTTPException(status_code=500, detail="Internal server error")
560
+
561
+ if __name__ == "__main__":
562
+ # Run the FastAPI application
563
+ uvicorn.run(
564
+ "main:api",
565
+ host="0.0.0.0",
566
+ port=int(os.getenv("PORT", 7860)),
567
+ log_level="info",
568
+ reload=os.getenv("ENVIRONMENT") == "development"
569
+ )
pyproject.toml ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "slack-url-bot"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "aiohttp>=3.12.13",
9
+ "fastapi>=0.115.14",
10
+ "httpx>=0.28.1",
11
+ "lxml[html-clean]>=6.0.0",
12
+ "newspaper3k>=0.2.8",
13
+ "pytest>=8.4.1",
14
+ "pytest-asyncio>=1.0.0",
15
+ "python-dotenv>=1.1.1",
16
+ "ruff>=0.12.2",
17
+ "slack-bolt>=1.23.0",
18
+ "slack-sdk>=3.35.0",
19
+ "uvicorn>=0.35.0",
20
+ ]
21
+
22
+ [dependency-groups]
23
+ dev = [
24
+ "ruff>=0.12.2",
25
+ ]
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ aiohttp>=3.12.13
2
+ fastapi>=0.115.14
3
+ httpx>=0.28.1
4
+ lxml[html-clean]>=6.0.0
5
+ newspaper3k>=0.2.8
6
+ python-dotenv>=1.1.1
7
+ slack-bolt>=1.23.0
8
+ slack-sdk>=3.35.0
9
+ uvicorn>=0.35.0
uv.lock ADDED
The diff for this file is too large to render. See raw diff