@aj-archipelago/cortex 1.3.66 → 1.3.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/helper-apps/cortex-autogen2/Dockerfile +88 -21
- package/helper-apps/cortex-autogen2/docker-compose.yml +15 -8
- package/helper-apps/cortex-autogen2/host.json +5 -0
- package/helper-apps/cortex-autogen2/pyproject.toml +82 -25
- package/helper-apps/cortex-autogen2/requirements.txt +84 -14
- package/helper-apps/cortex-autogen2/services/redis_publisher.py +129 -3
- package/helper-apps/cortex-autogen2/task_processor.py +432 -116
- package/helper-apps/cortex-autogen2/tools/__init__.py +2 -0
- package/helper-apps/cortex-autogen2/tools/azure_blob_tools.py +32 -0
- package/helper-apps/cortex-autogen2/tools/azure_foundry_agents.py +50 -14
- package/helper-apps/cortex-autogen2/tools/file_tools.py +169 -44
- package/helper-apps/cortex-autogen2/tools/google_cse.py +117 -0
- package/helper-apps/cortex-autogen2/tools/search_tools.py +655 -98
- package/lib/pathwayManager.js +42 -8
- package/lib/util.js +57 -1
- package/package.json +1 -1
- package/pathways/system/workspaces/run_workspace_prompt.js +0 -3
- package/server/executeWorkspace.js +381 -0
- package/server/graphql.js +5 -180
- package/tests/unit/core/parser.test.js +0 -1
- package/tests/unit/core/pathwayManagerWithFiles.test.js +256 -0
- package/tests/unit/graphql_executeWorkspace_transformation.test.js +244 -0
- package/tests/unit/server/graphql.test.js +122 -1
|
@@ -1,31 +1,98 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ────────────────────────────────────────────────────────────────
|
|
2
|
+
# BASE IMAGE (Python 3.11, Azure Functions Runtime v4)
|
|
3
|
+
# ────────────────────────────────────────────────────────────────
|
|
2
4
|
FROM mcr.microsoft.com/azure-functions/python:4-python3.11
|
|
3
5
|
|
|
4
|
-
#
|
|
5
|
-
|
|
6
|
+
# ------------------------------------------------------------------------------
|
|
7
|
+
# 1. Azure Functions environment variables
|
|
8
|
+
# ------------------------------------------------------------------------------
|
|
9
|
+
ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
|
|
10
|
+
AzureFunctionsJobHost__Logging__Console__IsEnabled=true \
|
|
11
|
+
WEBSITES_INCLUDE_CLOUD_CERTS=true \
|
|
12
|
+
FUNCTIONS_WORKER_RUNTIME=python \
|
|
13
|
+
AzureWebJobsFeatureFlags=EnableWorkerIndexing
|
|
14
|
+
|
|
15
|
+
# ------------------------------------------------------------------------------
|
|
16
|
+
# 2. System-level packages required by many of the new Python deps
|
|
17
|
+
# ------------------------------------------------------------------------------
|
|
6
18
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
RUN apt-get update && DEBIAN_FRONTEND=noninteractive \
|
|
20
|
+
apt-get install -y --no-install-recommends \
|
|
21
|
+
# ─ Fonts & Playwright basics (including Arabic/RTL support)
|
|
22
|
+
fonts-liberation fonts-noto fontconfig \
|
|
23
|
+
fonts-noto-core fonts-noto-ui-core \
|
|
24
|
+
fonts-noto-color-emoji fonts-noto-cjk \
|
|
25
|
+
fonts-dejavu fonts-dejavu-core fonts-dejavu-extra \
|
|
26
|
+
fonts-freefont-ttf fonts-liberation2 \
|
|
27
|
+
# ─ Arabic fonts specifically for matplotlib/reportlab
|
|
28
|
+
fonts-arabeyes fonts-farsiweb fonts-kacst fonts-kacst-one \
|
|
29
|
+
fonts-hosny-amiri fonts-sil-scheherazade fonts-sil-lateef \
|
|
30
|
+
# ─ Build chain for packages that still compile C/C++
|
|
31
|
+
build-essential gcc g++ make \
|
|
32
|
+
# ─ libmagic for `python-magic`
|
|
33
|
+
libmagic1 libmagic-dev \
|
|
34
|
+
# ─ FFmpeg for `moviepy`, `imageio[ffmpeg]`, `pydub`
|
|
35
|
+
ffmpeg \
|
|
36
|
+
# ─ HEIF/HEIC support for `pyheif`
|
|
37
|
+
libde265-dev libheif-dev \
|
|
38
|
+
# ─ Audio support for `librosa` / `soundfile`
|
|
39
|
+
libsndfile1 \
|
|
40
|
+
# ─ Computer-vision helpers sometimes needed by OpenCV
|
|
41
|
+
libsm6 libxext6 libglib2.0-0 \
|
|
42
|
+
# ─ HDF5 stack for `h5py`
|
|
43
|
+
libhdf5-serial-dev hdf5-tools \
|
|
44
|
+
# ─ Cairo / Pango / GDK-PixBuf for SVG->PNG rendering (CairoSVG optional deps)
|
|
45
|
+
libcairo2 libcairo2-dev \
|
|
46
|
+
libpango-1.0-0 libpangoft2-1.0-0 libpangocairo-1.0-0 \
|
|
47
|
+
libgdk-pixbuf-2.0-0 libgdk-pixbuf2.0-bin \
|
|
48
|
+
# ─ GDAL for GIS formats (and Python bindings)
|
|
49
|
+
gdal-bin libgdal-dev python3-gdal \
|
|
50
|
+
# ─ 7-Zip & RAR extractors (for patool / rarfile fall-back)
|
|
51
|
+
unrar-free \
|
|
52
|
+
# ─ Clean-up
|
|
53
|
+
&& apt-get clean \
|
|
17
54
|
&& rm -rf /var/lib/apt/lists/*
|
|
18
|
-
RUN curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg \
|
|
19
|
-
&& mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg \
|
|
20
|
-
&& sh -c 'echo "deb [arch=amd64,arm64,armhf] https://packages.microsoft.com/repos/microsoft-debian-bullseye-prod bullseye main" > /etc/apt/sources.list.d/azure-cli.list' \
|
|
21
|
-
&& apt-get update \
|
|
22
|
-
&& apt-get install -y azure-functions-core-tools
|
|
23
55
|
|
|
24
|
-
#
|
|
56
|
+
# Allow GDAL Python wheels to find headers
|
|
57
|
+
ENV CPLUS_INCLUDE_PATH=/usr/include/gdal
|
|
58
|
+
ENV C_INCLUDE_PATH=/usr/include/gdal
|
|
59
|
+
|
|
60
|
+
# ------------------------------------------------------------------------------
|
|
61
|
+
# 3. Python dependencies
|
|
62
|
+
# – We copy only the files that affect dependency resolution first
|
|
63
|
+
# (leverages Docker layer cache).
|
|
64
|
+
# ------------------------------------------------------------------------------
|
|
65
|
+
WORKDIR /tmp
|
|
66
|
+
|
|
67
|
+
# Copy requirements for pip-based install
|
|
25
68
|
COPY requirements.txt .
|
|
69
|
+
|
|
70
|
+
# Use a pip version compatible with packages that have legacy metadata (e.g., textract 1.6.5)
|
|
71
|
+
RUN python -m pip install --upgrade 'pip<24.1' setuptools wheel
|
|
72
|
+
|
|
73
|
+
# Install all Python packages — no cache -> smaller image
|
|
26
74
|
RUN pip install --no-cache-dir -r requirements.txt
|
|
27
75
|
|
|
28
|
-
#
|
|
76
|
+
# Rebuild font cache to ensure all Arabic fonts are detected
|
|
77
|
+
RUN fc-cache -fv
|
|
78
|
+
|
|
79
|
+
# ------------------------------------------------------------------------------
|
|
80
|
+
# 4. Playwright browsers (chromium only to keep image light)
|
|
81
|
+
# ------------------------------------------------------------------------------
|
|
82
|
+
RUN playwright install --with-deps chromium
|
|
83
|
+
|
|
84
|
+
# ------------------------------------------------------------------------------
|
|
85
|
+
# 5. Copy your Azure Functions source code
|
|
86
|
+
# ------------------------------------------------------------------------------
|
|
87
|
+
WORKDIR /home/site/wwwroot
|
|
29
88
|
COPY . .
|
|
30
|
-
CMD ["func", "start", "--port", "80", "--verbose"]
|
|
31
89
|
|
|
90
|
+
# Ports: Azure Functions host inside the container listens on 80 by default.
|
|
91
|
+
# (You normally don’t need EXPOSE for Azure App Service, but uncomment if you
|
|
92
|
+
# run the image locally.)
|
|
93
|
+
# EXPOSE 80
|
|
94
|
+
|
|
95
|
+
# ------------------------------------------------------------------------------
|
|
96
|
+
# 6. The base image already contains the correct ENTRYPOINT / CMD
|
|
97
|
+
# to launch the Functions host, so nothing to add here.
|
|
98
|
+
# ------------------------------------------------------------------------------
|
|
@@ -7,14 +7,21 @@ services:
|
|
|
7
7
|
- linux/amd64
|
|
8
8
|
ports:
|
|
9
9
|
- "7071:80"
|
|
10
|
+
env_file:
|
|
11
|
+
- .env
|
|
10
12
|
environment:
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
- FUNCTIONS_WORKER_RUNTIME=python
|
|
14
|
+
- AzureWebJobsFeatureFlags=EnableWorkerIndexing
|
|
15
|
+
- AZURE_STORAGE_CONNECTION_STRING=${AZURE_STORAGE_CONNECTION_STRING}
|
|
16
|
+
- AzureWebJobsStorage=${AZURE_STORAGE_CONNECTION_STRING}
|
|
17
|
+
- QUEUE_NAME=${AZURE_QUEUE_NAME:-${QUEUE_NAME}}
|
|
18
|
+
- CORTEX_API_BASE_URL=${CORTEX_API_BASE_URL:-http://host.docker.internal:4000/v1}
|
|
19
|
+
- REDIS_CONNECTION_STRING=${REDIS_CONNECTION_STRING:-redis://host.docker.internal:6379}
|
|
20
|
+
# healthcheck:
|
|
21
|
+
# test: ["CMD", "curl", "-f", "http://localhost:80/api/health"]
|
|
22
|
+
# interval: 30s
|
|
23
|
+
# timeout: 10s
|
|
24
|
+
# retries: 3
|
|
25
|
+
# start_period: 60s
|
|
19
26
|
restart: unless-stopped
|
|
20
27
|
network_mode: host
|
|
@@ -1,36 +1,93 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
|
-
name
|
|
3
|
-
version
|
|
2
|
+
name = "cortex-autogen2"
|
|
3
|
+
version = "0.1.0"
|
|
4
4
|
description = "Multi-agent coding assistant using AutoGen"
|
|
5
|
-
authors
|
|
6
|
-
readme
|
|
7
|
-
packages
|
|
5
|
+
authors = ["Your Name <your.email@example.com>"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
packages = [{ include = "cortex_autogen2", from = "src" }]
|
|
8
8
|
|
|
9
|
+
# ─────────────────────────────────────────────────
|
|
10
|
+
# Core runtime (always installed)
|
|
11
|
+
# ─────────────────────────────────────────────────
|
|
9
12
|
[tool.poetry.dependencies]
|
|
10
13
|
python = ">=3.11,<3.13"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
# File-handling essentials
|
|
16
|
+
Pillow = "^11.0.0"
|
|
17
|
+
python-magic = "^0.4.27"
|
|
18
|
+
chardet = "^5.2.0"
|
|
19
|
+
PyPDF2 = "^3.0.1"
|
|
20
|
+
pdfminer.six = "^20221105"
|
|
21
|
+
docx2txt = ">=0.9,<1.0"
|
|
22
|
+
openpyxl = "^3.1.2"
|
|
23
|
+
xlrd = "^2.0.1"
|
|
24
|
+
python-pptx = "^0.6.23"
|
|
25
|
+
odfpy = "^1.4.1"
|
|
26
|
+
|
|
27
|
+
# Infra / networking / AI you already had
|
|
28
|
+
autogen-agentchat = { extras = ["openai"], version = "^0.7.4" }
|
|
29
|
+
autogen-ext = { extras = ["docker"], version = "^0.7.4" }
|
|
30
|
+
openai = "^1.97.1"
|
|
31
|
+
tiktoken = "^0.9.0"
|
|
32
|
+
python-dotenv = "^1.0.1"
|
|
33
|
+
requests = "^2.32.3"
|
|
34
|
+
redis = "^5.0.4"
|
|
15
35
|
azure-storage-queue = "^12.10.1"
|
|
16
|
-
azure-storage-blob
|
|
17
|
-
sqlalchemy
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
playwright
|
|
21
|
-
markitdown
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
azure-storage-blob = "^12.19.0"
|
|
37
|
+
sqlalchemy = "^2.0.30"
|
|
38
|
+
pymysql = "^1.1.1"
|
|
39
|
+
docker = "^7.1.0"
|
|
40
|
+
playwright = "^1.54.0"
|
|
41
|
+
markitdown = "^0.1.2"
|
|
42
|
+
aiofiles = "^24.1.0"
|
|
43
|
+
aiohttp = "^3.12.14"
|
|
44
|
+
pandas = "^2.3.1"
|
|
45
|
+
matplotlib = "^3.10.3"
|
|
46
|
+
lxml = "^5.3.0"
|
|
47
|
+
reportlab = "^4.2.5"
|
|
48
|
+
fpdf2 = "^2.7.9"
|
|
49
|
+
|
|
50
|
+
# ─────────────────────────────────────────────────
|
|
51
|
+
# Optional groups (install with: poetry install --with media,archives,science …)
|
|
52
|
+
# ─────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
[tool.poetry.group.media.dependencies] # images / audio / video
|
|
55
|
+
opencv-python = "^4.9.0"
|
|
56
|
+
imageio = { version = "^2.34.0", extras = ["ffmpeg"] }
|
|
57
|
+
pyheif = "^0.6.3"
|
|
58
|
+
moviepy = "^1.0.3"
|
|
59
|
+
librosa = "^0.10.1"
|
|
60
|
+
soundfile = "^0.12.1"
|
|
61
|
+
pydub = "^0.25.1"
|
|
62
|
+
|
|
63
|
+
[tool.poetry.group.archives.dependencies]
|
|
64
|
+
patool = "^1.12"
|
|
65
|
+
py7zr = "^0.21.0"
|
|
66
|
+
rarfile = "^4.0"
|
|
67
|
+
|
|
68
|
+
[tool.poetry.group.science.dependencies]
|
|
69
|
+
tabula-py = "^2.8.1"
|
|
70
|
+
h5py = "^3.10.0"
|
|
71
|
+
netCDF4 = "^1.6.5"
|
|
72
|
+
ezdxf = "^1.1.1"
|
|
73
|
+
|
|
74
|
+
[tool.poetry.group.comm.dependencies] # email / web / e-books
|
|
75
|
+
eml-parser = "^1.15.0"
|
|
76
|
+
msg-parser = ">=1.2.0,<2.0"
|
|
77
|
+
beautifulsoup4 = "^4.12.3"
|
|
78
|
+
ebooklib = "^0.18"
|
|
79
|
+
|
|
80
|
+
[tool.poetry.group.extras.dependencies] # seldom-needed odds & ends
|
|
81
|
+
imageio-ffmpeg = "^0.4.9"
|
|
82
|
+
python-lzf = "^0.2.4"
|
|
30
83
|
|
|
84
|
+
# ─────────────────────────────────────────────────
|
|
85
|
+
# Development-only deps
|
|
86
|
+
# ─────────────────────────────────────────────────
|
|
31
87
|
[tool.poetry.group.dev.dependencies]
|
|
32
88
|
ipykernel = "^6.29.5"
|
|
33
89
|
|
|
90
|
+
# ─────────────────────────────────────────────────
|
|
34
91
|
[build-system]
|
|
35
|
-
requires
|
|
36
|
-
build-backend = "poetry.core.masonry.api"
|
|
92
|
+
requires = ["poetry-core"]
|
|
93
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -1,20 +1,90 @@
|
|
|
1
|
+
# -------------------------------------------------------------------
|
|
2
|
+
# ESSENTIAL – office docs, PDFs, images
|
|
3
|
+
# -------------------------------------------------------------------
|
|
4
|
+
Pillow>=11.0.0
|
|
5
|
+
python-magic>=0.4.27
|
|
6
|
+
chardet>=5.2.0
|
|
7
|
+
PyPDF2>=3.0.1
|
|
8
|
+
pdfminer.six>=20221105
|
|
9
|
+
docx2txt>=0.9,<1.0
|
|
10
|
+
openpyxl>=3.1.2
|
|
11
|
+
xlrd>=2.0.1
|
|
12
|
+
python-pptx>=0.6.23
|
|
13
|
+
odfpy>=1.4.1
|
|
14
|
+
|
|
15
|
+
# -------------------------------------------------------------------
|
|
16
|
+
# MEDIA – images / audio / video
|
|
17
|
+
# -------------------------------------------------------------------
|
|
18
|
+
opencv-python>=4.9.0
|
|
19
|
+
imageio[ffmpeg]>=2.34.0
|
|
20
|
+
CairoSVG>=2.7.1
|
|
21
|
+
pyheif>=0.6.3
|
|
22
|
+
moviepy>=1.0.3
|
|
23
|
+
librosa>=0.10.1
|
|
24
|
+
soundfile>=0.12.1
|
|
25
|
+
pydub>=0.25.1
|
|
26
|
+
|
|
27
|
+
# -------------------------------------------------------------------
|
|
28
|
+
# ARCHIVES
|
|
29
|
+
# -------------------------------------------------------------------
|
|
30
|
+
patool>=1.12
|
|
31
|
+
py7zr>=0.21.0
|
|
32
|
+
rarfile>=4.0
|
|
33
|
+
|
|
34
|
+
# -------------------------------------------------------------------
|
|
35
|
+
# DATA & SCIENTIFIC
|
|
36
|
+
# -------------------------------------------------------------------
|
|
37
|
+
pandas>=2.2.0
|
|
38
|
+
tabula-py>=2.8.1
|
|
39
|
+
h5py>=3.10.0
|
|
40
|
+
netCDF4>=1.6.5
|
|
41
|
+
ezdxf>=1.1.1
|
|
42
|
+
seaborn>=0.13.0
|
|
43
|
+
|
|
44
|
+
# -------------------------------------------------------------------
|
|
45
|
+
# ARABIC & RTL TEXT SUPPORT
|
|
46
|
+
# -------------------------------------------------------------------
|
|
47
|
+
arabic-reshaper>=3.0.0
|
|
48
|
+
python-bidi>=0.4.2
|
|
49
|
+
|
|
50
|
+
# -------------------------------------------------------------------
|
|
51
|
+
# EMAIL / WEB / E-BOOK
|
|
52
|
+
# -------------------------------------------------------------------
|
|
53
|
+
eml-parser>=1.15.0
|
|
54
|
+
msg-parser>=1.2.0,<2.0
|
|
55
|
+
beautifulsoup4>=4.12.3
|
|
56
|
+
ebooklib>=0.18
|
|
57
|
+
|
|
58
|
+
# -------------------------------------------------------------------
|
|
59
|
+
# CLOUD / INFRASTRUCTURE
|
|
60
|
+
# -------------------------------------------------------------------
|
|
1
61
|
azure-functions>=1.20.0
|
|
2
|
-
autogen-agentchat[openai]>=0.6.4
|
|
3
|
-
python-dotenv>=1.0.1
|
|
4
|
-
requests>=2.32.3
|
|
5
|
-
redis>=5.0.4
|
|
6
|
-
azure-storage-queue>=12.10.1
|
|
7
62
|
azure-storage-blob>=12.19.0
|
|
63
|
+
azure-storage-queue>=12.10.1
|
|
64
|
+
redis>=5.0.4
|
|
8
65
|
sqlalchemy>=2.0.30
|
|
9
|
-
Pillow>=11.0.0
|
|
10
66
|
pymysql>=1.1.1
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
67
|
+
requests>=2.32.3
|
|
68
|
+
aiohttp>=3.12.14
|
|
69
|
+
aiofiles>=24.1.0
|
|
70
|
+
|
|
71
|
+
# -------------------------------------------------------------------
|
|
72
|
+
# AI / AUTOMATION
|
|
73
|
+
# -------------------------------------------------------------------
|
|
15
74
|
openai>=1.97.1
|
|
16
75
|
tiktoken>=0.9.0
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
76
|
+
autogen-agentchat[openai]>=0.7.4
|
|
77
|
+
autogen-ext[docker]>=0.7.4
|
|
78
|
+
docker>=7.1.0
|
|
79
|
+
playwright>=1.54.0
|
|
80
|
+
markitdown>=0.1.2
|
|
81
|
+
python-dotenv>=1.0.1
|
|
82
|
+
reportlab>=4.2.5
|
|
83
|
+
fpdf2>=2.7.9
|
|
84
|
+
matplotlib>=3.9.0
|
|
85
|
+
|
|
86
|
+
# -------------------------------------------------------------------
|
|
87
|
+
# OPTIONAL EXTRAS
|
|
88
|
+
# -------------------------------------------------------------------
|
|
89
|
+
imageio-ffmpeg>=0.4.9
|
|
90
|
+
python-lzf>=0.2.4
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import redis
|
|
2
2
|
import json
|
|
3
3
|
import logging
|
|
4
|
-
|
|
4
|
+
import asyncio
|
|
5
|
+
import time
|
|
6
|
+
from typing import Dict, Any, Optional, List
|
|
5
7
|
|
|
6
8
|
import os
|
|
7
9
|
|
|
@@ -52,13 +54,31 @@ def connect_redis() -> bool:
|
|
|
52
54
|
return False
|
|
53
55
|
return False
|
|
54
56
|
|
|
57
|
+
_last_logged_progress: Dict[str, Any] = {}
|
|
58
|
+
|
|
55
59
|
def publish_request_progress(data: Dict[str, Any]) -> bool:
|
|
56
|
-
"""Publish progress data to Redis channel
|
|
60
|
+
"""Publish progress data to Redis channel with minimal logging (only when message changes)."""
|
|
57
61
|
if connect_redis():
|
|
58
62
|
try:
|
|
59
63
|
message = json.dumps(data)
|
|
60
64
|
result = redis_client.publish(os.getenv("REDIS_CHANNEL"), message)
|
|
61
|
-
|
|
65
|
+
try:
|
|
66
|
+
rid = data.get('requestId')
|
|
67
|
+
info = data.get('info')
|
|
68
|
+
pct = data.get('progress')
|
|
69
|
+
prev = _last_logged_progress.get(rid)
|
|
70
|
+
# Log only if info or integer progress changed
|
|
71
|
+
pct_bucket = None
|
|
72
|
+
try:
|
|
73
|
+
pct_bucket = int(float(pct) * 100)
|
|
74
|
+
except Exception:
|
|
75
|
+
pct_bucket = None
|
|
76
|
+
if not prev or prev.get('info') != info or prev.get('pct_bucket') != pct_bucket:
|
|
77
|
+
_last_logged_progress[rid] = {'info': info, 'pct_bucket': pct_bucket}
|
|
78
|
+
logger.info(f"Published progress update for request {rid}: progress={pct}, subscribers={result}")
|
|
79
|
+
except Exception:
|
|
80
|
+
# Safe fallback if logging diff fails
|
|
81
|
+
logger.debug("Progress publish logged without diff due to exception")
|
|
62
82
|
return True
|
|
63
83
|
except Exception as e:
|
|
64
84
|
logger.error(f"Error publishing message to Redis: {e}")
|
|
@@ -72,6 +92,23 @@ class RedisPublisher:
|
|
|
72
92
|
|
|
73
93
|
def __init__(self):
|
|
74
94
|
self.connected = False
|
|
95
|
+
# Heartbeat + transient caching
|
|
96
|
+
self._heartbeat_task: Optional[asyncio.Task] = None
|
|
97
|
+
try:
|
|
98
|
+
# Clamp to at most 1.0s to ensure the UI gets updates every second
|
|
99
|
+
interval = float(os.getenv("PROGRESS_HEARTBEAT_INTERVAL", "1.0"))
|
|
100
|
+
if interval > 1.0:
|
|
101
|
+
interval = 1.0
|
|
102
|
+
if interval <= 0:
|
|
103
|
+
interval = 1.0
|
|
104
|
+
self._interval_seconds = interval
|
|
105
|
+
except Exception:
|
|
106
|
+
self._interval_seconds = 1.0
|
|
107
|
+
# We cache only summarized progress strings (emoji sentence) with progress float
|
|
108
|
+
self._transient_latest: Dict[str, Dict[str, Any]] = {}
|
|
109
|
+
self._transient_all: Dict[str, List[Dict[str, Any]]] = {}
|
|
110
|
+
self._finalized: Dict[str, bool] = {}
|
|
111
|
+
self._lock = asyncio.Lock()
|
|
75
112
|
|
|
76
113
|
async def connect(self):
|
|
77
114
|
"""Initialize Redis connection"""
|
|
@@ -81,6 +118,13 @@ class RedisPublisher:
|
|
|
81
118
|
else:
|
|
82
119
|
logger.warning("Failed to connect to Redis")
|
|
83
120
|
logger.warning("Redis progress publishing will be disabled")
|
|
121
|
+
# Start heartbeat loop once per process
|
|
122
|
+
if self._heartbeat_task is None:
|
|
123
|
+
try:
|
|
124
|
+
self._heartbeat_task = asyncio.create_task(self._heartbeat_loop())
|
|
125
|
+
logger.info("Started Redis progress heartbeat task")
|
|
126
|
+
except Exception as e:
|
|
127
|
+
logger.warning(f"Failed to start heartbeat task: {e}")
|
|
84
128
|
|
|
85
129
|
def publish_request_progress(self, data: Dict[str, Any]) -> bool:
|
|
86
130
|
"""Publish progress data to Redis channel"""
|
|
@@ -99,6 +143,68 @@ class RedisPublisher:
|
|
|
99
143
|
message_data["data"] = data
|
|
100
144
|
|
|
101
145
|
return self.publish_request_progress(message_data)
|
|
146
|
+
|
|
147
|
+
async def set_transient_update(self, request_id: str, progress: float, info: str) -> None:
|
|
148
|
+
"""Cache the latest summarized transient progress (short sentence with emoji).
|
|
149
|
+
Heartbeat will re-publish this every second until final. No raw chat content here."""
|
|
150
|
+
try:
|
|
151
|
+
async with self._lock:
|
|
152
|
+
# Skip if already finalized
|
|
153
|
+
if self._finalized.get(request_id):
|
|
154
|
+
return
|
|
155
|
+
self._transient_latest[request_id] = {"progress": progress, "info": info, "ts": time.time()}
|
|
156
|
+
lst = self._transient_all.get(request_id)
|
|
157
|
+
if lst is None:
|
|
158
|
+
lst = []
|
|
159
|
+
self._transient_all[request_id] = lst
|
|
160
|
+
lst.append({"progress": progress, "info": info, "ts": time.time()})
|
|
161
|
+
# Avoid unbounded growth
|
|
162
|
+
if len(lst) > 200:
|
|
163
|
+
del lst[: len(lst) - 200]
|
|
164
|
+
except Exception as e:
|
|
165
|
+
logger.warning(f"set_transient_update error for {request_id}: {e}")
|
|
166
|
+
|
|
167
|
+
async def mark_final(self, request_id: str) -> None:
|
|
168
|
+
"""Mark a request as finalized to stop transient heartbeat for it."""
|
|
169
|
+
try:
|
|
170
|
+
async with self._lock:
|
|
171
|
+
self._finalized[request_id] = True
|
|
172
|
+
# Optionally clear cached transient
|
|
173
|
+
if request_id in self._transient_latest:
|
|
174
|
+
del self._transient_latest[request_id]
|
|
175
|
+
except Exception as e:
|
|
176
|
+
logger.warning(f"mark_final error for {request_id}: {e}")
|
|
177
|
+
|
|
178
|
+
async def _heartbeat_loop(self):
|
|
179
|
+
"""Background loop that emits latest transient updates every interval."""
|
|
180
|
+
try:
|
|
181
|
+
while True:
|
|
182
|
+
try:
|
|
183
|
+
# Snapshot under lock
|
|
184
|
+
async with self._lock:
|
|
185
|
+
items = [
|
|
186
|
+
(rid, payload)
|
|
187
|
+
for rid, payload in self._transient_latest.items()
|
|
188
|
+
if not self._finalized.get(rid)
|
|
189
|
+
]
|
|
190
|
+
if items:
|
|
191
|
+
for rid, payload in items:
|
|
192
|
+
try:
|
|
193
|
+
message_data = {
|
|
194
|
+
"requestId": rid,
|
|
195
|
+
"progress": float(payload.get("progress", 0.0)),
|
|
196
|
+
"info": str(payload.get("info", ""))
|
|
197
|
+
}
|
|
198
|
+
self.publish_request_progress(message_data)
|
|
199
|
+
except Exception as pub_err:
|
|
200
|
+
logger.debug(f"Heartbeat publish error for {rid}: {pub_err}")
|
|
201
|
+
except Exception as loop_err:
|
|
202
|
+
logger.debug(f"Heartbeat loop iteration error: {loop_err}")
|
|
203
|
+
await asyncio.sleep(self._interval_seconds)
|
|
204
|
+
except asyncio.CancelledError:
|
|
205
|
+
logger.info("Redis progress heartbeat task cancelled")
|
|
206
|
+
except Exception as e:
|
|
207
|
+
logger.warning(f"Heartbeat loop terminated unexpectedly: {e}")
|
|
102
208
|
|
|
103
209
|
def store_final_result(self, request_id: str, result_data: Dict[str, Any], expiry_seconds: int = 3600) -> bool:
|
|
104
210
|
"""Store final result in Redis key for retrieval"""
|
|
@@ -129,6 +235,18 @@ class RedisPublisher:
|
|
|
129
235
|
async def close(self):
|
|
130
236
|
"""Close Redis connection gracefully"""
|
|
131
237
|
global redis_client
|
|
238
|
+
# Stop heartbeat
|
|
239
|
+
if self._heartbeat_task is not None:
|
|
240
|
+
try:
|
|
241
|
+
self._heartbeat_task.cancel()
|
|
242
|
+
try:
|
|
243
|
+
await self._heartbeat_task
|
|
244
|
+
except asyncio.CancelledError:
|
|
245
|
+
pass
|
|
246
|
+
except Exception as e:
|
|
247
|
+
logger.debug(f"Error cancelling heartbeat task: {e}")
|
|
248
|
+
finally:
|
|
249
|
+
self._heartbeat_task = None
|
|
132
250
|
if redis_client:
|
|
133
251
|
try:
|
|
134
252
|
# Don't actually close the connection in non-continuous mode
|
|
@@ -150,4 +268,12 @@ async def get_redis_publisher() -> RedisPublisher:
|
|
|
150
268
|
if _redis_publisher is None:
|
|
151
269
|
_redis_publisher = RedisPublisher()
|
|
152
270
|
await _redis_publisher.connect()
|
|
271
|
+
else:
|
|
272
|
+
# Ensure connectivity and heartbeat are active for new tasks
|
|
273
|
+
try:
|
|
274
|
+
if (not getattr(_redis_publisher, 'connected', False)) or getattr(_redis_publisher, '_heartbeat_task', None) is None:
|
|
275
|
+
await _redis_publisher.connect()
|
|
276
|
+
except Exception:
|
|
277
|
+
# Best-effort reconnect
|
|
278
|
+
await _redis_publisher.connect()
|
|
153
279
|
return _redis_publisher
|