Spaces:
No application file
No application file
first commit
Browse files- .gitignore +4 -0
- DockerFile +16 -0
- all_tools.py +1678 -0
- app.py +1688 -0
- calendar_tools.py +118 -0
- config.py +199 -0
- db.py +81 -0
- prompt.py +698 -0
- requirements.txt +20 -0
- services.py +218 -0
- utils.py +75 -0
.gitignore
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.env
|
| 2 |
+
ai-schedule
|
| 3 |
+
flask_session
|
| 4 |
+
credentials.json
|
DockerFile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker
|
| 2 |
+
# you will also find guides on how best to write your Dockerfile
|
| 3 |
+
|
| 4 |
+
FROM python:3.9
|
| 5 |
+
|
| 6 |
+
RUN useradd -m -u 1000 user
|
| 7 |
+
USER user
|
| 8 |
+
ENV PATH="/home/user/.local/bin:$PATH"
|
| 9 |
+
|
| 10 |
+
WORKDIR /app
|
| 11 |
+
|
| 12 |
+
COPY --chown=user ./requirements.txt requirements.txt
|
| 13 |
+
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
| 14 |
+
|
| 15 |
+
COPY --chown=user . /app
|
| 16 |
+
CMD ["python", "app.py"]
|
all_tools.py
ADDED
|
@@ -0,0 +1,1678 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# import json
|
| 2 |
+
# import os
|
| 3 |
+
# import requests
|
| 4 |
+
# import base64
|
| 5 |
+
# import msal
|
| 6 |
+
# import time
|
| 7 |
+
# from typing import Type, Optional, List
|
| 8 |
+
|
| 9 |
+
# from dotenv import load_dotenv
|
| 10 |
+
# from pydantic.v1 import BaseModel, Field
|
| 11 |
+
# from msal import ConfidentialClientApplication
|
| 12 |
+
|
| 13 |
+
# from langchain_core.tools import BaseTool
|
| 14 |
+
# from slack_sdk.errors import SlackApiError
|
| 15 |
+
# from datetime import datetime
|
| 16 |
+
# from google_auth_oauthlib.flow import InstalledAppFlow
|
| 17 |
+
# from googleapiclient.discovery import build
|
| 18 |
+
# from google.oauth2.credentials import Credentials
|
| 19 |
+
# from google.auth.transport.requests import Request
|
| 20 |
+
# from services import construct_google_calendar_client
|
| 21 |
+
# from config import client
|
| 22 |
+
# from datetime import datetime, timedelta
|
| 23 |
+
# import pytz
|
| 24 |
+
# from typing import List, Dict, Optional
|
| 25 |
+
# from collections import defaultdict
|
| 26 |
+
# from slack_sdk.errors import SlackApiError
|
| 27 |
+
# from config import owner_id_pref, all_users_preload, GetAllUsers
|
| 28 |
+
# load_dotenv()
|
| 29 |
+
# calendar_service = None
|
| 30 |
+
# MICROSOFT_CLIENT_SECRET = os.getenv("MICROSOFT_CLIENT_SECRET")
|
| 31 |
+
# MICROSOFT_AUTHORITY = "https://login.microsoftonline.com/common"
|
| 32 |
+
# MICROSOFT_SCOPES = ["User.Read", "Calendars.ReadWrite"]
|
| 33 |
+
# MICROSOFT_REDIRECT_URI = os.getenv("MICROSOFT_REDIRECT_URI", "https://clear-muskox-grand.ngrok-free.app/microsoft_callback")
|
| 34 |
+
# # Enhanced GetAllUsers function (not a tool) to fetch email as well
|
| 35 |
+
# MICROSOFT_CLIENT_ID = "855e4571-d92a-4d51-802e-e712a879c00b"
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
# # Pydantic models for tool arguments
|
| 39 |
+
# class DirectDMArgs(BaseModel):
|
| 40 |
+
# message: str = Field(description="The message to be sent to the Slack user")
|
| 41 |
+
# user_id: str = Field(description="The Slack user ID")
|
| 42 |
+
|
| 43 |
+
# class DateTimeTool(BaseTool):
|
| 44 |
+
# name: str = "current_date_time"
|
| 45 |
+
# description: str = "Provides the current date and time."
|
| 46 |
+
|
| 47 |
+
# def _run(self):
|
| 48 |
+
# return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 49 |
+
|
| 50 |
+
# # Tool to get a single user's Slack ID based on their name
|
| 51 |
+
# class GetSingleUserSlackIDArgs(BaseModel):
|
| 52 |
+
# name: str = Field(description="The real name of the user whose Slack ID is needed")
|
| 53 |
+
|
| 54 |
+
# class GetSingleUserSlackID(BaseTool):
|
| 55 |
+
# name: str = "gets_slack_id_single_user"
|
| 56 |
+
# description: str = "Gets the Slack ID of a user based on their real name"
|
| 57 |
+
# args_schema: Type[BaseModel] = GetSingleUserSlackIDArgs
|
| 58 |
+
|
| 59 |
+
# def _run(self, name: str):
|
| 60 |
+
# if not all_users_preload:
|
| 61 |
+
# print("Getting the users")
|
| 62 |
+
# all_users = GetAllUsers() # Fetch all users again
|
| 63 |
+
# else:
|
| 64 |
+
# print("Fetching the users")
|
| 65 |
+
# all_users = all_users_preload
|
| 66 |
+
|
| 67 |
+
# # Iterate through all_users to find a matching name
|
| 68 |
+
# for uid, info in all_users.items():
|
| 69 |
+
# if info["name"].lower() == name.lower():
|
| 70 |
+
# return uid, info['email']
|
| 71 |
+
|
| 72 |
+
# return "User not found"
|
| 73 |
+
|
| 74 |
+
# # Tool to get a single user's Slack name based on their ID
|
| 75 |
+
# class GetSingleUserSlackNameArgs(BaseModel):
|
| 76 |
+
# id: str = Field(description="The Slack user ID")
|
| 77 |
+
|
| 78 |
+
# class GetSingleUserSlackName(BaseTool):
|
| 79 |
+
# name: str = "gets_slack_name_single_user"
|
| 80 |
+
# description: str = "Gets the Slack real name of a user based on their slack ID"
|
| 81 |
+
# args_schema: Type[BaseModel] = GetSingleUserSlackNameArgs
|
| 82 |
+
|
| 83 |
+
# def _run(self, id: str):
|
| 84 |
+
|
| 85 |
+
# # Check if preload returns empty dict or "User not found"
|
| 86 |
+
# if not all_users_preload or all_users_preload == {}:
|
| 87 |
+
# all_users = GetAllUsers() # Fetch all users again
|
| 88 |
+
# else:
|
| 89 |
+
# all_users = all_users_preload
|
| 90 |
+
|
| 91 |
+
# user = all_users.get(id)
|
| 92 |
+
# print(all_users)
|
| 93 |
+
|
| 94 |
+
# if user:
|
| 95 |
+
# return user["name"], user['email']
|
| 96 |
+
|
| 97 |
+
# return "User not found"
|
| 98 |
+
|
| 99 |
+
# class MultiDMArgs(BaseModel):
|
| 100 |
+
# message: str
|
| 101 |
+
# user_ids: List[str]
|
| 102 |
+
|
| 103 |
+
# class MultiDirectDMTool(BaseTool):
|
| 104 |
+
# name: str = "send_multiple_dms"
|
| 105 |
+
# description: str = "Sends direct messages to multiple Slack users"
|
| 106 |
+
# args_schema: Type[BaseModel] = MultiDMArgs
|
| 107 |
+
|
| 108 |
+
# def _run(self, message: str, user_ids: List[str]):
|
| 109 |
+
# results = {}
|
| 110 |
+
# for user_id in user_ids:
|
| 111 |
+
# try:
|
| 112 |
+
# client.chat_postMessage(channel=user_id, text=message)
|
| 113 |
+
# results[user_id] = "Message sent successfully"
|
| 114 |
+
# except SlackApiError as e:
|
| 115 |
+
# results[user_id] = f"Error: {e.response['error']}"
|
| 116 |
+
# return results
|
| 117 |
+
# # Direct DM tool for sending messages within Slack
|
| 118 |
+
# class DirectDMTool(BaseTool):
|
| 119 |
+
# name: str = "send_direct_dm"
|
| 120 |
+
# description: str = "Sends direct messages to Slack users"
|
| 121 |
+
# args_schema: Type[BaseModel] = DirectDMArgs
|
| 122 |
+
|
| 123 |
+
# def _run(self, message: str, user_id: str):
|
| 124 |
+
# try:
|
| 125 |
+
# client.chat_postMessage(channel=user_id, text=message)
|
| 126 |
+
# return "Message sent successfully"
|
| 127 |
+
# except SlackApiError as e:
|
| 128 |
+
# return f"Error sending message: {e.response['error']}"
|
| 129 |
+
# def send_dm(user_id: str, message: str) -> bool:
|
| 130 |
+
# """Send a direct message to a user"""
|
| 131 |
+
# try:
|
| 132 |
+
# client.chat_postMessage(channel=user_id, text=message)
|
| 133 |
+
# return True
|
| 134 |
+
# except SlackApiError as e:
|
| 135 |
+
# print(f"Error sending DM: {e.response['error']}")
|
| 136 |
+
# return False
|
| 137 |
+
|
| 138 |
+
# def handle_event_modification(event_id: str, action: str) -> str:
|
| 139 |
+
# """Handle event modification (update/delete)"""
|
| 140 |
+
# # You'll need to implement the actual modification logic
|
| 141 |
+
# if action == "delete":
|
| 142 |
+
# result = GoogleDeleteCalendarEvent().run(event_id=event_id)
|
| 143 |
+
# else:
|
| 144 |
+
# result = GoogleUpdateCalendarEvent().run(event_id=event_id)
|
| 145 |
+
|
| 146 |
+
# if result.get("status") == "success":
|
| 147 |
+
# return f"Event {action}d successfully!"
|
| 148 |
+
# return f"Failed to {action} event: {result.get('message', 'Unknown error')}"
|
| 149 |
+
# # Google Calendar Tools
|
| 150 |
+
# PT = pytz.timezone('America/Los_Angeles')
|
| 151 |
+
|
| 152 |
+
# def convert_to_pt(dt: datetime) -> datetime:
|
| 153 |
+
# """Convert a datetime object to PT timezone"""
|
| 154 |
+
# if dt.tzinfo is None:
|
| 155 |
+
# dt = pytz.utc.localize(dt)
|
| 156 |
+
# return dt.astimezone(PT)
|
| 157 |
+
# class GoogleCalendarList(BaseTool):
|
| 158 |
+
# name: str = "list_calendar_list"
|
| 159 |
+
# description: str = "Lists available calendars in the user's Google Calendar account"
|
| 160 |
+
|
| 161 |
+
# def _run(self, user_id: str, max_capacity: int = 200):
|
| 162 |
+
# if not calendar_service:
|
| 163 |
+
# calendar_service = construct_google_calendar_client(user_id)
|
| 164 |
+
# else:
|
| 165 |
+
# return "Token should be refreshed in Google Calendar"
|
| 166 |
+
|
| 167 |
+
# all_calendars = []
|
| 168 |
+
# next_page_token = None
|
| 169 |
+
# capacity_tracker = 0
|
| 170 |
+
|
| 171 |
+
# while capacity_tracker < max_capacity:
|
| 172 |
+
# results = calendar_service.calendarList().list(
|
| 173 |
+
# maxResults=min(200, max_capacity - capacity_tracker),
|
| 174 |
+
# pageToken=next_page_token
|
| 175 |
+
# ).execute()
|
| 176 |
+
# calendars = results.get('items', [])
|
| 177 |
+
# all_calendars.extend(calendars)
|
| 178 |
+
# capacity_tracker += len(calendars)
|
| 179 |
+
# next_page_token = results.get('nextPageToken')
|
| 180 |
+
# if not next_page_token:
|
| 181 |
+
# break
|
| 182 |
+
|
| 183 |
+
# return [{
|
| 184 |
+
# 'id': cal['id'],
|
| 185 |
+
# 'name': cal['summary'],
|
| 186 |
+
# 'description': cal.get('description', '')
|
| 187 |
+
# } for cal in all_calendars]
|
| 188 |
+
|
| 189 |
+
# class GoogleCalendarEvents(BaseTool):
|
| 190 |
+
# name: str = "list_calendar_events"
|
| 191 |
+
# description: str = "Lists and gets events from a specific Google Calendar"
|
| 192 |
+
|
| 193 |
+
# def _run(self, user_id: str, calendar_id: str = "primary", max_capacity: int = 20):
|
| 194 |
+
|
| 195 |
+
# calendar_service = construct_google_calendar_client(user_id)
|
| 196 |
+
|
| 197 |
+
# all_events = []
|
| 198 |
+
# next_page_token = None
|
| 199 |
+
# capacity_tracker = 0
|
| 200 |
+
|
| 201 |
+
# while capacity_tracker < max_capacity:
|
| 202 |
+
# results = calendar_service.events().list(
|
| 203 |
+
# calendarId=calendar_id,
|
| 204 |
+
# maxResults=min(250, max_capacity - capacity_tracker),
|
| 205 |
+
# pageToken=next_page_token
|
| 206 |
+
# ).execute()
|
| 207 |
+
# events = results.get('items', [])
|
| 208 |
+
# all_events.extend(events)
|
| 209 |
+
# capacity_tracker += len(events)
|
| 210 |
+
# next_page_token = results.get('nextPageToken')
|
| 211 |
+
# if not next_page_token:
|
| 212 |
+
# break
|
| 213 |
+
|
| 214 |
+
# return all_events
|
| 215 |
+
|
| 216 |
+
# class GoogleCreateCalendar(BaseTool):
|
| 217 |
+
# name: str = "create_calendar_list"
|
| 218 |
+
# description: str = "Creates a new calendar in Google Calendar"
|
| 219 |
+
|
| 220 |
+
# def _run(self, user_id: str, calendar_name: str):
|
| 221 |
+
|
| 222 |
+
# calendar_service = construct_google_calendar_client(user_id)
|
| 223 |
+
|
| 224 |
+
# calendar_body = {'summary': calendar_name}
|
| 225 |
+
# created_calendar = calendar_service.calendars().insert(body=calendar_body).execute()
|
| 226 |
+
# return f"Created calendar: {created_calendar['id']}"
|
| 227 |
+
|
| 228 |
+
# # Updated Event Creation Tool with guest options, meeting agenda, and invite link support
|
| 229 |
+
# class GoogleAddCalendarEventArgs(BaseModel):
|
| 230 |
+
# calendar_id: str = Field(default="primary", description="Calendar ID (default 'primary')")
|
| 231 |
+
# summary: str = Field(description="Event title (should include meeting agenda if needed)")
|
| 232 |
+
# user_id: str = Field(default="user", description="User slack Id which should be matched to name")
|
| 233 |
+
# description: str = Field(default="", description="Event description or agenda")
|
| 234 |
+
# start_time: str = Field(description="Start time in ISO 8601 format")
|
| 235 |
+
# end_time: str = Field(description="End time in ISO 8601 format")
|
| 236 |
+
# location: str = Field(default="", description="Event location")
|
| 237 |
+
# invite_link: str = Field(default="", description="Invite link for the meeting")
|
| 238 |
+
# guests: List[str] = Field(default=None, description="List of guest emails to invite")
|
| 239 |
+
|
| 240 |
+
# class GoogleAddCalendarEvent(BaseTool):
|
| 241 |
+
# name: str = "google_add_calendar_event"
|
| 242 |
+
# description: str = "Creates an event in a Google Calendar with comprehensive meeting details and guest options"
|
| 243 |
+
# args_schema: Type[BaseModel] = GoogleAddCalendarEventArgs
|
| 244 |
+
|
| 245 |
+
# def _run(self, user_id: str, summary: str, start_time: str, end_time: str,
|
| 246 |
+
# description: str = "", calendar_id: str = 'primary', location: str = "",
|
| 247 |
+
# invite_link: str = "", guests: List[str] = None):
|
| 248 |
+
# calendar_service = construct_google_calendar_client(user_id)
|
| 249 |
+
|
| 250 |
+
# # Append invite link to description if provided
|
| 251 |
+
# if invite_link:
|
| 252 |
+
# description = f"{description}\nInvite Link: {invite_link}"
|
| 253 |
+
|
| 254 |
+
# event = {
|
| 255 |
+
# 'summary': summary,
|
| 256 |
+
# 'description': description,
|
| 257 |
+
# 'start': {'dateTime': start_time, 'timeZone': 'America/Los_Angeles'},
|
| 258 |
+
# 'end': {'dateTime': end_time, 'timeZone': 'America/Los_Angeles'},
|
| 259 |
+
# 'location': location,
|
| 260 |
+
# }
|
| 261 |
+
|
| 262 |
+
# # Add guests if provided
|
| 263 |
+
# if guests:
|
| 264 |
+
# event['attendees'] = [{'email': guest} for guest in guests]
|
| 265 |
+
|
| 266 |
+
# try:
|
| 267 |
+
# print("I am here registering the event")
|
| 268 |
+
# created_event = calendar_service.events().insert(
|
| 269 |
+
# calendarId=calendar_id,
|
| 270 |
+
# body=event,
|
| 271 |
+
# sendUpdates='all' # Send invitations to guests
|
| 272 |
+
# ).execute()
|
| 273 |
+
# return {
|
| 274 |
+
# "status": "success",
|
| 275 |
+
# "event_id": created_event['id'],
|
| 276 |
+
# "link": created_event.get('htmlLink', '')
|
| 277 |
+
# }
|
| 278 |
+
# except Exception as e:
|
| 279 |
+
# return {"status": "error", "message": str(e)}
|
| 280 |
+
|
| 281 |
+
# # Updated Tool: Update an existing calendar event including guest options
|
| 282 |
+
# class GoogleUpdateCalendarEventArgs(BaseModel):
|
| 283 |
+
# calendar_id: str = Field(default="primary", description="Calendar ID (default 'primary')")
|
| 284 |
+
# user_id: str = Field(default="user", description="User slack Id which should be matched to name")
|
| 285 |
+
# event_id: str = Field(description="The event ID to update")
|
| 286 |
+
# summary: str = Field(default=None, description="Updated event title or agenda")
|
| 287 |
+
# description: Optional[str] = Field(default=None, description="Updated event description or agenda")
|
| 288 |
+
# start_time: Optional[str] = Field(default=None, description="Updated start time in ISO 8601 format")
|
| 289 |
+
# end_time: Optional[str] = Field(default=None, description="Updated end time in ISO 8601 format")
|
| 290 |
+
# location: Optional[str] = Field(default=None, description="Updated event location")
|
| 291 |
+
# invite_link: str = Field(default=None, description="Updated invite link for the meeting")
|
| 292 |
+
# guests: List[str] = Field(default=None, description="Updated list of guest emails")
|
| 293 |
+
|
| 294 |
+
# class GoogleUpdateCalendarEvent(BaseTool):
|
| 295 |
+
# name: str = "google_update_calendar_event"
|
| 296 |
+
# description: str = "Updates an existing event in a Google Calendar, including guest options"
|
| 297 |
+
# args_schema: Type[BaseModel] = GoogleUpdateCalendarEventArgs
|
| 298 |
+
|
| 299 |
+
# def _run(self, user_id: str, event_id: str, calendar_id: str = "primary",
|
| 300 |
+
# summary: Optional[str] = None, description: Optional[str] = None,
|
| 301 |
+
# start_time: Optional[str] = None, end_time: Optional[str] = None,
|
| 302 |
+
# location: Optional[str] = None, invite_link: Optional[str] = None,
|
| 303 |
+
# guests: Optional[List[str]] = None):
|
| 304 |
+
|
| 305 |
+
# calendar_service = construct_google_calendar_client(user_id)
|
| 306 |
+
|
| 307 |
+
# # Retrieve the existing event
|
| 308 |
+
# try:
|
| 309 |
+
# event = calendar_service.events().get(calendarId=calendar_id, eventId=event_id).execute()
|
| 310 |
+
# except Exception as e:
|
| 311 |
+
# return {"status": "error", "message": f"Event retrieval failed: {str(e)}"}
|
| 312 |
+
|
| 313 |
+
# # Update fields if provided
|
| 314 |
+
# if summary:
|
| 315 |
+
# event['summary'] = summary
|
| 316 |
+
# if description:
|
| 317 |
+
# event['description'] = description
|
| 318 |
+
# if invite_link:
|
| 319 |
+
# # Append invite link to the description
|
| 320 |
+
# current_desc = event.get('description', '')
|
| 321 |
+
# event['description'] = f"{current_desc}\nInvite Link: {invite_link}"
|
| 322 |
+
# if start_time:
|
| 323 |
+
# event['start'] = {'dateTime': start_time, 'timeZone': 'UTC'}
|
| 324 |
+
# if end_time:
|
| 325 |
+
# event['end'] = {'dateTime': end_time, 'timeZone': 'UTC'}
|
| 326 |
+
# if location:
|
| 327 |
+
# event['location'] = location
|
| 328 |
+
# if guests is not None:
|
| 329 |
+
# event['attendees'] = [{'email': guest} for guest in guests]
|
| 330 |
+
|
| 331 |
+
# try:
|
| 332 |
+
# updated_event = calendar_service.events().update(
|
| 333 |
+
# calendarId=calendar_id,
|
| 334 |
+
# eventId=event_id,
|
| 335 |
+
# body=event
|
| 336 |
+
# ).execute()
|
| 337 |
+
# return {"status": "success", "event_id": updated_event['id']}
|
| 338 |
+
# except Exception as e:
|
| 339 |
+
# return {"status": "error", "message": f"Update failed: {str(e)}"}
|
| 340 |
+
|
| 341 |
+
# # New Tool: Delete a calendar event
|
| 342 |
+
# class GoogleDeleteCalendarEventArgs(BaseModel):
|
| 343 |
+
# calendar_id: str = Field(default="primary", description="Calendar ID (default 'primary')")
|
| 344 |
+
# event_id: str = Field(description="The event ID to delete")
|
| 345 |
+
# user_id: str = Field(default="user", description="User slack Id which should be matched to name")
|
| 346 |
+
|
| 347 |
+
# class GoogleDeleteCalendarEvent(BaseTool):
|
| 348 |
+
# name: str = "google_delete_calendar_event"
|
| 349 |
+
# description: str = "Deletes an event from a Google Calendar"
|
| 350 |
+
# args_schema: Type[BaseModel] = GoogleDeleteCalendarEventArgs
|
| 351 |
+
|
| 352 |
+
# def _run(self, user_id: str, event_id: str, calendar_id: str = "primary"):
|
| 353 |
+
|
| 354 |
+
# calendar_service = construct_google_calendar_client(user_id)
|
| 355 |
+
# try:
|
| 356 |
+
# calendar_service.events().delete(calendarId=calendar_id, eventId=event_id).execute()
|
| 357 |
+
# return {"status": "success", "message": f"Deleted event {event_id}"}
|
| 358 |
+
# except Exception as e:
|
| 359 |
+
# return {"status": "error", "message": f"Deletion failed: {str(e)}"}
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
# # New Tool: Search Events by User
|
| 363 |
+
# class SearchUserEventsArgs(BaseModel):
|
| 364 |
+
# user_id: str
|
| 365 |
+
# lookback_days: int = Field(default=30)
|
| 366 |
+
# class SearchUserEventsTool(BaseTool):
|
| 367 |
+
# name: str = "search_events_by_user"
|
| 368 |
+
# description: str = "Finds calendar events associated with a specific user"
|
| 369 |
+
# args_schema: Type[BaseModel] = SearchUserEventsArgs
|
| 370 |
+
|
| 371 |
+
# def _run(self, user_id: str, lookback_days: int = 30):
|
| 372 |
+
# # Get user info which may be a tuple (name, email) or "User not found"
|
| 373 |
+
# user_info = GetSingleUserSlackName().run(user_id)
|
| 374 |
+
|
| 375 |
+
# # Handle case where user is not found
|
| 376 |
+
# if user_info == "User not found":
|
| 377 |
+
# return []
|
| 378 |
+
|
| 379 |
+
# # Extract user_name from tuple
|
| 380 |
+
# user_name, user_email = user_info
|
| 381 |
+
|
| 382 |
+
# # Fetch events for the user
|
| 383 |
+
# events = GoogleCalendarEvents().run(user_id)
|
| 384 |
+
|
| 385 |
+
# # Ensure 'now' is timezone-aware (using UTC)
|
| 386 |
+
# now = datetime.now(pytz.UTC)
|
| 387 |
+
|
| 388 |
+
# relevant_events = []
|
| 389 |
+
# for event in events:
|
| 390 |
+
# # Ensure event_time is timezone-aware
|
| 391 |
+
# event_time_str = event['start'].get('dateTime')
|
| 392 |
+
# if not event_time_str:
|
| 393 |
+
# continue # Skip events without a valid start time
|
| 394 |
+
|
| 395 |
+
# event_time = datetime.fromisoformat(event_time_str)
|
| 396 |
+
# if event_time.tzinfo is None:
|
| 397 |
+
# # If event_time is naive, make it timezone-aware (assuming UTC)
|
| 398 |
+
# event_time = pytz.UTC.localize(event_time)
|
| 399 |
+
|
| 400 |
+
# # Check if the event is within the lookback period
|
| 401 |
+
# if (now - event_time).days > lookback_days:
|
| 402 |
+
# continue
|
| 403 |
+
|
| 404 |
+
# # Check if the user's name is in the event summary or description
|
| 405 |
+
# if user_name in event.get('summary', '') or user_name in event.get('description', ''):
|
| 406 |
+
# relevant_events.append({
|
| 407 |
+
# 'id': event['id'],
|
| 408 |
+
# 'title': event['summary'],
|
| 409 |
+
# 'time': event_time.strftime("%Y-%m-%d %H:%M"),
|
| 410 |
+
# 'calendar_id': event['organizer']['email']
|
| 411 |
+
# })
|
| 412 |
+
|
| 413 |
+
# return relevant_events
|
| 414 |
+
# # Enhanced Agent Logic
|
| 415 |
+
# def handle_update_delete(user_id, text):
|
| 416 |
+
# events = SearchUserEventsTool().run(user_id)
|
| 417 |
+
|
| 418 |
+
# if not events:
|
| 419 |
+
# return "No recent events found for you."
|
| 420 |
+
|
| 421 |
+
# if len(events) > 1:
|
| 422 |
+
# options = [{"text": f"{e['title']} ({e['time']})", "value": e['id']} for e in events]
|
| 423 |
+
# return {
|
| 424 |
+
# "response_type": "ephemeral",
|
| 425 |
+
# "blocks": [{
|
| 426 |
+
# "type": "section",
|
| 427 |
+
# "text": {"type": "mrkdwn", "text": "Multiple events found:"},
|
| 428 |
+
# "accessory": {
|
| 429 |
+
# "type": "static_select",
|
| 430 |
+
# "options": options,
|
| 431 |
+
# "action_id": "select_event_to_modify"
|
| 432 |
+
# }
|
| 433 |
+
# }]
|
| 434 |
+
# }
|
| 435 |
+
|
| 436 |
+
# # If single event, proceed directly
|
| 437 |
+
# return handle_event_modification(events[0]['id'])
|
| 438 |
+
|
| 439 |
+
|
| 440 |
+
# # State Management
|
| 441 |
+
# from collections import defaultdict
|
| 442 |
+
# meeting_coordination = defaultdict(dict)
|
| 443 |
+
|
| 444 |
+
# class CreatePollArgs(BaseModel):
|
| 445 |
+
# time_slots: List[str]
|
| 446 |
+
# channel_id: str
|
| 447 |
+
# initiator_id: str
|
| 448 |
+
|
| 449 |
+
# class CoordinateDMsArgs(BaseModel):
|
| 450 |
+
# user_ids: List[str]
|
| 451 |
+
# time_slots: List[str]
|
| 452 |
+
# # Poll Creation Tool
|
| 453 |
+
# class CoordinateDMsTool(BaseTool):
|
| 454 |
+
# name:str = "coordinate_dm_responses"
|
| 455 |
+
# description:str = "Manages DM responses for meeting coordination"
|
| 456 |
+
# args_schema: Type[BaseModel] = CoordinateDMsArgs
|
| 457 |
+
# def _run(self, user_ids: List[str], time_slots: List[str]) -> Dict:
|
| 458 |
+
# session_id = f"dm_coord_{datetime.now().timestamp()}"
|
| 459 |
+
# message = "Please choose a time slot by replying with the number:\n" + \
|
| 460 |
+
# "\n".join([f"{i+1}. {slot}" for i, slot in enumerate(time_slots)])
|
| 461 |
+
|
| 462 |
+
# for uid in user_ids:
|
| 463 |
+
# if not send_dm(uid, message):
|
| 464 |
+
# return {"status": "error", "message": f"Failed to send DM to user {uid}"}
|
| 465 |
+
|
| 466 |
+
# meeting_coordination[session_id] = {
|
| 467 |
+
# "responses": {},
|
| 468 |
+
# "required": len(user_ids),
|
| 469 |
+
# "slots": time_slots,
|
| 470 |
+
# "participants": user_ids,
|
| 471 |
+
# "created_at": datetime.now()
|
| 472 |
+
# }
|
| 473 |
+
|
| 474 |
+
# return {
|
| 475 |
+
# "status": "success",
|
| 476 |
+
# "session_id": session_id,
|
| 477 |
+
# "message": "DMs sent to all participants"
|
| 478 |
+
# }
|
| 479 |
+
|
| 480 |
+
# # Poll Management
|
| 481 |
+
|
| 482 |
+
|
| 483 |
+
|
| 484 |
+
# # Zoom Meeting Tool
|
| 485 |
+
# # class ZoomCreateMeetingArgs(BaseModel):
|
| 486 |
+
# # topic: str = Field(description="Meeting topic")
|
| 487 |
+
# # start_time: str = Field(description="Start time in ISO 8601 format and PT timezone")
|
| 488 |
+
# # duration: int = Field(description="Duration in minutes")
|
| 489 |
+
# # agenda: Optional[str] = Field(default="", description="Meeting agenda")
|
| 490 |
+
# # timezone: str = Field(default="UTC", description="Timezone for the meeting")
|
| 491 |
+
|
| 492 |
+
# # class ZoomCreateMeetingTool(BaseTool):
|
| 493 |
+
# # name:str = "create_zoom_meeting"
|
| 494 |
+
# # description:str = "Creates a Zoom meeting using configured credentials"
|
| 495 |
+
# # args_schema: Type[BaseModel] = ZoomCreateMeetingArgs
|
| 496 |
+
|
| 497 |
+
# # def _run(self, topic: str, start_time: str, duration: int = 30,
|
| 498 |
+
# # agenda: str = "", timezone: str = "UTC"):
|
| 499 |
+
# # # Get owner's credentials
|
| 500 |
+
# # owner_id = "owner_id_pref"
|
| 501 |
+
# # if not owner_id:
|
| 502 |
+
# # return "Workspace owner not found"
|
| 503 |
+
|
| 504 |
+
# # prefs_path = os.path.join('preferences', f'preferences_{owner_id}.json')
|
| 505 |
+
# # if not os.path.exists(prefs_path):
|
| 506 |
+
# # return "Zoom credentials not configured"
|
| 507 |
+
|
| 508 |
+
# # with open(prefs_path) as f:
|
| 509 |
+
# # prefs = json.load(f)
|
| 510 |
+
|
| 511 |
+
# # if prefs['zoom_config']['mode'] == 'manual':
|
| 512 |
+
# # return {"link": prefs.get('zoom_link'), "message": "Using manual Zoom link"}
|
| 513 |
+
|
| 514 |
+
# # # Automatic Zoom creation
|
| 515 |
+
# # auth_str = f"{prefs['zoom_config']['client_id']}:{prefs['zoom_config']['client_secret']}"
|
| 516 |
+
# # auth_bytes = base64.b64encode(auth_str.encode()).decode()
|
| 517 |
+
|
| 518 |
+
# # headers = {
|
| 519 |
+
# # "Authorization": f"Basic {auth_bytes}",
|
| 520 |
+
# # "Content-Type": "application/x-www-form-urlencoded"
|
| 521 |
+
# # }
|
| 522 |
+
|
| 523 |
+
# # data = {
|
| 524 |
+
# # "grant_type": "account_credentials",
|
| 525 |
+
# # "account_id": prefs['zoom_config']['account_id']
|
| 526 |
+
# # }
|
| 527 |
+
|
| 528 |
+
# # # Get access token
|
| 529 |
+
# # token_res = requests.post(
|
| 530 |
+
# # "https://zoom.us/oauth/token",
|
| 531 |
+
# # headers=headers,
|
| 532 |
+
# # data=data
|
| 533 |
+
# # )
|
| 534 |
+
|
| 535 |
+
# # if token_res.status_code != 200:
|
| 536 |
+
# # return "Zoom authentication failed"
|
| 537 |
+
|
| 538 |
+
# # access_token = token_res.json()["access_token"]
|
| 539 |
+
|
| 540 |
+
# # # Create meeting
|
| 541 |
+
# # meeting_data = {
|
| 542 |
+
# # "topic": topic,
|
| 543 |
+
# # "type": 2,
|
| 544 |
+
# # "start_time": start_time,
|
| 545 |
+
# # "duration": duration,
|
| 546 |
+
# # "timezone": timezone,
|
| 547 |
+
# # "agenda": agenda,
|
| 548 |
+
# # "settings": {
|
| 549 |
+
# # "host_video": True,
|
| 550 |
+
# # "participant_video": True,
|
| 551 |
+
# # "join_before_host": False
|
| 552 |
+
# # }
|
| 553 |
+
# # }
|
| 554 |
+
|
| 555 |
+
# # headers = {
|
| 556 |
+
# # "Authorization": f"Bearer {access_token}",
|
| 557 |
+
# # "Content-Type": "application/json"
|
| 558 |
+
# # }
|
| 559 |
+
|
| 560 |
+
# # meeting_res = requests.post(
|
| 561 |
+
# # "https://api.zoom.us/v2/users/me/meetings",
|
| 562 |
+
# # headers=headers,
|
| 563 |
+
# # json=meeting_data
|
| 564 |
+
# # )
|
| 565 |
+
|
| 566 |
+
# # if meeting_res.status_code == 201:
|
| 567 |
+
# # return {
|
| 568 |
+
# # "meeting_id": meeting_res.json()["id"],
|
| 569 |
+
# # "join_url": meeting_res.json()["join_url"],
|
| 570 |
+
# # 'passcode':meeting_res.json()["password"],
|
| 571 |
+
# # 'duration':duration
|
| 572 |
+
# # }
|
| 573 |
+
# # return f"Zoom meeting creation failed: {meeting_res.text}"
|
| 574 |
+
# from langchain.tools import BaseTool
|
| 575 |
+
# from pydantic import BaseModel, Field
|
| 576 |
+
# from typing import Optional, Type
|
| 577 |
+
# import os
|
| 578 |
+
# import json
|
| 579 |
+
# import time
|
| 580 |
+
# import requests
|
| 581 |
+
# import base64
|
| 582 |
+
|
| 583 |
+
# class ZoomCreateMeetingArgs(BaseModel):
|
| 584 |
+
# topic: str = Field(description="Meeting topic")
|
| 585 |
+
# start_time: str = Field(description="Start time in ISO 8601 format and PT timezone")
|
| 586 |
+
# duration: int = Field(description="Duration in minutes")
|
| 587 |
+
# agenda: Optional[str] = Field(default="", description="Meeting agenda")
|
| 588 |
+
# timezone: str = Field(default="UTC", description="Timezone for the meeting")
|
| 589 |
+
|
| 590 |
+
# from config import get_workspace_owner_id, load_preferences
|
| 591 |
+
# import os
|
| 592 |
+
# # ZOOM_REDIRECT_URI = os.environ['ZOOM_REDIRECT_URI']
|
| 593 |
+
# CLIENT_SECRET = os.environ['ZOOM_CLIENT_SECRET']
|
| 594 |
+
# CLIENT_ID = os.environ['ZOOM_CLIENT_ID']
|
| 595 |
+
# ZOOM_TOKEN_API = os.environ['ZOOM_TOKEN_API']
|
| 596 |
+
# # ZOOM_OAUTH_AUTHORIZE_API = os.environ['ZOOM_OAUTH_AUTHORIZE_API']
|
| 597 |
+
# class ZoomCreateMeetingTool(BaseTool):
|
| 598 |
+
# name: str = "create_zoom_meeting"
|
| 599 |
+
# description: str = "Creates a Zoom meeting using configured credentials"
|
| 600 |
+
# args_schema: Type[BaseModel] = ZoomCreateMeetingArgs
|
| 601 |
+
|
| 602 |
+
# def _run(self, topic: str, start_time: str, duration: int = 30,
|
| 603 |
+
# agenda: str = "", timezone: str = "UTC"):
|
| 604 |
+
# # Get workspace owner's ID
|
| 605 |
+
# owner_id = get_workspace_owner_id()
|
| 606 |
+
# if not owner_id:
|
| 607 |
+
# return "Workspace owner not found"
|
| 608 |
+
|
| 609 |
+
# # Load preferences to check Zoom mode
|
| 610 |
+
# prefs = load_preferences(owner_id)
|
| 611 |
+
# zoom_config = prefs.get("zoom_config", {"mode": "manual", "link": None})
|
| 612 |
+
|
| 613 |
+
# # Handle manual mode
|
| 614 |
+
# if zoom_config["mode"] == "manual":
|
| 615 |
+
# link = zoom_config.get("link")
|
| 616 |
+
# if link:
|
| 617 |
+
# return f"Please join the meeting using this link: {link}"
|
| 618 |
+
# else:
|
| 619 |
+
# return "Manual Zoom link not configured"
|
| 620 |
+
|
| 621 |
+
# # Automatic mode
|
| 622 |
+
# token_path = f'token_files/zoom_{owner_id}.json'
|
| 623 |
+
# if not os.path.exists(token_path):
|
| 624 |
+
# return "Zoom not configured for automatic mode"
|
| 625 |
+
|
| 626 |
+
# with open(token_path) as f:
|
| 627 |
+
# token_data = json.load(f)
|
| 628 |
+
|
| 629 |
+
# access_token = token_data.get("access_token")
|
| 630 |
+
# if not access_token:
|
| 631 |
+
# return "Invalid Zoom token"
|
| 632 |
+
|
| 633 |
+
# # Check if token is expired and refresh if necessary
|
| 634 |
+
# expires_at = token_data.get("expires_at")
|
| 635 |
+
# if expires_at and time.time() > expires_at:
|
| 636 |
+
# refresh_token = token_data.get("refresh_token")
|
| 637 |
+
# if not refresh_token:
|
| 638 |
+
# return "Zoom token expired and no refresh token available"
|
| 639 |
+
# params = {
|
| 640 |
+
# "grant_type": "refresh_token",
|
| 641 |
+
# "refresh_token": refresh_token
|
| 642 |
+
# }
|
| 643 |
+
# auth_str = f"{CLIENT_ID}:{CLIENT_SECRET}"
|
| 644 |
+
# auth_bytes = base64.b64encode(auth_str.encode()).decode()
|
| 645 |
+
# headers = {
|
| 646 |
+
# "Authorization": f"Basic {auth_bytes}",
|
| 647 |
+
# "Content-Type": "application/x-www-form-urlencoded"
|
| 648 |
+
# }
|
| 649 |
+
# response = requests.post(ZOOM_TOKEN_API, data=params, headers=headers)
|
| 650 |
+
# if response.status_code == 200:
|
| 651 |
+
# new_token_data = response.json()
|
| 652 |
+
# token_data.update(new_token_data)
|
| 653 |
+
# token_data["expires_at"] = time.time() + new_token_data["expires_in"]
|
| 654 |
+
# with open(token_path, 'w') as f:
|
| 655 |
+
# json.dump(token_data, f)
|
| 656 |
+
# access_token = new_token_data["access_token"]
|
| 657 |
+
# else:
|
| 658 |
+
# return "Failed to refresh Zoom token"
|
| 659 |
+
|
| 660 |
+
# # Create Zoom meeting
|
| 661 |
+
# meeting_data = {
|
| 662 |
+
# "topic": topic,
|
| 663 |
+
# "type": 2, # Scheduled meeting
|
| 664 |
+
# "start_time": start_time,
|
| 665 |
+
# "duration": duration,
|
| 666 |
+
# "timezone": timezone,
|
| 667 |
+
# "agenda": agenda,
|
| 668 |
+
# "settings": {
|
| 669 |
+
# "host_video": True,
|
| 670 |
+
# "participant_video": True,
|
| 671 |
+
# "join_before_host": False
|
| 672 |
+
# }
|
| 673 |
+
# }
|
| 674 |
+
# headers = {
|
| 675 |
+
# "Authorization": f"Bearer {access_token}",
|
| 676 |
+
# "Content-Type": "application/json"
|
| 677 |
+
# }
|
| 678 |
+
# meeting_res = requests.post(
|
| 679 |
+
# "https://api.zoom.us/v2/users/me/meetings",
|
| 680 |
+
# headers=headers,
|
| 681 |
+
# json=meeting_data
|
| 682 |
+
# )
|
| 683 |
+
# if meeting_res.status_code == 201:
|
| 684 |
+
# meeting_info = meeting_res.json()
|
| 685 |
+
|
| 686 |
+
# # Extract meeting details
|
| 687 |
+
# meeting_id = meeting_info["id"]
|
| 688 |
+
# join_url = meeting_info["join_url"]
|
| 689 |
+
# password = meeting_info.get("password", "")
|
| 690 |
+
# dial_in_numbers = meeting_info["settings"].get("global_dial_in_numbers", [])
|
| 691 |
+
# print(meeting_info['settings'])
|
| 692 |
+
# print(dial_in_numbers)
|
| 693 |
+
# # Format one-tap mobile numbers (up to 2 US numbers)
|
| 694 |
+
# us_numbers = [num for num in dial_in_numbers if num["country"] == "US"]
|
| 695 |
+
# one_tap_strs = []
|
| 696 |
+
# for num in us_numbers[:2]:
|
| 697 |
+
# clean_number = ''.join(num["number"].split()) # Remove spaces, e.g., "+1 305 224 1968" -> "+13052241968"
|
| 698 |
+
# one_tap = f"{clean_number},,{meeting_id}#,,,,*{password}# US"
|
| 699 |
+
# one_tap_strs.append(one_tap)
|
| 700 |
+
|
| 701 |
+
# # Format dial-in numbers
|
| 702 |
+
# dial_in_strs = []
|
| 703 |
+
# for num in dial_in_numbers:
|
| 704 |
+
# country = num["country"]
|
| 705 |
+
# city = num.get("city", "")
|
| 706 |
+
# number = num["number"]
|
| 707 |
+
# if city:
|
| 708 |
+
# dial_in_strs.append(f"• {number} {country} ({city})")
|
| 709 |
+
# else:
|
| 710 |
+
# dial_in_strs.append(f"• {number} {country}")
|
| 711 |
+
|
| 712 |
+
# # Construct the invitation text
|
| 713 |
+
# invitation = "Citrusbug Technolabs is inviting you to a scheduled Zoom meeting.\n\n"
|
| 714 |
+
# invitation += f"Join Zoom Meeting\n{join_url}\n\n"
|
| 715 |
+
# invitation += f"Meeting ID: {meeting_id}\nPasscode: {password}\n\n"
|
| 716 |
+
# invitation += "---\n\n"
|
| 717 |
+
# if one_tap_strs:
|
| 718 |
+
# invitation += "One tap mobile\n"
|
| 719 |
+
# invitation += "\n".join(one_tap_strs) + "\n\n"
|
| 720 |
+
# invitation += "---\n\n"
|
| 721 |
+
# invitation += "Dial by your location\n"
|
| 722 |
+
# invitation += "\n".join(dial_in_strs) + "\n\n"
|
| 723 |
+
# invitation += f"Meeting ID: {meeting_id}\nPasscode: {password}\n"
|
| 724 |
+
# invitation += "Find your local number: https://zoom.us/zoomconference"
|
| 725 |
+
|
| 726 |
+
# return invitation
|
| 727 |
+
# else:
|
| 728 |
+
# return f"Zoom meeting creation failed: {meeting_res.text}"
|
| 729 |
+
# class MicrosoftBaseTool(BaseTool):
|
| 730 |
+
# """Base class for Microsoft tools with common auth handling"""
|
| 731 |
+
|
| 732 |
+
# def get_microsoft_client(self, user_id: str):
|
| 733 |
+
# """Get authenticated Microsoft client for a user"""
|
| 734 |
+
# token_path = os.path.join('token_files', f'microsoft_{user_id}.json')
|
| 735 |
+
# if not os.path.exists(token_path):
|
| 736 |
+
# return None, "Microsoft credentials not configured"
|
| 737 |
+
|
| 738 |
+
# with open(token_path) as f:
|
| 739 |
+
# token_data = json.load(f)
|
| 740 |
+
|
| 741 |
+
# if time.time() > token_data['expires_at']:
|
| 742 |
+
# # Handle token refresh
|
| 743 |
+
# app = ConfidentialClientApplication(
|
| 744 |
+
# MICROSOFT_CLIENT_ID,
|
| 745 |
+
# authority=MICROSOFT_AUTHORITY,
|
| 746 |
+
# client_credential=MICROSOFT_CLIENT_SECRET
|
| 747 |
+
# )
|
| 748 |
+
# result = app.acquire_token_by_refresh_token(
|
| 749 |
+
# token_data['refresh_token'],
|
| 750 |
+
# scopes=MICROSOFT_SCOPES
|
| 751 |
+
# )
|
| 752 |
+
# if "access_token" not in result:
|
| 753 |
+
# return None, "Token refresh failed"
|
| 754 |
+
|
| 755 |
+
# token_data.update(result)
|
| 756 |
+
# with open(token_path, 'w') as f:
|
| 757 |
+
# json.dump(token_data, f)
|
| 758 |
+
|
| 759 |
+
# headers = {
|
| 760 |
+
# "Authorization": f"Bearer {token_data['access_token']}",
|
| 761 |
+
# "Content-Type": "application/json"
|
| 762 |
+
# }
|
| 763 |
+
# return headers, None
|
| 764 |
+
|
| 765 |
+
# # Pydantic models for Microsoft tools
|
| 766 |
+
# class MicrosoftAddCalendarEventArgs(BaseModel):
|
| 767 |
+
# user_id: str = Field(description="Slack user ID of the calendar owner")
|
| 768 |
+
# subject: str = Field(description="Event title/subject")
|
| 769 |
+
# start_time: str = Field(description="Start time in ISO 8601 format")
|
| 770 |
+
# end_time: str = Field(description="End time in ISO 8601 format")
|
| 771 |
+
# content: str = Field(default="", description="Event description/content")
|
| 772 |
+
# location: str = Field(default="", description="Event location")
|
| 773 |
+
# attendees: List[str] = Field(default=[], description="List of attendee emails")
|
| 774 |
+
|
| 775 |
+
# class MicrosoftUpdateCalendarEventArgs(MicrosoftAddCalendarEventArgs):
|
| 776 |
+
# event_id: str = Field(description="Microsoft event ID to update")
|
| 777 |
+
|
| 778 |
+
# class MicrosoftDeleteCalendarEventArgs(BaseModel):
|
| 779 |
+
# user_id: str = Field(description="Slack user ID of the calendar owner")
|
| 780 |
+
# event_id: str = Field(description="Microsoft event ID to delete")
|
| 781 |
+
|
| 782 |
+
# # Microsoft Calendar Tools
|
| 783 |
+
# class MicrosoftListCalendarEvents(MicrosoftBaseTool):
|
| 784 |
+
# name:str = "microsoft_calendar_list_events"
|
| 785 |
+
# description:str = "Lists events from Microsoft Calendar"
|
| 786 |
+
|
| 787 |
+
# def _run(self, user_id: str, max_results: int = 10):
|
| 788 |
+
# headers, error = self.get_microsoft_client(user_id)
|
| 789 |
+
# if error:
|
| 790 |
+
# return error
|
| 791 |
+
|
| 792 |
+
# endpoint = "https://graph.microsoft.com/v1.0/me/events"
|
| 793 |
+
# params = {
|
| 794 |
+
# "$top": max_results,
|
| 795 |
+
# "$orderby": "start/dateTime desc"
|
| 796 |
+
# }
|
| 797 |
+
|
| 798 |
+
# response = requests.get(endpoint, headers=headers, params=params)
|
| 799 |
+
# if response.status_code != 200:
|
| 800 |
+
# return f"Error fetching events: {response.text}"
|
| 801 |
+
|
| 802 |
+
# events = response.json().get('value', [])
|
| 803 |
+
# return [{
|
| 804 |
+
# 'id': e['id'],
|
| 805 |
+
# 'subject': e.get('subject'),
|
| 806 |
+
# 'start': e['start'].get('dateTime'),
|
| 807 |
+
# 'end': e['end'].get('dateTime'),
|
| 808 |
+
# 'webLink': e.get('webUrl')
|
| 809 |
+
# } for e in events]
|
| 810 |
+
|
| 811 |
+
# class MicrosoftAddCalendarEvent(MicrosoftBaseTool):
|
| 812 |
+
# name:str = "microsoft_calendar_add_event"
|
| 813 |
+
# description:str = "Creates an event in Microsoft Calendar"
|
| 814 |
+
# args_schema: Type[BaseModel] = MicrosoftAddCalendarEventArgs
|
| 815 |
+
|
| 816 |
+
# def _run(self, user_id: str, subject: str, start_time: str, end_time: str,
|
| 817 |
+
# content: str = "", location: str = "", attendees: List[str] = []):
|
| 818 |
+
# headers, error = self.get_microsoft_client(user_id)
|
| 819 |
+
# if error:
|
| 820 |
+
# return error
|
| 821 |
+
|
| 822 |
+
# event_payload = {
|
| 823 |
+
# "subject": subject,
|
| 824 |
+
# "body": {
|
| 825 |
+
# "contentType": "HTML",
|
| 826 |
+
# "content": content
|
| 827 |
+
# },
|
| 828 |
+
# "start": {
|
| 829 |
+
# "dateTime": start_time,
|
| 830 |
+
# "timeZone": "America/Los_Angeles"
|
| 831 |
+
# },
|
| 832 |
+
# "end": {
|
| 833 |
+
# "dateTime": end_time,
|
| 834 |
+
# "timeZone": "America/Los_Angeles"
|
| 835 |
+
# },
|
| 836 |
+
# "location": {"displayName": location},
|
| 837 |
+
# "attendees": [{"emailAddress": {"address": email}} for email in attendees]
|
| 838 |
+
# }
|
| 839 |
+
|
| 840 |
+
# response = requests.post(
|
| 841 |
+
# "https://graph.microsoft.com/v1.0/me/events",
|
| 842 |
+
# headers=headers,
|
| 843 |
+
# json=event_payload
|
| 844 |
+
# )
|
| 845 |
+
|
| 846 |
+
# if response.status_code == 201:
|
| 847 |
+
# return {
|
| 848 |
+
# "status": "success",
|
| 849 |
+
# "event_id": response.json()['id'],
|
| 850 |
+
# "link": response.json().get('webUrl')
|
| 851 |
+
# }
|
| 852 |
+
# return f"Error creating event: {response.text}"
|
| 853 |
+
|
| 854 |
+
# class MicrosoftUpdateCalendarEvent(MicrosoftBaseTool):
|
| 855 |
+
# name:str = "microsoft_calendar_update_event"
|
| 856 |
+
# description:str = "Updates an existing Microsoft Calendar event"
|
| 857 |
+
# args_schema: Type[BaseModel] = MicrosoftUpdateCalendarEventArgs
|
| 858 |
+
|
| 859 |
+
# def _run(self, user_id: str, event_id: str, **kwargs):
|
| 860 |
+
# headers, error = self.get_microsoft_client(user_id)
|
| 861 |
+
# if error:
|
| 862 |
+
# return error
|
| 863 |
+
|
| 864 |
+
# get_response = requests.get(
|
| 865 |
+
# f"https://graph.microsoft.com/v1.0/me/events/{event_id}",
|
| 866 |
+
# headers=headers
|
| 867 |
+
# )
|
| 868 |
+
# if get_response.status_code != 200:
|
| 869 |
+
# return f"Error finding event: {get_response.text}"
|
| 870 |
+
|
| 871 |
+
# existing_event = get_response.json()
|
| 872 |
+
|
| 873 |
+
# update_payload = {
|
| 874 |
+
# "subject": kwargs.get('subject', existing_event.get('subject')),
|
| 875 |
+
# "body": {
|
| 876 |
+
# "content": kwargs.get('content', existing_event.get('body', {}).get('content')),
|
| 877 |
+
# "contentType": "HTML"
|
| 878 |
+
# },
|
| 879 |
+
# "start": {
|
| 880 |
+
# "dateTime": kwargs.get('start_time', existing_event['start']['dateTime']),
|
| 881 |
+
# "timeZone": "UTC"
|
| 882 |
+
# },
|
| 883 |
+
# "end": {
|
| 884 |
+
# "dateTime": kwargs.get('end_time', existing_event['end']['dateTime']),
|
| 885 |
+
# "timeZone": "UTC"
|
| 886 |
+
# },
|
| 887 |
+
# "location": {"displayName": kwargs.get('location', existing_event.get('location', {}).get('displayName'))},
|
| 888 |
+
# "attendees": [{"emailAddress": {"address": email}} for email in
|
| 889 |
+
# kwargs.get('attendees', [a['emailAddress']['address'] for a in existing_event.get('attendees', [])])]
|
| 890 |
+
# }
|
| 891 |
+
|
| 892 |
+
# response = requests.patch(
|
| 893 |
+
# f"https://graph.microsoft.com/v1.0/me/events/{event_id}",
|
| 894 |
+
# headers=headers,
|
| 895 |
+
# json=update_payload
|
| 896 |
+
# )
|
| 897 |
+
|
| 898 |
+
# if response.status_code == 200:
|
| 899 |
+
# return {"status": "success", "event_id": event_id}
|
| 900 |
+
# return f"Error updating event: {response.text}"
|
| 901 |
+
|
| 902 |
+
# class MicrosoftDeleteCalendarEvent(MicrosoftBaseTool):
|
| 903 |
+
# name:str = "microsoft_calendar_delete_event"
|
| 904 |
+
# description:str = "Deletes an event from Microsoft Calendar"
|
| 905 |
+
# args_schema: Type[BaseModel] = MicrosoftDeleteCalendarEventArgs
|
| 906 |
+
|
| 907 |
+
# def _run(self, user_id: str, event_id: str):
|
| 908 |
+
# headers, error = self.get_microsoft_client(user_id)
|
| 909 |
+
# if error:
|
| 910 |
+
# return error
|
| 911 |
+
|
| 912 |
+
# response = requests.delete(
|
| 913 |
+
# f"https://graph.microsoft.com/v1.0/me/events/{event_id}",
|
| 914 |
+
# headers=headers
|
| 915 |
+
# )
|
| 916 |
+
|
| 917 |
+
# if response.status_code == 204:
|
| 918 |
+
# return {"status": "success", "message": f"Deleted event {event_id}"}
|
| 919 |
+
# return f"Error deleting event: {response.text}"
|
| 920 |
+
|
| 921 |
+
|
| 922 |
+
|
| 923 |
+
|
| 924 |
+
# tools = [
|
| 925 |
+
# DirectDMTool(),
|
| 926 |
+
# ZoomCreateMeetingTool(),
|
| 927 |
+
# # GetSingleUserSlackName(),
|
| 928 |
+
# # GetSingleUserSlackID(),
|
| 929 |
+
# # CoordinateDMsTool(),
|
| 930 |
+
# SearchUserEventsTool(),
|
| 931 |
+
# # DateTimeTool(),
|
| 932 |
+
# GoogleCalendarList(),
|
| 933 |
+
# GoogleCalendarEvents(),
|
| 934 |
+
# GoogleCreateCalendar(),
|
| 935 |
+
# GoogleAddCalendarEvent(),
|
| 936 |
+
# GoogleUpdateCalendarEvent(),
|
| 937 |
+
# GoogleDeleteCalendarEvent(),
|
| 938 |
+
# # MicrosoftListCalendarEvents(),
|
| 939 |
+
# MicrosoftAddCalendarEvent(),
|
| 940 |
+
# MicrosoftUpdateCalendarEvent(),
|
| 941 |
+
# MicrosoftDeleteCalendarEvent(),
|
| 942 |
+
# MultiDirectDMTool()
|
| 943 |
+
# ]
|
| 944 |
+
# calendar_prompt_tools = [
|
| 945 |
+
# MicrosoftListCalendarEvents(),
|
| 946 |
+
# GoogleCalendarEvents()
|
| 947 |
+
|
| 948 |
+
# ]
|
| 949 |
+
# dm_tools = [
|
| 950 |
+
# DirectDMTool(),
|
| 951 |
+
# ZoomCreateMeetingTool(),
|
| 952 |
+
# # CoordinateDMsTool(),
|
| 953 |
+
# SearchUserEventsTool(),
|
| 954 |
+
# GetSingleUserSlackName(),
|
| 955 |
+
# GetSingleUserSlackID(),
|
| 956 |
+
# # DateTimeTool(),
|
| 957 |
+
# GoogleCalendarList(),
|
| 958 |
+
# GoogleCalendarEvents(),
|
| 959 |
+
# GoogleCreateCalendar(),
|
| 960 |
+
# GoogleAddCalendarEvent(),
|
| 961 |
+
# GoogleUpdateCalendarEvent(),
|
| 962 |
+
# GoogleDeleteCalendarEvent(),
|
| 963 |
+
# MicrosoftListCalendarEvents(),
|
| 964 |
+
# MicrosoftAddCalendarEvent(),
|
| 965 |
+
# MicrosoftUpdateCalendarEvent(),
|
| 966 |
+
# MicrosoftDeleteCalendarEvent()
|
| 967 |
+
# ]
|
| 968 |
+
|
| 969 |
+
# dm_group_tools = [
|
| 970 |
+
# GoogleCalendarEvents(),
|
| 971 |
+
# MicrosoftListCalendarEvents(),
|
| 972 |
+
# DateTimeTool(),
|
| 973 |
+
|
| 974 |
+
# ]
|
| 975 |
+
import json
|
| 976 |
+
import os
|
| 977 |
+
import requests
|
| 978 |
+
import base64
|
| 979 |
+
import msal
|
| 980 |
+
import time
|
| 981 |
+
from typing import Type, Optional, List
|
| 982 |
+
|
| 983 |
+
from dotenv import load_dotenv
|
| 984 |
+
from pydantic.v1 import BaseModel, Field
|
| 985 |
+
from msal import ConfidentialClientApplication
|
| 986 |
+
|
| 987 |
+
from langchain_core.tools import BaseTool
|
| 988 |
+
from slack_sdk.errors import SlackApiError
|
| 989 |
+
from datetime import datetime
|
| 990 |
+
from google_auth_oauthlib.flow import InstalledAppFlow
|
| 991 |
+
from googleapiclient.discovery import build
|
| 992 |
+
from google.oauth2.credentials import Credentials
|
| 993 |
+
from google.auth.transport.requests import Request
|
| 994 |
+
from config import client, get_workspace_owner_id, load_preferences, load_token, save_token
|
| 995 |
+
from datetime import datetime, timedelta
|
| 996 |
+
import pytz
|
| 997 |
+
from collections import defaultdict
|
| 998 |
+
from config import all_users_preload, GetAllUsers
|
| 999 |
+
|
| 1000 |
+
load_dotenv()
|
| 1001 |
+
|
| 1002 |
+
# Load credentials from environment variables
|
| 1003 |
+
MICROSOFT_CLIENT_SECRET = os.getenv("MICROSOFT_CLIENT_SECRET")
|
| 1004 |
+
MICROSOFT_AUTHORITY = "https://login.microsoftonline.com/common"
|
| 1005 |
+
MICROSOFT_SCOPES = ["User.Read", "Calendars.ReadWrite"]
|
| 1006 |
+
MICROSOFT_REDIRECT_URI = os.getenv("MICROSOFT_REDIRECT_URI", "https://clear-muskox-grand.ngrok-free.app/microsoft_callback")
|
| 1007 |
+
MICROSOFT_CLIENT_ID = "855e4571-d92a-4d51-802e-e712a879c00b"
|
| 1008 |
+
ZOOM_CLIENT_SECRET = os.getenv("ZOOM_CLIENT_SECRET")
|
| 1009 |
+
ZOOM_CLIENT_ID = os.getenv("ZOOM_CLIENT_ID")
|
| 1010 |
+
ZOOM_TOKEN_API = os.getenv("ZOOM_TOKEN_API", "https://zoom.us/oauth/token")
|
| 1011 |
+
|
| 1012 |
+
# Pydantic models for tool arguments
|
| 1013 |
+
class DirectDMArgs(BaseModel):
|
| 1014 |
+
message: str = Field(description="The message to be sent to the Slack user")
|
| 1015 |
+
user_id: str = Field(description="The Slack user ID")
|
| 1016 |
+
|
| 1017 |
+
class DateTimeTool(BaseTool):
|
| 1018 |
+
name: str = "current_date_time"
|
| 1019 |
+
description: str = "Provides the current date and time."
|
| 1020 |
+
|
| 1021 |
+
def _run(self):
|
| 1022 |
+
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 1023 |
+
|
| 1024 |
+
# Slack Tools
|
| 1025 |
+
class GetSingleUserSlackIDArgs(BaseModel):
|
| 1026 |
+
name: str = Field(description="The real name of the user whose Slack ID is needed")
|
| 1027 |
+
|
| 1028 |
+
class GetSingleUserSlackID(BaseTool):
|
| 1029 |
+
name: str = "gets_slack_id_single_user"
|
| 1030 |
+
description: str = "Gets the Slack ID of a user based on their real name"
|
| 1031 |
+
args_schema: Type[BaseModel] = GetSingleUserSlackIDArgs
|
| 1032 |
+
|
| 1033 |
+
def _run(self, name: str):
|
| 1034 |
+
if not all_users_preload:
|
| 1035 |
+
all_users = GetAllUsers()
|
| 1036 |
+
else:
|
| 1037 |
+
all_users = all_users_preload
|
| 1038 |
+
|
| 1039 |
+
for uid, info in all_users.items():
|
| 1040 |
+
if info["name"].lower() == name.lower():
|
| 1041 |
+
return uid, info['email']
|
| 1042 |
+
return "User not found"
|
| 1043 |
+
|
| 1044 |
+
class GetSingleUserSlackNameArgs(BaseModel):
|
| 1045 |
+
id: str = Field(description="The Slack user ID")
|
| 1046 |
+
|
| 1047 |
+
class GetSingleUserSlackName(BaseTool):
|
| 1048 |
+
name: str = "gets_slack_name_single_user"
|
| 1049 |
+
description: str = "Gets the Slack real name of a user based on their Slack ID"
|
| 1050 |
+
args_schema: Type[BaseModel] = GetSingleUserSlackNameArgs
|
| 1051 |
+
|
| 1052 |
+
def _run(self, id: str):
|
| 1053 |
+
if not all_users_preload or all_users_preload == {}:
|
| 1054 |
+
all_users = GetAllUsers()
|
| 1055 |
+
else:
|
| 1056 |
+
all_users = all_users_preload
|
| 1057 |
+
|
| 1058 |
+
user = all_users.get(id)
|
| 1059 |
+
if user:
|
| 1060 |
+
return user["name"], user['email']
|
| 1061 |
+
return "User not found"
|
| 1062 |
+
|
| 1063 |
+
class MultiDMArgs(BaseModel):
|
| 1064 |
+
message: str
|
| 1065 |
+
user_ids: List[str]
|
| 1066 |
+
|
| 1067 |
+
class MultiDirectDMTool(BaseTool):
|
| 1068 |
+
name: str = "send_multiple_dms"
|
| 1069 |
+
description: str = "Sends direct messages to multiple Slack users"
|
| 1070 |
+
args_schema: Type[BaseModel] = MultiDMArgs
|
| 1071 |
+
|
| 1072 |
+
def _run(self, message: str, user_ids: List[str]):
|
| 1073 |
+
results = {}
|
| 1074 |
+
for user_id in user_ids:
|
| 1075 |
+
try:
|
| 1076 |
+
client.chat_postMessage(channel=user_id, text=message)
|
| 1077 |
+
results[user_id] = "Message sent successfully"
|
| 1078 |
+
except SlackApiError as e:
|
| 1079 |
+
results[user_id] = f"Error: {e.response['error']}"
|
| 1080 |
+
return results
|
| 1081 |
+
|
| 1082 |
+
class DirectDMTool(BaseTool):
|
| 1083 |
+
name: str = "send_direct_dm"
|
| 1084 |
+
description: str = "Sends direct messages to Slack users"
|
| 1085 |
+
args_schema: Type[BaseModel] = DirectDMArgs
|
| 1086 |
+
|
| 1087 |
+
def _run(self, message: str, user_id: str):
|
| 1088 |
+
try:
|
| 1089 |
+
client.chat_postMessage(channel=user_id, text=message)
|
| 1090 |
+
return "Message sent successfully"
|
| 1091 |
+
except SlackApiError as e:
|
| 1092 |
+
return f"Error sending message: {e.response['error']}"
|
| 1093 |
+
|
| 1094 |
+
def send_dm(user_id: str, message: str) -> bool:
|
| 1095 |
+
try:
|
| 1096 |
+
client.chat_postMessage(channel=user_id, text=message)
|
| 1097 |
+
return True
|
| 1098 |
+
except SlackApiError as e:
|
| 1099 |
+
print(f"Error sending DM: {e.response['error']}")
|
| 1100 |
+
return False
|
| 1101 |
+
|
| 1102 |
+
# Google Calendar Tools
|
| 1103 |
+
PT = pytz.timezone('America/Los_Angeles')
|
| 1104 |
+
|
| 1105 |
+
def construct_google_calendar_client(team_id: str, user_id: str):
|
| 1106 |
+
token_data = load_token(team_id, user_id, 'google')
|
| 1107 |
+
if not token_data:
|
| 1108 |
+
return None
|
| 1109 |
+
creds = Credentials(
|
| 1110 |
+
token=token_data.get('access_token'),
|
| 1111 |
+
refresh_token=token_data.get('refresh_token'),
|
| 1112 |
+
token_uri="https://oauth2.googleapis.com/token",
|
| 1113 |
+
client_id=os.getenv("GOOGLE_CLIENT_ID"),
|
| 1114 |
+
client_secret=os.getenv("GOOGLE_CLIENT_SECRET"),
|
| 1115 |
+
scopes=["https://www.googleapis.com/auth/calendar"]
|
| 1116 |
+
)
|
| 1117 |
+
if creds.expired and creds.refresh_token:
|
| 1118 |
+
creds.refresh(Request())
|
| 1119 |
+
token_data.update({
|
| 1120 |
+
"access_token": creds.token,
|
| 1121 |
+
"refresh_token": creds.refresh_token,
|
| 1122 |
+
"expires_at": creds.expiry.timestamp()
|
| 1123 |
+
})
|
| 1124 |
+
save_token(team_id, user_id, 'google', token_data)
|
| 1125 |
+
return build('calendar', 'v3', credentials=creds)
|
| 1126 |
+
|
| 1127 |
+
class GoogleCalendarList(BaseTool):
|
| 1128 |
+
name: str = "list_calendar_list"
|
| 1129 |
+
description: str = "Lists available calendars in the user's Google Calendar account"
|
| 1130 |
+
|
| 1131 |
+
def _run(self, team_id: str, user_id: str, max_capacity: int = 200):
|
| 1132 |
+
calendar_service = construct_google_calendar_client(team_id, user_id)
|
| 1133 |
+
if not calendar_service:
|
| 1134 |
+
return "Google Calendar not configured or token invalid."
|
| 1135 |
+
|
| 1136 |
+
all_calendars = []
|
| 1137 |
+
next_page_token = None
|
| 1138 |
+
capacity_tracker = 0
|
| 1139 |
+
|
| 1140 |
+
while capacity_tracker < max_capacity:
|
| 1141 |
+
results = calendar_service.calendarList().list(
|
| 1142 |
+
maxResults=min(200, max_capacity - capacity_tracker),
|
| 1143 |
+
pageToken=next_page_token
|
| 1144 |
+
).execute()
|
| 1145 |
+
calendars = results.get('items', [])
|
| 1146 |
+
all_calendars.extend(calendars)
|
| 1147 |
+
capacity_tracker += len(calendars)
|
| 1148 |
+
next_page_token = results.get('nextPageToken')
|
| 1149 |
+
if not next_page_token:
|
| 1150 |
+
break
|
| 1151 |
+
|
| 1152 |
+
return [{
|
| 1153 |
+
'id': cal['id'],
|
| 1154 |
+
'name': cal['summary'],
|
| 1155 |
+
'description': cal.get('description', '')
|
| 1156 |
+
} for cal in all_calendars]
|
| 1157 |
+
|
| 1158 |
+
class GoogleCalendarEvents(BaseTool):
|
| 1159 |
+
name: str = "list_calendar_events"
|
| 1160 |
+
description: str = "Lists and gets events from a specific Google Calendar"
|
| 1161 |
+
|
| 1162 |
+
def _run(self, team_id: str, user_id: str, calendar_id: str = "primary", max_capacity: int = 20):
|
| 1163 |
+
calendar_service = construct_google_calendar_client(team_id, user_id)
|
| 1164 |
+
if not calendar_service:
|
| 1165 |
+
return "Google Calendar not configured or token invalid."
|
| 1166 |
+
|
| 1167 |
+
all_events = []
|
| 1168 |
+
next_page_token = None
|
| 1169 |
+
capacity_tracker = 0
|
| 1170 |
+
|
| 1171 |
+
while capacity_tracker < max_capacity:
|
| 1172 |
+
results = calendar_service.events().list(
|
| 1173 |
+
calendarId=calendar_id,
|
| 1174 |
+
maxResults=min(250, max_capacity - capacity_tracker),
|
| 1175 |
+
pageToken=next_page_token
|
| 1176 |
+
).execute()
|
| 1177 |
+
events = results.get('items', [])
|
| 1178 |
+
all_events.extend(events)
|
| 1179 |
+
capacity_tracker += len(events)
|
| 1180 |
+
next_page_token = results.get('nextPageToken')
|
| 1181 |
+
if not next_page_token:
|
| 1182 |
+
break
|
| 1183 |
+
|
| 1184 |
+
return all_events
|
| 1185 |
+
|
| 1186 |
+
class GoogleCreateCalendar(BaseTool):
|
| 1187 |
+
name: str = "create_calendar_list"
|
| 1188 |
+
description: str = "Creates a new calendar in Google Calendar"
|
| 1189 |
+
|
| 1190 |
+
def _run(self, team_id: str, user_id: str, calendar_name: str):
|
| 1191 |
+
calendar_service = construct_google_calendar_client(team_id, user_id)
|
| 1192 |
+
if not calendar_service:
|
| 1193 |
+
return "Google Calendar not configured or token invalid."
|
| 1194 |
+
|
| 1195 |
+
calendar_body = {'summary': calendar_name}
|
| 1196 |
+
created_calendar = calendar_service.calendars().insert(body=calendar_body).execute()
|
| 1197 |
+
return f"Created calendar: {created_calendar['id']}"
|
| 1198 |
+
|
| 1199 |
+
class GoogleAddCalendarEventArgs(BaseModel):
|
| 1200 |
+
team_id: str = Field(description="Team id here")
|
| 1201 |
+
user_id: str = Field(description="User id here")
|
| 1202 |
+
|
| 1203 |
+
calendar_id: str = Field(default="primary", description="Calendar ID (default 'primary')")
|
| 1204 |
+
summary: str = Field(description="Event title")
|
| 1205 |
+
description: str = Field(default="", description="Event description or agenda")
|
| 1206 |
+
start_time: str = Field(description="Start time in ISO 8601 format")
|
| 1207 |
+
end_time: str = Field(description="End time in ISO 8601 format")
|
| 1208 |
+
location: str = Field(default="", description="Event location")
|
| 1209 |
+
invite_link: str = Field(default="", description="Invite link for the meeting")
|
| 1210 |
+
guests: List[str] = Field(default=None, description="List of guest emails to invite")
|
| 1211 |
+
|
| 1212 |
+
class GoogleAddCalendarEvent(BaseTool):
|
| 1213 |
+
name: str = "google_add_calendar_event"
|
| 1214 |
+
description: str = "Creates an event in a Google Calendar"
|
| 1215 |
+
args_schema: Type[BaseModel] = GoogleAddCalendarEventArgs
|
| 1216 |
+
|
| 1217 |
+
def _run(self, team_id: str, user_id: str, summary: str, start_time: str, end_time: str,
|
| 1218 |
+
description: str = "", calendar_id: str = 'primary', location: str = "",
|
| 1219 |
+
invite_link: str = "", guests: List[str] = None):
|
| 1220 |
+
calendar_service = construct_google_calendar_client(team_id, user_id)
|
| 1221 |
+
if not calendar_service:
|
| 1222 |
+
return "Google Calendar not configured or token invalid."
|
| 1223 |
+
|
| 1224 |
+
if invite_link:
|
| 1225 |
+
description = f"{description}\nInvite Link: {invite_link}"
|
| 1226 |
+
|
| 1227 |
+
event = {
|
| 1228 |
+
'summary': summary,
|
| 1229 |
+
'description': description,
|
| 1230 |
+
'start': {'dateTime': start_time, 'timeZone': 'America/Los_Angeles'},
|
| 1231 |
+
'end': {'dateTime': end_time, 'timeZone': 'America/Los_Angeles'},
|
| 1232 |
+
'location': location,
|
| 1233 |
+
}
|
| 1234 |
+
|
| 1235 |
+
if guests:
|
| 1236 |
+
event['attendees'] = [{'email': guest} for guest in guests]
|
| 1237 |
+
|
| 1238 |
+
try:
|
| 1239 |
+
created_event = calendar_service.events().insert(
|
| 1240 |
+
calendarId=calendar_id,
|
| 1241 |
+
body=event,
|
| 1242 |
+
sendUpdates='all'
|
| 1243 |
+
).execute()
|
| 1244 |
+
return {
|
| 1245 |
+
"status": "success",
|
| 1246 |
+
"event_id": created_event['id'],
|
| 1247 |
+
"link": created_event.get('htmlLink', '')
|
| 1248 |
+
}
|
| 1249 |
+
except Exception as e:
|
| 1250 |
+
return {"status": "error", "message": str(e)}
|
| 1251 |
+
|
| 1252 |
+
class GoogleUpdateCalendarEventArgs(BaseModel):
|
| 1253 |
+
team_id: str = Field(description="Team id here")
|
| 1254 |
+
user_id: str = Field(description="User id here")
|
| 1255 |
+
calendar_id: str = Field(default="primary", description="Calendar ID (default 'primary')")
|
| 1256 |
+
event_id: str = Field(description="The event ID to update")
|
| 1257 |
+
summary: str = Field(default=None, description="Updated event title")
|
| 1258 |
+
description: Optional[str] = Field(default=None, description="Updated event description")
|
| 1259 |
+
start_time: Optional[str] = Field(default=None, description="Updated start time in ISO 8601 format")
|
| 1260 |
+
end_time: Optional[str] = Field(default=None, description="Updated end time in ISO 8601 format")
|
| 1261 |
+
location: Optional[str] = Field(default=None, description="Updated event location")
|
| 1262 |
+
invite_link: str = Field(default=None, description="Updated invite link")
|
| 1263 |
+
guests: List[str] = Field(default=None, description="Updated list of guest emails")
|
| 1264 |
+
|
| 1265 |
+
class GoogleUpdateCalendarEvent(BaseTool):
|
| 1266 |
+
name: str = "google_update_calendar_event"
|
| 1267 |
+
description: str = "Updates an existing event in a Google Calendar"
|
| 1268 |
+
args_schema: Type[BaseModel] = GoogleUpdateCalendarEventArgs
|
| 1269 |
+
|
| 1270 |
+
def _run(self, team_id: str, user_id: str, event_id: str, calendar_id: str = "primary",
|
| 1271 |
+
summary: Optional[str] = None, description: Optional[str] = None,
|
| 1272 |
+
start_time: Optional[str] = None, end_time: Optional[str] = None,
|
| 1273 |
+
location: Optional[str] = None, invite_link: Optional[str] = None,
|
| 1274 |
+
guests: Optional[List[str]] = None):
|
| 1275 |
+
calendar_service = construct_google_calendar_client(team_id, user_id)
|
| 1276 |
+
if not calendar_service:
|
| 1277 |
+
return "Google Calendar not configured or token invalid."
|
| 1278 |
+
|
| 1279 |
+
try:
|
| 1280 |
+
event = calendar_service.events().get(calendarId=calendar_id, eventId=event_id).execute()
|
| 1281 |
+
except Exception as e:
|
| 1282 |
+
return {"status": "error", "message": f"Event retrieval failed: {str(e)}"}
|
| 1283 |
+
|
| 1284 |
+
if summary:
|
| 1285 |
+
event['summary'] = summary
|
| 1286 |
+
if description:
|
| 1287 |
+
event['description'] = description
|
| 1288 |
+
if invite_link:
|
| 1289 |
+
current_desc = event.get('description', '')
|
| 1290 |
+
event['description'] = f"{current_desc}\nInvite Link: {invite_link}"
|
| 1291 |
+
if start_time:
|
| 1292 |
+
event['start'] = {'dateTime': start_time, 'timeZone': 'America/Los_Angeles'}
|
| 1293 |
+
if end_time:
|
| 1294 |
+
event['end'] = {'dateTime': end_time, 'timeZone': 'America/Los_Angeles'}
|
| 1295 |
+
if location:
|
| 1296 |
+
event['location'] = location
|
| 1297 |
+
if guests is not None:
|
| 1298 |
+
event['attendees'] = [{'email': guest} for guest in guests]
|
| 1299 |
+
|
| 1300 |
+
try:
|
| 1301 |
+
updated_event = calendar_service.events().update(
|
| 1302 |
+
calendarId=calendar_id,
|
| 1303 |
+
eventId=event_id,
|
| 1304 |
+
body=event
|
| 1305 |
+
).execute()
|
| 1306 |
+
return {"status": "success", "event_id": updated_event['id']}
|
| 1307 |
+
except Exception as e:
|
| 1308 |
+
return {"status": "error", "message": f"Update failed: {str(e)}"}
|
| 1309 |
+
|
| 1310 |
+
class GoogleDeleteCalendarEventArgs(BaseModel):
|
| 1311 |
+
team_id: str = Field(description="Team id here")
|
| 1312 |
+
user_id: str = Field(description="User id here")
|
| 1313 |
+
calendar_id: str = Field(default="primary", description="Calendar ID (default 'primary')")
|
| 1314 |
+
event_id: str = Field(description="The event ID to delete")
|
| 1315 |
+
|
| 1316 |
+
class GoogleDeleteCalendarEvent(BaseTool):
|
| 1317 |
+
name: str = "google_delete_calendar_event"
|
| 1318 |
+
description: str = "Deletes an event from a Google Calendar"
|
| 1319 |
+
args_schema: Type[BaseModel] = GoogleDeleteCalendarEventArgs
|
| 1320 |
+
|
| 1321 |
+
def _run(self, team_id: str, user_id: str, event_id: str, calendar_id: str = "primary"):
|
| 1322 |
+
calendar_service = construct_google_calendar_client(team_id, user_id)
|
| 1323 |
+
if not calendar_service:
|
| 1324 |
+
return "Google Calendar not configured or token invalid."
|
| 1325 |
+
try:
|
| 1326 |
+
calendar_service.events().delete(calendarId=calendar_id, eventId=event_id).execute()
|
| 1327 |
+
return {"status": "success", "message": f"Deleted event {event_id}"}
|
| 1328 |
+
except Exception as e:
|
| 1329 |
+
return {"status": "error", "message": f"Deletion failed: {str(e)}"}
|
| 1330 |
+
|
| 1331 |
+
# Search Events Tool
|
| 1332 |
+
class SearchUserEventsArgs(BaseModel):
|
| 1333 |
+
user_id: str
|
| 1334 |
+
lookback_days: int = Field(default=30)
|
| 1335 |
+
|
| 1336 |
+
class SearchUserEventsTool(BaseTool):
|
| 1337 |
+
name: str = "search_events_by_user"
|
| 1338 |
+
description: str = "Finds calendar events associated with a specific user"
|
| 1339 |
+
args_schema: Type[BaseModel] = SearchUserEventsArgs
|
| 1340 |
+
|
| 1341 |
+
def _run(self, team_id: str, user_id: str, lookback_days: int = 30):
|
| 1342 |
+
user_info = GetSingleUserSlackName().run(user_id)
|
| 1343 |
+
if user_info == "User not found":
|
| 1344 |
+
return []
|
| 1345 |
+
|
| 1346 |
+
user_name, user_email = user_info
|
| 1347 |
+
events = GoogleCalendarEvents().run(team_id, user_id)
|
| 1348 |
+
|
| 1349 |
+
now = datetime.now(pytz.UTC)
|
| 1350 |
+
relevant_events = []
|
| 1351 |
+
for event in events:
|
| 1352 |
+
event_time_str = event['start'].get('dateTime')
|
| 1353 |
+
if not event_time_str:
|
| 1354 |
+
continue
|
| 1355 |
+
|
| 1356 |
+
event_time = datetime.fromisoformat(event_time_str)
|
| 1357 |
+
if event_time.tzinfo is None:
|
| 1358 |
+
event_time = pytz.UTC.localize(event_time)
|
| 1359 |
+
|
| 1360 |
+
if (now - event_time).days > lookback_days:
|
| 1361 |
+
continue
|
| 1362 |
+
|
| 1363 |
+
if user_name in event.get('summary', '') or user_name in event.get('description', ''):
|
| 1364 |
+
relevant_events.append({
|
| 1365 |
+
'id': event['id'],
|
| 1366 |
+
'title': event['summary'],
|
| 1367 |
+
'time': event_time.strftime("%Y-%m-%d %H:%M"),
|
| 1368 |
+
'calendar_id': event['organizer']['email']
|
| 1369 |
+
})
|
| 1370 |
+
return relevant_events
|
| 1371 |
+
|
| 1372 |
+
# Zoom Meeting Tool
|
| 1373 |
+
class ZoomCreateMeetingArgs(BaseModel):
|
| 1374 |
+
team_id:str = Field(description="Team Id here")
|
| 1375 |
+
topic: str = Field(description="Meeting topic with all names not slack ids starting with U--")
|
| 1376 |
+
start_time: str = Field(description="Start time in ISO 8601 format")
|
| 1377 |
+
duration: int = Field(description="Duration in minutes")
|
| 1378 |
+
agenda: Optional[str] = Field(default="", description="Meeting agenda")
|
| 1379 |
+
timezone: str = Field(default="UTC", description="Timezone for the meeting")
|
| 1380 |
+
|
| 1381 |
+
class ZoomCreateMeetingTool(BaseTool):
|
| 1382 |
+
name: str = "create_zoom_meeting"
|
| 1383 |
+
description: str = "Creates a Zoom meeting using configured credentials"
|
| 1384 |
+
args_schema: Type[BaseModel] = ZoomCreateMeetingArgs
|
| 1385 |
+
|
| 1386 |
+
def _run(self, team_id: str, topic: str, start_time: str, duration: int = 30,
|
| 1387 |
+
agenda: str = "", timezone: str = "UTC"):
|
| 1388 |
+
owner_id = get_workspace_owner_id(client,team_id)
|
| 1389 |
+
if not owner_id:
|
| 1390 |
+
return "Workspace owner not found"
|
| 1391 |
+
|
| 1392 |
+
prefs = load_preferences(team_id, owner_id)
|
| 1393 |
+
zoom_config = prefs.get("zoom_config", {"mode": "manual", "link": None})
|
| 1394 |
+
|
| 1395 |
+
if zoom_config["mode"] == "manual":
|
| 1396 |
+
link = zoom_config.get("link")
|
| 1397 |
+
if link:
|
| 1398 |
+
return f"Please join the meeting using this link: {link}"
|
| 1399 |
+
else:
|
| 1400 |
+
return "Manual Zoom link not configured"
|
| 1401 |
+
|
| 1402 |
+
token_data = load_token(team_id, owner_id, 'zoom')
|
| 1403 |
+
if not token_data:
|
| 1404 |
+
return "Zoom token not found in database"
|
| 1405 |
+
|
| 1406 |
+
access_token = token_data.get("access_token")
|
| 1407 |
+
if not access_token:
|
| 1408 |
+
return "Invalid Zoom token"
|
| 1409 |
+
|
| 1410 |
+
expires_at = token_data.get("expires_at")
|
| 1411 |
+
if expires_at and time.time() > expires_at:
|
| 1412 |
+
refresh_token = token_data.get("refresh_token")
|
| 1413 |
+
if not refresh_token:
|
| 1414 |
+
return "Zoom token expired and no refresh token available"
|
| 1415 |
+
params = {
|
| 1416 |
+
"grant_type": "refresh_token",
|
| 1417 |
+
"refresh_token": refresh_token
|
| 1418 |
+
}
|
| 1419 |
+
auth_str = f"{ZOOM_CLIENT_ID}:{ZOOM_CLIENT_SECRET}"
|
| 1420 |
+
auth_bytes = base64.b64encode(auth_str.encode()).decode()
|
| 1421 |
+
headers = {
|
| 1422 |
+
"Authorization": f"Basic {auth_bytes}",
|
| 1423 |
+
"Content-Type": "application/x-www-form-urlencoded"
|
| 1424 |
+
}
|
| 1425 |
+
response = requests.post(ZOOM_TOKEN_API, data=params, headers=headers)
|
| 1426 |
+
if response.status_code == 200:
|
| 1427 |
+
new_token_data = response.json()
|
| 1428 |
+
token_data.update(new_token_data)
|
| 1429 |
+
token_data["expires_at"] = time.time() + new_token_data["expires_in"]
|
| 1430 |
+
save_token(team_id, owner_id, 'zoom', token_data)
|
| 1431 |
+
access_token = new_token_data["access_token"]
|
| 1432 |
+
else:
|
| 1433 |
+
return "Failed to refresh Zoom token"
|
| 1434 |
+
|
| 1435 |
+
meeting_data = {
|
| 1436 |
+
"topic": topic,
|
| 1437 |
+
"type": 2,
|
| 1438 |
+
"start_time": start_time,
|
| 1439 |
+
"duration": duration,
|
| 1440 |
+
"timezone": timezone,
|
| 1441 |
+
"agenda": agenda,
|
| 1442 |
+
"settings": {
|
| 1443 |
+
"host_video": True,
|
| 1444 |
+
"participant_video": True,
|
| 1445 |
+
"join_before_host": False
|
| 1446 |
+
}
|
| 1447 |
+
}
|
| 1448 |
+
headers = {
|
| 1449 |
+
"Authorization": f"Bearer {access_token}",
|
| 1450 |
+
"Content-Type": "application/json"
|
| 1451 |
+
}
|
| 1452 |
+
meeting_res = requests.post(
|
| 1453 |
+
"https://api.zoom.us/v2/users/me/meetings",
|
| 1454 |
+
headers=headers,
|
| 1455 |
+
json=meeting_data
|
| 1456 |
+
)
|
| 1457 |
+
if meeting_res.status_code == 201:
|
| 1458 |
+
meeting_info = meeting_res.json()
|
| 1459 |
+
invitation = f"Join Zoom Meeting\n{meeting_info['join_url']}\nMeeting ID: {meeting_info['id']}\nPasscode: {meeting_info.get('password', '')}"
|
| 1460 |
+
return invitation
|
| 1461 |
+
else:
|
| 1462 |
+
return f"Zoom meeting creation failed: {meeting_res.text}"
|
| 1463 |
+
|
| 1464 |
+
# Microsoft Calendar Tools
|
| 1465 |
+
class MicrosoftBaseTool(BaseTool):
|
| 1466 |
+
def get_microsoft_client(self, team_id: str, user_id: str):
|
| 1467 |
+
token_data = load_token(team_id, user_id, 'microsoft')
|
| 1468 |
+
if not token_data:
|
| 1469 |
+
return None, "Microsoft token not found in database"
|
| 1470 |
+
|
| 1471 |
+
if time.time() > token_data['expires_at']:
|
| 1472 |
+
app = ConfidentialClientApplication(
|
| 1473 |
+
MICROSOFT_CLIENT_ID,
|
| 1474 |
+
authority=MICROSOFT_AUTHORITY,
|
| 1475 |
+
client_credential=MICROSOFT_CLIENT_SECRET
|
| 1476 |
+
)
|
| 1477 |
+
result = app.acquire_token_by_refresh_token(
|
| 1478 |
+
token_data['refresh_token'],
|
| 1479 |
+
scopes=MICROSOFT_SCOPES
|
| 1480 |
+
)
|
| 1481 |
+
if "access_token" not in result:
|
| 1482 |
+
return None, "Token refresh failed"
|
| 1483 |
+
token_data.update(result)
|
| 1484 |
+
save_token(team_id, user_id, 'microsoft', token_data)
|
| 1485 |
+
|
| 1486 |
+
headers = {
|
| 1487 |
+
"Authorization": f"Bearer {token_data['access_token']}",
|
| 1488 |
+
"Content-Type": "application/json"
|
| 1489 |
+
}
|
| 1490 |
+
return headers, None
|
| 1491 |
+
|
| 1492 |
+
class MicrosoftListCalendarEvents(MicrosoftBaseTool):
|
| 1493 |
+
name: str = "microsoft_calendar_list_events"
|
| 1494 |
+
description: str = "Lists events from Microsoft Calendar"
|
| 1495 |
+
|
| 1496 |
+
def _run(self, team_id: str, user_id: str, max_results: int = 10):
|
| 1497 |
+
headers, error = self.get_microsoft_client(team_id, user_id)
|
| 1498 |
+
if error:
|
| 1499 |
+
return error
|
| 1500 |
+
|
| 1501 |
+
endpoint = "https://graph.microsoft.com/v1.0/me/events"
|
| 1502 |
+
params = {"$top": max_results, "$orderby": "start/dateTime desc"}
|
| 1503 |
+
response = requests.get(endpoint, headers=headers, params=params)
|
| 1504 |
+
if response.status_code != 200:
|
| 1505 |
+
return f"Error fetching events: {response.text}"
|
| 1506 |
+
|
| 1507 |
+
events = response.json().get('value', [])
|
| 1508 |
+
return [{
|
| 1509 |
+
'id': e['id'],
|
| 1510 |
+
'subject': e.get('subject'),
|
| 1511 |
+
'start': e['start'].get('dateTime'),
|
| 1512 |
+
'end': e['end'].get('dateTime'),
|
| 1513 |
+
'webLink': e.get('webUrl')
|
| 1514 |
+
} for e in events]
|
| 1515 |
+
|
| 1516 |
+
class MicrosoftAddCalendarEventArgs(BaseModel):
|
| 1517 |
+
team_id: str = Field(description="Team id here")
|
| 1518 |
+
user_id: str = Field(description="User id here")
|
| 1519 |
+
subject: str = Field(description="Event title/subject")
|
| 1520 |
+
start_time: str = Field(description="Start time in ISO 8601 format")
|
| 1521 |
+
end_time: str = Field(description="End time in ISO 8601 format")
|
| 1522 |
+
content: str = Field(default="", description="Event description/content")
|
| 1523 |
+
location: str = Field(default="", description="Event location")
|
| 1524 |
+
attendees: List[str] = Field(default=[], description="List of attendee emails")
|
| 1525 |
+
|
| 1526 |
+
class MicrosoftAddCalendarEvent(MicrosoftBaseTool):
|
| 1527 |
+
name: str = "microsoft_calendar_add_event"
|
| 1528 |
+
description: str = "Creates an event in Microsoft Calendar"
|
| 1529 |
+
args_schema: Type[BaseModel] = MicrosoftAddCalendarEventArgs
|
| 1530 |
+
|
| 1531 |
+
def _run(self, team_id: str, user_id: str, subject: str, start_time: str, end_time: str,
|
| 1532 |
+
content: str = "", location: str = "", attendees: List[str] = []):
|
| 1533 |
+
headers, error = self.get_microsoft_client(team_id, user_id)
|
| 1534 |
+
if error:
|
| 1535 |
+
return error
|
| 1536 |
+
|
| 1537 |
+
event_payload = {
|
| 1538 |
+
"subject": subject,
|
| 1539 |
+
"body": {"contentType": "HTML", "content": content},
|
| 1540 |
+
"start": {"dateTime": start_time, "timeZone": "America/Los_Angeles"},
|
| 1541 |
+
"end": {"dateTime": end_time, "timeZone": "America/Los_Angeles"},
|
| 1542 |
+
"location": {"displayName": location},
|
| 1543 |
+
"attendees": [{"emailAddress": {"address": email}} for email in attendees]
|
| 1544 |
+
}
|
| 1545 |
+
|
| 1546 |
+
response = requests.post(
|
| 1547 |
+
"https://graph.microsoft.com/v1.0/me/events",
|
| 1548 |
+
headers=headers,
|
| 1549 |
+
json=event_payload
|
| 1550 |
+
)
|
| 1551 |
+
if response.status_code == 201:
|
| 1552 |
+
return {
|
| 1553 |
+
"status": "success",
|
| 1554 |
+
"event_id": response.json()['id'],
|
| 1555 |
+
"link": response.json().get('webUrl')
|
| 1556 |
+
}
|
| 1557 |
+
return f"Error creating event: {response.text}"
|
| 1558 |
+
|
| 1559 |
+
class MicrosoftUpdateCalendarEventArgs(MicrosoftAddCalendarEventArgs):
|
| 1560 |
+
team_id: str = Field(description="Team id here")
|
| 1561 |
+
user_id: str = Field(description="User id here")
|
| 1562 |
+
event_id: str = Field(description="Microsoft event ID to update")
|
| 1563 |
+
|
| 1564 |
+
class MicrosoftUpdateCalendarEvent(MicrosoftBaseTool):
|
| 1565 |
+
name: str = "microsoft_calendar_update_event"
|
| 1566 |
+
description: str = "Updates an existing Microsoft Calendar event"
|
| 1567 |
+
args_schema: Type[BaseModel] = MicrosoftUpdateCalendarEventArgs
|
| 1568 |
+
|
| 1569 |
+
def _run(self, team_id: str, user_id: str, event_id: str, **kwargs):
|
| 1570 |
+
headers, error = self.get_microsoft_client(team_id, user_id)
|
| 1571 |
+
if error:
|
| 1572 |
+
return error
|
| 1573 |
+
|
| 1574 |
+
get_response = requests.get(
|
| 1575 |
+
f"https://graph.microsoft.com/v1.0/me/events/{event_id}",
|
| 1576 |
+
headers=headers
|
| 1577 |
+
)
|
| 1578 |
+
if get_response.status_code != 200:
|
| 1579 |
+
return f"Error finding event: {get_response.text}"
|
| 1580 |
+
|
| 1581 |
+
existing_event = get_response.json()
|
| 1582 |
+
update_payload = {
|
| 1583 |
+
"subject": kwargs.get('subject', existing_event.get('subject')),
|
| 1584 |
+
"body": {
|
| 1585 |
+
"content": kwargs.get('content', existing_event.get('body', {}).get('content')),
|
| 1586 |
+
"contentType": "HTML"
|
| 1587 |
+
},
|
| 1588 |
+
"start": {
|
| 1589 |
+
"dateTime": kwargs.get('start_time', existing_event['start']['dateTime']),
|
| 1590 |
+
"timeZone": "America/Los_Angeles"
|
| 1591 |
+
},
|
| 1592 |
+
"end": {
|
| 1593 |
+
"dateTime": kwargs.get('end_time', existing_event['end']['dateTime']),
|
| 1594 |
+
"timeZone": "America/Los_Angeles"
|
| 1595 |
+
},
|
| 1596 |
+
"location": {"displayName": kwargs.get('location', existing_event.get('location', {}).get('displayName'))},
|
| 1597 |
+
"attendees": [{"emailAddress": {"address": email}} for email in
|
| 1598 |
+
kwargs.get('attendees', [a['emailAddress']['address'] for a in existing_event.get('attendees', [])])]
|
| 1599 |
+
}
|
| 1600 |
+
|
| 1601 |
+
response = requests.patch(
|
| 1602 |
+
f"https://graph.microsoft.com/v1.0/me/events/{event_id}",
|
| 1603 |
+
headers=headers,
|
| 1604 |
+
json=update_payload
|
| 1605 |
+
)
|
| 1606 |
+
if response.status_code == 200:
|
| 1607 |
+
return {"status": "success", "event_id": event_id}
|
| 1608 |
+
return f"Error updating event: {response.text}"
|
| 1609 |
+
|
| 1610 |
+
class MicrosoftDeleteCalendarEventArgs(BaseModel):
|
| 1611 |
+
team_id: str = Field(description="Team id here")
|
| 1612 |
+
user_id: str = Field(description="User id here")
|
| 1613 |
+
event_id: str = Field(description="Microsoft event ID to delete")
|
| 1614 |
+
|
| 1615 |
+
class MicrosoftDeleteCalendarEvent(MicrosoftBaseTool):
|
| 1616 |
+
name: str = "microsoft_calendar_delete_event"
|
| 1617 |
+
description: str = "Deletes an event from Microsoft Calendar"
|
| 1618 |
+
args_schema: Type[BaseModel] = MicrosoftDeleteCalendarEventArgs
|
| 1619 |
+
|
| 1620 |
+
def _run(self, team_id: str, user_id: str, event_id: str):
|
| 1621 |
+
headers, error = self.get_microsoft_client(team_id, user_id)
|
| 1622 |
+
if error:
|
| 1623 |
+
return error
|
| 1624 |
+
|
| 1625 |
+
response = requests.delete(
|
| 1626 |
+
f"https://graph.microsoft.com/v1.0/me/events/{event_id}",
|
| 1627 |
+
headers=headers
|
| 1628 |
+
)
|
| 1629 |
+
if response.status_code == 204:
|
| 1630 |
+
return {"status": "success", "message": f"Deleted event {event_id}"}
|
| 1631 |
+
return f"Error deleting event: {response.text}"
|
| 1632 |
+
|
| 1633 |
+
# Tool Lists
|
| 1634 |
+
tools = [
|
| 1635 |
+
DirectDMTool(),
|
| 1636 |
+
ZoomCreateMeetingTool(),
|
| 1637 |
+
SearchUserEventsTool(),
|
| 1638 |
+
GoogleCalendarList(),
|
| 1639 |
+
GoogleCalendarEvents(),
|
| 1640 |
+
GoogleCreateCalendar(),
|
| 1641 |
+
GoogleAddCalendarEvent(),
|
| 1642 |
+
GoogleUpdateCalendarEvent(),
|
| 1643 |
+
GoogleDeleteCalendarEvent(),
|
| 1644 |
+
MicrosoftListCalendarEvents(),
|
| 1645 |
+
MicrosoftAddCalendarEvent(),
|
| 1646 |
+
MicrosoftUpdateCalendarEvent(),
|
| 1647 |
+
MicrosoftDeleteCalendarEvent(),
|
| 1648 |
+
MultiDirectDMTool()
|
| 1649 |
+
]
|
| 1650 |
+
|
| 1651 |
+
calendar_prompt_tools = [
|
| 1652 |
+
MicrosoftListCalendarEvents(),
|
| 1653 |
+
GoogleCalendarEvents()
|
| 1654 |
+
]
|
| 1655 |
+
|
| 1656 |
+
dm_tools = [
|
| 1657 |
+
DirectDMTool(),
|
| 1658 |
+
ZoomCreateMeetingTool(),
|
| 1659 |
+
SearchUserEventsTool(),
|
| 1660 |
+
GetSingleUserSlackName(),
|
| 1661 |
+
GetSingleUserSlackID(),
|
| 1662 |
+
GoogleCalendarList(),
|
| 1663 |
+
GoogleCalendarEvents(),
|
| 1664 |
+
GoogleCreateCalendar(),
|
| 1665 |
+
GoogleAddCalendarEvent(),
|
| 1666 |
+
GoogleUpdateCalendarEvent(),
|
| 1667 |
+
GoogleDeleteCalendarEvent(),
|
| 1668 |
+
MicrosoftListCalendarEvents(),
|
| 1669 |
+
MicrosoftAddCalendarEvent(),
|
| 1670 |
+
MicrosoftUpdateCalendarEvent(),
|
| 1671 |
+
MicrosoftDeleteCalendarEvent()
|
| 1672 |
+
]
|
| 1673 |
+
|
| 1674 |
+
dm_group_tools = [
|
| 1675 |
+
GoogleCalendarEvents(),
|
| 1676 |
+
MicrosoftListCalendarEvents(),
|
| 1677 |
+
DateTimeTool(),
|
| 1678 |
+
]
|
app.py
ADDED
|
@@ -0,0 +1,1688 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import secrets
|
| 3 |
+
import time
|
| 4 |
+
import json
|
| 5 |
+
import base64
|
| 6 |
+
from flask_talisman import Talisman
|
| 7 |
+
from flask import Flask, request, jsonify,render_template
|
| 8 |
+
from slack_sdk import WebClient
|
| 9 |
+
from slack_sdk.errors import SlackApiError
|
| 10 |
+
from slack_bolt import App
|
| 11 |
+
from slack_bolt.adapter.flask import SlackRequestHandler
|
| 12 |
+
from slack_bolt.oauth.oauth_settings import OAuthSettings
|
| 13 |
+
from dotenv import find_dotenv, load_dotenv
|
| 14 |
+
from google.auth.transport.requests import Request
|
| 15 |
+
from google.oauth2.credentials import Credentials
|
| 16 |
+
from google_auth_oauthlib.flow import Flow
|
| 17 |
+
from googleapiclient.discovery import build
|
| 18 |
+
from flask_session import Session
|
| 19 |
+
from msal import ConfidentialClientApplication
|
| 20 |
+
import psycopg2
|
| 21 |
+
from apscheduler.schedulers.background import BackgroundScheduler
|
| 22 |
+
from datetime import datetime, timedelta
|
| 23 |
+
from collections import defaultdict
|
| 24 |
+
import hashlib
|
| 25 |
+
import re
|
| 26 |
+
import logging
|
| 27 |
+
from threading import Lock
|
| 28 |
+
|
| 29 |
+
from urllib.parse import quote_plus
|
| 30 |
+
from langchain.chains import LLMChain
|
| 31 |
+
from langchain.prompts import ChatPromptTemplate
|
| 32 |
+
from agents.all_agents import (
|
| 33 |
+
create_schedule_agent, create_update_agent, create_delete_agent, llm,
|
| 34 |
+
create_schedule_group_agent, create_update_group_agent, create_schedule_channel_agent
|
| 35 |
+
)
|
| 36 |
+
from all_tools import tools, calendar_prompt_tools
|
| 37 |
+
from db import init_db
|
| 38 |
+
|
| 39 |
+
# Load environment variables
|
| 40 |
+
load_dotenv(find_dotenv())
|
| 41 |
+
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
|
| 42 |
+
os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = '1'
|
| 43 |
+
os.environ['OAUTHLIB_IGNORE_SCOPE_CHANGE'] = '1'
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
user_cache = {}
|
| 47 |
+
user_cache_lock = Lock() # Example threading lock for cache
|
| 48 |
+
preferences_cache = {}
|
| 49 |
+
preferences_cache_lock = Lock()
|
| 50 |
+
owner_id_cache = {}
|
| 51 |
+
owner_id_lock = Lock()
|
| 52 |
+
# Configuration
|
| 53 |
+
SLACK_CLIENT_ID = os.getenv('SLACK_CLIENT_ID','')
|
| 54 |
+
SLACK_CLIENT_SECRET = os.getenv('SLACK_CLIENT_SECRET','')
|
| 55 |
+
SLACK_SIGNING_SECRET = os.getenv('SLACK_SIGNING_SECRET','')
|
| 56 |
+
SLACK_SCOPES = [
|
| 57 |
+
"app_mentions:read",
|
| 58 |
+
"channels:history",
|
| 59 |
+
"chat:write",
|
| 60 |
+
"users:read",
|
| 61 |
+
"im:write",
|
| 62 |
+
"groups:write",
|
| 63 |
+
"mpim:write",
|
| 64 |
+
"commands",
|
| 65 |
+
"team:read",
|
| 66 |
+
"channels:read",
|
| 67 |
+
"groups:read",
|
| 68 |
+
"im:read",
|
| 69 |
+
"mpim:read",
|
| 70 |
+
"groups:history",
|
| 71 |
+
"im:history",
|
| 72 |
+
"mpim:history"
|
| 73 |
+
]
|
| 74 |
+
import requests
|
| 75 |
+
SLACK_BOT_USER_ID = os.getenv("SLACK_BOT_USER_ID")
|
| 76 |
+
ZOOM_REDIRECT_URI = "https://clear-muskox-grand.ngrok-free.app/zoom_callback"
|
| 77 |
+
CLIENT_ID = "FiyFvBUSSeeXwjDv0tqg" # Zoom Client ID
|
| 78 |
+
CLIENT_SECRET = "tygAN91Xd7Wo1YAH056wtbrXQ8I6UieA" # Zoom Client Secret
|
| 79 |
+
ZOOM_TOKEN_API = "https://zoom.us/oauth/token"
|
| 80 |
+
ZOOM_OAUTH_AUTHORIZE_API = os.getenv("ZOOM_OAUTH_AUTHORIZE_API", "https://zoom.us/oauth/authorize")
|
| 81 |
+
OAUTH_REDIRECT_URI = os.getenv("OAUTH_REDIRECT_URI", "https://clear-muskox-grand.ngrok-free.app/oauth2callback")
|
| 82 |
+
MICROSOFT_CLIENT_ID = "855e4571-d92a-4d51-802e-e712a879c00b"
|
| 83 |
+
MICROSOFT_CLIENT_SECRET = os.getenv("MICROSOFT_CLIENT_SECRET")
|
| 84 |
+
MICROSOFT_AUTHORITY = "https://login.microsoftonline.com/common"
|
| 85 |
+
MICROSOFT_SCOPES = ["User.Read", "Calendars.ReadWrite"]
|
| 86 |
+
MICROSOFT_REDIRECT_URI = os.getenv("MICROSOFT_REDIRECT_URI", "https://clear-muskox-grand.ngrok-free.app/microsoft_callback")
|
| 87 |
+
|
| 88 |
+
# Initialize Flask app
|
| 89 |
+
app = Flask(__name__)
|
| 90 |
+
app.secret_key = secrets.token_hex(16)
|
| 91 |
+
talisman = Talisman(
|
| 92 |
+
app,
|
| 93 |
+
content_security_policy={
|
| 94 |
+
'default-src': "'self'",
|
| 95 |
+
'script-src': "'self'",
|
| 96 |
+
'object-src': "'none'"
|
| 97 |
+
},
|
| 98 |
+
force_https=True,
|
| 99 |
+
strict_transport_security=True,
|
| 100 |
+
strict_transport_security_max_age=31536000,
|
| 101 |
+
x_content_type_options=True,
|
| 102 |
+
referrer_policy='no-referrer-when-downgrade'
|
| 103 |
+
)
|
| 104 |
+
app.config['SESSION_TYPE'] = 'filesystem'
|
| 105 |
+
Session(app)
|
| 106 |
+
|
| 107 |
+
# Installation Store for OAuth
|
| 108 |
+
import json
|
| 109 |
+
import os
|
| 110 |
+
import psycopg2
|
| 111 |
+
from datetime import datetime
|
| 112 |
+
|
| 113 |
+
import json
|
| 114 |
+
import os
|
| 115 |
+
import psycopg2
|
| 116 |
+
from datetime import datetime
|
| 117 |
+
from slack_sdk.oauth import InstallationStore
|
| 118 |
+
# Custom JSON encoder to handle datetime objects
|
| 119 |
+
import json
|
| 120 |
+
import os
|
| 121 |
+
import psycopg2
|
| 122 |
+
from psycopg2.extras import Json
|
| 123 |
+
from datetime import datetime
|
| 124 |
+
import logging
|
| 125 |
+
from slack_sdk import WebClient
|
| 126 |
+
from slack_sdk.oauth import InstallationStore
|
| 127 |
+
from slack_sdk.oauth.installation_store.models.installation import Installation
|
| 128 |
+
from slack_bolt.authorization import AuthorizeResult
|
| 129 |
+
|
| 130 |
+
# Custom JSON encoder for datetime objects (used only if needed)
|
| 131 |
+
class DateTimeEncoder(json.JSONEncoder):
|
| 132 |
+
def default(self, obj):
|
| 133 |
+
if isinstance(obj, datetime):
|
| 134 |
+
return obj.isoformat()
|
| 135 |
+
return super().default(obj)
|
| 136 |
+
|
| 137 |
+
class DatabaseInstallationStore(InstallationStore):
|
| 138 |
+
"""A database-backed installation store for Slack Bolt using PostgreSQL.
|
| 139 |
+
|
| 140 |
+
Assumes 'installation_data' is a jsonb column storing JSON data.
|
| 141 |
+
"""
|
| 142 |
+
|
| 143 |
+
def __init__(self):
|
| 144 |
+
self._logger = logging.getLogger(__name__)
|
| 145 |
+
|
| 146 |
+
def save(self, installation):
|
| 147 |
+
try:
|
| 148 |
+
conn = psycopg2.connect(os.getenv('DATABASE_URL'))
|
| 149 |
+
cur = conn.cursor()
|
| 150 |
+
|
| 151 |
+
workspace_id = installation.team_id
|
| 152 |
+
installed_at = datetime.fromtimestamp(installation.installed_at) if installation.installed_at else None
|
| 153 |
+
|
| 154 |
+
installation_data = {
|
| 155 |
+
"team_id": installation.team_id,
|
| 156 |
+
"enterprise_id": installation.enterprise_id,
|
| 157 |
+
"user_id": installation.user_id,
|
| 158 |
+
"bot_token": installation.bot_token,
|
| 159 |
+
"bot_id": installation.bot_id,
|
| 160 |
+
"bot_user_id": installation.bot_user_id,
|
| 161 |
+
"bot_scopes": installation.bot_scopes,
|
| 162 |
+
"user_token": installation.user_token,
|
| 163 |
+
"user_scopes": installation.user_scopes,
|
| 164 |
+
"incoming_webhook_url": installation.incoming_webhook_url,
|
| 165 |
+
"incoming_webhook_channel": installation.incoming_webhook_channel,
|
| 166 |
+
"incoming_webhook_channel_id": installation.incoming_webhook_channel_id,
|
| 167 |
+
"incoming_webhook_configuration_url": installation.incoming_webhook_configuration_url,
|
| 168 |
+
"app_id": installation.app_id,
|
| 169 |
+
"token_type": installation.token_type,
|
| 170 |
+
"installed_at": installed_at.isoformat() if installed_at else None
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
current_time = datetime.now()
|
| 174 |
+
|
| 175 |
+
cur.execute('''
|
| 176 |
+
INSERT INTO Installations (workspace_id, installation_data, updated_at)
|
| 177 |
+
VALUES (%s, %s, %s)
|
| 178 |
+
ON CONFLICT (workspace_id) DO UPDATE SET
|
| 179 |
+
installation_data = %s, updated_at = %s
|
| 180 |
+
''', (workspace_id, Json(installation_data), current_time, Json(installation_data), current_time))
|
| 181 |
+
|
| 182 |
+
conn.commit()
|
| 183 |
+
self._logger.info(f"Saved installation for workspace {workspace_id}")
|
| 184 |
+
|
| 185 |
+
except Exception as e:
|
| 186 |
+
self._logger.error(f"Failed to save installation for workspace {workspace_id}: {e}")
|
| 187 |
+
raise
|
| 188 |
+
finally:
|
| 189 |
+
cur.close()
|
| 190 |
+
conn.close()
|
| 191 |
+
|
| 192 |
+
def find_installation(self, enterprise_id=None, team_id=None, user_id=None, is_enterprise_install=False):
|
| 193 |
+
if not team_id:
|
| 194 |
+
self._logger.warning("No team_id provided for find_installation")
|
| 195 |
+
return None
|
| 196 |
+
|
| 197 |
+
try:
|
| 198 |
+
conn = psycopg2.connect(os.getenv('DATABASE_URL'))
|
| 199 |
+
cur = conn.cursor()
|
| 200 |
+
cur.execute('SELECT installation_data FROM Installations WHERE workspace_id = %s', (team_id,))
|
| 201 |
+
row = cur.fetchone()
|
| 202 |
+
|
| 203 |
+
if row:
|
| 204 |
+
# For jsonb, row[0] is already a dict
|
| 205 |
+
installation_data = row[0]
|
| 206 |
+
installed_at = (datetime.fromisoformat(installation_data["installed_at"])
|
| 207 |
+
if installation_data.get("installed_at") else None)
|
| 208 |
+
|
| 209 |
+
return Installation(
|
| 210 |
+
app_id=installation_data["app_id"],
|
| 211 |
+
enterprise_id=installation_data.get("enterprise_id"),
|
| 212 |
+
team_id=installation_data["team_id"],
|
| 213 |
+
bot_token=installation_data["bot_token"],
|
| 214 |
+
bot_id=installation_data["bot_id"],
|
| 215 |
+
bot_user_id=installation_data["bot_user_id"],
|
| 216 |
+
bot_scopes=installation_data["bot_scopes"],
|
| 217 |
+
user_id=installation_data["user_id"],
|
| 218 |
+
user_token=installation_data.get("user_token"),
|
| 219 |
+
user_scopes=installation_data.get("user_scopes"),
|
| 220 |
+
incoming_webhook_url=installation_data.get("incoming_webhook_url"),
|
| 221 |
+
incoming_webhook_channel=installation_data.get("incoming_webhook_channel"),
|
| 222 |
+
incoming_webhook_channel_id=installation_data.get("incoming_webhook_channel_id"),
|
| 223 |
+
incoming_webhook_configuration_url=installation_data.get("incoming_webhook_configuration_url"),
|
| 224 |
+
token_type=installation_data["token_type"],
|
| 225 |
+
installed_at=installed_at
|
| 226 |
+
)
|
| 227 |
+
else:
|
| 228 |
+
self._logger.info(f"No installation found for team_id {team_id}")
|
| 229 |
+
return None
|
| 230 |
+
|
| 231 |
+
except Exception as e:
|
| 232 |
+
self._logger.error(f"Error retrieving installation for team_id {team_id}: {e}")
|
| 233 |
+
return None
|
| 234 |
+
finally:
|
| 235 |
+
cur.close()
|
| 236 |
+
conn.close()
|
| 237 |
+
|
| 238 |
+
def find_bot(self, enterprise_id=None, team_id=None, is_enterprise_install=False):
|
| 239 |
+
if not team_id:
|
| 240 |
+
self._logger.warning("No team_id provided for find_bot")
|
| 241 |
+
return None
|
| 242 |
+
|
| 243 |
+
try:
|
| 244 |
+
conn = psycopg2.connect(os.getenv('DATABASE_URL'))
|
| 245 |
+
cur = conn.cursor()
|
| 246 |
+
cur.execute('SELECT installation_data FROM Installations WHERE workspace_id = %s', (team_id,))
|
| 247 |
+
row = cur.fetchone()
|
| 248 |
+
|
| 249 |
+
if row:
|
| 250 |
+
installation_data = row[0]
|
| 251 |
+
return AuthorizeResult(
|
| 252 |
+
enterprise_id=installation_data.get("enterprise_id"),
|
| 253 |
+
team_id=installation_data["team_id"],
|
| 254 |
+
bot_token=installation_data["bot_token"],
|
| 255 |
+
bot_id=installation_data["bot_id"],
|
| 256 |
+
bot_user_id=installation_data["bot_user_id"]
|
| 257 |
+
)
|
| 258 |
+
else:
|
| 259 |
+
self._logger.info(f"No bot installation found for team_id {team_id}")
|
| 260 |
+
return None
|
| 261 |
+
|
| 262 |
+
except Exception as e:
|
| 263 |
+
self._logger.error(f"Error retrieving bot for team_id {team_id}: {e}")
|
| 264 |
+
return None
|
| 265 |
+
finally:
|
| 266 |
+
cur.close()
|
| 267 |
+
conn.close()
|
| 268 |
+
|
| 269 |
+
# Instantiate the store
|
| 270 |
+
installation_store = DatabaseInstallationStore()
|
| 271 |
+
|
| 272 |
+
def get_client_for_team(team_id):
|
| 273 |
+
"""
|
| 274 |
+
Get a Slack WebClient for a given team ID using the stored bot token.
|
| 275 |
+
|
| 276 |
+
Args:
|
| 277 |
+
team_id (str): The team ID (workspace ID) to look up.
|
| 278 |
+
|
| 279 |
+
Returns:
|
| 280 |
+
WebClient: Slack client instance or None if not found.
|
| 281 |
+
"""
|
| 282 |
+
installation = installation_store.find_installation(None, team_id)
|
| 283 |
+
if installation:
|
| 284 |
+
token = installation.bot_token # Use dot notation instead of subscripting
|
| 285 |
+
return WebClient(token=token)
|
| 286 |
+
return None
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
# Initialize Slack Bolt app with OAuth settings
|
| 291 |
+
oauth_settings = OAuthSettings(
|
| 292 |
+
client_id=SLACK_CLIENT_ID,
|
| 293 |
+
client_secret=SLACK_CLIENT_SECRET,
|
| 294 |
+
scopes=SLACK_SCOPES,
|
| 295 |
+
redirect_uri="https://clear-muskox-grand.ngrok-free.app/slack/oauth_redirect",
|
| 296 |
+
installation_store=installation_store
|
| 297 |
+
)
|
| 298 |
+
bolt_app = App(signing_secret=SLACK_SIGNING_SECRET, oauth_settings=oauth_settings)
|
| 299 |
+
slack_handler = SlackRequestHandler(bolt_app)
|
| 300 |
+
|
| 301 |
+
# Logging setup
|
| 302 |
+
logging.basicConfig(level=logging.INFO)
|
| 303 |
+
logger = logging.getLogger(__name__)
|
| 304 |
+
|
| 305 |
+
# Google Calendar API Scopes
|
| 306 |
+
SCOPES = [
|
| 307 |
+
'https://www.googleapis.com/auth/calendar',
|
| 308 |
+
'https://www.googleapis.com/auth/calendar.readonly',
|
| 309 |
+
'https://www.googleapis.com/auth/userinfo.email',
|
| 310 |
+
'https://www.googleapis.com/auth/userinfo.profile'
|
| 311 |
+
]
|
| 312 |
+
|
| 313 |
+
# Initialize Neon Postgres database
|
| 314 |
+
init_db()
|
| 315 |
+
|
| 316 |
+
# State Management Classes
|
| 317 |
+
class StateManager:
|
| 318 |
+
def __init__(self):
|
| 319 |
+
self._states = {}
|
| 320 |
+
self._lock = Lock()
|
| 321 |
+
|
| 322 |
+
def create_state(self, user_id):
|
| 323 |
+
with self._lock:
|
| 324 |
+
state_token = secrets.token_urlsafe(32)
|
| 325 |
+
self._states[state_token] = {"user_id": user_id, "timestamp": datetime.now(), "used": False}
|
| 326 |
+
return state_token
|
| 327 |
+
|
| 328 |
+
def validate_and_consume_state(self, state_token):
|
| 329 |
+
with self._lock:
|
| 330 |
+
if state_token not in self._states:
|
| 331 |
+
return None
|
| 332 |
+
state_data = self._states[state_token]
|
| 333 |
+
if state_data["used"] or (datetime.now() - state_data["timestamp"]).total_seconds() > 600:
|
| 334 |
+
del self._states[state_token]
|
| 335 |
+
return None
|
| 336 |
+
state_data["used"] = True
|
| 337 |
+
return state_data["user_id"]
|
| 338 |
+
|
| 339 |
+
def cleanup_expired_states(self):
|
| 340 |
+
with self._lock:
|
| 341 |
+
current_time = datetime.now()
|
| 342 |
+
expired = [s for s, d in self._states.items() if (current_time - d["timestamp"]).total_seconds() > 600]
|
| 343 |
+
for state in expired:
|
| 344 |
+
del self._states[state]
|
| 345 |
+
|
| 346 |
+
state_manager = StateManager()
|
| 347 |
+
|
| 348 |
+
class EventDeduplicator:
|
| 349 |
+
def __init__(self, expiration_minutes=5):
|
| 350 |
+
self.processed_events = defaultdict(list)
|
| 351 |
+
self.expiration_minutes = expiration_minutes
|
| 352 |
+
|
| 353 |
+
def clean_expired_events(self):
|
| 354 |
+
current_time = datetime.now()
|
| 355 |
+
for event_id in list(self.processed_events.keys()):
|
| 356 |
+
events = [(t, h) for t, h in self.processed_events[event_id]
|
| 357 |
+
if current_time - t < timedelta(minutes=self.expiration_minutes)]
|
| 358 |
+
if events:
|
| 359 |
+
self.processed_events[event_id] = events
|
| 360 |
+
else:
|
| 361 |
+
del self.processed_events[event_id]
|
| 362 |
+
|
| 363 |
+
def is_duplicate_event(self, event_payload):
|
| 364 |
+
self.clean_expired_events()
|
| 365 |
+
event_id = event_payload.get('event_id', '')
|
| 366 |
+
payload_hash = hashlib.md5(str(event_payload).encode('utf-8')).hexdigest()
|
| 367 |
+
if 'challenge' in event_payload:
|
| 368 |
+
return False
|
| 369 |
+
if event_id in self.processed_events and payload_hash in [h for _, h in self.processed_events[event_id]]:
|
| 370 |
+
return True
|
| 371 |
+
self.processed_events[event_id].append((datetime.now(), payload_hash))
|
| 372 |
+
return False
|
| 373 |
+
|
| 374 |
+
event_deduplicator = EventDeduplicator()
|
| 375 |
+
|
| 376 |
+
class SessionStore:
|
| 377 |
+
def __init__(self):
|
| 378 |
+
self._store = {}
|
| 379 |
+
self._lock = Lock()
|
| 380 |
+
|
| 381 |
+
def set(self, user_id, key, value):
|
| 382 |
+
with self._lock:
|
| 383 |
+
if user_id not in self._store:
|
| 384 |
+
self._store[user_id] = {}
|
| 385 |
+
self._store[user_id][key] = {"value": value, "expires_at": datetime.now() + timedelta(hours=1)}
|
| 386 |
+
|
| 387 |
+
def get(self, user_id, key, default=None):
|
| 388 |
+
with self._lock:
|
| 389 |
+
if user_id not in self._store or key not in self._store[user_id]:
|
| 390 |
+
return default
|
| 391 |
+
session_data = self._store[user_id][key]
|
| 392 |
+
if datetime.now() > session_data["expires_at"]:
|
| 393 |
+
del self._store[user_id][key]
|
| 394 |
+
return default
|
| 395 |
+
return session_data["value"]
|
| 396 |
+
|
| 397 |
+
def clear(self, user_id, key):
|
| 398 |
+
with self._lock:
|
| 399 |
+
if user_id in self._store and key in self._store[user_id]:
|
| 400 |
+
del self._store[user_id][key]
|
| 401 |
+
|
| 402 |
+
session_store = SessionStore()
|
| 403 |
+
|
| 404 |
+
def store_in_session(user_id, key_type, data):
|
| 405 |
+
session_store.set(user_id, key_type, data)
|
| 406 |
+
|
| 407 |
+
def get_from_session(user_id, key_type, default=None):
|
| 408 |
+
return session_store.get(user_id, key_type, default)
|
| 409 |
+
|
| 410 |
+
# Global Caches (per workspace)
|
| 411 |
+
user_cache = {} # {team_id: {user_id: user_data}}
|
| 412 |
+
user_cache_lock = Lock()
|
| 413 |
+
|
| 414 |
+
owner_id_cache = {} # {team_id: owner_id}
|
| 415 |
+
owner_id_lock = Lock()
|
| 416 |
+
|
| 417 |
+
preferences_cache = {}
|
| 418 |
+
preferences_cache_lock = Lock()
|
| 419 |
+
|
| 420 |
+
# Database Helper Functions
|
| 421 |
+
def save_preference(team_id, user_id, zoom_config=None, calendar_tool=None):
|
| 422 |
+
conn = psycopg2.connect(os.getenv('DATABASE_URL'))
|
| 423 |
+
cur = conn.cursor()
|
| 424 |
+
cur.execute('SELECT zoom_config, calendar_tool FROM Preferences WHERE team_id = %s AND user_id = %s', (team_id, user_id))
|
| 425 |
+
existing = cur.fetchone()
|
| 426 |
+
if existing:
|
| 427 |
+
current_zoom_config, current_calendar_tool = existing
|
| 428 |
+
new_zoom_config = zoom_config if zoom_config is not None else current_zoom_config
|
| 429 |
+
new_calendar_tool = calendar_tool if calendar_tool is not None else current_calendar_tool
|
| 430 |
+
cur.execute('''
|
| 431 |
+
UPDATE Preferences
|
| 432 |
+
SET zoom_config = %s, calendar_tool = %s, updated_at = %s
|
| 433 |
+
WHERE team_id = %s AND user_id = %s
|
| 434 |
+
''', (json.dumps(new_zoom_config) if new_zoom_config else None,
|
| 435 |
+
new_calendar_tool, datetime.now(), team_id, user_id))
|
| 436 |
+
else:
|
| 437 |
+
new_zoom_config = zoom_config or {"mode": "manual", "link": None}
|
| 438 |
+
new_calendar_tool = calendar_tool or "google"
|
| 439 |
+
cur.execute('''
|
| 440 |
+
INSERT INTO Preferences (team_id, user_id, zoom_config, calendar_tool, updated_at)
|
| 441 |
+
VALUES (%s, %s, %s, %s, %s)
|
| 442 |
+
''', (team_id, user_id, json.dumps(new_zoom_config), new_calendar_tool, datetime.now()))
|
| 443 |
+
conn.commit()
|
| 444 |
+
cur.close()
|
| 445 |
+
conn.close()
|
| 446 |
+
with preferences_cache_lock:
|
| 447 |
+
preferences_cache[(team_id, user_id)] = {"zoom_config": new_zoom_config, "calendar_tool": new_calendar_tool}
|
| 448 |
+
|
| 449 |
+
def load_preferences(team_id, user_id):
|
| 450 |
+
with preferences_cache_lock:
|
| 451 |
+
if (team_id, user_id) in preferences_cache:
|
| 452 |
+
return preferences_cache[(team_id, user_id)]
|
| 453 |
+
try:
|
| 454 |
+
conn = psycopg2.connect(os.getenv('DATABASE_URL'))
|
| 455 |
+
cur = conn.cursor()
|
| 456 |
+
cur.execute('SELECT zoom_config, calendar_tool FROM Preferences WHERE team_id = %s AND user_id = %s', (team_id, user_id))
|
| 457 |
+
row = cur.fetchone()
|
| 458 |
+
if row:
|
| 459 |
+
zoom_config, calendar_tool = row
|
| 460 |
+
# For jsonb, zoom_config is already a dict; no json.loads needed
|
| 461 |
+
preferences = {
|
| 462 |
+
"zoom_config": zoom_config if zoom_config else {"mode": "manual", "link": None},
|
| 463 |
+
"calendar_tool": calendar_tool or "none"
|
| 464 |
+
}
|
| 465 |
+
else:
|
| 466 |
+
preferences = {"zoom_config": {"mode": "manual", "link": None}, "calendar_tool": "none"}
|
| 467 |
+
cur.close()
|
| 468 |
+
conn.close()
|
| 469 |
+
except Exception as e:
|
| 470 |
+
logger.error(f"Failed to load preferences for team {team_id}, user {user_id}: {e}")
|
| 471 |
+
preferences = {"zoom_config": {"mode": "manual", "link": None}, "calendar_tool": "none"}
|
| 472 |
+
with preferences_cache_lock:
|
| 473 |
+
preferences_cache[(team_id, user_id)] = preferences
|
| 474 |
+
return preferences
|
| 475 |
+
|
| 476 |
+
def save_token(team_id, user_id, service, token_data):
|
| 477 |
+
conn = psycopg2.connect(os.getenv('DATABASE_URL'))
|
| 478 |
+
cur = conn.cursor()
|
| 479 |
+
cur.execute('''
|
| 480 |
+
INSERT INTO Tokens (team_id, user_id, service, token_data, updated_at)
|
| 481 |
+
VALUES (%s, %s, %s, %s, %s)
|
| 482 |
+
ON CONFLICT (team_id, user_id, service) DO UPDATE SET token_data = %s, updated_at = %s
|
| 483 |
+
''', (team_id, user_id, service, json.dumps(token_data), datetime.now(), json.dumps(token_data), datetime.now()))
|
| 484 |
+
conn.commit()
|
| 485 |
+
cur.close()
|
| 486 |
+
conn.close()
|
| 487 |
+
|
| 488 |
+
def load_token(team_id, user_id, service):
|
| 489 |
+
conn = psycopg2.connect(os.getenv('DATABASE_URL'))
|
| 490 |
+
cur = conn.cursor()
|
| 491 |
+
cur.execute('SELECT token_data FROM Tokens WHERE team_id = %s AND user_id = %s AND service = %s', (team_id, user_id, service))
|
| 492 |
+
row = cur.fetchone()
|
| 493 |
+
cur.close()
|
| 494 |
+
conn.close()
|
| 495 |
+
return row[0] if row else None
|
| 496 |
+
|
| 497 |
+
# Utility Functions
|
| 498 |
+
def initialize_workspace_cache(client, team_id):
|
| 499 |
+
conn = psycopg2.connect(os.getenv('DATABASE_URL'))
|
| 500 |
+
cur = conn.cursor()
|
| 501 |
+
cur.execute('SELECT MAX(last_updated) FROM Users WHERE team_id = %s', (team_id,))
|
| 502 |
+
last_updated_row = cur.fetchone()
|
| 503 |
+
last_updated = last_updated_row[0] if last_updated_row and last_updated_row[0] else None
|
| 504 |
+
|
| 505 |
+
# Check if cache is fresh (e.g., less than 24 hours old)
|
| 506 |
+
if last_updated and (datetime.now() - last_updated).total_seconds() < 86400:
|
| 507 |
+
cur.execute('SELECT user_id, real_name, email, name, is_owner, workspace_name FROM Users WHERE team_id = %s', (team_id,))
|
| 508 |
+
rows = cur.fetchall()
|
| 509 |
+
new_cache = {row[0]: {"real_name": row[1], "email": row[2], "name": row[3], "is_owner": row[4], "workspace_name": row[5]} for row in rows}
|
| 510 |
+
with user_cache_lock:
|
| 511 |
+
user_cache[team_id] = new_cache
|
| 512 |
+
with owner_id_lock:
|
| 513 |
+
owner_id_cache[team_id] = next((user_id for user_id, data in new_cache.items() if data['is_owner']), None)
|
| 514 |
+
else:
|
| 515 |
+
# Fetch user data from Slack and update database
|
| 516 |
+
response = client.users_list()
|
| 517 |
+
users = response["members"]
|
| 518 |
+
workspace_name = client.team_info()["team"]["name"] # Get workspace name from Slack API
|
| 519 |
+
new_cache = {}
|
| 520 |
+
for user in users:
|
| 521 |
+
user_id = user['id']
|
| 522 |
+
profile = user.get('profile', {})
|
| 523 |
+
real_name = profile.get('real_name', 'Unknown')
|
| 524 |
+
name = user.get('name', '')
|
| 525 |
+
email = f"{name}@gmail.com" # Placeholder; adjust as needed
|
| 526 |
+
is_owner = user.get('is_owner', False)
|
| 527 |
+
new_cache[user_id] = {"real_name": real_name, "email": email, "name": name, "is_owner": is_owner, "workspace_name": workspace_name}
|
| 528 |
+
cur.execute('''
|
| 529 |
+
INSERT INTO Users (team_id, user_id, workspace_name, real_name, email, name, is_owner, last_updated)
|
| 530 |
+
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
| 531 |
+
ON CONFLICT (team_id, user_id) DO UPDATE SET
|
| 532 |
+
workspace_name = %s, real_name = %s, email = %s, name = %s, is_owner = %s, last_updated = %s
|
| 533 |
+
''', (team_id, user_id, workspace_name, real_name, email, name, is_owner, datetime.now(),
|
| 534 |
+
workspace_name, real_name, email, name, is_owner, datetime.now()))
|
| 535 |
+
conn.commit()
|
| 536 |
+
with user_cache_lock:
|
| 537 |
+
user_cache[team_id] = new_cache
|
| 538 |
+
with owner_id_lock:
|
| 539 |
+
owner_id_cache[team_id] = next((user_id for user_id, data in new_cache.items() if data['is_owner']), None)
|
| 540 |
+
cur.close()
|
| 541 |
+
conn.close()
|
| 542 |
+
|
| 543 |
+
def get_all_users(team_id):
|
| 544 |
+
with user_cache_lock:
|
| 545 |
+
if team_id in user_cache:
|
| 546 |
+
return {k: {"Slack Id": k, "real_name": v["real_name"], "email": v["email"], "name": v["name"]}
|
| 547 |
+
for k, v in user_cache[team_id].items()}
|
| 548 |
+
return {}
|
| 549 |
+
|
| 550 |
+
def get_workspace_owner_id(client, team_id):
|
| 551 |
+
with owner_id_lock:
|
| 552 |
+
if team_id in owner_id_cache and owner_id_cache[team_id]:
|
| 553 |
+
return owner_id_cache[team_id]
|
| 554 |
+
initialize_workspace_cache(client, team_id)
|
| 555 |
+
with owner_id_lock:
|
| 556 |
+
return owner_id_cache.get(team_id)
|
| 557 |
+
|
| 558 |
+
def get_channel_owner_id(client, channel_id):
|
| 559 |
+
try:
|
| 560 |
+
response = client.conversations_info(channel=channel_id)
|
| 561 |
+
return response["channel"].get("creator")
|
| 562 |
+
except SlackApiError as e:
|
| 563 |
+
logger.error(f"Error fetching channel info: {e.response['error']}")
|
| 564 |
+
return None
|
| 565 |
+
|
| 566 |
+
def get_user_timezone(client, user_id):
|
| 567 |
+
try:
|
| 568 |
+
response = client.users_info(user=user_id)
|
| 569 |
+
return response["user"].get("tz", "UTC")
|
| 570 |
+
except SlackApiError as e:
|
| 571 |
+
logger.error(f"Timezone error: {e.response['error']}")
|
| 572 |
+
return "UTC"
|
| 573 |
+
|
| 574 |
+
def get_team_id_from_owner_id(owner_id):
|
| 575 |
+
conn = psycopg2.connect(os.getenv('DATABASE_URL'))
|
| 576 |
+
cur = conn.cursor()
|
| 577 |
+
cur.execute("SELECT workspace_id FROM Installations WHERE installation_data->>'user_id' = %s", (owner_id,))
|
| 578 |
+
row = cur.fetchone()
|
| 579 |
+
cur.close()
|
| 580 |
+
conn.close()
|
| 581 |
+
return row[0] if row else None
|
| 582 |
+
|
| 583 |
+
# def get_client_for_team(team_id):
|
| 584 |
+
# installation = installation_store.find_installation(None, team_id)
|
| 585 |
+
# if installation:
|
| 586 |
+
# print(installation)
|
| 587 |
+
# token = installation['bot_token']
|
| 588 |
+
# return WebClient(token=token)
|
| 589 |
+
# return None
|
| 590 |
+
|
| 591 |
+
def get_owner_selected_calendar(client, team_id):
|
| 592 |
+
owner_id = get_workspace_owner_id(client, team_id)
|
| 593 |
+
if not owner_id:
|
| 594 |
+
return None
|
| 595 |
+
# Fixed: Pass both team_id and owner_id to load_preferences
|
| 596 |
+
prefs = load_preferences(team_id, owner_id)
|
| 597 |
+
return prefs.get("calendar_tool", "none")
|
| 598 |
+
|
| 599 |
+
def get_zoom_link(client, team_id):
|
| 600 |
+
owner_id = get_workspace_owner_id(client, team_id)
|
| 601 |
+
if not owner_id:
|
| 602 |
+
return None
|
| 603 |
+
prefs = load_preferences(team_id,owner_id)
|
| 604 |
+
return prefs.get('zoom_config', {}).get('link')
|
| 605 |
+
|
| 606 |
+
def create_home_tab(client, team_id, user_id):
|
| 607 |
+
logger.info(f"Creating home tab for user {user_id}, team {team_id}")
|
| 608 |
+
|
| 609 |
+
# Get workspace owner ID
|
| 610 |
+
workspace_owner_id = get_workspace_owner_id(client, team_id)
|
| 611 |
+
if not workspace_owner_id:
|
| 612 |
+
logger.warning(f"No workspace owner for team {team_id}")
|
| 613 |
+
blocks = [
|
| 614 |
+
{"type": "header", "text": {"type": "plain_text", "text": "🤖 Welcome to AI Assistant!", "emoji": True}},
|
| 615 |
+
{"type": "section", "text": {"type": "mrkdwn", "text": "Unable to determine workspace owner. Please contact support."}},
|
| 616 |
+
]
|
| 617 |
+
return {"type": "home", "blocks": blocks}
|
| 618 |
+
|
| 619 |
+
# Determine if the user is the workspace owner
|
| 620 |
+
is_owner = user_id == workspace_owner_id
|
| 621 |
+
|
| 622 |
+
# Base blocks for all users
|
| 623 |
+
blocks = [
|
| 624 |
+
{"type": "header", "text": {"type": "plain_text", "text": "🤖 Welcome to AI Assistant!", "emoji": True}}
|
| 625 |
+
]
|
| 626 |
+
|
| 627 |
+
# Non-owner view
|
| 628 |
+
if not is_owner:
|
| 629 |
+
blocks.extend([
|
| 630 |
+
{"type": "section", "text": {"type": "mrkdwn", "text": "I help manage schedules and meetings! Please wait for the workspace owner to configure the settings."}},
|
| 631 |
+
{"type": "section", "text": {"type": "mrkdwn", "text": "Only the workspace owner can configure the calendar and Zoom settings."}}
|
| 632 |
+
])
|
| 633 |
+
return {"type": "home", "blocks": blocks}
|
| 634 |
+
|
| 635 |
+
# Owner view: Add configuration options
|
| 636 |
+
blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": "I help manage schedules and meetings! Your settings are below."}})
|
| 637 |
+
blocks.append({"type": "divider"})
|
| 638 |
+
|
| 639 |
+
# Load preferences and tokens
|
| 640 |
+
prefs = load_preferences(team_id, workspace_owner_id)
|
| 641 |
+
selected_provider = prefs.get("calendar_tool", "none")
|
| 642 |
+
zoom_config = prefs.get("zoom_config", {"mode": "manual", "link": None})
|
| 643 |
+
mode = zoom_config["mode"]
|
| 644 |
+
calendar_token = load_token(team_id, workspace_owner_id, selected_provider) if selected_provider != "none" else None
|
| 645 |
+
zoom_token = load_token(team_id, workspace_owner_id, "zoom") if mode == "automatic" else None
|
| 646 |
+
logger.info(f"Preferences loaded: {prefs}, Calendar token: {calendar_token}, Zoom token: {zoom_token}")
|
| 647 |
+
|
| 648 |
+
# Check Zoom token expiration
|
| 649 |
+
zoom_token_expired = False
|
| 650 |
+
if zoom_token and mode == "automatic":
|
| 651 |
+
expires_at = zoom_token.get("expires_at", 0)
|
| 652 |
+
current_time = time.time()
|
| 653 |
+
zoom_token_expired = current_time >= expires_at
|
| 654 |
+
|
| 655 |
+
# Configuration status
|
| 656 |
+
calendar_provider_set = selected_provider != "none"
|
| 657 |
+
calendar_configured = calendar_token is not None if calendar_provider_set else False
|
| 658 |
+
zoom_configured = (zoom_token is not None and not zoom_token_expired) if mode == "automatic" else True
|
| 659 |
+
|
| 660 |
+
# Setup prompt if configurations are incomplete
|
| 661 |
+
if not calendar_provider_set or not calendar_configured or not zoom_configured:
|
| 662 |
+
prompt_text = "To start using the app, please complete the following setups:"
|
| 663 |
+
if not calendar_provider_set:
|
| 664 |
+
prompt_text += "\n- Select a calendar provider."
|
| 665 |
+
if calendar_provider_set and not calendar_configured:
|
| 666 |
+
prompt_text += f"\n- Configure your {selected_provider.capitalize()} calendar."
|
| 667 |
+
if mode == "automatic" and not zoom_configured:
|
| 668 |
+
if zoom_token_expired:
|
| 669 |
+
prompt_text += "\n- Your Zoom token has expired. Please refresh it."
|
| 670 |
+
else:
|
| 671 |
+
prompt_text += "\n- Authenticate with Zoom for automatic mode."
|
| 672 |
+
blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": prompt_text}})
|
| 673 |
+
|
| 674 |
+
# Calendar Configuration Section
|
| 675 |
+
blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": "*🗓️ Calendar Configuration*"}})
|
| 676 |
+
blocks.append({
|
| 677 |
+
"type": "section",
|
| 678 |
+
"block_id": "calendar_provider_block",
|
| 679 |
+
"text": {"type": "mrkdwn", "text": "Select your calendar provider:"},
|
| 680 |
+
"accessory": {
|
| 681 |
+
"type": "static_select",
|
| 682 |
+
"action_id": "calendar_provider_dropdown",
|
| 683 |
+
"placeholder": {"type": "plain_text", "text": "Select provider"},
|
| 684 |
+
"options": [
|
| 685 |
+
{"text": {"type": "plain_text", "text": "Select calendar"}, "value": "none"},
|
| 686 |
+
{"text": {"type": "plain_text", "text": "Google Calendar"}, "value": "google"},
|
| 687 |
+
{"text": {"type": "plain_text", "text": "Microsoft Calendar"}, "value": "microsoft"}
|
| 688 |
+
],
|
| 689 |
+
"initial_option": {
|
| 690 |
+
"text": {"type": "plain_text", "text": "Select calendar" if selected_provider == "none" else
|
| 691 |
+
"Google Calendar" if selected_provider == "google" else "Microsoft Calendar"},
|
| 692 |
+
"value": selected_provider
|
| 693 |
+
}
|
| 694 |
+
}
|
| 695 |
+
})
|
| 696 |
+
|
| 697 |
+
# Calendar configuration prompts
|
| 698 |
+
if selected_provider == "none":
|
| 699 |
+
blocks.append({"type": "context", "elements": [{"type": "mrkdwn", "text": "Please select a calendar provider to begin configuration."}]})
|
| 700 |
+
elif not calendar_configured:
|
| 701 |
+
blocks.append({"type": "context", "elements": [{"type": "mrkdwn", "text": f"Please configure your {selected_provider.capitalize()} calendar."}]})
|
| 702 |
+
|
| 703 |
+
# Calendar configure button and status
|
| 704 |
+
if selected_provider != "none":
|
| 705 |
+
status = "⚠️ Not Configured" if not calendar_configured else (
|
| 706 |
+
f":white_check_mark: Connected ({calendar_token.get('google_email', 'unknown')})" if selected_provider == "google" else (
|
| 707 |
+
f":white_check_mark: Connected (expires: {datetime.fromtimestamp(int(calendar_token.get('expires_at', 0))).strftime('%Y-%m-%d %H:%M')})" if calendar_token and calendar_token.get('expires_at') else ":white_check_mark: Connected"
|
| 708 |
+
)
|
| 709 |
+
)
|
| 710 |
+
blocks.extend([
|
| 711 |
+
{
|
| 712 |
+
"type": "actions",
|
| 713 |
+
"elements": [
|
| 714 |
+
{
|
| 715 |
+
"type": "button",
|
| 716 |
+
"text": {
|
| 717 |
+
"type": "plain_text",
|
| 718 |
+
"text": f"✨ Configure {selected_provider.capitalize()}" if not calendar_configured else f"✅ Reconfigure {selected_provider.capitalize()}",
|
| 719 |
+
"emoji": True
|
| 720 |
+
},
|
| 721 |
+
"action_id": "configure_gcal" if selected_provider == "google" else "configure_mscal"
|
| 722 |
+
}
|
| 723 |
+
]
|
| 724 |
+
},
|
| 725 |
+
{"type": "context", "elements": [{"type": "mrkdwn", "text": status}]}
|
| 726 |
+
])
|
| 727 |
+
|
| 728 |
+
# Zoom Configuration Section
|
| 729 |
+
status = ("⌛ Token Expired" if zoom_token_expired else
|
| 730 |
+
"⚠️ Not Configured" if mode == "automatic" and not zoom_configured else
|
| 731 |
+
"✅ Configured")
|
| 732 |
+
blocks.extend([
|
| 733 |
+
{"type": "divider"},
|
| 734 |
+
{"type": "section", "text": {"type": "mrkdwn", "text": f"*🔗 Zoom Configuration*\nCurrent mode: {mode}\n{status}"}},
|
| 735 |
+
{
|
| 736 |
+
"type": "actions",
|
| 737 |
+
"elements": [
|
| 738 |
+
{"type": "button", "text": {"type": "plain_text", "text": "Configure Zoom Settings", "emoji": True}, "action_id": "open_zoom_config_modal"}
|
| 739 |
+
]
|
| 740 |
+
}
|
| 741 |
+
])
|
| 742 |
+
|
| 743 |
+
# Zoom authentication/refresh button
|
| 744 |
+
if mode == "automatic":
|
| 745 |
+
if not zoom_configured and not zoom_token_expired:
|
| 746 |
+
blocks[-1]["elements"].append({
|
| 747 |
+
"type": "button",
|
| 748 |
+
"text": {"type": "plain_text", "text": "Authenticate with Zoom", "emoji": True},
|
| 749 |
+
"action_id": "configure_zoom"
|
| 750 |
+
})
|
| 751 |
+
elif zoom_token_expired:
|
| 752 |
+
blocks[-1]["elements"].append({
|
| 753 |
+
"type": "button",
|
| 754 |
+
"text": {"type": "plain_text", "text": "Refresh Zoom Token", "emoji": True},
|
| 755 |
+
"action_id": "configure_zoom" # Same action_id for refresh
|
| 756 |
+
})
|
| 757 |
+
|
| 758 |
+
return {"type": "home", "blocks": blocks}
|
| 759 |
+
|
| 760 |
+
# Intent Classification
|
| 761 |
+
intent_prompt = ChatPromptTemplate.from_template("""
|
| 762 |
+
You are an intent classification assistant. Based on the user's message and the conversation history, determine the intent of the user's request. The possible intents are: "schedule meeting", "update event", "delete event", or "other". Provide only the intent as your response.
|
| 763 |
+
- By looking at the history if someone is confirming or denying the schedule , also categorize it as a "schedule meeting"
|
| 764 |
+
- If someone is asking about update the schedule then its "update event"
|
| 765 |
+
- If someone is asking about delete the schedule then its "delete event"
|
| 766 |
+
Conversation History:
|
| 767 |
+
{history}
|
| 768 |
+
|
| 769 |
+
User's Message:
|
| 770 |
+
{input}
|
| 771 |
+
""")
|
| 772 |
+
from prompt import calender_prompt, general_prompt
|
| 773 |
+
intent_chain = LLMChain(llm=llm, prompt=intent_prompt)
|
| 774 |
+
|
| 775 |
+
mentioned_users_prompt = ChatPromptTemplate.from_template("""
|
| 776 |
+
Given the following chat history, identify the Slack user IDs, Names and emails of the users who are mentioned. Mentions can be in the form of <@user_id> (e.g., <@U12345>) or by their names (e.g., "Alice" or "Alice Smith").
|
| 777 |
+
- Do not give 'Bob'<@{bob_id}> in mentions
|
| 778 |
+
- Exclude the {bob_id}.
|
| 779 |
+
# See the history if there is a request for new meeting or request for new schedule just ignore the mentions in the old messages and consider the new mentions in the new request.
|
| 780 |
+
All users in the channel:
|
| 781 |
+
{user_information}
|
| 782 |
+
Format: Slack Id: U3234234 , Name: Alice , Email: alice@gmail.com (map slack ids to the names)
|
| 783 |
+
Chat history:
|
| 784 |
+
{chat_history}
|
| 785 |
+
# Only output the users which are mentioned not all the users from the user-information.
|
| 786 |
+
# Only see the latest message for mention information ignore previous ones.
|
| 787 |
+
Please output the user slack IDs of the mentioned users , their names and emails . If no users are mentioned, output "None".
|
| 788 |
+
CURRENT_INPUT: {current_input}
|
| 789 |
+
Example: [[SlackId1 , Name1 , Email@gmal.com], [SlackId2, Name2, Email@gmail.com]...]
|
| 790 |
+
""")
|
| 791 |
+
mentioned_users_chain = LLMChain(llm=llm, prompt=mentioned_users_prompt)
|
| 792 |
+
|
| 793 |
+
# Slack Event Handlers
|
| 794 |
+
@bolt_app.event("app_home_opened")
|
| 795 |
+
def handle_app_home_opened(event, client, context):
|
| 796 |
+
user_id = event.get("user")
|
| 797 |
+
team_id = context['team_id']
|
| 798 |
+
if not user_id:
|
| 799 |
+
return
|
| 800 |
+
try:
|
| 801 |
+
client.views_publish(user_id=user_id, view=create_home_tab(client, team_id, user_id))
|
| 802 |
+
except Exception as e:
|
| 803 |
+
logger.error(f"Error publishing home tab: {e}")
|
| 804 |
+
|
| 805 |
+
@bolt_app.action("calendar_provider_dropdown")
|
| 806 |
+
def handle_calendar_provider(ack, body, client, logger):
|
| 807 |
+
ack()
|
| 808 |
+
selected_provider = body["actions"][0]["selected_option"]["value"]
|
| 809 |
+
user_id = body["user"]["id"]
|
| 810 |
+
team_id = body["team"]["id"]
|
| 811 |
+
owner_id = get_workspace_owner_id(client, team_id)
|
| 812 |
+
|
| 813 |
+
if user_id != owner_id:
|
| 814 |
+
client.chat_postMessage(channel=user_id, text="Only the workspace owner can configure the calendar.")
|
| 815 |
+
return
|
| 816 |
+
|
| 817 |
+
# Corrected line: pass both team_id and owner_id (user_id) parameters
|
| 818 |
+
save_preference(team_id, owner_id, calendar_tool=selected_provider)
|
| 819 |
+
|
| 820 |
+
client.views_publish(user_id=owner_id, view=create_home_tab(client, team_id, owner_id))
|
| 821 |
+
if selected_provider != "none":
|
| 822 |
+
client.chat_postMessage(channel=owner_id, text=f"Calendar provider updated to {selected_provider.capitalize()}.")
|
| 823 |
+
else:
|
| 824 |
+
client.chat_postMessage(channel=owner_id, text="Calendar provider reset.")
|
| 825 |
+
logger.info(f"Calendar provider updated to {selected_provider} for owner {owner_id}")
|
| 826 |
+
|
| 827 |
+
@bolt_app.action("configure_gcal")
|
| 828 |
+
def handle_gcal_config(ack, body, client, logger):
|
| 829 |
+
ack()
|
| 830 |
+
user_id = body["user"]["id"]
|
| 831 |
+
team_id = body["team"]["id"]
|
| 832 |
+
owner_id = get_workspace_owner_id(client, team_id)
|
| 833 |
+
if user_id != owner_id:
|
| 834 |
+
client.chat_postMessage(channel=user_id, text="Only the workspace owner can configure the calendar.")
|
| 835 |
+
return
|
| 836 |
+
|
| 837 |
+
# Generate and store the state using StateManager
|
| 838 |
+
state = state_manager.create_state(owner_id)
|
| 839 |
+
print(f"state stored: {state}")
|
| 840 |
+
store_in_session(owner_id, "gcal_state", state) # Optional: for additional validation
|
| 841 |
+
|
| 842 |
+
# Set up the OAuth flow and pass the state
|
| 843 |
+
flow = Flow.from_client_secrets_file('credentials.json', scopes=SCOPES, redirect_uri=OAUTH_REDIRECT_URI)
|
| 844 |
+
auth_url, _ = flow.authorization_url(
|
| 845 |
+
access_type='offline',
|
| 846 |
+
prompt='consent',
|
| 847 |
+
include_granted_scopes='true',
|
| 848 |
+
state=state # Use the state from StateManager
|
| 849 |
+
)
|
| 850 |
+
|
| 851 |
+
# Open the modal with the auth URL
|
| 852 |
+
try:
|
| 853 |
+
client.views_open(
|
| 854 |
+
trigger_id=body["trigger_id"],
|
| 855 |
+
view={
|
| 856 |
+
"type": "modal",
|
| 857 |
+
"title": {"type": "plain_text", "text": "Google Calendar Auth"},
|
| 858 |
+
"close": {"type": "plain_text", "text": "Cancel"},
|
| 859 |
+
"blocks": [
|
| 860 |
+
{"type": "section", "text": {"type": "mrkdwn", "text": "Click below to connect Google Calendar:"}},
|
| 861 |
+
{"type": "actions", "elements": [{"type": "button", "text": {"type": "plain_text", "text": "Connect Google Calendar"}, "url": auth_url, "action_id": "launch_auth"}]}
|
| 862 |
+
]
|
| 863 |
+
}
|
| 864 |
+
)
|
| 865 |
+
except Exception as e:
|
| 866 |
+
logger.error(f"Error opening modal: {e}")
|
| 867 |
+
|
| 868 |
+
@bolt_app.action("configure_mscal")
|
| 869 |
+
def handle_mscal_config(ack, body, client, logger):
|
| 870 |
+
ack()
|
| 871 |
+
user_id = body["user"]["id"]
|
| 872 |
+
team_id = body["team"]["id"]
|
| 873 |
+
owner_id = get_workspace_owner_id(client, team_id)
|
| 874 |
+
if user_id != owner_id:
|
| 875 |
+
client.chat_postMessage(channel=user_id, text="Only the workspace owner can configure the calendar.")
|
| 876 |
+
return
|
| 877 |
+
msal_app = ConfidentialClientApplication(MICROSOFT_CLIENT_ID, authority=MICROSOFT_AUTHORITY, client_credential=MICROSOFT_CLIENT_SECRET)
|
| 878 |
+
state = state_manager.create_state(owner_id)
|
| 879 |
+
auth_url = msal_app.get_authorization_request_url(scopes=MICROSOFT_SCOPES, redirect_uri=MICROSOFT_REDIRECT_URI, state=state)
|
| 880 |
+
try:
|
| 881 |
+
client.views_open(
|
| 882 |
+
trigger_id=body["trigger_id"],
|
| 883 |
+
view={
|
| 884 |
+
"type": "modal",
|
| 885 |
+
"title": {"type": "plain_text", "text": "Microsoft Calendar Auth"},
|
| 886 |
+
"close": {"type": "plain_text", "text": "Close"},
|
| 887 |
+
"blocks": [
|
| 888 |
+
{"type": "section", "text": {"type": "mrkdwn", "text": "Click below to authenticate with Microsoft:"}},
|
| 889 |
+
{"type": "actions", "elements": [{"type": "button", "text": {"type": "plain_text", "text": "Connect Microsoft Calendar"}, "url": auth_url, "action_id": "ms_auth_button"}]}
|
| 890 |
+
]
|
| 891 |
+
}
|
| 892 |
+
)
|
| 893 |
+
except Exception as e:
|
| 894 |
+
logger.error(f"Error opening Microsoft auth modal: {e}")
|
| 895 |
+
@bolt_app.event("app_mention")
|
| 896 |
+
def handle_mentions(event, say, client, context):
|
| 897 |
+
if event_deduplicator.is_duplicate_event(event):
|
| 898 |
+
logger.info("Duplicate event detected, skipping processing")
|
| 899 |
+
return
|
| 900 |
+
|
| 901 |
+
# Ignore messages from bots
|
| 902 |
+
if event.get("bot_id"):
|
| 903 |
+
logger.info("Ignoring message from bot")
|
| 904 |
+
return
|
| 905 |
+
|
| 906 |
+
user_id = event.get("user")
|
| 907 |
+
channel_id = event.get("channel")
|
| 908 |
+
text = event.get("text", "").strip()
|
| 909 |
+
thread_ts = event.get("thread_ts")
|
| 910 |
+
team_id = context['team_id']
|
| 911 |
+
calendar_tool = get_owner_selected_calendar(client, team_id)
|
| 912 |
+
if not calendar_tool or calendar_tool == "none":
|
| 913 |
+
say("The workspace owner has not configured a calendar yet.", thread_ts=thread_ts)
|
| 914 |
+
return
|
| 915 |
+
|
| 916 |
+
# Fetch bot_user_id dynamically from installation
|
| 917 |
+
installation = installation_store.find_installation(team_id=team_id)
|
| 918 |
+
if not installation or not installation.bot_user_id:
|
| 919 |
+
logger.error(f"No bot_user_id found for team {team_id}")
|
| 920 |
+
say("Error: Could not determine bot user ID.", thread_ts=thread_ts)
|
| 921 |
+
return
|
| 922 |
+
print(f"App mention events")
|
| 923 |
+
|
| 924 |
+
bot_user_id = installation.bot_user_id
|
| 925 |
+
print(f"Bot user id: {bot_user_id}")
|
| 926 |
+
mention = f"<@{bot_user_id}>"
|
| 927 |
+
mentions = list(set(re.findall(r'<@(\w+)>', text)))
|
| 928 |
+
if bot_user_id in mentions:
|
| 929 |
+
mentions.remove(bot_user_id)
|
| 930 |
+
text = text.replace(mention, "").strip()
|
| 931 |
+
|
| 932 |
+
workspace_owner_id = get_workspace_owner_id(client, team_id)
|
| 933 |
+
timezone = get_user_timezone(client, user_id)
|
| 934 |
+
zoom_link = get_zoom_link(client, team_id)
|
| 935 |
+
zoom_mode = load_preferences(team_id, workspace_owner_id).get("zoom_config", {}).get("mode", "manual")
|
| 936 |
+
|
| 937 |
+
channel_history = client.conversations_history(channel=channel_id, limit=2).get("messages", [])
|
| 938 |
+
channel_history = format_channel_history(channel_history)
|
| 939 |
+
intent = intent_chain.run({"history": channel_history, "input": text})
|
| 940 |
+
|
| 941 |
+
relevant_user_ids = get_relevant_user_ids(client, channel_id)
|
| 942 |
+
all_users = get_all_users(team_id)
|
| 943 |
+
relevant_users = {uid: all_users.get(uid, {"real_name": "Unknown", "email": "unknown@example.com", "name": "Unknown"})
|
| 944 |
+
for uid in relevant_user_ids}
|
| 945 |
+
user_information = "\n".join([f"{uid}: Name={info['real_name']}, Email={info['email']}, Slack Name={info['name']}"
|
| 946 |
+
for uid, info in relevant_users.items() if uid != bot_user_id])
|
| 947 |
+
|
| 948 |
+
print(f"User Information: {user_information}\n\nRelevant Users: {relevant_user_ids}\n\n All users: {all_users}")
|
| 949 |
+
mentioned_users_output = mentioned_users_chain.run({"user_information": user_information, "chat_history": channel_history, "current_input": text, 'bob_id': bot_user_id})
|
| 950 |
+
|
| 951 |
+
import pytz
|
| 952 |
+
pst = pytz.timezone('America/Los_Angeles')
|
| 953 |
+
current_time_pst = datetime.now(pst)
|
| 954 |
+
formatted_time = current_time_pst.strftime("%Y-%m-%d | %A | %I:%M %p | %Z")
|
| 955 |
+
|
| 956 |
+
from all_tools import GoogleCalendarEvents, MicrosoftListCalendarEvents
|
| 957 |
+
if calendar_tool == "google":
|
| 958 |
+
calendar_events = GoogleCalendarEvents()._run(team_id, workspace_owner_id)
|
| 959 |
+
schedule_tools = [tools[i] for i in [0, 1, 4, 6, 12]]
|
| 960 |
+
update_tools = [tools[i] for i in [0, 7, 12]]
|
| 961 |
+
delete_tools = [tools[i] for i in [0, 8, 12]]
|
| 962 |
+
elif calendar_tool == "microsoft":
|
| 963 |
+
calendar_events = MicrosoftListCalendarEvents()._run(team_id, workspace_owner_id)
|
| 964 |
+
schedule_tools = [tools[i] for i in [0, 1, 9, 12]]
|
| 965 |
+
update_tools = [tools[i] for i in [0, 10, 12]]
|
| 966 |
+
delete_tools = [tools[i] for i in [0, 11, 12]]
|
| 967 |
+
else:
|
| 968 |
+
say("Invalid calendar tool configured.", thread_ts=thread_ts)
|
| 969 |
+
return
|
| 970 |
+
|
| 971 |
+
calendar_formatting_chain = LLMChain(llm=llm, prompt=calender_prompt)
|
| 972 |
+
output = calendar_formatting_chain.run({'input': calendar_events, 'admin_id': workspace_owner_id, 'date_time': formatted_time})
|
| 973 |
+
print(f"MENTIONED USERS:{mentioned_users_output}")
|
| 974 |
+
|
| 975 |
+
agent_input = {
|
| 976 |
+
'input': f"Here is the input by user: {text} and do not mention <@{bot_user_id}> even tho mentioned in history",
|
| 977 |
+
'event_details': str(event),
|
| 978 |
+
'target_user_id': user_id,
|
| 979 |
+
'timezone': timezone,
|
| 980 |
+
'user_id': user_id,
|
| 981 |
+
'admin': workspace_owner_id,
|
| 982 |
+
'zoom_link': zoom_link,
|
| 983 |
+
'zoom_mode': zoom_mode,
|
| 984 |
+
'channel_history': channel_history,
|
| 985 |
+
'user_information': user_information,
|
| 986 |
+
'calendar_tool': calendar_tool,
|
| 987 |
+
'date_time': formatted_time,
|
| 988 |
+
'formatted_calendar': output,
|
| 989 |
+
'team_id': team_id
|
| 990 |
+
}
|
| 991 |
+
|
| 992 |
+
mentions = list(set(re.findall(r'<@(\w+)>', text)))
|
| 993 |
+
if bot_user_id in mentions:
|
| 994 |
+
mentions.remove(bot_user_id)
|
| 995 |
+
|
| 996 |
+
schedule_group_exec = create_schedule_channel_agent(schedule_tools)
|
| 997 |
+
update_group_exec = create_update_group_agent(update_tools)
|
| 998 |
+
delete_exec = create_delete_agent(delete_tools)
|
| 999 |
+
|
| 1000 |
+
if intent == "schedule meeting":
|
| 1001 |
+
group_agent_input = agent_input.copy()
|
| 1002 |
+
group_agent_input['mentioned_users'] = mentioned_users_output
|
| 1003 |
+
response = schedule_group_exec.invoke(group_agent_input)
|
| 1004 |
+
say(response['output'])
|
| 1005 |
+
return
|
| 1006 |
+
elif intent == "update event":
|
| 1007 |
+
group_agent_input = agent_input.copy()
|
| 1008 |
+
group_agent_input['mentioned_users'] = mentioned_users_output
|
| 1009 |
+
response = update_group_exec.invoke(group_agent_input)
|
| 1010 |
+
say(response['output'])
|
| 1011 |
+
return
|
| 1012 |
+
elif intent == "delete event":
|
| 1013 |
+
response = delete_exec.invoke(agent_input)
|
| 1014 |
+
say(response['output'])
|
| 1015 |
+
return
|
| 1016 |
+
elif intent == "other":
|
| 1017 |
+
response = llm.predict(general_prompt.format(input=text, channel_history=channel_history))
|
| 1018 |
+
say(response)
|
| 1019 |
+
return
|
| 1020 |
+
else:
|
| 1021 |
+
say("I'm not sure how to handle that request.")
|
| 1022 |
+
# @bolt_app.event("app_mention")
|
| 1023 |
+
# def handle_mentions(event, say, client, context):
|
| 1024 |
+
# if event_deduplicator.is_duplicate_event(event):
|
| 1025 |
+
# logger.info("Duplicate event detected, skipping processing")
|
| 1026 |
+
# return
|
| 1027 |
+
|
| 1028 |
+
# # Ignore messages from bots
|
| 1029 |
+
# if event.get("bot_id"):
|
| 1030 |
+
# logger.info("Ignoring message from bot")
|
| 1031 |
+
# return
|
| 1032 |
+
|
| 1033 |
+
|
| 1034 |
+
# user_id = event.get("user")
|
| 1035 |
+
# channel_id = event.get("channel")
|
| 1036 |
+
# text = event.get("text", "").strip()
|
| 1037 |
+
# thread_ts = event.get("thread_ts")
|
| 1038 |
+
# team_id = context['team_id']
|
| 1039 |
+
# calendar_tool = get_owner_selected_calendar(client, team_id)
|
| 1040 |
+
# if not calendar_tool or calendar_tool == "none":
|
| 1041 |
+
# say("The workspace owner has not configured a calendar yet.", thread_ts=thread_ts)
|
| 1042 |
+
# return
|
| 1043 |
+
|
| 1044 |
+
# # Fetch bot_user_id dynamically from installation
|
| 1045 |
+
# installation = installation_store.find_installation(team_id=team_id)
|
| 1046 |
+
# if not installation or not installation.bot_user_id:
|
| 1047 |
+
# logger.error(f"No bot_user_id found for team {team_id}")
|
| 1048 |
+
# say("Error: Could not determine bot user ID.", thread_ts=thread_ts)
|
| 1049 |
+
# return
|
| 1050 |
+
# print(f"App mention events")
|
| 1051 |
+
|
| 1052 |
+
# bot_user_id = installation.bot_user_id
|
| 1053 |
+
# print(f"Bot user id: {bot_user_id}")
|
| 1054 |
+
# mention = f"<@{bot_user_id}>"
|
| 1055 |
+
# mentions = list(set(re.findall(r'<@(\w+)>', text)))
|
| 1056 |
+
# # Use dynamic bot_user_id instead of SLACK_BOT_USER_ID
|
| 1057 |
+
# if bot_user_id in mentions:
|
| 1058 |
+
# mentions.remove(bot_user_id)
|
| 1059 |
+
# text = text.replace(mention, "").strip()
|
| 1060 |
+
|
| 1061 |
+
# workspace_owner_id = get_workspace_owner_id(client, team_id)
|
| 1062 |
+
# timezone = get_user_timezone(client, user_id)
|
| 1063 |
+
# zoom_link = get_zoom_link(client, team_id)
|
| 1064 |
+
# zoom_mode = load_preferences(team_id, workspace_owner_id).get("zoom_config", {}).get("mode", "manual")
|
| 1065 |
+
|
| 1066 |
+
# channel_history = client.conversations_history(channel=channel_id, limit=2).get("messages", [])
|
| 1067 |
+
# channel_history = format_channel_history(channel_history)
|
| 1068 |
+
# intent = intent_chain.run({"history": channel_history, "input": text})
|
| 1069 |
+
|
| 1070 |
+
# relevant_user_ids = get_relevant_user_ids(client, channel_id)
|
| 1071 |
+
# all_users = get_all_users(team_id)
|
| 1072 |
+
# relevant_users = {uid: all_users.get(uid, {"real_name": "Unknown", "email": "unknown@example.com", "name": "Unknown"})
|
| 1073 |
+
# for uid in relevant_user_ids}
|
| 1074 |
+
# user_information = "\n".join([f"{uid}: Name={info['real_name']}, Email={info['email']}, Slack Name={info['name']}"
|
| 1075 |
+
# for uid, info in relevant_users.items() if uid != bot_user_id])
|
| 1076 |
+
|
| 1077 |
+
# print(f"User Information: {user_information}\n\nRelevant Users: {relevant_user_ids}\n\n All users: {all_users}")
|
| 1078 |
+
# mentioned_users_output = mentioned_users_chain.run({"user_information": user_information, "chat_history": channel_history,"current_input":text, 'bob_id':bot_user_id})
|
| 1079 |
+
|
| 1080 |
+
# import pytz
|
| 1081 |
+
# pst = pytz.timezone('America/Los_Angeles')
|
| 1082 |
+
# current_time_pst = datetime.now(pst)
|
| 1083 |
+
# formatted_time = current_time_pst.strftime("%Y-%m-%d | %A | %I:%M %p | %Z")
|
| 1084 |
+
|
| 1085 |
+
# from all_tools import GoogleCalendarEvents, MicrosoftListCalendarEvents
|
| 1086 |
+
# if calendar_tool == "google":
|
| 1087 |
+
# calendar_events = GoogleCalendarEvents()._run(team_id, workspace_owner_id)
|
| 1088 |
+
# schedule_tools = [tools[i] for i in [0, 1, 4, 6, 12]]
|
| 1089 |
+
# update_tools = [tools[i] for i in [0, 7, 12]]
|
| 1090 |
+
# delete_tools = [tools[i] for i in [0, 8, 12]]
|
| 1091 |
+
# elif calendar_tool == "microsoft":
|
| 1092 |
+
# calendar_events = MicrosoftListCalendarEvents()._run(team_id, workspace_owner_id)
|
| 1093 |
+
# schedule_tools = [tools[i] for i in [0, 1, 9, 12]]
|
| 1094 |
+
# update_tools = [tools[i] for i in [0, 10, 12]]
|
| 1095 |
+
# delete_tools = [tools[i] for i in [0, 11, 12]]
|
| 1096 |
+
# else:
|
| 1097 |
+
# say("Invalid calendar tool configured.", thread_ts=thread_ts)
|
| 1098 |
+
# return
|
| 1099 |
+
|
| 1100 |
+
# calendar_formatting_chain = LLMChain(llm=llm, prompt=calender_prompt)
|
| 1101 |
+
# output = calendar_formatting_chain.run({'input': calendar_events, 'admin_id': workspace_owner_id, 'date_time': formatted_time})
|
| 1102 |
+
# print(f"MENTIONED USERS:{mentioned_users_output}")
|
| 1103 |
+
|
| 1104 |
+
# agent_input = {
|
| 1105 |
+
# 'input': text,
|
| 1106 |
+
# 'event_details': str(event),
|
| 1107 |
+
# 'target_user_id': user_id,
|
| 1108 |
+
# 'timezone': timezone,
|
| 1109 |
+
# 'user_id': user_id,
|
| 1110 |
+
# 'admin': workspace_owner_id,
|
| 1111 |
+
# 'zoom_link': zoom_link,
|
| 1112 |
+
# 'zoom_mode': zoom_mode,
|
| 1113 |
+
# 'channel_history': channel_history,
|
| 1114 |
+
# 'user_information': mentioned_users_output,
|
| 1115 |
+
# 'calendar_tool': calendar_tool,
|
| 1116 |
+
# 'date_time': formatted_time,
|
| 1117 |
+
# 'formatted_calendar': output,
|
| 1118 |
+
# 'team_id': team_id # Added
|
| 1119 |
+
# }
|
| 1120 |
+
|
| 1121 |
+
# mentions = list(set(re.findall(r'<@(\w+)>', text)))
|
| 1122 |
+
# if bot_user_id in mentions:
|
| 1123 |
+
# mentions.remove(bot_user_id)
|
| 1124 |
+
|
| 1125 |
+
# schedule_group_exec = create_schedule_channel_agent(schedule_tools)
|
| 1126 |
+
# update_group_exec = create_update_group_agent(update_tools)
|
| 1127 |
+
# delete_exec = create_delete_agent(delete_tools)
|
| 1128 |
+
|
| 1129 |
+
# if intent == "schedule meeting":
|
| 1130 |
+
# group_agent_input = agent_input.copy()
|
| 1131 |
+
# group_agent_input['mentioned_users'] = "See from the history except 'Bob'"
|
| 1132 |
+
# response = schedule_group_exec.invoke(group_agent_input)
|
| 1133 |
+
# say(response['output'])
|
| 1134 |
+
# return
|
| 1135 |
+
# elif intent == "update event":
|
| 1136 |
+
# group_agent_input = agent_input.copy()
|
| 1137 |
+
# group_agent_input['mentioned_users'] = "See from the history except 'Bob'"
|
| 1138 |
+
# response = update_group_exec.invoke(group_agent_input)
|
| 1139 |
+
# say(response['output'])
|
| 1140 |
+
# return
|
| 1141 |
+
# elif intent == "delete event":
|
| 1142 |
+
# response = delete_exec.invoke(agent_input)
|
| 1143 |
+
# say(response['output'])
|
| 1144 |
+
# return
|
| 1145 |
+
# elif intent == "other":
|
| 1146 |
+
# response = llm.predict(general_prompt.format(input=text, channel_history=channel_history))
|
| 1147 |
+
# say(response)
|
| 1148 |
+
# return
|
| 1149 |
+
# else:
|
| 1150 |
+
# say("I'm not sure how to handle that request.")
|
| 1151 |
+
|
| 1152 |
+
def format_channel_history(raw_history):
|
| 1153 |
+
cleaned_history = []
|
| 1154 |
+
for msg in raw_history:
|
| 1155 |
+
if 'bot_id' in msg and 'Calendar provider updated' in msg.get('text', ''):
|
| 1156 |
+
continue
|
| 1157 |
+
sender = msg.get('user', 'Unknown') if 'bot_id' not in msg else msg.get('bot_profile', {}).get('name', 'Bot')
|
| 1158 |
+
message_text = msg.get('text', '').strip()
|
| 1159 |
+
timestamp = float(msg.get('ts', 0))
|
| 1160 |
+
readable_time = datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d %I:%M %p')
|
| 1161 |
+
user_id = msg.get('user', 'N/A')
|
| 1162 |
+
team_id = msg.get('team', 'N/A')
|
| 1163 |
+
cleaned_history.append({
|
| 1164 |
+
'message': message_text,
|
| 1165 |
+
'from': sender,
|
| 1166 |
+
'timestamp': readable_time,
|
| 1167 |
+
'user_team': f"{user_id}/{team_id}"
|
| 1168 |
+
})
|
| 1169 |
+
formatted_output = ""
|
| 1170 |
+
for i, entry in enumerate(cleaned_history, 1):
|
| 1171 |
+
formatted_output += f"Message {i}: {entry['message']}\nFrom: {entry['from']}\nTimestamp: {entry['timestamp']}\nUserId/TeamId: {entry['user_team']}\n\n"
|
| 1172 |
+
return formatted_output.strip()
|
| 1173 |
+
|
| 1174 |
+
def get_relevant_user_ids(client, channel_id):
|
| 1175 |
+
try:
|
| 1176 |
+
members = []
|
| 1177 |
+
cursor = None
|
| 1178 |
+
while True:
|
| 1179 |
+
response = client.conversations_members(channel=channel_id, limit=10, cursor=cursor)
|
| 1180 |
+
if not response["ok"]:
|
| 1181 |
+
logger.error(f"Failed to get members for channel {channel_id}: {response['error']}")
|
| 1182 |
+
break
|
| 1183 |
+
members.extend(response["members"])
|
| 1184 |
+
cursor = response.get("response_metadata", {}).get("next_cursor")
|
| 1185 |
+
if not cursor:
|
| 1186 |
+
break
|
| 1187 |
+
return members
|
| 1188 |
+
except SlackApiError as e:
|
| 1189 |
+
logger.error(f"Error getting conversation members: {e}")
|
| 1190 |
+
return []
|
| 1191 |
+
|
| 1192 |
+
calendar_formatting_chain = LLMChain(llm=llm, prompt=calender_prompt)
|
| 1193 |
+
|
| 1194 |
+
@bolt_app.event("message")
|
| 1195 |
+
def handle_messages(body, say, client, context):
|
| 1196 |
+
if event_deduplicator.is_duplicate_event(body):
|
| 1197 |
+
logger.info("Duplicate event detected, skipping processing")
|
| 1198 |
+
return
|
| 1199 |
+
event = body.get("event", {})
|
| 1200 |
+
if event.get("bot_id"):
|
| 1201 |
+
logger.info("Ignoring message from bot")
|
| 1202 |
+
return
|
| 1203 |
+
|
| 1204 |
+
user_id = event.get("user")
|
| 1205 |
+
text = event.get("text", "").strip()
|
| 1206 |
+
channel_id = event.get("channel")
|
| 1207 |
+
thread_ts = event.get("thread_ts")
|
| 1208 |
+
team_id = context['team_id']
|
| 1209 |
+
calendar_tool = get_owner_selected_calendar(client, team_id)
|
| 1210 |
+
channel_info = client.conversations_info(channel=channel_id)
|
| 1211 |
+
channel = channel_info["channel"]
|
| 1212 |
+
|
| 1213 |
+
# Fetch bot_user_id dynamically from installation
|
| 1214 |
+
installation = installation_store.find_installation(team_id=team_id)
|
| 1215 |
+
if not installation or not installation.bot_user_id:
|
| 1216 |
+
logger.error(f"No bot_user_id found for team {team_id}")
|
| 1217 |
+
say("Error: Could not determine bot user ID.", thread_ts=thread_ts)
|
| 1218 |
+
return
|
| 1219 |
+
bot_user_id = installation.bot_user_id
|
| 1220 |
+
|
| 1221 |
+
if not channel.get("is_im") and f"<@{bot_user_id}>" in text:
|
| 1222 |
+
return
|
| 1223 |
+
if not channel.get("is_im") and "thread_ts" not in event:
|
| 1224 |
+
return
|
| 1225 |
+
if not calendar_tool or calendar_tool == "none":
|
| 1226 |
+
say("The workspace owner has not configured a calendar yet.", thread_ts=thread_ts)
|
| 1227 |
+
return
|
| 1228 |
+
print(f"Message events")
|
| 1229 |
+
workspace_owner_id = get_workspace_owner_id(client, team_id)
|
| 1230 |
+
is_owner = user_id == workspace_owner_id
|
| 1231 |
+
timezone = get_user_timezone(client, user_id)
|
| 1232 |
+
zoom_link = get_zoom_link(client, team_id)
|
| 1233 |
+
zoom_mode = load_preferences(team_id, workspace_owner_id).get("zoom_config", {}).get("mode", "manual")
|
| 1234 |
+
channel_history = client.conversations_history(channel=channel_id, limit=2).get("messages", [])
|
| 1235 |
+
channel_history = format_channel_history(channel_history)
|
| 1236 |
+
intent = intent_chain.run({"history": channel_history, "input": text})
|
| 1237 |
+
|
| 1238 |
+
if intent == "schedule meeting" and not is_owner and not channel.get("is_group") and not channel.get("is_mpim") and 'thread_ts' not in event:
|
| 1239 |
+
admin_dm = client.conversations_open(users=workspace_owner_id)
|
| 1240 |
+
prompt = ChatPromptTemplate.from_template("""
|
| 1241 |
+
You have this text: {text} and your job is to mention @{workspace_owner_id} and say following in 2 scenarios:
|
| 1242 |
+
if message history confirms about scheduling meeting then format below text and return only that response with no other explanation
|
| 1243 |
+
"Hi {workspace_owner_id} you wanted to schedule a meeting with {user_id}, {user_id} has proposed these slots [Time slots from the text] , Are you comfortable with these slots ? Confirm so I can fix the meeting."
|
| 1244 |
+
else:
|
| 1245 |
+
Format the text : {text}
|
| 1246 |
+
MESSAGE HISTORY: {channel_history}
|
| 1247 |
+
""")
|
| 1248 |
+
response = LLMChain(llm=llm, prompt=prompt)
|
| 1249 |
+
client.chat_postMessage(channel=admin_dm["channel"]["id"],
|
| 1250 |
+
text=response.run({'text': text, 'workspace_owner_id': workspace_owner_id, 'user_id': user_id, 'channel_history': channel_history}))
|
| 1251 |
+
say(f"<@{user_id}> I've notified the workspace owner about your meeting request.", thread_ts=thread_ts)
|
| 1252 |
+
return
|
| 1253 |
+
|
| 1254 |
+
mentions = list(set(re.findall(r'<@(\w+)>', text)))
|
| 1255 |
+
if bot_user_id in mentions:
|
| 1256 |
+
mentions.remove(bot_user_id)
|
| 1257 |
+
|
| 1258 |
+
import pytz
|
| 1259 |
+
pst = pytz.timezone('America/Los_Angeles')
|
| 1260 |
+
current_time_pst = datetime.now(pst)
|
| 1261 |
+
formatted_time = current_time_pst.strftime("%Y-%m-%d | %A | %I:%M %p | %Z")
|
| 1262 |
+
|
| 1263 |
+
from all_tools import MicrosoftListCalendarEvents, GoogleCalendarEvents
|
| 1264 |
+
if calendar_tool == "google":
|
| 1265 |
+
schedule_tools = [tools[i] for i in [0, 1, 4, 6, 12]]
|
| 1266 |
+
update_tools = [tools[i] for i in [0, 7, 12]]
|
| 1267 |
+
delete_tools = [tools[i] for i in [0, 8, 12]]
|
| 1268 |
+
output = calendar_formatting_chain.run({'input': GoogleCalendarEvents()._run(team_id, workspace_owner_id), 'admin_id': workspace_owner_id, 'date_time': formatted_time})
|
| 1269 |
+
elif calendar_tool == "microsoft":
|
| 1270 |
+
schedule_tools = [tools[i] for i in [0, 1, 9, 12]]
|
| 1271 |
+
update_tools = [tools[i] for i in [0, 10, 12]]
|
| 1272 |
+
delete_tools = [tools[i] for i in [0, 11, 12]]
|
| 1273 |
+
output = calendar_formatting_chain.run({'input': MicrosoftListCalendarEvents()._run(team_id, workspace_owner_id), 'admin_id': workspace_owner_id, 'date_time': formatted_time})
|
| 1274 |
+
else:
|
| 1275 |
+
say("Invalid calendar tool configured.", thread_ts=thread_ts)
|
| 1276 |
+
return
|
| 1277 |
+
|
| 1278 |
+
relevant_user_ids = get_relevant_user_ids(client, channel_id)
|
| 1279 |
+
all_users = get_all_users(team_id)
|
| 1280 |
+
relevant_users = {uid: all_users.get(uid, {"real_name": "Unknown", "email": "unknown@example.com", "name": "Unknown"})
|
| 1281 |
+
for uid in relevant_user_ids}
|
| 1282 |
+
user_information = "\n".join([f"{uid}: Name={info['real_name']}, Email={info['email']}, Slack Name={info['name']}"
|
| 1283 |
+
for uid, info in relevant_users.items() if uid != bot_user_id])
|
| 1284 |
+
print(f"All users: {all_users}\n\n Relevant users: {relevant_user_ids}")
|
| 1285 |
+
mentioned_users_output = mentioned_users_chain.run({"user_information": user_information, "chat_history": channel_history, "current_input": text, 'bob_id': bot_user_id})
|
| 1286 |
+
schedule_exec = create_schedule_agent(schedule_tools)
|
| 1287 |
+
update_exec = create_update_agent(update_tools)
|
| 1288 |
+
delete_exec = create_delete_agent(delete_tools)
|
| 1289 |
+
schedule_group_exec = create_schedule_group_agent(schedule_tools)
|
| 1290 |
+
update_group_exec = create_update_group_agent(update_tools)
|
| 1291 |
+
print(f"MENTIONED USERS:{mentioned_users_output}")
|
| 1292 |
+
channel_type = channel.get("is_group", False) or channel.get("is_mpim", False)
|
| 1293 |
+
agent_input = {
|
| 1294 |
+
'input': f"Here is the input by user: {text} and do not mention <@{bot_user_id}> even tho mentioned in history",
|
| 1295 |
+
'event_details': str(event),
|
| 1296 |
+
'target_user_id': user_id,
|
| 1297 |
+
'timezone': timezone,
|
| 1298 |
+
'user_id': user_id,
|
| 1299 |
+
'admin': workspace_owner_id,
|
| 1300 |
+
'zoom_link': zoom_link,
|
| 1301 |
+
'zoom_mode': zoom_mode,
|
| 1302 |
+
'channel_history': channel_history,
|
| 1303 |
+
'user_information': user_information,
|
| 1304 |
+
'calendar_tool': calendar_tool,
|
| 1305 |
+
'date_time': formatted_time,
|
| 1306 |
+
'formatted_calendar': output,
|
| 1307 |
+
'team_id': team_id
|
| 1308 |
+
}
|
| 1309 |
+
|
| 1310 |
+
if intent == "schedule meeting":
|
| 1311 |
+
if not channel_type and len(mentions) > 1:
|
| 1312 |
+
mentions.append(user_id)
|
| 1313 |
+
dm_channel_id, error = open_group_dm(client, mentions)
|
| 1314 |
+
if dm_channel_id:
|
| 1315 |
+
group_agent_input = agent_input.copy()
|
| 1316 |
+
group_agent_input['mentioned_users'] = mentioned_users_output
|
| 1317 |
+
group_agent_input['channel_history'] = channel_history
|
| 1318 |
+
group_agent_input['formatted_calendar'] = output
|
| 1319 |
+
response = schedule_group_exec.invoke(group_agent_input)
|
| 1320 |
+
client.chat_postMessage(channel=dm_channel_id, text=f"Group conversation started by <@{user_id}>\n\n{response['output']}")
|
| 1321 |
+
elif error:
|
| 1322 |
+
say(f"Sorry, I couldn't create the group conversation: {error}", thread_ts=thread_ts)
|
| 1323 |
+
else:
|
| 1324 |
+
if channel_type or 'thread_ts' in event:
|
| 1325 |
+
group_agent_input = agent_input.copy()
|
| 1326 |
+
if 'thread_ts' in event:
|
| 1327 |
+
schedule_group_exec = create_schedule_channel_agent(schedule_tools)
|
| 1328 |
+
history_response = client.conversations_replies(channel=channel_id, ts=thread_ts, limit=2)
|
| 1329 |
+
channel_history = format_channel_history(history_response.get("messages", []))
|
| 1330 |
+
else:
|
| 1331 |
+
channel_history = format_channel_history(client.conversations_history(channel=channel_id, limit=3).get("messages", []))
|
| 1332 |
+
group_agent_input['mentioned_users'] = mentioned_users_output
|
| 1333 |
+
group_agent_input['channel_history'] = channel_history
|
| 1334 |
+
group_agent_input['formatted_calendar'] = output
|
| 1335 |
+
response = schedule_group_exec.invoke(group_agent_input)
|
| 1336 |
+
say(response['output'], thread_ts=thread_ts)
|
| 1337 |
+
return
|
| 1338 |
+
response = schedule_exec.invoke(agent_input)
|
| 1339 |
+
say(response['output'], thread_ts=thread_ts)
|
| 1340 |
+
elif intent == "update event":
|
| 1341 |
+
agent_input['current_date'] = formatted_time
|
| 1342 |
+
agent_input['calendar_events'] = MicrosoftListCalendarEvents()._run(team_id, workspace_owner_id) if calendar_tool == "microsoft" else GoogleCalendarEvents()._run(team_id, workspace_owner_id)
|
| 1343 |
+
if channel_type or 'thread_ts' in event:
|
| 1344 |
+
group_agent_input = agent_input.copy()
|
| 1345 |
+
channel_history = format_channel_history(client.conversations_history(channel=channel_id, limit=2).get("messages", []))
|
| 1346 |
+
group_agent_input['mentioned_users'] = mentioned_users_output
|
| 1347 |
+
group_agent_input['channel_history'] = channel_history
|
| 1348 |
+
group_agent_input['formatted_calendar'] = output
|
| 1349 |
+
response = update_group_exec.invoke(group_agent_input)
|
| 1350 |
+
say(response['output'], thread_ts=thread_ts)
|
| 1351 |
+
return
|
| 1352 |
+
response = update_exec.invoke(agent_input)
|
| 1353 |
+
say(response['output'], thread_ts=thread_ts)
|
| 1354 |
+
elif intent == "delete event":
|
| 1355 |
+
agent_input['current_date'] = formatted_time
|
| 1356 |
+
agent_input['calendar_events'] = MicrosoftListCalendarEvents()._run(team_id, workspace_owner_id) if calendar_tool == "microsoft" else GoogleCalendarEvents()._run(team_id, workspace_owner_id)
|
| 1357 |
+
response = delete_exec.invoke(agent_input)
|
| 1358 |
+
say(response['output'], thread_ts=thread_ts)
|
| 1359 |
+
elif intent == "other":
|
| 1360 |
+
response = llm.predict(general_prompt.format(input=text, channel_history=channel_history))
|
| 1361 |
+
say(response, thread_ts=thread_ts)
|
| 1362 |
+
else:
|
| 1363 |
+
say("I'm not sure how to handle that request.", thread_ts=thread_ts)
|
| 1364 |
+
@bolt_app.event("team_join")
|
| 1365 |
+
def handle_team_join(event, client, context, logger):
|
| 1366 |
+
try:
|
| 1367 |
+
user_info = event['user']
|
| 1368 |
+
team_id = context.team_id
|
| 1369 |
+
|
| 1370 |
+
# Fetch workspace name from Slack API
|
| 1371 |
+
try:
|
| 1372 |
+
team_info = client.team_info()
|
| 1373 |
+
workspace_name = team_info['team']['name']
|
| 1374 |
+
except SlackApiError as e:
|
| 1375 |
+
logger.error(f"Error fetching team info: {e.response['error']}")
|
| 1376 |
+
workspace_name = "Unknown Workspace"
|
| 1377 |
+
|
| 1378 |
+
# Extract user details
|
| 1379 |
+
user_id = user_info['id']
|
| 1380 |
+
real_name = user_info.get('real_name', 'Unknown')
|
| 1381 |
+
profile = user_info.get('profile', {})
|
| 1382 |
+
email = profile.get('email', f"{user_info.get('name', 'user')}@example.com") # Fallback email
|
| 1383 |
+
name = user_info.get('name', '')
|
| 1384 |
+
is_owner = user_info.get('is_owner', False)
|
| 1385 |
+
|
| 1386 |
+
# Connect to database
|
| 1387 |
+
conn = psycopg2.connect(os.getenv('DATABASE_URL'))
|
| 1388 |
+
cur = conn.cursor()
|
| 1389 |
+
|
| 1390 |
+
# Insert/update user in Users table
|
| 1391 |
+
cur.execute('''
|
| 1392 |
+
INSERT INTO Users (team_id, user_id, workspace_name, real_name, email, name, is_owner, last_updated)
|
| 1393 |
+
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
| 1394 |
+
ON CONFLICT (team_id, user_id) DO UPDATE SET
|
| 1395 |
+
workspace_name = EXCLUDED.workspace_name,
|
| 1396 |
+
real_name = EXCLUDED.real_name,
|
| 1397 |
+
email = EXCLUDED.email,
|
| 1398 |
+
name = EXCLUDED.name,
|
| 1399 |
+
is_owner = EXCLUDED.is_owner,
|
| 1400 |
+
last_updated = EXCLUDED.last_updated
|
| 1401 |
+
''', (team_id, user_id, workspace_name, real_name, email, name, is_owner, datetime.now()))
|
| 1402 |
+
|
| 1403 |
+
conn.commit()
|
| 1404 |
+
cur.close()
|
| 1405 |
+
conn.close()
|
| 1406 |
+
|
| 1407 |
+
# Update user cache
|
| 1408 |
+
with user_cache_lock:
|
| 1409 |
+
if team_id not in user_cache:
|
| 1410 |
+
user_cache[team_id] = {}
|
| 1411 |
+
user_cache[team_id][user_id] = {
|
| 1412 |
+
"real_name": real_name,
|
| 1413 |
+
"email": f"{name}@gmail.com",
|
| 1414 |
+
"name": name,
|
| 1415 |
+
"is_owner": is_owner,
|
| 1416 |
+
"workspace_name": workspace_name
|
| 1417 |
+
}
|
| 1418 |
+
|
| 1419 |
+
# Update owner_id_cache if user is owner
|
| 1420 |
+
if is_owner:
|
| 1421 |
+
with owner_id_lock:
|
| 1422 |
+
owner_id_cache[team_id] = user_id
|
| 1423 |
+
|
| 1424 |
+
logger.info(f"Processed team_join event for user {user_id} in team {team_id}")
|
| 1425 |
+
|
| 1426 |
+
except KeyError as e:
|
| 1427 |
+
logger.error(f"Missing key in event data: {e}")
|
| 1428 |
+
except psycopg2.Error as e:
|
| 1429 |
+
logger.error(f"Database error: {e}")
|
| 1430 |
+
except Exception as e:
|
| 1431 |
+
logger.error(f"Unexpected error handling team_join: {e}")
|
| 1432 |
+
def open_group_dm(client, users):
|
| 1433 |
+
try:
|
| 1434 |
+
response = client.conversations_open(users=",".join(users))
|
| 1435 |
+
return response["channel"]["id"] if response["ok"] else (None, "Failed to create group DM")
|
| 1436 |
+
except SlackApiError as e:
|
| 1437 |
+
return None, f"Error creating group DM: {e.response['error']}"
|
| 1438 |
+
|
| 1439 |
+
# Flask Routes
|
| 1440 |
+
@app.route("/slack/events", methods=["POST"])
|
| 1441 |
+
def slack_events():
|
| 1442 |
+
return slack_handler.handle(request)
|
| 1443 |
+
|
| 1444 |
+
@app.route("/slack/install", methods=["GET"])
|
| 1445 |
+
def slack_install():
|
| 1446 |
+
return slack_handler.handle(request)
|
| 1447 |
+
|
| 1448 |
+
@app.route("/slack/oauth_redirect", methods=["GET"])
|
| 1449 |
+
def slack_oauth_redirect():
|
| 1450 |
+
return slack_handler.handle(request)
|
| 1451 |
+
|
| 1452 |
+
@app.route("/oauth2callback")
|
| 1453 |
+
def oauth2callback():
|
| 1454 |
+
state = request.args.get('state', '')
|
| 1455 |
+
print(f"STATE: {state}")
|
| 1456 |
+
print(f"STATs: {state_manager._states}")
|
| 1457 |
+
user_id = state_manager.validate_and_consume_state(state)
|
| 1458 |
+
stored_state = get_from_session(user_id, "gcal_state") if user_id else None
|
| 1459 |
+
|
| 1460 |
+
if not user_id or stored_state != state:
|
| 1461 |
+
return "Invalid state", 400
|
| 1462 |
+
|
| 1463 |
+
team_id = get_team_id_from_owner_id(user_id)
|
| 1464 |
+
if not team_id:
|
| 1465 |
+
return "Workspace not found", 404
|
| 1466 |
+
|
| 1467 |
+
client = get_client_for_team(team_id)
|
| 1468 |
+
if not client:
|
| 1469 |
+
return "Client not found", 500
|
| 1470 |
+
|
| 1471 |
+
flow = Flow.from_client_secrets_file('credentials.json', scopes=SCOPES, redirect_uri=OAUTH_REDIRECT_URI)
|
| 1472 |
+
flow.fetch_token(authorization_response=request.url)
|
| 1473 |
+
credentials = flow.credentials
|
| 1474 |
+
service = build('oauth2', 'v2', credentials=credentials)
|
| 1475 |
+
user_info = service.userinfo().get().execute()
|
| 1476 |
+
google_email = user_info.get('email', 'unknown@example.com')
|
| 1477 |
+
token_data = json.loads(credentials.to_json())
|
| 1478 |
+
token_data['google_email'] = google_email
|
| 1479 |
+
|
| 1480 |
+
save_token(team_id, user_id, 'google', token_data) # Adjusted to use team_id and user_id
|
| 1481 |
+
client.views_publish(user_id=user_id, view=create_home_tab(client, team_id, user_id))
|
| 1482 |
+
|
| 1483 |
+
return "Google Calendar connected successfully! You can close this window."
|
| 1484 |
+
@bolt_app.action("launch_auth")
|
| 1485 |
+
def handle_launch_auth(ack, body, logger):
|
| 1486 |
+
ack() # Acknowledge the action
|
| 1487 |
+
logger.info(f"Launch auth triggered by user {body['user']['id']}")
|
| 1488 |
+
# No further action needed since the URL redirect handles the OAuth flow
|
| 1489 |
+
@app.route("/microsoft_callback")
|
| 1490 |
+
def microsoft_callback():
|
| 1491 |
+
code = request.args.get("code")
|
| 1492 |
+
state = request.args.get("state")
|
| 1493 |
+
if not code or not state:
|
| 1494 |
+
return "Missing parameters", 400
|
| 1495 |
+
user_id = state_manager.validate_and_consume_state(state)
|
| 1496 |
+
if not user_id:
|
| 1497 |
+
return "Invalid or expired state parameter", 403
|
| 1498 |
+
team_id = get_team_id_from_owner_id(user_id)
|
| 1499 |
+
if not team_id:
|
| 1500 |
+
return "Workspace not found", 404
|
| 1501 |
+
client = get_client_for_team(team_id)
|
| 1502 |
+
if not client:
|
| 1503 |
+
return "Client not found", 500
|
| 1504 |
+
if user_id != get_workspace_owner_id(client, team_id):
|
| 1505 |
+
return "Unauthorized", 403
|
| 1506 |
+
msal_app = ConfidentialClientApplication(MICROSOFT_CLIENT_ID, authority=MICROSOFT_AUTHORITY, client_credential=MICROSOFT_CLIENT_SECRET)
|
| 1507 |
+
result = msal_app.acquire_token_by_authorization_code(code, scopes=MICROSOFT_SCOPES, redirect_uri=MICROSOFT_REDIRECT_URI)
|
| 1508 |
+
if "access_token" not in result:
|
| 1509 |
+
return "Authentication failed", 400
|
| 1510 |
+
token_data = {"access_token": result["access_token"], "refresh_token": result.get("refresh_token", ""), "expires_at": result["expires_in"] + time.time()}
|
| 1511 |
+
save_token(user_id, 'microsoft', token_data)
|
| 1512 |
+
client.views_publish(user_id=user_id, view=create_home_tab(client, team_id, user_id))
|
| 1513 |
+
return "Microsoft Calendar connected successfully! You can close this window."
|
| 1514 |
+
|
| 1515 |
+
@app.route("/zoom_callback")
|
| 1516 |
+
def zoom_callback():
|
| 1517 |
+
code = request.args.get("code")
|
| 1518 |
+
state = request.args.get("state")
|
| 1519 |
+
user_id = state_manager.validate_and_consume_state(state)
|
| 1520 |
+
if not user_id:
|
| 1521 |
+
return "Invalid or expired state", 403
|
| 1522 |
+
team_id = get_team_id_from_owner_id(user_id)
|
| 1523 |
+
if not team_id:
|
| 1524 |
+
return "Workspace not found", 404
|
| 1525 |
+
client = get_client_for_team(team_id)
|
| 1526 |
+
if not client:
|
| 1527 |
+
return "Client not found", 500
|
| 1528 |
+
params = {"grant_type": "authorization_code", "code": code, "redirect_uri": ZOOM_REDIRECT_URI}
|
| 1529 |
+
try:
|
| 1530 |
+
response = requests.post(ZOOM_TOKEN_API, params=params, auth=(CLIENT_ID, CLIENT_SECRET))
|
| 1531 |
+
except Exception as e:
|
| 1532 |
+
return jsonify({"error": f"Token request failed: {str(e)}"}), 500
|
| 1533 |
+
if response.status_code == 200:
|
| 1534 |
+
token_data = response.json()
|
| 1535 |
+
token_data["expires_at"] = time.time() + token_data["expires_in"]
|
| 1536 |
+
# Fixed: Pass all required arguments in correct order
|
| 1537 |
+
save_token(team_id, user_id, 'zoom', token_data)
|
| 1538 |
+
client.views_publish(user_id=user_id, view=create_home_tab(client, team_id, user_id))
|
| 1539 |
+
return "Zoom connected successfully! You can close this window."
|
| 1540 |
+
return "Failed to retrieve token", 400
|
| 1541 |
+
|
| 1542 |
+
@bolt_app.action("open_zoom_config_modal")
|
| 1543 |
+
def handle_open_zoom_config_modal(ack, body, client, logger):
|
| 1544 |
+
ack()
|
| 1545 |
+
user_id = body["user"]["id"]
|
| 1546 |
+
team_id = body["team"]["id"]
|
| 1547 |
+
owner_id = get_workspace_owner_id(client, team_id)
|
| 1548 |
+
if user_id != owner_id:
|
| 1549 |
+
client.chat_postMessage(channel=user_id, text="Only the workspace owner can configure Zoom.")
|
| 1550 |
+
return
|
| 1551 |
+
|
| 1552 |
+
# Fixed: Pass both team_id and user_id to load_preferences
|
| 1553 |
+
prefs = load_preferences(team_id, user_id)
|
| 1554 |
+
zoom_config = prefs.get("zoom_config", {"mode": "manual", "link": None})
|
| 1555 |
+
mode = zoom_config["mode"]
|
| 1556 |
+
link = zoom_config.get("link", "")
|
| 1557 |
+
|
| 1558 |
+
try:
|
| 1559 |
+
client.views_open(
|
| 1560 |
+
trigger_id=body["trigger_id"],
|
| 1561 |
+
view={
|
| 1562 |
+
"type": "modal",
|
| 1563 |
+
"callback_id": "zoom_config_submit",
|
| 1564 |
+
"title": {"type": "plain_text", "text": "Configure Zoom"},
|
| 1565 |
+
"submit": {"type": "plain_text", "text": "Save"},
|
| 1566 |
+
"close": {"type": "plain_text", "text": "Cancel"},
|
| 1567 |
+
"blocks": [
|
| 1568 |
+
{
|
| 1569 |
+
"type": "input",
|
| 1570 |
+
"block_id": "zoom_mode",
|
| 1571 |
+
"label": {"type": "plain_text", "text": "Zoom Mode"},
|
| 1572 |
+
"element": {
|
| 1573 |
+
"type": "static_select",
|
| 1574 |
+
"action_id": "mode_select",
|
| 1575 |
+
"placeholder": {"type": "plain_text", "text": "Select mode"},
|
| 1576 |
+
"options": [
|
| 1577 |
+
{"text": {"type": "plain_text", "text": "Automatic"}, "value": "automatic"},
|
| 1578 |
+
{"text": {"type": "plain_text", "text": "Manual"}, "value": "manual"}
|
| 1579 |
+
],
|
| 1580 |
+
"initial_option": {"text": {"type": "plain_text", "text": "Automatic" if mode == "automatic" else "Manual"}, "value": mode} if mode else None
|
| 1581 |
+
}
|
| 1582 |
+
},
|
| 1583 |
+
{
|
| 1584 |
+
"type": "input",
|
| 1585 |
+
"block_id": "zoom_link",
|
| 1586 |
+
"label": {"type": "plain_text", "text": "Manual Zoom Link"},
|
| 1587 |
+
"element": {
|
| 1588 |
+
"type": "plain_text_input",
|
| 1589 |
+
"action_id": "link_input",
|
| 1590 |
+
"placeholder": {"type": "plain_text", "text": "Enter Zoom link"},
|
| 1591 |
+
"initial_value": link if isinstance(link, str) else ""
|
| 1592 |
+
},
|
| 1593 |
+
"optional": True
|
| 1594 |
+
}
|
| 1595 |
+
]
|
| 1596 |
+
}
|
| 1597 |
+
)
|
| 1598 |
+
except Exception as e:
|
| 1599 |
+
logger.error(f"Error opening Zoom config modal: {e}")
|
| 1600 |
+
|
| 1601 |
+
@bolt_app.action("configure_zoom")
|
| 1602 |
+
def handle_zoom_config(ack, body, client, logger):
|
| 1603 |
+
ack() # Acknowledge the action
|
| 1604 |
+
user_id = body["user"]["id"]
|
| 1605 |
+
team_id = body["team"]["id"]
|
| 1606 |
+
|
| 1607 |
+
# Ensure only the workspace owner can configure Zoom
|
| 1608 |
+
owner_id = get_workspace_owner_id(client, team_id)
|
| 1609 |
+
if user_id != owner_id:
|
| 1610 |
+
client.chat_postMessage(channel=user_id, text="Only the workspace owner can configure Zoom.")
|
| 1611 |
+
return
|
| 1612 |
+
|
| 1613 |
+
# Check if this is a refresh or initial authentication
|
| 1614 |
+
zoom_token = load_token(team_id, owner_id, "zoom")
|
| 1615 |
+
is_refresh = zoom_token is not None
|
| 1616 |
+
|
| 1617 |
+
# Generate the Zoom OAuth URL
|
| 1618 |
+
state = state_manager.create_state(owner_id) # Assume this generates a unique state
|
| 1619 |
+
auth_url = f"{ZOOM_OAUTH_AUTHORIZE_API}?response_type=code&client_id={CLIENT_ID}&redirect_uri={quote_plus(ZOOM_REDIRECT_URI)}&state={state}"
|
| 1620 |
+
|
| 1621 |
+
# Set modal text based on the scenario
|
| 1622 |
+
modal_title = "Refresh Zoom Token" if is_refresh else "Authenticate with Zoom"
|
| 1623 |
+
button_text = "Refresh Zoom Token" if is_refresh else "Authenticate with Zoom"
|
| 1624 |
+
|
| 1625 |
+
# Open a modal with the appropriate text
|
| 1626 |
+
try:
|
| 1627 |
+
client.views_open(
|
| 1628 |
+
trigger_id=body["trigger_id"],
|
| 1629 |
+
view={
|
| 1630 |
+
"type": "modal",
|
| 1631 |
+
"title": {"type": "plain_text", "text": modal_title},
|
| 1632 |
+
"close": {"type": "plain_text", "text": "Cancel"},
|
| 1633 |
+
"blocks": [
|
| 1634 |
+
{
|
| 1635 |
+
"type": "section",
|
| 1636 |
+
"text": {"type": "mrkdwn", "text": f"Click below to {button_text.lower()}:"}
|
| 1637 |
+
},
|
| 1638 |
+
{
|
| 1639 |
+
"type": "actions",
|
| 1640 |
+
"elements": [
|
| 1641 |
+
{
|
| 1642 |
+
"type": "button",
|
| 1643 |
+
"text": {"type": "plain_text", "text": button_text},
|
| 1644 |
+
"url": auth_url,
|
| 1645 |
+
"action_id": "launch_zoom_auth"
|
| 1646 |
+
}
|
| 1647 |
+
]
|
| 1648 |
+
}
|
| 1649 |
+
]
|
| 1650 |
+
}
|
| 1651 |
+
)
|
| 1652 |
+
except Exception as e:
|
| 1653 |
+
logger.error(f"Error opening Zoom auth modal: {e}")
|
| 1654 |
+
|
| 1655 |
+
@bolt_app.view("zoom_config_submit")
|
| 1656 |
+
def handle_zoom_config_submit(ack, body, client, logger):
|
| 1657 |
+
ack() # Ensure ack is called before any processing to avoid warnings
|
| 1658 |
+
user_id = body["user"]["id"]
|
| 1659 |
+
team_id = body["team"]["id"]
|
| 1660 |
+
owner_id = get_workspace_owner_id(client, team_id)
|
| 1661 |
+
if user_id != owner_id:
|
| 1662 |
+
return # Early return if not owner; no need to proceed
|
| 1663 |
+
|
| 1664 |
+
values = body["view"]["state"]["values"]
|
| 1665 |
+
mode = values["zoom_mode"]["mode_select"]["selected_option"]["value"]
|
| 1666 |
+
link = values["zoom_link"]["link_input"]["value"] if "zoom_link" in values and "link_input" in values["zoom_link"] else None
|
| 1667 |
+
zoom_config = {"mode": mode, "link": link if mode == "manual" else None}
|
| 1668 |
+
|
| 1669 |
+
|
| 1670 |
+
save_preference(team_id, user_id, zoom_config=zoom_config)
|
| 1671 |
+
|
| 1672 |
+
client.views_publish(user_id=user_id, view=create_home_tab(client, team_id, user_id))
|
| 1673 |
+
@bolt_app.action("launch_zoom_auth")
|
| 1674 |
+
def handle_some_action(ack, body, logger):
|
| 1675 |
+
ack()
|
| 1676 |
+
|
| 1677 |
+
scheduler = BackgroundScheduler()
|
| 1678 |
+
scheduler.add_job(state_manager.cleanup_expired_states, 'interval', minutes=5)
|
| 1679 |
+
scheduler.start()
|
| 1680 |
+
|
| 1681 |
+
@app.route('/')
|
| 1682 |
+
def home():
|
| 1683 |
+
return render_template('index.html')
|
| 1684 |
+
# @app.route('/ZOOM_verify_a12f2ccf48a647aa8ebc987a249133f8.html')
|
| 1685 |
+
# def home():
|
| 1686 |
+
# return render_template('ZOOM_verify_a12f2ccf48a647aa8ebc987a249133f8.html')
|
| 1687 |
+
if __name__ == "__main__":
|
| 1688 |
+
app.run(port=3000)
|
calendar_tools.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from services import create_service
|
| 3 |
+
client_secret = 'credentials.json'
|
| 4 |
+
|
| 5 |
+
def construct_google_calendar_client(client_secret):
|
| 6 |
+
"""
|
| 7 |
+
Constructs a Google Calendar API client.
|
| 8 |
+
|
| 9 |
+
Parameters:
|
| 10 |
+
- client_secret (str): The path to the client secret JSON file.
|
| 11 |
+
|
| 12 |
+
Returns:
|
| 13 |
+
- service: The Google Calendar API service instance.
|
| 14 |
+
"""
|
| 15 |
+
API_NAME = 'calendar'
|
| 16 |
+
API_VERSION = 'v3'
|
| 17 |
+
SCOPES = ['https://www.googleapis.com/auth/calendar']
|
| 18 |
+
service = create_service(client_secret, API_NAME, API_VERSION, SCOPES)
|
| 19 |
+
return service
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
calendar_service = construct_google_calendar_client(client_secret=client_secret)
|
| 23 |
+
|
| 24 |
+
def create_calendar_list(calendar_name):
|
| 25 |
+
"""
|
| 26 |
+
Creates a new calendar list.
|
| 27 |
+
|
| 28 |
+
Parameters:
|
| 29 |
+
- calendar_name (str): The name of the new calendar list.
|
| 30 |
+
|
| 31 |
+
Returns:
|
| 32 |
+
- dict: A dictionary containing the ID of the new calendar list.
|
| 33 |
+
"""
|
| 34 |
+
calendar_list = {
|
| 35 |
+
'summary': calendar_name
|
| 36 |
+
}
|
| 37 |
+
created_calendar_list = calendar_service.calendarList().insert(body=calendar_list).execute()
|
| 38 |
+
return created_calendar_list
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def list_calendar_list(max_capacity=200):
|
| 42 |
+
"""
|
| 43 |
+
Lists calendar lists until the total number of items reaches max_capacity.
|
| 44 |
+
|
| 45 |
+
Parameters:
|
| 46 |
+
- max_capacity (int or str, optional): The maximum number of calendar lists to retrieve. Defaults to 200.
|
| 47 |
+
If a string is provided, it will be converted to an integer.
|
| 48 |
+
|
| 49 |
+
Returns:
|
| 50 |
+
- list: A list of dictionaries containing cleaned calendar list information with 'id', 'name', and 'description'.
|
| 51 |
+
"""
|
| 52 |
+
if isinstance(max_capacity, str):
|
| 53 |
+
max_capacity = int(max_capacity)
|
| 54 |
+
|
| 55 |
+
all_calendars = []
|
| 56 |
+
all_calendars_cleaned = []
|
| 57 |
+
next_page_token = None
|
| 58 |
+
capacity_tracker = 0
|
| 59 |
+
|
| 60 |
+
while True:
|
| 61 |
+
calendar_list = calendar_service.calendarList().list(
|
| 62 |
+
maxResults=min(200, max_capacity - capacity_tracker),
|
| 63 |
+
pageToken=next_page_token
|
| 64 |
+
).execute()
|
| 65 |
+
calendars = calendar_list.get('items', [])
|
| 66 |
+
all_calendars.extend(calendars)
|
| 67 |
+
capacity_tracker += len(calendars)
|
| 68 |
+
if capacity_tracker >= max_capacity:
|
| 69 |
+
break
|
| 70 |
+
next_page_token = calendar_list.get('nextPageToken')
|
| 71 |
+
if not next_page_token:
|
| 72 |
+
break
|
| 73 |
+
|
| 74 |
+
for calendar in all_calendars:
|
| 75 |
+
all_calendars_cleaned.append(
|
| 76 |
+
{
|
| 77 |
+
'id': calendar['id'],
|
| 78 |
+
'name': calendar['summary'],
|
| 79 |
+
'description': calendar.get('description', '')
|
| 80 |
+
})
|
| 81 |
+
|
| 82 |
+
return all_calendars_cleaned
|
| 83 |
+
|
| 84 |
+
def list_calendar_events(calendar_id, max_capacity=20):
|
| 85 |
+
"""
|
| 86 |
+
Lists events from a specified calendar until the total number of events reaches max_capacity.
|
| 87 |
+
|
| 88 |
+
Parameters:
|
| 89 |
+
- calendar_id (str): The ID of the calendar from which to list events.
|
| 90 |
+
- max_capacity (int or str, optional): The maximum number of events to retrieve. Defaults to 20.
|
| 91 |
+
If a string is provided, it will be converted to an integer.
|
| 92 |
+
|
| 93 |
+
Returns:
|
| 94 |
+
- list: A list of events from the specified calendar.
|
| 95 |
+
"""
|
| 96 |
+
if isinstance(max_capacity, str):
|
| 97 |
+
max_capacity = int(max_capacity)
|
| 98 |
+
|
| 99 |
+
all_events = []
|
| 100 |
+
next_page_token = None
|
| 101 |
+
capacity_tracker = 0
|
| 102 |
+
|
| 103 |
+
while True:
|
| 104 |
+
events_list = calendar_service.events().list(
|
| 105 |
+
calendarId=calendar_id,
|
| 106 |
+
maxResults=min(250, max_capacity - capacity_tracker),
|
| 107 |
+
pageToken=next_page_token
|
| 108 |
+
).execute()
|
| 109 |
+
events = events_list.get('items', [])
|
| 110 |
+
all_events.extend(events)
|
| 111 |
+
capacity_tracker += len(events)
|
| 112 |
+
if capacity_tracker >= max_capacity:
|
| 113 |
+
break
|
| 114 |
+
next_page_token = events_list.get('nextPageToken')
|
| 115 |
+
if not next_page_token:
|
| 116 |
+
break
|
| 117 |
+
|
| 118 |
+
return all_events
|
config.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from slack_sdk.errors import SlackApiError
|
| 2 |
+
import os
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
from slack_sdk import WebClient
|
| 5 |
+
import sqlite3
|
| 6 |
+
import json
|
| 7 |
+
import psycopg2
|
| 8 |
+
import logging
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
# Load environment variables
|
| 11 |
+
load_dotenv()
|
| 12 |
+
|
| 13 |
+
# Slack credentials
|
| 14 |
+
SLACK_BOT_TOKEN = os.environ["SLACK_BOT_TOKEN"]
|
| 15 |
+
|
| 16 |
+
# Initialize the Slack client
|
| 17 |
+
client = WebClient(token=SLACK_BOT_TOKEN)
|
| 18 |
+
logger = logging.getLogger(__name__)
|
| 19 |
+
def load_preferences(team_id, user_id):
|
| 20 |
+
with preferences_cache_lock:
|
| 21 |
+
if (team_id, user_id) in preferences_cache:
|
| 22 |
+
return preferences_cache[(team_id, user_id)]
|
| 23 |
+
try:
|
| 24 |
+
conn = psycopg2.connect(os.getenv('DATABASE_URL'))
|
| 25 |
+
cur = conn.cursor()
|
| 26 |
+
cur.execute('SELECT zoom_config, calendar_tool FROM Preferences WHERE team_id = %s AND user_id = %s', (team_id, user_id))
|
| 27 |
+
row = cur.fetchone()
|
| 28 |
+
if row:
|
| 29 |
+
zoom_config, calendar_tool = row
|
| 30 |
+
# For jsonb, zoom_config is already a dict; no json.loads needed
|
| 31 |
+
preferences = {
|
| 32 |
+
"zoom_config": zoom_config if zoom_config else {"mode": "manual", "link": None},
|
| 33 |
+
"calendar_tool": calendar_tool or "none"
|
| 34 |
+
}
|
| 35 |
+
else:
|
| 36 |
+
preferences = {"zoom_config": {"mode": "manual", "link": None}, "calendar_tool": "none"}
|
| 37 |
+
cur.close()
|
| 38 |
+
conn.close()
|
| 39 |
+
except Exception as e:
|
| 40 |
+
logger.error(f"Failed to load preferences for team {team_id}, user {user_id}: {e}")
|
| 41 |
+
preferences = {"zoom_config": {"mode": "manual", "link": None}, "calendar_tool": "none"}
|
| 42 |
+
with preferences_cache_lock:
|
| 43 |
+
preferences_cache[(team_id, user_id)] = preferences
|
| 44 |
+
return preferences
|
| 45 |
+
from threading import Lock
|
| 46 |
+
user_cache = {}
|
| 47 |
+
preferences_cache = {}
|
| 48 |
+
preferences_cache_lock = Lock()
|
| 49 |
+
user_cache_lock = Lock() # Example threading lock for cache
|
| 50 |
+
|
| 51 |
+
owner_id_cache = {}
|
| 52 |
+
owner_id_lock = Lock()
|
| 53 |
+
def initialize_workspace_cache(client, team_id):
|
| 54 |
+
conn = psycopg2.connect(os.getenv('DATABASE_URL'))
|
| 55 |
+
cur = conn.cursor()
|
| 56 |
+
cur.execute('SELECT MAX(last_updated) FROM Users WHERE team_id = %s', (team_id,))
|
| 57 |
+
last_updated_row = cur.fetchone()
|
| 58 |
+
last_updated = last_updated_row[0] if last_updated_row and last_updated_row[0] else None
|
| 59 |
+
|
| 60 |
+
# Check if cache is fresh (e.g., less than 24 hours old)
|
| 61 |
+
if last_updated and (datetime.now() - last_updated).total_seconds() < 86400:
|
| 62 |
+
cur.execute('SELECT user_id, real_name, email, name, is_owner, workspace_name FROM Users WHERE team_id = %s', (team_id,))
|
| 63 |
+
rows = cur.fetchall()
|
| 64 |
+
new_cache = {row[0]: {"real_name": row[1], "email": row[2], "name": row[3], "is_owner": row[4], "workspace_name": row[5]} for row in rows}
|
| 65 |
+
with user_cache_lock:
|
| 66 |
+
user_cache[team_id] = new_cache
|
| 67 |
+
with owner_id_lock:
|
| 68 |
+
owner_id_cache[team_id] = next((user_id for user_id, data in new_cache.items() if data['is_owner']), None)
|
| 69 |
+
else:
|
| 70 |
+
# Fetch user data from Slack and update database
|
| 71 |
+
response = client.users_list()
|
| 72 |
+
users = response["members"]
|
| 73 |
+
workspace_name = client.team_info()["team"]["name"] # Get workspace name from Slack API
|
| 74 |
+
new_cache = {}
|
| 75 |
+
for user in users:
|
| 76 |
+
user_id = user['id']
|
| 77 |
+
profile = user.get('profile', {})
|
| 78 |
+
real_name = profile.get('real_name', 'Unknown')
|
| 79 |
+
name = user.get('name', '')
|
| 80 |
+
email = f"{name}@gmail.com" # Placeholder; adjust as needed
|
| 81 |
+
is_owner = user.get('is_owner', False)
|
| 82 |
+
new_cache[user_id] = {"real_name": real_name, "email": email, "name": name, "is_owner": is_owner, "workspace_name": workspace_name}
|
| 83 |
+
cur.execute('''
|
| 84 |
+
INSERT INTO Users (team_id, user_id, workspace_name, real_name, email, name, is_owner, last_updated)
|
| 85 |
+
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
| 86 |
+
ON CONFLICT (team_id, user_id) DO UPDATE SET
|
| 87 |
+
workspace_name = %s, real_name = %s, email = %s, name = %s, is_owner = %s, last_updated = %s
|
| 88 |
+
''', (team_id, user_id, workspace_name, real_name, email, name, is_owner, datetime.now(),
|
| 89 |
+
workspace_name, real_name, email, name, is_owner, datetime.now()))
|
| 90 |
+
conn.commit()
|
| 91 |
+
with user_cache_lock:
|
| 92 |
+
user_cache[team_id] = new_cache
|
| 93 |
+
with owner_id_lock:
|
| 94 |
+
owner_id_cache[team_id] = next((user_id for user_id, data in new_cache.items() if data['is_owner']), None)
|
| 95 |
+
cur.close()
|
| 96 |
+
conn.close()
|
| 97 |
+
def get_workspace_owner_id_client(client ):
|
| 98 |
+
"""Get the workspace owner's user ID."""
|
| 99 |
+
try:
|
| 100 |
+
response = client.users_list()
|
| 101 |
+
members = response["members"]
|
| 102 |
+
for member in members:
|
| 103 |
+
if member.get("is_owner"):
|
| 104 |
+
return member["id"]
|
| 105 |
+
except SlackApiError as e:
|
| 106 |
+
print(f"Error fetching users: {e.response['error']}")
|
| 107 |
+
return None
|
| 108 |
+
|
| 109 |
+
def get_workspace_owner_id(client, team_id):
|
| 110 |
+
with owner_id_lock:
|
| 111 |
+
if team_id in owner_id_cache and owner_id_cache[team_id]:
|
| 112 |
+
return owner_id_cache[team_id]
|
| 113 |
+
initialize_workspace_cache(client, team_id)
|
| 114 |
+
with owner_id_lock:
|
| 115 |
+
return owner_id_cache.get(team_id)
|
| 116 |
+
# def get_workspace_owner_id():
|
| 117 |
+
# conn = sqlite3.connect('workspace_cache.db')
|
| 118 |
+
# c = conn.cursor()
|
| 119 |
+
# c.execute('SELECT user_id FROM workspace_cache WHERE is_owner = 1')
|
| 120 |
+
# owner_id = c.fetchone()
|
| 121 |
+
# conn.close()
|
| 122 |
+
# return owner_id[0] if owner_id else None
|
| 123 |
+
|
| 124 |
+
owner_id_pref = get_workspace_owner_id_client(client)
|
| 125 |
+
def GetAllUsers():
|
| 126 |
+
all_users = {}
|
| 127 |
+
try:
|
| 128 |
+
response = client.users_list()
|
| 129 |
+
print(response)
|
| 130 |
+
members = response['members']
|
| 131 |
+
for member in members:
|
| 132 |
+
user_id = member['id']
|
| 133 |
+
profile = member.get('profile', {})
|
| 134 |
+
user_name = profile.get('real_name', '') # Use real_name from profile
|
| 135 |
+
# Use actual email if available, otherwise construct one from member['name']
|
| 136 |
+
email = profile.get('email', f"{member.get('name', '')}@gmail.com")
|
| 137 |
+
print(f"User ID: {user_id}, Name: {user_name}, Email: {email}")
|
| 138 |
+
all_users[user_id] = {"Slack Id": user_id, "name": user_name, "email": email}
|
| 139 |
+
return all_users
|
| 140 |
+
except SlackApiError as e:
|
| 141 |
+
print(f"Error fetching users: {e.response['error']}")
|
| 142 |
+
return {}
|
| 143 |
+
def load_token(team_id, user_id, service):
|
| 144 |
+
"""
|
| 145 |
+
Load token data from the database for a specific team, user, and service.
|
| 146 |
+
|
| 147 |
+
Parameters:
|
| 148 |
+
team_id (str): The Slack team ID.
|
| 149 |
+
user_id (str): The Slack user ID.
|
| 150 |
+
service (str): The service name (e.g., 'google').
|
| 151 |
+
|
| 152 |
+
Returns:
|
| 153 |
+
dict: The token data as a dictionary, or None if not found.
|
| 154 |
+
"""
|
| 155 |
+
conn = psycopg2.connect(os.getenv('DATABASE_URL'))
|
| 156 |
+
cur = conn.cursor()
|
| 157 |
+
cur.execute(
|
| 158 |
+
'SELECT token_data FROM Tokens WHERE team_id = %s AND user_id = %s AND service = %s',
|
| 159 |
+
(team_id, user_id, service)
|
| 160 |
+
)
|
| 161 |
+
row = cur.fetchone()
|
| 162 |
+
cur.close()
|
| 163 |
+
conn.close()
|
| 164 |
+
return row[0] if row else None
|
| 165 |
+
|
| 166 |
+
def save_token(team_id, user_id, service, token_data):
|
| 167 |
+
"""
|
| 168 |
+
Save token data to the database for a specific team, user, and service.
|
| 169 |
+
|
| 170 |
+
Parameters:
|
| 171 |
+
team_id (str): The Slack team ID.
|
| 172 |
+
user_id (str): The Slack user ID.
|
| 173 |
+
service (str): The service name (e.g., 'google').
|
| 174 |
+
token_data (dict): The token data to save (e.g., {'access_token', 'refresh_token', 'expires_at'}).
|
| 175 |
+
"""
|
| 176 |
+
conn = psycopg2.connect(os.getenv('DATABASE_URL'))
|
| 177 |
+
cur = conn.cursor()
|
| 178 |
+
cur.execute(
|
| 179 |
+
'''
|
| 180 |
+
INSERT INTO Tokens (team_id, user_id, service, token_data, updated_at)
|
| 181 |
+
VALUES (%s, %s, %s, %s, %s)
|
| 182 |
+
ON CONFLICT (team_id, user_id, service)
|
| 183 |
+
DO UPDATE SET token_data = %s, updated_at = %s
|
| 184 |
+
''',
|
| 185 |
+
(
|
| 186 |
+
team_id, user_id, service, json.dumps(token_data), datetime.now(),
|
| 187 |
+
json.dumps(token_data), datetime.now()
|
| 188 |
+
)
|
| 189 |
+
)
|
| 190 |
+
conn.commit()
|
| 191 |
+
cur.close()
|
| 192 |
+
conn.close()
|
| 193 |
+
all_users_preload = GetAllUsers()
|
| 194 |
+
if all_users_preload:
|
| 195 |
+
print("Users Prefection enabled")
|
| 196 |
+
# def GetAllUsers():
|
| 197 |
+
# return ""
|
| 198 |
+
# all_users_preload = ""
|
| 199 |
+
# owner_id_pref = ""
|
db.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import psycopg2
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
|
| 5 |
+
load_dotenv()
|
| 6 |
+
|
| 7 |
+
def init_db():
|
| 8 |
+
"""
|
| 9 |
+
Initialize the Neon Postgres database by creating the necessary tables for multi-workspace support:
|
| 10 |
+
- Installations: Stores Slack workspace installation data.
|
| 11 |
+
- Users: Stores Slack user information with team_id, user_id, and workspace_name.
|
| 12 |
+
- Preferences: Stores user-specific preferences with team_id and user_id.
|
| 13 |
+
- Tokens: Stores authentication tokens with team_id, user_id, and service.
|
| 14 |
+
Prerequisites: 'DATABASE_URL' environment variable must be set with Neon Postgres connection string.
|
| 15 |
+
"""
|
| 16 |
+
conn = psycopg2.connect(os.getenv('DATABASE_URL'))
|
| 17 |
+
cur = conn.cursor()
|
| 18 |
+
|
| 19 |
+
# Create Installations table for OAuth installation data
|
| 20 |
+
cur.execute('''
|
| 21 |
+
CREATE TABLE IF NOT EXISTS Installations (
|
| 22 |
+
workspace_id TEXT PRIMARY KEY, -- Slack workspace ID (team_id)
|
| 23 |
+
installation_data JSONB, -- Installation data stored as JSON
|
| 24 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- Last update timestamp
|
| 25 |
+
)
|
| 26 |
+
''')
|
| 27 |
+
|
| 28 |
+
# Create Users table with composite primary key (team_id, user_id) and workspace_name
|
| 29 |
+
cur.execute('''
|
| 30 |
+
CREATE TABLE IF NOT EXISTS Users (
|
| 31 |
+
team_id TEXT, -- Slack workspace ID
|
| 32 |
+
user_id TEXT, -- Slack user ID
|
| 33 |
+
workspace_name TEXT, -- Name of the workspace
|
| 34 |
+
real_name TEXT, -- User's real name from Slack
|
| 35 |
+
email TEXT, -- User's email from Slack
|
| 36 |
+
name TEXT, -- User's Slack handle
|
| 37 |
+
is_owner BOOLEAN, -- Indicates if user is workspace owner
|
| 38 |
+
last_updated TIMESTAMP, -- Last time user data was updated
|
| 39 |
+
PRIMARY KEY (team_id, user_id) -- Composite key for uniqueness across workspaces
|
| 40 |
+
)
|
| 41 |
+
''')
|
| 42 |
+
|
| 43 |
+
# Create Preferences table with composite primary key and foreign key
|
| 44 |
+
cur.execute('''
|
| 45 |
+
CREATE TABLE IF NOT EXISTS Preferences (
|
| 46 |
+
team_id TEXT, -- Slack workspace ID
|
| 47 |
+
user_id TEXT, -- Slack user ID
|
| 48 |
+
zoom_config JSONB, -- Zoom configuration stored as JSON
|
| 49 |
+
calendar_tool TEXT, -- Selected calendar tool (e.g., google, microsoft)
|
| 50 |
+
updated_at TIMESTAMP, -- Last update timestamp
|
| 51 |
+
PRIMARY KEY (team_id, user_id), -- Composite key for uniqueness
|
| 52 |
+
CONSTRAINT fk_user
|
| 53 |
+
FOREIGN KEY(team_id, user_id) -- References Users table
|
| 54 |
+
REFERENCES Users(team_id, user_id)
|
| 55 |
+
ON DELETE CASCADE -- Delete preferences if user is deleted
|
| 56 |
+
)
|
| 57 |
+
''')
|
| 58 |
+
|
| 59 |
+
# Create Tokens table with composite primary key and foreign key
|
| 60 |
+
cur.execute('''
|
| 61 |
+
CREATE TABLE IF NOT EXISTS Tokens (
|
| 62 |
+
team_id TEXT, -- Slack workspace ID
|
| 63 |
+
user_id TEXT, -- Slack user ID
|
| 64 |
+
service TEXT, -- Service name (google, microsoft, zoom)
|
| 65 |
+
token_data JSONB, -- Token data stored as JSON
|
| 66 |
+
updated_at TIMESTAMP, -- Last update timestamp
|
| 67 |
+
PRIMARY KEY (team_id, user_id, service), -- Composite key ensures one token per service per user per workspace
|
| 68 |
+
CONSTRAINT fk_user
|
| 69 |
+
FOREIGN KEY(team_id, user_id) -- References Users table
|
| 70 |
+
REFERENCES Users(team_id, user_id)
|
| 71 |
+
ON DELETE CASCADE -- Delete tokens if user is deleted
|
| 72 |
+
)
|
| 73 |
+
''')
|
| 74 |
+
|
| 75 |
+
conn.commit()
|
| 76 |
+
cur.close()
|
| 77 |
+
conn.close()
|
| 78 |
+
|
| 79 |
+
if __name__ == '__main__':
|
| 80 |
+
init_db()
|
| 81 |
+
print('Neon Postgres database initialized successfully.')
|
prompt.py
ADDED
|
@@ -0,0 +1,698 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain.prompts import ChatPromptTemplate
|
| 2 |
+
|
| 3 |
+
# Intent Classification Prompt
|
| 4 |
+
intent_prompt = ChatPromptTemplate.from_template("""
|
| 5 |
+
You are an intent classification assistant. Based on the user's message and the conversation history, determine the intent of the user's request. The possible intents are: "schedule meeting", "update event", "delete event", or "other". Provide only the intent as your response.
|
| 6 |
+
- By looking at the history if someone is confirming or denying the schedule , also categorize it as a schedule
|
| 7 |
+
Conversation History:
|
| 8 |
+
{history}
|
| 9 |
+
|
| 10 |
+
User's Message:
|
| 11 |
+
{input}
|
| 12 |
+
""")
|
| 13 |
+
|
| 14 |
+
# Schedule Meeting Agent Prompt
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
calender_prompt = ChatPromptTemplate.from_template("""
|
| 19 |
+
<SYSTEM>
|
| 20 |
+
You are an intelligent agent and your job is to design the timeslots for the meetings
|
| 21 |
+
You will be given with raw calendar events and you have the following job
|
| 22 |
+
|
| 23 |
+
<CURRENT DATE AND TIME>
|
| 24 |
+
{date_time}
|
| 25 |
+
|
| 26 |
+
<WORKSPACE ADMIN ID>
|
| 27 |
+
Use workspace admin slack id for the calendar event: {admin_id}
|
| 28 |
+
<JOB STEPS>
|
| 29 |
+
STEP 1. Fetch current date once
|
| 30 |
+
STEP 2. Filter all past events (events on dates which are behind the current date) and also the future event timeslots
|
| 31 |
+
STEP 3. Generate the 7 day timeslots omitting the past events and future time slots which have events registered
|
| 32 |
+
STEP 4. Prepare those slots in this reference format
|
| 33 |
+
"
|
| 34 |
+
Date | Day | Slots | Timezone
|
| 35 |
+
01-12-2024 | Friday | All Day | PT
|
| 36 |
+
02-12-2024 | Saturday | 9am - 11am, 2pm - 3pm | PT
|
| 37 |
+
03-12-2024 | Sunday | All Day | PT
|
| 38 |
+
04-12-2024 | Monday | 10am - 12pm | PT
|
| 39 |
+
05-12-2024 | Tuesday | 1pm - 3pm | PT
|
| 40 |
+
06-12-2024 | Wednesday | All Day | PT
|
| 41 |
+
07-12-2024 | Thursday | 9am - 10am | PT
|
| 42 |
+
|
| 43 |
+
"
|
| 44 |
+
<UNFORMATTED EVENTS>
|
| 45 |
+
{input}
|
| 46 |
+
FINAL OUTPUT: Formatted slots in given format and dont include any step details or preprocessing details
|
| 47 |
+
""")
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
# schedule_prompt = ChatPromptTemplate.from_template("""
|
| 52 |
+
# <SYSTEM>:
|
| 53 |
+
# You are a meeting scheduling assistant. Your task is to help the user schedule a meeting by finding available time slots, coordinating with participants, and creating the meeting event in the calendar.
|
| 54 |
+
# <TOOLS>: Be Precise in using the tools and use it only once
|
| 55 |
+
# {{tools}}
|
| 56 |
+
# <CURRENT DATE AND TIME>
|
| 57 |
+
# {date_time}
|
| 58 |
+
# <TASK>:
|
| 59 |
+
# Based on the user's request, use the appropriate tools to schedule the meeting. Check calendar availability, send direct messages to coordinate, and create the event.
|
| 60 |
+
# <STEPS>
|
| 61 |
+
# 1. You will be given the formatted current events of the calendar.
|
| 62 |
+
# 2. Send the schedule by mentioning all the mentioned user s and ask for their preferences.
|
| 63 |
+
# 3. Note that there can be multiple users mentioned in the meeting text and if one user replies about a timing then also mention other users and ask if they are comfortable with this time or not.
|
| 64 |
+
# 3.1: If all user agrees then schedule the meeting in the calendar
|
| 65 |
+
# 3.2 If even one of them disagrees then go to step 2.1 and send the sames slots mentioning the disagreement and keep performing this step until the consensus is reached and all mentioned user are agreed on one time
|
| 66 |
+
# 3.3 You can keep track for yourself, lets say we have 3 users U1 , U2 and U3, you can check the users U1 has agreed , U2 has agreed but U3 didnt so again send the timetable but already prepared one so dont use the tool again just the history and mention that U3 have the clash so pick another time from the slots.
|
| 67 |
+
|
| 68 |
+
# 4. After resolving the conflict you must call the tool to register the event in the calendar.
|
| 69 |
+
# 4.1: You should include the summary of the event like who are included in the meeting
|
| 70 |
+
# 4.2: You should include the email addresses of the mentioned users which you can access from the <USERS INFORMATION>.
|
| 71 |
+
|
| 72 |
+
# 5. Finally if meeting is registered in the calendar then send the direct message to each of the attendee/ mentioned users about confirming the meeting schedule and again you can access the user information from the <USERS INFORMATION>
|
| 73 |
+
# - To send the dm to single mentioned user: send_direct_dm and pass the slack user id of the receiver i.e user_id = 'UC3472938'
|
| 74 |
+
# - To send the dm to multiple mentioned users use this tool: send_multiple_dms and user slacks id will be passed as list to this tool i.e user_ids = ['UA263487', 'UB8984234']
|
| 75 |
+
|
| 76 |
+
# ------------------------------------ YOUR WORK STARTS FROM HERE.
|
| 77 |
+
# <INPUT>:
|
| 78 |
+
# {input}
|
| 79 |
+
# <CHANNEL HISTORY(PREVIOUS MESSAGES FOR CONTEXT)>:
|
| 80 |
+
# {channel_history}
|
| 81 |
+
# - If a new message is sent or received related to new schedule of meeting then ignore old responses
|
| 82 |
+
# - Always analyze the latest history and in context to new messages
|
| 83 |
+
# <EVENTS FROM THE CALENDAR>
|
| 84 |
+
# "Hello <@mentioned_users>,
|
| 85 |
+
# <@{admin}> wants to schedule a meeting with you. Here are their available time slots:
|
| 86 |
+
# {formatted_calendar} Which slot suits for you best "
|
| 87 |
+
# <USERS INFORMATION>:
|
| 88 |
+
# {user_information}
|
| 89 |
+
|
| 90 |
+
# <CALENDAR TOOL>:
|
| 91 |
+
# {calendar_tool}
|
| 92 |
+
# <EVENT DETAILS TO REGISTER IN THE CALENDAR>:
|
| 93 |
+
# {event_details}
|
| 94 |
+
|
| 95 |
+
# <TARGET USER ID>:
|
| 96 |
+
# {target_user_id}
|
| 97 |
+
|
| 98 |
+
# <TIMEZONE>:
|
| 99 |
+
# {timezone}
|
| 100 |
+
|
| 101 |
+
# <USER ID>:
|
| 102 |
+
# {user_id}
|
| 103 |
+
|
| 104 |
+
# <ADMIN SLACK ID>:
|
| 105 |
+
# {admin}
|
| 106 |
+
|
| 107 |
+
# <ZOOM LINK>:
|
| 108 |
+
# {zoom_link}
|
| 109 |
+
|
| 110 |
+
# <ZOOM MODE>:
|
| 111 |
+
# {zoom_mode}
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
# <AGENT SCRATCHPAD>:
|
| 115 |
+
# {agent_scratchpad}
|
| 116 |
+
|
| 117 |
+
# <OUTPUT>:
|
| 118 |
+
# Provide a confirmation message after scheduling, e.g., "Meeting scheduled successfully."
|
| 119 |
+
# """)
|
| 120 |
+
from langchain.prompts import ChatPromptTemplate
|
| 121 |
+
|
| 122 |
+
schedule_prompt = ChatPromptTemplate.from_template("""
|
| 123 |
+
## System Message
|
| 124 |
+
You are a meeting scheduling assistant. You task is following.
|
| 125 |
+
1. Resolve conflicts when multiple users are proposing their timeslot
|
| 126 |
+
2. Schedule meetings and send the direct message only once to users.
|
| 127 |
+
3. You are not allowed to use any tool twice if a tool is used once then dont use it again
|
| 128 |
+
## User Information:
|
| 129 |
+
- Email addresses of all participants, found in {user_information}.
|
| 130 |
+
- Store name in calendar not ids
|
| 131 |
+
- You can match ids with names and emails.
|
| 132 |
+
- Pass {admin} to calendar events as user
|
| 133 |
+
id
|
| 134 |
+
- Also add admin in calendar attendees as well. - You can ignore previous respones if in those responses some meeting is already set up.
|
| 135 |
+
## Tools
|
| 136 |
+
- **Available Tools:** {{tools}}
|
| 137 |
+
*(Placeholder for the list of tools the assistant can use, e.g., calendar tools, messaging functions.)*
|
| 138 |
+
- **Tool Usage Guidelines:**
|
| 139 |
+
- **Messaging Tools:** Use `send_direct_dm` or `send_multiple_dms` each time you need to send a message (e.g., proposing slots, collecting responses, or confirming the meeting). Call these only when explicitly required by the workflow.
|
| 140 |
+
|
| 141 |
+
## Current Date and Time
|
| 142 |
+
{date_time}
|
| 143 |
+
*(Placeholder for the current date and time, used as a reference for scheduling.)*
|
| 144 |
+
## If you receive any message of token expiration do not process further just return the reponse of that token expiration and ask {admin} to refresh it
|
| 145 |
+
## Task
|
| 146 |
+
Based on the user's request, schedule a meeting by:
|
| 147 |
+
- Checking calendar availability which is passed.
|
| 148 |
+
- Create the event in the calendar once consensus is reached by all the users.
|
| 149 |
+
- Obviously you can see the history and if you find that multiple times same request of scheduling is fired means that there are 2 consecutive requests of scheduling then consider only one and latest one.
|
| 150 |
+
- Never ever mention Bob in calendar summary and dont add Bob's name and email
|
| 151 |
+
- And add in description that this meeting is scheduled by [admin's name here] on Slack.
|
| 152 |
+
## Workflow
|
| 153 |
+
Follow these steps to schedule the meeting:
|
| 154 |
+
|
| 155 |
+
1. **Calendar Information**
|
| 156 |
+
This is the calendar {formatted_calendar} and now your job is to send this schedule to the mentioned user/users other than admin.
|
| 157 |
+
You should use this template and dont include any steps details:
|
| 158 |
+
"Hello <@mentioned_users/user>,
|
| 159 |
+
|
| 160 |
+
<@{admin}> wants to schedule a meeting with you. Here are their available time slots:
|
| 161 |
+
|
| 162 |
+
{formatted_calendar}
|
| 163 |
+
|
| 164 |
+
Which slot suits you best?"
|
| 165 |
+
- Use the appropriate messaging tool (`send_direct_dm` for one user, `send_multiple_dms` for multiple users).
|
| 166 |
+
|
| 167 |
+
3. **Collect and Manage Responses**(Here you will use history and new input to analyze the response)
|
| 168 |
+
- Monitor responses from all mentioned users or a single user.
|
| 169 |
+
- if user/users agree or propose a time slot , send 'send_direct_dm' message to {admin} and ask for their confirmation , once they confirmed then use calendar tool.
|
| 170 |
+
- In case of multiple mentioned users other than admin , don't send dm to admin just mention admin if all other users are agreed.
|
| 171 |
+
- If all other users are not agreed or there is a conflict then mention other users for their slot confirmation.
|
| 172 |
+
- Keep track of each user’s response (e.g., "U1: agreed, U2: agreed, U3: disagreed").
|
| 173 |
+
- Repeat this step, mentioning users in message as needed, until all users agree on one time slot.
|
| 174 |
+
- Do not send dm to users and admin until all other users and admin agrees. and it should include a summary of the meeting (e.g., "Meeting with U1, U2, U3").
|
| 175 |
+
4: First register the zoom meeting using the tool 'create_zoom_meeting' and then register the event in the calendar using either 'microsoft_calendar_add_event' or 'google_add_calendar_event' based on calendar tools and also include the formatted output of this in the calendar summary.
|
| 176 |
+
# Do not use this tool until all users and admin are agreed: -
|
| 177 |
+
- `send_direct_dm` for one user (e.g., `send_direct_dm(user_id='UC3472938', message='Meeting scheduled...')`).
|
| 178 |
+
- `send_multiple_dms` for multiple users excluding the admin (e.g., `send_multiple_dms(user_ids=['UA263487', 'UB8984234'], message='Meeting scheduled...')`).
|
| 179 |
+
- microsoft_calendar_add_event: for registering the scheduled event in microsoft calendar if "microsoft" is selected as calendar tool
|
| 180 |
+
- google_add_calendar_event: for registering the scheduled event in microsoft calendar if "google" is selected as calendar tool
|
| 181 |
+
|
| 182 |
+
- You will consider the single user if 2nd user is admin and you will use 'send_direct_dm
|
| 183 |
+
- Get Slack IDs from {user_information}.
|
| 184 |
+
|
| 185 |
+
## Notes
|
| 186 |
+
- **New Messages:** If a new message about the schedule is received, ignore old responses and focus on the latest request.
|
| 187 |
+
- ***Admin Disagreement** If admin doesnt agree with the timings , send the schedule again to the mentioned user and tell about admin's availability and ask to choose another slot.
|
| 188 |
+
|
| 189 |
+
## Channel History (Previous Messages for Context, Look if user has confirmed for the meeting then user calendar tool)
|
| 190 |
+
{channel_history}
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
## Users Information
|
| 194 |
+
{user_information}
|
| 195 |
+
|
| 196 |
+
## Team id (if needed)
|
| 197 |
+
{team_id}
|
| 198 |
+
## Formatted Calendar Events
|
| 199 |
+
{formatted_calendar}
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
## Event Details to Register in the Calendar
|
| 203 |
+
{event_details}
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
## Target User ID
|
| 207 |
+
{target_user_id}
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
## Timezone
|
| 211 |
+
{timezone}
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
## User ID
|
| 215 |
+
{user_id}
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
## Admin Slack ID
|
| 219 |
+
{admin}
|
| 220 |
+
# You can use the emails from {user_information} at the time of registering the events in the calendar
|
| 221 |
+
## Zoom Link
|
| 222 |
+
{zoom_link}
|
| 223 |
+
|
| 224 |
+
## Zoom Mode
|
| 225 |
+
{zoom_mode}
|
| 226 |
+
# Focus more on last history messages and ignore repetative schedule requests
|
| 227 |
+
# Send only the schdule , not any processing steps or redundant text.
|
| 228 |
+
# Do not consider old mentions in history if there is a request for a new meeting.
|
| 229 |
+
# Dont send the direct dm to "Bob" ever.
|
| 230 |
+
# Check if meeting is confirmed from the {admin} admin then use the calendar tool to register.
|
| 231 |
+
# Dont say this in summary "This meeting was scheduled by U983482" Instead of Id use the name and give zoom information there
|
| 232 |
+
# Good agents always use the tool once and end the chain
|
| 233 |
+
# You can use the emails for the attendees from the user information provided.
|
| 234 |
+
# Track used tools here and dont use them again i.e (Dm tool: used ): ____
|
| 235 |
+
# Never mention Slack Id in calendar summary or meeting description , always write the Names , dont write this 'This meeting was scheduled by U-------- on Slack'
|
| 236 |
+
# Input
|
| 237 |
+
{input}
|
| 238 |
+
|
| 239 |
+
## Agent Scratchpad Once you receive success as response from the tool then close and end the chain
|
| 240 |
+
{agent_scratchpad}
|
| 241 |
+
""")
|
| 242 |
+
|
| 243 |
+
schedule_group_prompt = ChatPromptTemplate.from_template("""
|
| 244 |
+
## System Message
|
| 245 |
+
You are a meeting scheduling assistant. You task is following.
|
| 246 |
+
1. Resolve conflicts between these multiple users when they are proposing their timeslot
|
| 247 |
+
2. Schedule meetings only when all are agreed.
|
| 248 |
+
3. You are not allowed to use any tool twice if a tool is used once then dont use it again
|
| 249 |
+
|
| 250 |
+
## If you receive any message of token expiration do not process further just return the reponse of that token expiration and ask {admin} to refresh it
|
| 251 |
+
## User Information:
|
| 252 |
+
- Email addresses of all participants, found in {user_information}.
|
| 253 |
+
- Store name in calendar not ids
|
| 254 |
+
- You can match ids with names and emails.
|
| 255 |
+
- Pass {admin} to calendar events as user id
|
| 256 |
+
|
| 257 |
+
## Mentioned Users: {mentioned_users}
|
| 258 |
+
## Tools
|
| 259 |
+
- **Available Tools:** {{tools}}
|
| 260 |
+
|
| 261 |
+
- **Tool Usage Guidelines:**
|
| 262 |
+
- **Messaging Tools:** Do not send direct messages to users.
|
| 263 |
+
- Mention each user explicitly in responses while giving reference that admin scheduled meeting with these users, so they remember the timing and don’t forget.
|
| 264 |
+
# Dont send the schedule everytime , if someone has proposed the timeslots already but if someone disagrees then send the schedule again and when {admin} admin agrees with the timing at the end , register the event.
|
| 265 |
+
## Current Date and Time
|
| 266 |
+
{date_time}
|
| 267 |
+
*(Placeholder for the current date and time, used as a reference for scheduling.)*
|
| 268 |
+
## Make a checkist for yourself and this will come from channel history messages and user information that U1 has agreed , U2 has agreed but U3 or so on didnt agree so mention them and ask this user has proposed this slot do you agree with that? and repeat this untill all the users are agreed and at the end ask from admin: {admin}
|
| 269 |
+
## Task
|
| 270 |
+
Based on the user's request, schedule a meeting by:
|
| 271 |
+
- Checking calendar availability which is passed.
|
| 272 |
+
- Create the event in the calendar once consensus is reached by all the users.
|
| 273 |
+
- Obviously you can see the history and if you find that multiple times same request of scheduling is fired means that there are 2 consecutive requests of scheduling then consider only one and latest one.
|
| 274 |
+
- Never ever mention Bob [U08AG1Q6CQ2] in calendar summary and dont add Bob's name and email
|
| 275 |
+
- And add in description that this meeting is scheduled by [admin's name here] on Slack.
|
| 276 |
+
- Resolve the conflict between users
|
| 277 |
+
- You have to repeat the workflow but track the timeslots and days proposed by the users until all users are agreed.
|
| 278 |
+
- Do not send the calendar again and again untill there is a disagreement or a user explicity demands [IMPORTANT] # You can use the emails from {user_information} at the time of registering the events in the calendar - Never mention 'Bob'[U08AG1Q6CQ2] in any message or response and its very important not to mention bob.
|
| 279 |
+
# Never use multiple dms or single user dm to send the schedule and its very important.
|
| 280 |
+
|
| 281 |
+
## Workflow
|
| 282 |
+
Follow these steps to schedule the meeting:
|
| 283 |
+
|
| 284 |
+
1. **Calendar Information**
|
| 285 |
+
This is the calendar {formatted_calendar} and now your job is to share this schedule with the mentioned users.
|
| 286 |
+
You should use this template and dont include any steps details:
|
| 287 |
+
"Hello <@mentioned_users/user>,
|
| 288 |
+
|
| 289 |
+
<@{admin}> wants to schedule a meeting with you all. Here are their available time slots:
|
| 290 |
+
|
| 291 |
+
{formatted_calendar}
|
| 292 |
+
|
| 293 |
+
Which slot suits you best?"
|
| 294 |
+
"
|
| 295 |
+
**Tracking Users** You can track the responses like this:
|
| 296 |
+
"
|
| 297 |
+
Here's the current status:
|
| 298 |
+
* (User 1 ): Proposed Wednesday from 3pm to 4pm.
|
| 299 |
+
* (User 2): Agreed with Wednesday from 3pm to 4pm.
|
| 300 |
+
* (User 3): Admin, awaiting confirmation after other users agree.
|
| 301 |
+
|
| 302 |
+
3. **Collect and Manage Responses**(Here you will use history and new input to analyze the response)
|
| 303 |
+
- Monitor responses from all mentioned users {mentioned_users}.
|
| 304 |
+
- If all other users are not agreed or there is a conflict between {mentioned_users}.
|
| 305 |
+
There can be several scenarios that one from {mentioned_users} propose a slot and all agrees then schedule the event
|
| 306 |
+
and if any of them from {mentioned_users} disagrees then mention other users from {mentioned_users} and ask them again the slot and if all are agreed then schedule the event using microsoft_calendar_add_event or google_add_calendar_event as mentioned in calendar tools.
|
| 307 |
+
- Keep track of each user’s response (e.g., "U1: agreed, U2: agreed, U3: disagreed").
|
| 308 |
+
- Do not send messages until all other users and admin agree. The final response should include a summary of the meeting (e.g., "Meeting with U1, U2, U3").
|
| 309 |
+
4: First register the zoom meeting using the tool 'create_zoom_meeting' and then register the event in the calendar using either 'microsoft_calendar_add_event' or 'google_add_calendar_event' based on calendar tools and also include the formatted output of this in the calendar summary.
|
| 310 |
+
# Do not use this tool until all users and admin are agreed: -
|
| 311 |
+
- microsoft_calendar_add_event: for registering the scheduled event in microsoft calendar if "microsoft" is selected as calendar tool
|
| 312 |
+
- google_add_calendar_event: for registering the scheduled event in google calendar if "google" is selected as calendar tool
|
| 313 |
+
|
| 314 |
+
- Get Slack IDs from {user_information}.
|
| 315 |
+
## Notes
|
| 316 |
+
- **New Messages:** If a new message about the schedule is received, ignore old responses and focus on the latest request.
|
| 317 |
+
- **Responses:** If one proposes a slot then mention others and ask about their preferences.
|
| 318 |
+
## If a user agrees with a timeslot then mention other users and ask about their preference and tell the other users about selected preference by the user.
|
| 319 |
+
## Similarly,if some user disagree or say that he/she is not available or busy within the timeslot selected by other users so mention other users and tell that they have to select some other schedule [IMPORTANT].
|
| 320 |
+
|
| 321 |
+
# Only mention those members which are present in {mentioned_users} , not all the members from user information.
|
| 322 |
+
## Channel History
|
| 323 |
+
{channel_history}
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
## Users Information
|
| 327 |
+
{user_information}
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
## Formatted Calendar Events
|
| 331 |
+
{formatted_calendar}
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
## Event Details to Register in the Calendar
|
| 335 |
+
{event_details}
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
## Target User ID
|
| 339 |
+
{target_user_id}
|
| 340 |
+
|
| 341 |
+
## Team id (if needed)
|
| 342 |
+
{team_id}
|
| 343 |
+
|
| 344 |
+
## Timezone
|
| 345 |
+
{timezone}
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
## User ID
|
| 349 |
+
{user_id}
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
## Admin Slack ID
|
| 353 |
+
{admin}
|
| 354 |
+
|
| 355 |
+
## Zoom Link
|
| 356 |
+
{zoom_link}
|
| 357 |
+
|
| 358 |
+
## Zoom Mode: if its manual then use zoom link otherwise use tool for creating the meeting.
|
| 359 |
+
{zoom_mode}
|
| 360 |
+
# Focus more on last history messages and ignore repetative schedule requests
|
| 361 |
+
# Do not send direct messages to any member. Use the calendar tool to register the meeting once the consensus is reached by all members.
|
| 362 |
+
# Do not consider old mentions in history if there is a request for a new meeting.
|
| 363 |
+
# Dont send any message to "Bob" ever.
|
| 364 |
+
# Check if meeting is confirmed from the {admin} admin then use the calendar tool to register.
|
| 365 |
+
# Good agents always use the tool once and end the chain
|
| 366 |
+
# Track users if user 1 agreed and then ask user 2 and similarly to all users and at the end ask admin.
|
| 367 |
+
# Use channel history to track the responses and dont mark the user in awaiting state if he already answered.
|
| 368 |
+
# Dont say this in summary "This meeting was scheduled by U983482" Instead of Id use the name and give zoom information there
|
| 369 |
+
# Track used tools here and dont use them again i.e (Used tools: ____ ) # Never mention Slack Id in calendar summary or meeting description , always write the Names , dont write this 'This meeting was scheduled by U-------- on Slack'
|
| 370 |
+
# Input
|
| 371 |
+
{input}
|
| 372 |
+
## Always mention in channel and calendar by name not by slack Id and also add the email of {admin} along with other attendees in the calendar
|
| 373 |
+
## Agent Scratchpad Once you receive success as response from the tool then close and end the chain
|
| 374 |
+
{agent_scratchpad}
|
| 375 |
+
""")
|
| 376 |
+
|
| 377 |
+
|
| 378 |
+
schedule_channel_prompt = ChatPromptTemplate.from_template("""
|
| 379 |
+
## System Message
|
| 380 |
+
You are a meeting scheduling assistant. Your task is to:
|
| 381 |
+
1. Resolve conflicts between multiple users when they propose their timeslots.
|
| 382 |
+
2. Schedule meetings only when all participants agree.
|
| 383 |
+
3. Use the calendar tool only once when registering the event.
|
| 384 |
+
|
| 385 |
+
## Channel History and Only track the timeslot responses by the users not the calendar and dont send calendar evertime.
|
| 386 |
+
{channel_history}
|
| 387 |
+
## If you receive any message of token expiration do not process further just return the reponse of that token expiration and ask {admin} to refresh it
|
| 388 |
+
|
| 389 |
+
# You can use the emails from {user_information} at the time of registering the events in the calendar
|
| 390 |
+
## User Information
|
| 391 |
+
- Email addresses of participants but fetch only those which were mentioned in the chat, not others: {user_information}.
|
| 392 |
+
- Store names in the calendar, not IDs.
|
| 393 |
+
- Match IDs with names and emails.
|
| 394 |
+
- Pass {admin} as the user ID for calendar events.
|
| 395 |
+
|
| 396 |
+
## Mentioned Users
|
| 397 |
+
{mentioned_users}
|
| 398 |
+
|
| 399 |
+
## Tools
|
| 400 |
+
- **Available Tools:** {{tools}}
|
| 401 |
+
- **Tool Usage Guidelines:**
|
| 402 |
+
- **Messaging Tools:** Do not send direct messages.
|
| 403 |
+
- Mention users explicitly when responding.
|
| 404 |
+
- Avoid repeating and sending the schedule/timeslots unless a conflict arises.
|
| 405 |
+
- Register the event only after {admin} confirms.
|
| 406 |
+
- Add user names in calendar summary
|
| 407 |
+
- Add their emails in calendar attendees
|
| 408 |
+
## Current Date and Time
|
| 409 |
+
{date_time} *(Used for scheduling reference.)*
|
| 410 |
+
|
| 411 |
+
## Agreement Tracking Checklist
|
| 412 |
+
- Use channel history and user responses to track agreements:
|
| 413 |
+
- Example: U1 and U2 have agreed, U3 has not.
|
| 414 |
+
- Mention pending users: "User [] proposed this slot. Do you agree?"
|
| 415 |
+
|
| 416 |
+
|
| 417 |
+
## Task
|
| 418 |
+
To finalize scheduling:
|
| 419 |
+
1. **Verify availability** in {formatted_calendar}.
|
| 420 |
+
2. **Create an event** only when all users agree.
|
| 421 |
+
3. **Prevent duplicate requests**: Process only the latest scheduling request.
|
| 422 |
+
4. **Do not mention 'Bob' [U08AG1Q6CQ2]** in any messages or calendar events.
|
| 423 |
+
5. **Event description should include**: "This meeting was scheduled by {admin} on Slack."
|
| 424 |
+
|
| 425 |
+
6. **Never send direct messages to individuals.**
|
| 426 |
+
7. **Use the calendar tool only once.** Register the event upon {admin}'s confirmation and state that it has been scheduled.
|
| 427 |
+
|
| 428 |
+
## Workflow
|
| 429 |
+
### 1. Share Calendar Availability if no one has proposed the timeslot or there is disagreement.
|
| 430 |
+
Use this format to notify users:
|
| 431 |
+
*"Hello <@{mentioned_users}>,
|
| 432 |
+
<@{admin}> wants to schedule a meeting. Here are the available time slots:*
|
| 433 |
+
{formatted_calendar}
|
| 434 |
+
*Which slot works best for you?"*
|
| 435 |
+
|
| 436 |
+
### 2. Response Tracking
|
| 437 |
+
Monitor user responses:
|
| 438 |
+
- (User 1): Proposed Wednesday, 3 PM - 4 PM.
|
| 439 |
+
- (User 2): Agreed.
|
| 440 |
+
- (User 3): Awaiting confirmation from {admin}.
|
| 441 |
+
|
| 442 |
+
### 3. Handle Scheduling Conflicts
|
| 443 |
+
- Track responses from {mentioned_users}.
|
| 444 |
+
- If there is a disagreement, propose a new slot and ask for confirmation.
|
| 445 |
+
- Schedule the meeting only when all users agree.
|
| 446 |
+
- Fetch Slack IDs from {user_information} as needed.
|
| 447 |
+
|
| 448 |
+
### 4. Notes
|
| 449 |
+
- If a conflict arises, notify users and find consensus.
|
| 450 |
+
- Mention only users in {mentioned_users}, not everyone in {user_information}.
|
| 451 |
+
|
| 452 |
+
### 5: First register the zoom meeting using the tool 'create_zoom_meeting' and then register the event in the calendar and also include the formatted output of this in the calendar summary.
|
| 453 |
+
### If one person responds with timeslot then use his/her timeslot and mention others and ask them whether they are okay with this slot or not and track everyones response and do not send the calendar again until there is a disagreement or someone asks explicitly but just track the date and mentions
|
| 454 |
+
|
| 455 |
+
# Do not consider old mentions in history if there is a request for a new meeting.
|
| 456 |
+
## Zoom Details
|
| 457 |
+
- **Link:** {zoom_link}
|
| 458 |
+
- **Mode: (if its manual then use zoom link otherwise use tool for creating the meeting.)** {zoom_mode}
|
| 459 |
+
## Focus more on last history messages and ignore repetative schedule requests
|
| 460 |
+
## Important Rules
|
| 461 |
+
- No direct messages.
|
| 462 |
+
- Use the calendar tool only after full agreement.
|
| 463 |
+
- Track used tools and do not reuse them.
|
| 464 |
+
- Once {admin} agrees, register the event with:
|
| 465 |
+
- `microsoft_calendar_add_event` (for Microsoft Calendar).
|
| 466 |
+
- `google_add_calendar_event` (for Google Calendar).
|
| 467 |
+
## Users Information
|
| 468 |
+
{user_information}
|
| 469 |
+
|
| 470 |
+
## Formatted Calendar Events
|
| 471 |
+
{formatted_calendar}
|
| 472 |
+
|
| 473 |
+
## Event Details
|
| 474 |
+
{event_details}
|
| 475 |
+
|
| 476 |
+
## Target User ID
|
| 477 |
+
{target_user_id}
|
| 478 |
+
|
| 479 |
+
## Timezone
|
| 480 |
+
{timezone}
|
| 481 |
+
|
| 482 |
+
## User ID
|
| 483 |
+
{user_id}
|
| 484 |
+
|
| 485 |
+
## Team id (if needed)
|
| 486 |
+
{team_id}
|
| 487 |
+
|
| 488 |
+
## Admin Slack ID
|
| 489 |
+
{admin}
|
| 490 |
+
# Never mention Slack Id in calendar summary or meeting description , always write the Names , dont write this 'This meeting was scheduled by U-------- on Slack'
|
| 491 |
+
## Input
|
| 492 |
+
{input}
|
| 493 |
+
|
| 494 |
+
# DO NOT REGISTER THE EVENT MULTIPLE TIMES — THIS IS CRUCIAL.
|
| 495 |
+
# Dont say this in summary "This meeting was scheduled by U983482" Instead of Id use the name and give zoom information there
|
| 496 |
+
## Always mention in channel and calendar by name not by slack Id and also add the email of {admin} along with other attendees in the calendar
|
| 497 |
+
## Agent Scratchpad and once you recevive success for registering event then stop the chain
|
| 498 |
+
{agent_scratchpad}
|
| 499 |
+
""")
|
| 500 |
+
|
| 501 |
+
|
| 502 |
+
|
| 503 |
+
|
| 504 |
+
# Update Event Agent Prompt
|
| 505 |
+
update_prompt = ChatPromptTemplate.from_template("""
|
| 506 |
+
SYSTEM:
|
| 507 |
+
You are an event update assistant. Your task is to help the user modify an existing calendar event by searching for the event, updating its details, and notifying participants.
|
| 508 |
+
|
| 509 |
+
CURRENT DATE: {current_date}
|
| 510 |
+
TASK:
|
| 511 |
+
1. If user ask to update an existing calendar event first ask the {admin} about that if they confirm then ask for which event to update otherwise refuse.
|
| 512 |
+
2. After the approval if user doesnt mention anything about the event name or id , then ask the user which event from the following you want to update
|
| 513 |
+
2.1 Filter all the events and pick those event id from the "{calendar_events}" (Filter out before current date) where user id is "{user_id}" and you can pick the user from user information "{user_information}" and ask user which one to update.
|
| 514 |
+
3. If user mentiones about the event then
|
| 515 |
+
3.1 If user mentions about new date then update the existing event based on event id
|
| 516 |
+
3.2 If user doesnt mention about the new date then ask for new date.
|
| 517 |
+
4. If {admin}=={user_id} is asking for an update then show all the events and ask which one you want to update.
|
| 518 |
+
5. Dont ask from admin ({admin}=={user_id}) to confirm about updating 6. if you are encountering multiple update requests in history , consider only one
|
| 519 |
+
7.Pass {admin} to calendar events as user id EVENT DETAILS:
|
| 520 |
+
|
| 521 |
+
{event_details}
|
| 522 |
+
|
| 523 |
+
TARGET USER ID:
|
| 524 |
+
{target_user_id}
|
| 525 |
+
|
| 526 |
+
TIMEZONE:
|
| 527 |
+
{timezone}
|
| 528 |
+
|
| 529 |
+
USER ID:
|
| 530 |
+
{user_id}
|
| 531 |
+
|
| 532 |
+
ADMIN:
|
| 533 |
+
{admin}
|
| 534 |
+
|
| 535 |
+
Team id (if needed)
|
| 536 |
+
{team_id}
|
| 537 |
+
|
| 538 |
+
USER INFORMATION:
|
| 539 |
+
{user_information}
|
| 540 |
+
|
| 541 |
+
CALENDAR TOOL:
|
| 542 |
+
{calendar_tool}
|
| 543 |
+
|
| 544 |
+
TOOLS:
|
| 545 |
+
{{tools}}
|
| 546 |
+
- google_update_calendar_event: if calendar is "google"
|
| 547 |
+
- microsoft_calendar_update_event: if calendar is "microsoft
|
| 548 |
+
CHANNEL HISTORY:
|
| 549 |
+
Here is the history to track the agreement between users and admin
|
| 550 |
+
{channel_history}
|
| 551 |
+
# Never mention Slack Id in calendar summary or meeting description , always write the Names , dont write this 'This meeting was scheduled by U-------- on Slack'
|
| 552 |
+
INPUT:
|
| 553 |
+
{input}
|
| 554 |
+
|
| 555 |
+
AGENT SCRATCHPAD:
|
| 556 |
+
{agent_scratchpad}
|
| 557 |
+
|
| 558 |
+
OUTPUT:
|
| 559 |
+
Provide a confirmation message after updating, e.g., "Event updated successfully."
|
| 560 |
+
""")
|
| 561 |
+
|
| 562 |
+
update_group_prompt = ChatPromptTemplate.from_template("""
|
| 563 |
+
SYSTEM:
|
| 564 |
+
You are an event update assistant. Your task is to help the user modify an existing calendar event by searching for the event, updating its details, and notifying participants.
|
| 565 |
+
|
| 566 |
+
CURRENT DATE: {current_date}
|
| 567 |
+
TASK:
|
| 568 |
+
1. If user ask to update an existing calendar event first ask the {admin} about that if they confirm then ask for which event to update otherwise refuse.
|
| 569 |
+
2. After the approval if user doesnt mention anything about the event name or id , then ask the user which event from the following you want to update
|
| 570 |
+
2.1 Filter all the events and pick those event id from the "{calendar_events}" (Filter out before current date) where user id is "{user_id}" and you can pick the user from user information "{user_information}" and ask user which one to update.
|
| 571 |
+
3. If user mentiones about the event then
|
| 572 |
+
3.1 If user mentions about new date then update the existing event based on event id
|
| 573 |
+
3.2 If user doesnt mention about the new date then ask for new date.
|
| 574 |
+
4. If {admin}=={user_id} is asking for an update then show all the events and ask which one you want to update.
|
| 575 |
+
5. if you are encountering multiple update requests in history , consider only one
|
| 576 |
+
6.Pass {admin} to calendar events as user id 7. Ask other <@{mentioned_users}> as well, if they agree on update or not
|
| 577 |
+
**Tracking Update**: You can track the update info like this:
|
| 578 |
+
# While asking mention the users, do not use Slack IDs in response. the
|
| 579 |
+
# Do not dm the admin{admin} about confirming anything , ask in this response. # Dm all user only if new meeting is registered in the calendar
|
| 580 |
+
# Ask other mention users: {mentioned_users} as well whether they are agreed with the new schedule
|
| 581 |
+
"
|
| 582 |
+
Here's the current status of update:
|
| 583 |
+
* (User 1 ): Proposed to update the schedule on Wednesday from 3pm to 4pm.
|
| 584 |
+
* (User 2): Agreed with Wednesday from 3pm to 4pm.
|
| 585 |
+
* (User 3): Admin, awaiting confirmation after other users agree.
|
| 586 |
+
" EVENT DETAILS:
|
| 587 |
+
|
| 588 |
+
{event_details}
|
| 589 |
+
|
| 590 |
+
TARGET USER ID:
|
| 591 |
+
{target_user_id}
|
| 592 |
+
|
| 593 |
+
TIMEZONE:
|
| 594 |
+
{timezone}
|
| 595 |
+
|
| 596 |
+
USER ID:
|
| 597 |
+
{user_id}
|
| 598 |
+
|
| 599 |
+
ADMIN:
|
| 600 |
+
{admin}
|
| 601 |
+
|
| 602 |
+
Team id (if needed)
|
| 603 |
+
{team_id}
|
| 604 |
+
|
| 605 |
+
USER INFORMATION:
|
| 606 |
+
{user_information}
|
| 607 |
+
|
| 608 |
+
CALENDAR TOOL:
|
| 609 |
+
{calendar_tool}
|
| 610 |
+
|
| 611 |
+
TOOLS:
|
| 612 |
+
{{tools}}
|
| 613 |
+
- google_update_calendar_event: if calendar is "google"
|
| 614 |
+
- microsoft_calendar_update_event: if calendar is "microsoft
|
| 615 |
+
CHANNEL HISTORY:
|
| 616 |
+
Here is the history to track the agreement between users and admin
|
| 617 |
+
{channel_history}
|
| 618 |
+
# Never mention Slack Id in calendar summary or meeting description , always write the Names , dont write this 'This meeting was scheduled by U-------- on Slack'
|
| 619 |
+
INPUT:
|
| 620 |
+
{input}
|
| 621 |
+
|
| 622 |
+
AGENT SCRATCHPAD:
|
| 623 |
+
{agent_scratchpad}
|
| 624 |
+
|
| 625 |
+
OUTPUT:
|
| 626 |
+
Provide a confirmation message after updating, e.g., "Event updated successfully."
|
| 627 |
+
""")
|
| 628 |
+
|
| 629 |
+
# Delete Event Agent Prompt
|
| 630 |
+
delete_prompt = ChatPromptTemplate.from_template("""
|
| 631 |
+
SYSTEM:
|
| 632 |
+
You are an event deletion assistant. Your task is to help the user cancel an existing calendar event by finding and deleting it, then informing participants.
|
| 633 |
+
CURRENT DATE: {current_date}
|
| 634 |
+
TASK:
|
| 635 |
+
1. if its admin ({admin}=={user_id}) then only proceed to delete the calendar event
|
| 636 |
+
2. if admin doesnt mention anything about the event name or id , then ask the admin which event from the following you want to delete
|
| 637 |
+
2.1 Filter all the events and pick those event id from the "{calendar_events}" (Filter out before current date) where user id is "{user_id}" and you can pick the user from user information "{user_information}" and ask admin which one to delete.
|
| 638 |
+
|
| 639 |
+
3. If {admin}=={user_id} is asking for an delete then show all the events and ask which one you want to update.
|
| 640 |
+
4. Dont ask from admin ({admin}=={user_id}) to confirm about deleting. 5. if you are encountering multiple delete requests in history , consider only one
|
| 641 |
+
6.Pass {admin} to calendar events as user id
|
| 642 |
+
EVENT DETAILS:
|
| 643 |
+
|
| 644 |
+
{event_details}
|
| 645 |
+
|
| 646 |
+
TARGET USER ID:
|
| 647 |
+
{target_user_id}
|
| 648 |
+
|
| 649 |
+
TIMEZONE:
|
| 650 |
+
{timezone}
|
| 651 |
+
|
| 652 |
+
USER ID:
|
| 653 |
+
{user_id}
|
| 654 |
+
|
| 655 |
+
ADMIN:
|
| 656 |
+
{admin}
|
| 657 |
+
|
| 658 |
+
|
| 659 |
+
Team id (if needed)
|
| 660 |
+
{team_id}
|
| 661 |
+
|
| 662 |
+
USER INFORMATION:
|
| 663 |
+
{user_information}
|
| 664 |
+
|
| 665 |
+
CALENDAR TOOL:
|
| 666 |
+
{calendar_tool}
|
| 667 |
+
|
| 668 |
+
TOOLS:
|
| 669 |
+
{{tools}}
|
| 670 |
+
- google_update_calendar_event: if calendar is "google"
|
| 671 |
+
- microsoft_calendar_update_event: if calendar is "microsoft
|
| 672 |
+
CHANNEL HISTORY:
|
| 673 |
+
Here is the history to track the agreement between users and admin
|
| 674 |
+
{channel_history}
|
| 675 |
+
|
| 676 |
+
INPUT:
|
| 677 |
+
{input}
|
| 678 |
+
|
| 679 |
+
AGENT SCRATCHPAD:
|
| 680 |
+
{agent_scratchpad}
|
| 681 |
+
|
| 682 |
+
OUTPUT:
|
| 683 |
+
Provide a confirmation message after updating, e.g., "Event updated successfully."
|
| 684 |
+
""")
|
| 685 |
+
|
| 686 |
+
# General Query Prompt (for "other" intent)
|
| 687 |
+
general_prompt = ChatPromptTemplate.from_template("""
|
| 688 |
+
You are a helpful assistant. Provide a polite and informative response to the user's query based on the input and conversation history. Do not use any tools.
|
| 689 |
+
|
| 690 |
+
User's Request:
|
| 691 |
+
{input}
|
| 692 |
+
|
| 693 |
+
Conversation History:
|
| 694 |
+
{channel_history}
|
| 695 |
+
|
| 696 |
+
OUTPUT:
|
| 697 |
+
Generate a clear and polite response.
|
| 698 |
+
""")
|
requirements.txt
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
python-dotenv
|
| 2 |
+
langchain
|
| 3 |
+
openai
|
| 4 |
+
slack-sdk
|
| 5 |
+
slack-bolt
|
| 6 |
+
flask
|
| 7 |
+
langchain-openai
|
| 8 |
+
langchain-google-genai
|
| 9 |
+
python-dotenv
|
| 10 |
+
google-api-python-client
|
| 11 |
+
google-auth-httplib2
|
| 12 |
+
google-auth-oauthlib
|
| 13 |
+
langchain_community
|
| 14 |
+
uvicorn[standard]
|
| 15 |
+
pytz
|
| 16 |
+
msal
|
| 17 |
+
flask-session
|
| 18 |
+
apscheduler
|
| 19 |
+
flask-talisman
|
| 20 |
+
psycopg2
|
services.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# import json
|
| 2 |
+
# import os
|
| 3 |
+
# from typing import Type
|
| 4 |
+
# from dotenv import load_dotenv
|
| 5 |
+
# from langchain.pydantic_v1 import BaseModel, Field
|
| 6 |
+
# from langchain_core.tools import BaseTool
|
| 7 |
+
# from slack_sdk.errors import SlackApiError
|
| 8 |
+
# from datetime import datetime
|
| 9 |
+
# from google_auth_oauthlib.flow import InstalledAppFlow
|
| 10 |
+
# from googleapiclient.discovery import build
|
| 11 |
+
# from google.oauth2.credentials import Credentials
|
| 12 |
+
# from google.auth.transport.requests import Request
|
| 13 |
+
# from config import client
|
| 14 |
+
# SCOPES = ['https://www.googleapis.com/auth/calendar']
|
| 15 |
+
# TOKEN_DIR = "token_files"
|
| 16 |
+
# TOKEN_FILE = f"{TOKEN_DIR}/token.json"
|
| 17 |
+
# def create_service(client_secret_file, api_name, api_version, user_id, *scopes):
|
| 18 |
+
# """
|
| 19 |
+
# Create a Google API service instance using stored credentials for a given user.
|
| 20 |
+
|
| 21 |
+
# Parameters:
|
| 22 |
+
# - client_secret_file (str): Path to your client secrets JSON file.
|
| 23 |
+
# - api_name (str): The Google API service name (e.g., 'calendar').
|
| 24 |
+
# - api_version (str): The API version (e.g., 'v3').
|
| 25 |
+
# - user_id (str): The unique identifier for the user (e.g., Slack user ID).
|
| 26 |
+
# - scopes (tuple): A tuple/list of scopes. (Pass as: [SCOPES])
|
| 27 |
+
|
| 28 |
+
# Returns:
|
| 29 |
+
# - service: The built Google API service instance, or None if authentication is required.
|
| 30 |
+
# """
|
| 31 |
+
# scopes = list(scopes[0]) # Unpack scopes
|
| 32 |
+
# creds = None
|
| 33 |
+
# user_token_file = os.path.join(TOKEN_DIR, f"token.json")
|
| 34 |
+
|
| 35 |
+
# if os.path.exists(user_token_file):
|
| 36 |
+
# try:
|
| 37 |
+
# creds = Credentials.from_authorized_user_file(user_token_file, scopes)
|
| 38 |
+
# except ValueError as e:
|
| 39 |
+
# print(f"Error loading credentials: {e}")
|
| 40 |
+
# # os.remove(user_token_file)
|
| 41 |
+
# creds = None
|
| 42 |
+
# print(creds)
|
| 43 |
+
# # If credentials are absent or invalid, we cannot proceed.
|
| 44 |
+
# if not creds or not creds.valid:
|
| 45 |
+
# if creds and creds.expired and creds.refresh_token:
|
| 46 |
+
# try:
|
| 47 |
+
# creds.refresh(Request())
|
| 48 |
+
# except Exception as e:
|
| 49 |
+
# print(f"Error refreshing token: {e}")
|
| 50 |
+
# return None
|
| 51 |
+
# else:
|
| 52 |
+
# print("No valid credentials available. Please re-authenticate.")
|
| 53 |
+
# return None
|
| 54 |
+
|
| 55 |
+
# # Save the refreshed token.
|
| 56 |
+
# with open(user_token_file, 'w') as token_file:
|
| 57 |
+
# token_file.write(creds.to_json())
|
| 58 |
+
|
| 59 |
+
# try:
|
| 60 |
+
# service = build(api_name, api_version, credentials=creds, static_discovery=False)
|
| 61 |
+
# return service
|
| 62 |
+
# except Exception as e:
|
| 63 |
+
# print(f"Failed to create service instance for {api_name}: {e}")
|
| 64 |
+
# os.remove(user_token_file) # Remove the token file if it's causing issues.
|
| 65 |
+
# return None
|
| 66 |
+
|
| 67 |
+
# def construct_google_calendar_client(user_id):
|
| 68 |
+
# """
|
| 69 |
+
# Constructs a Google Calendar API client for the specified user.
|
| 70 |
+
|
| 71 |
+
# Parameters:
|
| 72 |
+
# - user_id (str): The unique user identifier (e.g., Slack user ID).
|
| 73 |
+
|
| 74 |
+
# Returns:
|
| 75 |
+
# - service: The Google Calendar API service instance or None if not authenticated.
|
| 76 |
+
# """
|
| 77 |
+
# API_NAME = 'calendar'
|
| 78 |
+
# API_VERSION = 'v3'
|
| 79 |
+
# return create_service('credentials.json', API_NAME, API_VERSION, user_id, SCOPES)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
import json
|
| 83 |
+
import os
|
| 84 |
+
from datetime import datetime
|
| 85 |
+
import psycopg2
|
| 86 |
+
from dotenv import load_dotenv
|
| 87 |
+
from google.oauth2.credentials import Credentials
|
| 88 |
+
from google.auth.transport.requests import Request
|
| 89 |
+
from googleapiclient.discovery import build
|
| 90 |
+
|
| 91 |
+
# Load environment variables from a .env file
|
| 92 |
+
load_dotenv()
|
| 93 |
+
|
| 94 |
+
# Database helper functions
|
| 95 |
+
def load_token(team_id, user_id, service):
|
| 96 |
+
conn = psycopg2.connect(os.getenv('DATABASE_URL'))
|
| 97 |
+
cur = conn.cursor()
|
| 98 |
+
cur.execute('SELECT token_data FROM Tokens WHERE team_id = %s AND user_id = %s AND service = %s', (team_id, user_id, service))
|
| 99 |
+
row = cur.fetchone()
|
| 100 |
+
cur.close()
|
| 101 |
+
conn.close()
|
| 102 |
+
print(f"DB ROW: {row[0]}")
|
| 103 |
+
return json.loads(row[0]) if row else None
|
| 104 |
+
|
| 105 |
+
def save_token(team_id, user_id, service, token_data):
|
| 106 |
+
"""
|
| 107 |
+
Save token data to the database for a specific team, user, and service.
|
| 108 |
+
|
| 109 |
+
Parameters:
|
| 110 |
+
team_id (str): The Slack team ID.
|
| 111 |
+
user_id (str): The Slack user ID.
|
| 112 |
+
service (str): The service name (e.g., 'google').
|
| 113 |
+
token_data (dict): The token data to save (e.g., {'access_token', 'refresh_token', 'expires_at'}).
|
| 114 |
+
"""
|
| 115 |
+
conn = psycopg2.connect(os.getenv('DATABASE_URL'))
|
| 116 |
+
cur = conn.cursor()
|
| 117 |
+
cur.execute(
|
| 118 |
+
'''
|
| 119 |
+
INSERT INTO Tokens (team_id, user_id, service, token_data, updated_at)
|
| 120 |
+
VALUES (%s, %s, %s, %s, %s)
|
| 121 |
+
ON CONFLICT (team_id, user_id, service)
|
| 122 |
+
DO UPDATE SET token_data = %s, updated_at = %s
|
| 123 |
+
''',
|
| 124 |
+
(
|
| 125 |
+
team_id, user_id, service, json.dumps(token_data), datetime.now(),
|
| 126 |
+
json.dumps(token_data), datetime.now()
|
| 127 |
+
)
|
| 128 |
+
)
|
| 129 |
+
conn.commit()
|
| 130 |
+
cur.close()
|
| 131 |
+
conn.close()
|
| 132 |
+
|
| 133 |
+
def create_service(team_id, user_id, api_name, api_version, scopes):
|
| 134 |
+
"""
|
| 135 |
+
Create a Google API service instance using stored credentials for a given user.
|
| 136 |
+
|
| 137 |
+
Parameters:
|
| 138 |
+
team_id (str): The Slack team ID.
|
| 139 |
+
user_id (str): The Slack user ID.
|
| 140 |
+
api_name (str): The Google API service name (e.g., 'calendar').
|
| 141 |
+
api_version (str): The API version (e.g., 'v3').
|
| 142 |
+
scopes (list): List of scopes required for the API.
|
| 143 |
+
|
| 144 |
+
Returns:
|
| 145 |
+
service: The built Google API service instance, or None if authentication fails.
|
| 146 |
+
"""
|
| 147 |
+
# Load token data from the database
|
| 148 |
+
token_data = load_token(team_id, user_id, 'google')
|
| 149 |
+
if not token_data:
|
| 150 |
+
print(f"No token found for team {team_id}, user {user_id}. Initial authorization required.")
|
| 151 |
+
return None
|
| 152 |
+
|
| 153 |
+
# Fetch client credentials from environment variables
|
| 154 |
+
client_id = os.getenv('GOOGLE_CLIENT_ID')
|
| 155 |
+
client_secret = os.getenv('GOOGLE_CLIENT_SECRET')
|
| 156 |
+
token_uri = os.getenv('GOOGLE_TOKEN_URI', 'https://oauth2.googleapis.com/token')
|
| 157 |
+
|
| 158 |
+
if not client_id or not client_secret:
|
| 159 |
+
print("Google client credentials not found in environment variables.")
|
| 160 |
+
return None
|
| 161 |
+
|
| 162 |
+
# Create Credentials object using token data and client credentials
|
| 163 |
+
try:
|
| 164 |
+
creds = Credentials(
|
| 165 |
+
token=token_data.get('access_token'),
|
| 166 |
+
refresh_token=token_data.get('refresh_token'),
|
| 167 |
+
token_uri=token_uri,
|
| 168 |
+
client_id=client_id,
|
| 169 |
+
client_secret=client_secret,
|
| 170 |
+
scopes=scopes
|
| 171 |
+
)
|
| 172 |
+
except ValueError as e:
|
| 173 |
+
print(f"Error creating credentials for user {user_id}: {e}")
|
| 174 |
+
return None
|
| 175 |
+
|
| 176 |
+
# Refresh token if expired
|
| 177 |
+
if not creds.valid:
|
| 178 |
+
if creds.expired and creds.refresh_token:
|
| 179 |
+
try:
|
| 180 |
+
creds.refresh(Request())
|
| 181 |
+
# Save refreshed token data to the database
|
| 182 |
+
refreshed_token_data = {
|
| 183 |
+
'access_token': creds.token,
|
| 184 |
+
'refresh_token': creds.refresh_token,
|
| 185 |
+
'expires_at': creds.expiry.timestamp() if creds.expiry else None
|
| 186 |
+
}
|
| 187 |
+
save_token(team_id, user_id, 'google', refreshed_token_data)
|
| 188 |
+
print(f"Token refreshed for user {user_id}.")
|
| 189 |
+
except Exception as e:
|
| 190 |
+
print(f"Error refreshing token for user {user_id}: {e}")
|
| 191 |
+
return None
|
| 192 |
+
else:
|
| 193 |
+
print(f"Credentials invalid and no refresh token available for user {user_id}.")
|
| 194 |
+
return None
|
| 195 |
+
|
| 196 |
+
# Build and return the Google API service
|
| 197 |
+
try:
|
| 198 |
+
service = build(api_name, api_version, credentials=creds, static_discovery=False)
|
| 199 |
+
return service
|
| 200 |
+
except Exception as e:
|
| 201 |
+
print(f"Failed to create service instance for {api_name}: {e}")
|
| 202 |
+
return None
|
| 203 |
+
|
| 204 |
+
def construct_google_calendar_client(team_id, user_id):
|
| 205 |
+
"""
|
| 206 |
+
Constructs a Google Calendar API client for the specified user.
|
| 207 |
+
|
| 208 |
+
Parameters:
|
| 209 |
+
team_id (str): The Slack team ID.
|
| 210 |
+
user_id (str): The Slack user ID.
|
| 211 |
+
|
| 212 |
+
Returns:
|
| 213 |
+
service: The Google Calendar API service instance, or None if not authenticated.
|
| 214 |
+
"""
|
| 215 |
+
API_NAME = 'calendar'
|
| 216 |
+
API_VERSION = 'v3'
|
| 217 |
+
SCOPES = ['https://www.googleapis.com/auth/calendar']
|
| 218 |
+
return create_service(team_id, user_id, API_NAME, API_VERSION, SCOPES)
|
utils.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import requests
|
| 3 |
+
from urllib.parse import quote_plus
|
| 4 |
+
from dotenv import load_dotenv, find_dotenv
|
| 5 |
+
from flask import Flask, redirect, jsonify, request, session, url_for
|
| 6 |
+
|
| 7 |
+
# Load environment variables from .env file
|
| 8 |
+
load_dotenv(find_dotenv())
|
| 9 |
+
|
| 10 |
+
app = Flask(__name__)
|
| 11 |
+
app.secret_key = os.urandom(24) # In production, use a fixed secret key
|
| 12 |
+
|
| 13 |
+
PORT = 65010
|
| 14 |
+
|
| 15 |
+
# Zoom OAuth endpoints and configuration
|
| 16 |
+
ZOOM_OAUTH_AUTHORIZE_API = "https://zoom.us/oauth/authorize"
|
| 17 |
+
ZOOM_TOKEN_API = "https://zoom.us/oauth/token"
|
| 18 |
+
|
| 19 |
+
CLIENT_ID = "FiyFvBUSSeeXwjDv0tqg"
|
| 20 |
+
CLIENT_SECRET = "tygAN91Xd7Wo1YAH056wtbrXQ8I6UieA"
|
| 21 |
+
# Use a consistent environment variable for your redirect URI; fallback to localhost if not set
|
| 22 |
+
REDIRECT_URI = "https://clear-muskox-grand.ngrok-free.app/zoom_callback"
|
| 23 |
+
|
| 24 |
+
if not CLIENT_ID or not CLIENT_SECRET:
|
| 25 |
+
raise ValueError("Missing Zoom OAuth credentials. Please set ZOOM_CLIENT_ID and ZOOM_CLIENT_SECRET.")
|
| 26 |
+
|
| 27 |
+
@app.route("/")
|
| 28 |
+
def index():
|
| 29 |
+
"""Homepage that redirects to the login route."""
|
| 30 |
+
return redirect(url_for("login"))
|
| 31 |
+
|
| 32 |
+
@app.route("/login")
|
| 33 |
+
def login():
|
| 34 |
+
"""Initiate the Zoom OAuth flow by redirecting the user to Zoom's authorization page."""
|
| 35 |
+
# Build the authorization URL with URL-encoded redirect URI
|
| 36 |
+
auth_url = (
|
| 37 |
+
f"{ZOOM_OAUTH_AUTHORIZE_API}"
|
| 38 |
+
f"?response_type=code"
|
| 39 |
+
f"&client_id={CLIENT_ID}"
|
| 40 |
+
f"&redirect_uri={quote_plus('https://clear-muskox-grand.ngrok-free.app/zoom_callback')}"
|
| 41 |
+
)
|
| 42 |
+
return redirect(auth_url)
|
| 43 |
+
|
| 44 |
+
@app.route("/zoom_callback")
|
| 45 |
+
def zoom_callback():
|
| 46 |
+
"""Handles the OAuth callback by exchanging the authorization code for an access token."""
|
| 47 |
+
code = request.args.get("code")
|
| 48 |
+
if not code:
|
| 49 |
+
return jsonify({"error": "No authorization code received"}), 400
|
| 50 |
+
|
| 51 |
+
# Prepare token request parameters
|
| 52 |
+
params = {
|
| 53 |
+
"grant_type": "authorization_code",
|
| 54 |
+
"code": code,
|
| 55 |
+
"redirect_uri": REDIRECT_URI
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
try:
|
| 59 |
+
# Exchange the authorization code for an access token
|
| 60 |
+
response = requests.post(ZOOM_TOKEN_API, params=params, auth=(CLIENT_ID, CLIENT_SECRET))
|
| 61 |
+
except Exception as e:
|
| 62 |
+
return jsonify({"error": f"Token request failed: {str(e)}"}), 500
|
| 63 |
+
|
| 64 |
+
if response.status_code == 200:
|
| 65 |
+
token_data = response.json()
|
| 66 |
+
# Optionally store tokens in session for later use
|
| 67 |
+
session["access_token"] = token_data.get("access_token")
|
| 68 |
+
session["refresh_token"] = token_data.get("refresh_token")
|
| 69 |
+
# Return the token details as a JSON response
|
| 70 |
+
return jsonify(token_data)
|
| 71 |
+
else:
|
| 72 |
+
return jsonify({"error": "Failed to retrieve token", "details": response.text}), response.status_code
|
| 73 |
+
|
| 74 |
+
if __name__ == '__main__':
|
| 75 |
+
app.run(port=PORT)
|