Spaces:
Runtime error
Runtime error
feat: Set up initial project structure for slack_url_bot including core application, dependencies, documentation, and build automation.
Browse files- .python-version +1 -0
- Dockerfile +62 -0
- Makefile +60 -0
- PRD.md +313 -0
- README.md +80 -11
- main.py +569 -0
- pyproject.toml +25 -0
- requirements.txt +9 -0
- 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
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo: purple
|
| 6 |
-
sdk: docker
|
| 7 |
-
pinned: false
|
| 8 |
-
license: mit
|
| 9 |
-
---
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|