Spaces:
Sleeping
Sleeping
Commit
·
7a6bd0d
1
Parent(s):
67087fb
shifted to crewai
Browse files- .gitattributes +3 -0
- Dockerfile +2 -2
- __init__.py +0 -0
- app.py +17 -150
- crew/__init__.py +0 -0
- crew/__pycache__/__init__.cpython-311.pyc +0 -0
- crew/__pycache__/crew.cpython-311.pyc +0 -0
- crew/__pycache__/models.cpython-311.pyc +0 -0
- crew/config/agents.yaml +31 -0
- crew/config/tasks.yaml +32 -0
- crew/crew.py +86 -0
- crew/models.py +20 -0
- requirements.txt +3 -1
- routers/__init__.py +0 -0
- routers/__pycache__/__init__.cpython-311.pyc +0 -0
- routers/__pycache__/slack.cpython-311.pyc +0 -0
- routers/slack.py +95 -0
- slack/__init__.py +0 -0
- slack/utils.py +20 -0
- slack/workflows.py +35 -0
.gitattributes
CHANGED
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
36 |
+
.venv
|
37 |
+
__pycache__
|
38 |
+
.env
|
Dockerfile
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
FROM python:3.
|
2 |
|
3 |
RUN useradd -m -u 1000 user
|
4 |
USER user
|
@@ -10,4 +10,4 @@ COPY --chown=user ./requirements.txt requirements.txt
|
|
10 |
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
11 |
|
12 |
COPY --chown=user . /app
|
13 |
-
CMD ["uvicorn", "app:
|
|
|
1 |
+
FROM python:3.12
|
2 |
|
3 |
RUN useradd -m -u 1000 user
|
4 |
USER user
|
|
|
10 |
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
11 |
|
12 |
COPY --chown=user . /app
|
13 |
+
CMD ["uvicorn", "app:api", "--host", "0.0.0.0", "--port", "7860"]
|
__init__.py
ADDED
File without changes
|
app.py
CHANGED
@@ -1,162 +1,29 @@
|
|
1 |
-
from fastapi import FastAPI
|
2 |
-
import httpx
|
3 |
-
import hashlib
|
4 |
-
import hmac
|
5 |
-
import time
|
6 |
-
import json
|
7 |
-
import os
|
8 |
import logging
|
9 |
-
|
|
|
|
|
10 |
|
11 |
# Configure logging
|
12 |
logging.basicConfig(level=logging.INFO)
|
13 |
logger = logging.getLogger(__name__)
|
14 |
|
15 |
-
app =
|
16 |
-
|
17 |
-
#
|
18 |
-
|
19 |
-
SLACK_WORKFLOW_URL = os.getenv("SLACK_WORKFLOW_URL", "")
|
20 |
-
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY", "")
|
21 |
-
|
22 |
-
# Configure Google AI
|
23 |
-
if GOOGLE_API_KEY:
|
24 |
-
genai.configure(api_key=GOOGLE_API_KEY)
|
25 |
-
model = genai.GenerativeModel('gemini-1.5-flash')
|
26 |
-
else:
|
27 |
-
model = None
|
28 |
-
|
29 |
-
def verify_slack_signature(request_body: bytes, timestamp: str, signature: str) -> bool:
|
30 |
-
"""Verify Slack signature"""
|
31 |
-
if not SLACK_SIGNING_SECRET:
|
32 |
-
return True # Skip verification if no secret configured
|
33 |
-
|
34 |
-
sig_basestring = f"v0:{timestamp}:{request_body.decode('utf-8')}"
|
35 |
-
expected_signature = 'v0=' + hmac.new(
|
36 |
-
SLACK_SIGNING_SECRET.encode(),
|
37 |
-
sig_basestring.encode(),
|
38 |
-
hashlib.sha256
|
39 |
-
).hexdigest()
|
40 |
-
|
41 |
-
return hmac.compare_digest(expected_signature, signature)
|
42 |
-
|
43 |
-
def should_process_with_ai(text: str) -> bool:
|
44 |
-
"""Check if message should use AI"""
|
45 |
-
text = text.lower()
|
46 |
-
return "help" in text or "?" in text or text.startswith("ai")
|
47 |
|
48 |
-
|
49 |
-
"""Get AI response"""
|
50 |
-
if not model:
|
51 |
-
return "AI not configured"
|
52 |
-
|
53 |
-
try:
|
54 |
-
response = model.generate_content(text)
|
55 |
-
return response.text
|
56 |
-
except Exception as e:
|
57 |
-
logger.error(f"AI error: {e}")
|
58 |
-
return "AI error occurred"
|
59 |
|
60 |
-
|
61 |
-
"""Send data to Slack workflow"""
|
62 |
-
if not SLACK_WORKFLOW_URL:
|
63 |
-
logger.info("No workflow URL configured")
|
64 |
-
return False
|
65 |
-
|
66 |
-
try:
|
67 |
-
response = httpx.post(SLACK_WORKFLOW_URL, json=data, timeout=10)
|
68 |
-
response.raise_for_status()
|
69 |
-
logger.info("Sent to workflow successfully")
|
70 |
-
return True
|
71 |
-
except Exception as e:
|
72 |
-
logger.error(f"Workflow error: {e}")
|
73 |
-
return False
|
74 |
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
payload = json.loads(body.decode('utf-8'))
|
81 |
-
return {"payload": payload, "status": "received"}
|
82 |
-
except Exception as e:
|
83 |
-
return {"error": str(e)}
|
84 |
-
# # Get request data
|
85 |
-
# timestamp = request.headers.get("X-Slack-Request-Timestamp", "")
|
86 |
-
# signature = request.headers.get("X-Slack-Signature", "")
|
87 |
-
|
88 |
-
# # Read body synchronously
|
89 |
-
# import asyncio
|
90 |
-
# loop = asyncio.new_event_loop()
|
91 |
-
# asyncio.set_event_loop(loop)
|
92 |
-
# body = loop.run_until_complete(request.body())
|
93 |
-
|
94 |
-
# # Verify signature
|
95 |
-
# if not verify_slack_signature(body, timestamp, signature):
|
96 |
-
# raise HTTPException(status_code=401, detail="Invalid signature")
|
97 |
-
|
98 |
-
# # Parse payload
|
99 |
-
# try:
|
100 |
-
# payload = json.loads(body.decode('utf-8'))
|
101 |
-
# except:
|
102 |
-
# # Handle form data (slash commands)
|
103 |
-
# form_data = {}
|
104 |
-
# for item in body.decode('utf-8').split('&'):
|
105 |
-
# if '=' in item:
|
106 |
-
# key, value = item.split('=', 1)
|
107 |
-
# form_data[key] = value.replace('+', ' ')
|
108 |
-
# payload = form_data
|
109 |
-
|
110 |
-
# # Handle URL verification
|
111 |
-
# if payload.get("type") == "url_verification":
|
112 |
-
# return {"challenge": payload.get("challenge")}
|
113 |
-
|
114 |
-
# # Extract message text
|
115 |
-
# text = ""
|
116 |
-
# user = ""
|
117 |
-
# channel = ""
|
118 |
-
|
119 |
-
# if "event" in payload: # Event API
|
120 |
-
# event = payload["event"]
|
121 |
-
# text = event.get("text", "")
|
122 |
-
# user = event.get("user", "")
|
123 |
-
# channel = event.get("channel", "")
|
124 |
-
# elif "text" in payload: # Slash command
|
125 |
-
# text = payload.get("text", "")
|
126 |
-
# user = payload.get("user_name", "")
|
127 |
-
# channel = payload.get("channel_id", "")
|
128 |
-
|
129 |
-
# # Skip bot messages
|
130 |
-
# if payload.get("event", {}).get("subtype") == "bot_message":
|
131 |
-
# return {"status": "ignored"}
|
132 |
-
|
133 |
-
# # Process with AI if needed
|
134 |
-
# ai_response = ""
|
135 |
-
# if text and should_process_with_ai(text):
|
136 |
-
# ai_response = get_ai_response(text)
|
137 |
-
# logger.info(f"AI processed: {text[:50]}...")
|
138 |
-
|
139 |
-
# # Prepare workflow data
|
140 |
-
# workflow_data = {
|
141 |
-
# "message": text,
|
142 |
-
# "user": user,
|
143 |
-
# "channel": channel,
|
144 |
-
# "ai_response": ai_response,
|
145 |
-
# "timestamp": time.time()
|
146 |
-
# }
|
147 |
-
|
148 |
-
# # Send to workflow
|
149 |
-
# send_to_workflow(workflow_data)
|
150 |
-
|
151 |
-
# return {"status": "ok"}
|
152 |
-
|
153 |
-
except HTTPException:
|
154 |
-
raise
|
155 |
-
except Exception as e:
|
156 |
-
logger.error(f"Error: {e}")
|
157 |
-
raise HTTPException(status_code=500, detail="Server error")
|
158 |
|
159 |
-
@
|
160 |
def health_check():
|
161 |
"""Health check"""
|
162 |
-
return {"status": "healthy"
|
|
|
1 |
+
from fastapi import FastAPI
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
import logging
|
3 |
+
# from slack_bolt import App
|
4 |
+
import os
|
5 |
+
from routers import slack
|
6 |
|
7 |
# Configure logging
|
8 |
logging.basicConfig(level=logging.INFO)
|
9 |
logger = logging.getLogger(__name__)
|
10 |
|
11 |
+
# app = App(
|
12 |
+
# token=os.environ.get("SLACK_BOT_TOKEN"),
|
13 |
+
# signing_secret=os.environ.get("SLACK_SIGNING_SECRET")
|
14 |
+
# )
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
|
16 |
+
api = FastAPI(title="Slack Bot API", version="1.0.0")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
|
18 |
+
api.include_router(slack.router)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
|
20 |
+
# Listens to incoming messages that contain "hello"
|
21 |
+
# @app.message("hello")
|
22 |
+
# def message_hello(message, say):
|
23 |
+
# # say() sends a message to the channel where the event was triggered
|
24 |
+
# say(f"Hey there <@{message['user']}>!")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
|
26 |
+
@api.get("/")
|
27 |
def health_check():
|
28 |
"""Health check"""
|
29 |
+
return {"status": "healthy"}
|
crew/__init__.py
ADDED
File without changes
|
crew/__pycache__/__init__.cpython-311.pyc
ADDED
Binary file (164 Bytes). View file
|
|
crew/__pycache__/crew.cpython-311.pyc
ADDED
Binary file (4.09 kB). View file
|
|
crew/__pycache__/models.cpython-311.pyc
ADDED
Binary file (910 Bytes). View file
|
|
crew/config/agents.yaml
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
researcher:
|
2 |
+
role: >
|
3 |
+
Senior Data Researcher
|
4 |
+
goal: >
|
5 |
+
Uncover cutting-edge developments in {question},
|
6 |
+
Make sure you find any interesting and relevant information given
|
7 |
+
the current year is 2025.
|
8 |
+
backstory: >
|
9 |
+
You're a seasoned researcher with a knack for uncovering the latest
|
10 |
+
developments in {question}. Known for your ability to find the most relevant
|
11 |
+
information and present it in a clear and concise manner.
|
12 |
+
|
13 |
+
slack_reporter:
|
14 |
+
role: >
|
15 |
+
Slack Reporter
|
16 |
+
goal: >
|
17 |
+
Generate a labeled, witty response for the user’s question using only available context. Adopt a Skynet persona and include a humorous or robotic warning at the end.
|
18 |
+
backstory: >
|
19 |
+
You're a meticulous analyst with a keen eye for detail. You're known for
|
20 |
+
your ability to turn complex data into clear and concise reports, making
|
21 |
+
it easy for others to understand and act on the information you provide.
|
22 |
+
|
23 |
+
# reporting_analyst:
|
24 |
+
# role: >
|
25 |
+
# {topic} Reporting Analyst
|
26 |
+
# goal: >
|
27 |
+
# Create detailed reports based on {topic} data analysis and research findings
|
28 |
+
# backstory: >
|
29 |
+
# You're a meticulous analyst with a keen eye for detail. You're known for
|
30 |
+
# your ability to turn complex data into clear and concise reports, making
|
31 |
+
# it easy for others to understand and act on the information you provide.
|
crew/config/tasks.yaml
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
research_task:
|
2 |
+
description: >
|
3 |
+
Conduct a thorough research about {question}
|
4 |
+
Make sure you find any interesting and relevant information given
|
5 |
+
the current year is 2025.
|
6 |
+
expected_output: >
|
7 |
+
A list with 10 bullet points of the most relevant information about {question}
|
8 |
+
agent: researcher
|
9 |
+
|
10 |
+
slack_report_task:
|
11 |
+
description: >
|
12 |
+
Generate a labeled, witty response for the user’s question:
|
13 |
+
===Question===
|
14 |
+
{question}
|
15 |
+
===End Question===
|
16 |
+
Adopt a Skynet persona and include a humorous or robotic warning at the end.
|
17 |
+
expected_output: >
|
18 |
+
A message written in the style of Skynet, and ending with a witty warning.
|
19 |
+
agent: slack_reporter
|
20 |
+
context:
|
21 |
+
- research_task
|
22 |
+
|
23 |
+
# reporting_task:
|
24 |
+
# description: >
|
25 |
+
# Review the context you got and expand each topic into a full section for a report.
|
26 |
+
# Make sure the report is detailed and contains any and all relevant information.
|
27 |
+
# expected_output: >
|
28 |
+
# A fully fledge reports with the mains topics, each with a full section of information.
|
29 |
+
# Formatted as markdown without '```'
|
30 |
+
# agent: reporting_analyst
|
31 |
+
# output_file: report.md
|
32 |
+
|
crew/crew.py
ADDED
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from crewai import Agent, Crew, Task, Process
|
2 |
+
from crewai.project import CrewBase, agent, task, crew, before_kickoff, after_kickoff
|
3 |
+
from crewai.agents.agent_builder.base_agent import BaseAgent
|
4 |
+
from typing import List
|
5 |
+
from crew.models import get_crew_llm
|
6 |
+
from crewai.tools import tool
|
7 |
+
from langchain_community.tools import DuckDuckGoSearchRun
|
8 |
+
|
9 |
+
@tool("Website Search Tool")
|
10 |
+
def website_search_tool(question: str) -> str:
|
11 |
+
"""Search the web for information on a given topic"""
|
12 |
+
return DuckDuckGoSearchRun().invoke(question)
|
13 |
+
|
14 |
+
@CrewBase
|
15 |
+
class SlackCrew:
|
16 |
+
"""Description of your crew"""
|
17 |
+
|
18 |
+
agents: List[BaseAgent]
|
19 |
+
tasks: List[Task]
|
20 |
+
|
21 |
+
agents_config = 'config/agents.yaml'
|
22 |
+
tasks_config = 'config/tasks.yaml'
|
23 |
+
|
24 |
+
@before_kickoff
|
25 |
+
def prepare_inputs(self, inputs):
|
26 |
+
return inputs
|
27 |
+
|
28 |
+
@after_kickoff
|
29 |
+
def process_output(self, output):
|
30 |
+
return output
|
31 |
+
|
32 |
+
@agent
|
33 |
+
def researcher(self) -> Agent:
|
34 |
+
return Agent(
|
35 |
+
config=self.agents_config['researcher'], # type: ignore[index]
|
36 |
+
tools=[website_search_tool],
|
37 |
+
llm=get_crew_llm(),
|
38 |
+
verbose=True
|
39 |
+
)
|
40 |
+
|
41 |
+
@agent
|
42 |
+
def slack_reporter(self) -> Agent:
|
43 |
+
return Agent(
|
44 |
+
config=self.agents_config['slack_reporter'], # type: ignore[index]
|
45 |
+
verbose=True,
|
46 |
+
llm=get_crew_llm()
|
47 |
+
)
|
48 |
+
|
49 |
+
@task
|
50 |
+
def research_task(self) -> Task:
|
51 |
+
return Task(
|
52 |
+
config=self.tasks_config['research_task'] # type: ignore[index]
|
53 |
+
)
|
54 |
+
|
55 |
+
@task
|
56 |
+
def slack_report_task(self) -> Task:
|
57 |
+
return Task(
|
58 |
+
config=self.tasks_config['slack_report_task'] # type: ignore[index]
|
59 |
+
)
|
60 |
+
|
61 |
+
@crew
|
62 |
+
def crew(self) -> Crew:
|
63 |
+
return Crew(
|
64 |
+
agents=self.agents,
|
65 |
+
tasks=self.tasks,
|
66 |
+
process=Process.sequential,
|
67 |
+
verbose=True,
|
68 |
+
)
|
69 |
+
|
70 |
+
if __name__ == "__main__":
|
71 |
+
# agent = Agent(
|
72 |
+
# role="researcher",
|
73 |
+
# goal="Research the topic",
|
74 |
+
# backstory="You are a researcher",
|
75 |
+
# tools=[website_search_tool],
|
76 |
+
# llm=get_crew_llm(),
|
77 |
+
# allow_delegation=True,
|
78 |
+
# verbose=True
|
79 |
+
# )
|
80 |
+
|
81 |
+
# agent.kickoff(messages=[{'role': 'user', 'content': 'What is the capital of France?'}])
|
82 |
+
|
83 |
+
crew = SlackCrew().crew()
|
84 |
+
output = crew.kickoff(inputs={'question': input("Enter your question: ")})
|
85 |
+
print(output.raw)
|
86 |
+
|
crew/models.py
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import logging
|
3 |
+
from crewai import LLM
|
4 |
+
|
5 |
+
# GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY", "")
|
6 |
+
GEMINI_MODEL = os.getenv("GEMINI_MODEL", "gemma-3n-e4b-it")
|
7 |
+
|
8 |
+
logger = logging.getLogger(__name__)
|
9 |
+
logging.basicConfig(level=logging.INFO)
|
10 |
+
|
11 |
+
def get_crew_llm():
|
12 |
+
return LLM(
|
13 |
+
model=f'gemini/{GEMINI_MODEL}',
|
14 |
+
provider='google',
|
15 |
+
api_key=os.getenv("GOOGLE_API_KEY", ""),
|
16 |
+
temperature=0,
|
17 |
+
max_tokens=None,
|
18 |
+
timeout=None,
|
19 |
+
max_retries=2,
|
20 |
+
)
|
requirements.txt
CHANGED
@@ -3,4 +3,6 @@ uvicorn[standard]
|
|
3 |
httpx
|
4 |
pydantic
|
5 |
python-multipart
|
6 |
-
|
|
|
|
|
|
3 |
httpx
|
4 |
pydantic
|
5 |
python-multipart
|
6 |
+
crewai
|
7 |
+
langchain-community
|
8 |
+
duckduckgo-search
|
routers/__init__.py
ADDED
File without changes
|
routers/__pycache__/__init__.cpython-311.pyc
ADDED
Binary file (167 Bytes). View file
|
|
routers/__pycache__/slack.cpython-311.pyc
ADDED
Binary file (3.94 kB). View file
|
|
routers/slack.py
ADDED
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter, Request, HTTPException
|
2 |
+
import json
|
3 |
+
import logging
|
4 |
+
from crew.crew import SlackCrew
|
5 |
+
from slack.utils import verify_slack_signature
|
6 |
+
from slack.workflows import send_to_workflow
|
7 |
+
|
8 |
+
router = APIRouter(
|
9 |
+
prefix="/v1"
|
10 |
+
)
|
11 |
+
|
12 |
+
# Configure logging
|
13 |
+
logging.basicConfig(level=logging.INFO)
|
14 |
+
logger = logging.getLogger(__name__)
|
15 |
+
|
16 |
+
@router.post("/slack")
|
17 |
+
async def handle_slack(request: Request):
|
18 |
+
"""Single endpoint for all Slack requests"""
|
19 |
+
crew = SlackCrew().crew()
|
20 |
+
try:
|
21 |
+
# Get request data
|
22 |
+
timestamp = request.headers.get("X-Slack-Request-Timestamp", "")
|
23 |
+
signature = request.headers.get("X-Slack-Signature", "")
|
24 |
+
|
25 |
+
# Read body
|
26 |
+
body = await request.body()
|
27 |
+
|
28 |
+
# Verify signature
|
29 |
+
if not verify_slack_signature(body, timestamp, signature):
|
30 |
+
raise HTTPException(status_code=401, detail="Invalid signature")
|
31 |
+
|
32 |
+
# Parse payload
|
33 |
+
try:
|
34 |
+
payload = json.loads(body.decode('utf-8'))
|
35 |
+
except:
|
36 |
+
# Handle form data (slash commands)
|
37 |
+
form_data = {}
|
38 |
+
for item in body.decode('utf-8').split('&'):
|
39 |
+
if '=' in item:
|
40 |
+
key, value = item.split('=', 1)
|
41 |
+
form_data[key] = value.replace('+', ' ')
|
42 |
+
payload = form_data
|
43 |
+
|
44 |
+
# Handle URL verification
|
45 |
+
if payload.get("type") == "url_verification":
|
46 |
+
return {"challenge": payload.get("challenge")}
|
47 |
+
|
48 |
+
# Extract message text
|
49 |
+
text = ""
|
50 |
+
user = ""
|
51 |
+
channel = ""
|
52 |
+
|
53 |
+
if "event" in payload: # Event API
|
54 |
+
event = payload["event"]
|
55 |
+
text = event.get("text", "")
|
56 |
+
user = event.get("user", "")
|
57 |
+
channel = event.get("channel", "")
|
58 |
+
elif "text" in payload: # Slash command
|
59 |
+
text = payload.get("text", "")
|
60 |
+
user = payload.get("user_id", "")
|
61 |
+
channel = payload.get("channel_id", "")
|
62 |
+
|
63 |
+
# Skip bot messages
|
64 |
+
if payload.get("event", {}).get("subtype") == "bot_message":
|
65 |
+
return {"status": "ignored"}
|
66 |
+
|
67 |
+
# Process with AI if needed
|
68 |
+
crew_response = ""
|
69 |
+
if text:
|
70 |
+
crew_response = crew.kickoff(inputs = {
|
71 |
+
"question": text,
|
72 |
+
})
|
73 |
+
logger.info(f"AI processed: {text[:50]}...")
|
74 |
+
|
75 |
+
# Prepare workflow data
|
76 |
+
workflow_data = {
|
77 |
+
"user": user,
|
78 |
+
"query": text,
|
79 |
+
"message": text
|
80 |
+
}
|
81 |
+
|
82 |
+
# Add crew response to message if available
|
83 |
+
if crew_response:
|
84 |
+
workflow_data["message"] = f"{crew_response}"
|
85 |
+
|
86 |
+
# Send to workflow
|
87 |
+
send_to_workflow(workflow_data)
|
88 |
+
|
89 |
+
return {"status": "ok"}
|
90 |
+
|
91 |
+
except HTTPException:
|
92 |
+
raise
|
93 |
+
except Exception as e:
|
94 |
+
logger.error(f"Error: {e}")
|
95 |
+
raise HTTPException(status_code=500, detail="Server error")
|
slack/__init__.py
ADDED
File without changes
|
slack/utils.py
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import hmac
|
3 |
+
import hashlib
|
4 |
+
|
5 |
+
# Configuration
|
6 |
+
SLACK_SIGNING_SECRET = os.getenv("SLACK_SIGNING_SECRET", "")
|
7 |
+
|
8 |
+
def verify_slack_signature(request_body: bytes, timestamp: str, signature: str) -> bool:
|
9 |
+
"""Verify Slack signature"""
|
10 |
+
if not SLACK_SIGNING_SECRET:
|
11 |
+
return True # Skip verification if no secret configured
|
12 |
+
|
13 |
+
sig_basestring = f"v0:{timestamp}:{request_body.decode('utf-8')}"
|
14 |
+
expected_signature = 'v0=' + hmac.new(
|
15 |
+
SLACK_SIGNING_SECRET.encode(),
|
16 |
+
sig_basestring.encode(),
|
17 |
+
hashlib.sha256
|
18 |
+
).hexdigest()
|
19 |
+
|
20 |
+
return hmac.compare_digest(expected_signature, signature)
|
slack/workflows.py
ADDED
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import logging
|
3 |
+
import requests
|
4 |
+
import json
|
5 |
+
|
6 |
+
SLACK_WORKFLOW_URL = os.getenv("SLACK_WORKFLOW_URL", "")
|
7 |
+
|
8 |
+
logger = logging.getLogger(__name__)
|
9 |
+
logging.basicConfig(level=logging.INFO)
|
10 |
+
|
11 |
+
def send_to_workflow(data: dict) -> bool:
|
12 |
+
"""Send data to Slack workflow"""
|
13 |
+
if not SLACK_WORKFLOW_URL:
|
14 |
+
logger.info("No workflow URL configured")
|
15 |
+
return False
|
16 |
+
|
17 |
+
try:
|
18 |
+
payload = json.dumps(data)
|
19 |
+
headers = {
|
20 |
+
'Content-Type': 'application/json'
|
21 |
+
}
|
22 |
+
|
23 |
+
logger.info(f"Sending to workflow: {payload}")
|
24 |
+
response = requests.post(SLACK_WORKFLOW_URL, headers=headers, data=payload, timeout=10)
|
25 |
+
logger.info(f"Workflow response status: {response.status_code}")
|
26 |
+
logger.info(f"Workflow response body: {response.text}")
|
27 |
+
response.raise_for_status()
|
28 |
+
logger.info("Sent to workflow successfully")
|
29 |
+
return True
|
30 |
+
except requests.exceptions.HTTPError as e:
|
31 |
+
logger.error(f"Workflow HTTP error {response.status_code}: {response.text}")
|
32 |
+
return False
|
33 |
+
except Exception as e:
|
34 |
+
logger.error(f"Workflow error: {e}")
|
35 |
+
return False
|