@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.
@@ -1,31 +1,98 @@
1
- # Use the official Azure Functions Python base image
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
- # Set the working directory
5
- WORKDIR /home/site/wwwroot
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
- # Install system dependencies
8
- RUN apt-get update && apt-get install -y \
9
- build-essential \
10
- curl \
11
- git \
12
- libgirepository1.0-dev \
13
- pkg-config \
14
- libfreetype6-dev \
15
- libpng-dev \
16
- fontconfig \
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
- # Copy requirements and install Python dependencies
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
- # Copy application code
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
- - CORTEX_API_BASE_URL=http://host.docker.internal:4000/v1
12
- - REDIS_CONNECTION_STRING=redis://host.docker.internal:6379
13
- healthcheck:
14
- test: ["CMD", "curl", "-f", "http://localhost:80/api/health"]
15
- interval: 30s
16
- timeout: 10s
17
- retries: 3
18
- start_period: 60s
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
@@ -11,5 +11,10 @@
11
11
  "extensionBundle": {
12
12
  "id": "Microsoft.Azure.Functions.ExtensionBundle",
13
13
  "version": "[4.*, 5.0.0)"
14
+ },
15
+ "extensions": {
16
+ "queues": {
17
+ "maxPollingInterval": "00:00:01"
18
+ }
14
19
  }
15
20
  }
@@ -1,36 +1,93 @@
1
1
  [tool.poetry]
2
- name = "cortex-autogen2"
3
- version = "0.1.0"
2
+ name = "cortex-autogen2"
3
+ version = "0.1.0"
4
4
  description = "Multi-agent coding assistant using AutoGen"
5
- authors = ["Your Name <your.email@example.com>"]
6
- readme = "README.md"
7
- packages = [{include = "cortex_autogen2", from = "src"}]
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
- autogen-agentchat = {extras = ["openai"], version = "^0.6.4"}
12
- python-dotenv = "^1.0.1"
13
- requests = "^2.32.3"
14
- redis = "^5.0.4"
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 = "^12.19.0"
17
- sqlalchemy = "^2.0.30"
18
- Pillow = "^11.0.0"
19
- pymysql = "^1.1.1"
20
- playwright = "^1.54.0"
21
- markitdown = "^0.1.2"
22
- docker = "^7.1.0"
23
- autogen-ext = {extras = ["docker"], version = "^0.6.4"}
24
- openai = "^1.97.1"
25
- tiktoken = "^0.9.0"
26
- aiofiles = "^24.1.0"
27
- pandas = "^2.3.1"
28
- matplotlib = "^3.10.3"
29
- aiohttp = "^3.12.14"
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 = ["poetry-core"]
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
- playwright>=1.54.0
12
- markitdown>=0.1.2
13
- docker>=7.1.0
14
- autogen-ext[docker]>=0.6.4
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
- aiofiles>=24.1.0
18
- aiohttp>=3.12.14
19
- pandas
20
- matplotlib
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
- from typing import Dict, Any, Optional
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 - matches working version pattern"""
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
- logger.info(f"Published progress update for request {data.get('requestId')}: progress={data.get('progress')}, subscribers={result}")
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