synapse-orch-ai 1.8.4 → 1.8.6

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.
Files changed (53) hide show
  1. package/backend/core/orchestration/engine.py +12 -6
  2. package/backend/core/orchestration/runner.py +70 -0
  3. package/backend/core/react_engine.py +59 -26
  4. package/backend/core/routes/api_v1.py +17 -6
  5. package/backend/core/routes/chat.py +3 -0
  6. package/backend/core/routes/orchestrations.py +15 -66
  7. package/backend/requirements-coding.txt +5 -3
  8. package/backend/requirements-messaging.lock +167 -0
  9. package/backend/requirements-messaging.txt +8 -6
  10. package/backend/requirements.lock +617 -0
  11. package/backend/requirements.txt +45 -30
  12. package/frontend-build/.next/BUILD_ID +1 -1
  13. package/frontend-build/.next/build-manifest.json +3 -3
  14. package/frontend-build/.next/prerender-manifest.json +3 -3
  15. package/frontend-build/.next/server/app/_global-error.html +1 -1
  16. package/frontend-build/.next/server/app/_global-error.rsc +1 -1
  17. package/frontend-build/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  18. package/frontend-build/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  19. package/frontend-build/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  20. package/frontend-build/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  21. package/frontend-build/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  22. package/frontend-build/.next/server/app/_not-found.html +1 -1
  23. package/frontend-build/.next/server/app/_not-found.rsc +1 -1
  24. package/frontend-build/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  25. package/frontend-build/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  26. package/frontend-build/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  27. package/frontend-build/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  28. package/frontend-build/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  29. package/frontend-build/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  30. package/frontend-build/.next/server/app/index.html +1 -1
  31. package/frontend-build/.next/server/app/index.rsc +1 -1
  32. package/frontend-build/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  33. package/frontend-build/.next/server/app/index.segments/_full.segment.rsc +1 -1
  34. package/frontend-build/.next/server/app/index.segments/_head.segment.rsc +1 -1
  35. package/frontend-build/.next/server/app/index.segments/_index.segment.rsc +1 -1
  36. package/frontend-build/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  37. package/frontend-build/.next/server/app/login.html +1 -1
  38. package/frontend-build/.next/server/app/login.rsc +1 -1
  39. package/frontend-build/.next/server/app/login.segments/_full.segment.rsc +1 -1
  40. package/frontend-build/.next/server/app/login.segments/_head.segment.rsc +1 -1
  41. package/frontend-build/.next/server/app/login.segments/_index.segment.rsc +1 -1
  42. package/frontend-build/.next/server/app/login.segments/_tree.segment.rsc +1 -1
  43. package/frontend-build/.next/server/app/login.segments/login/__PAGE__.segment.rsc +1 -1
  44. package/frontend-build/.next/server/app/login.segments/login.segment.rsc +1 -1
  45. package/frontend-build/.next/server/middleware-build-manifest.js +3 -3
  46. package/frontend-build/.next/server/pages/404.html +1 -1
  47. package/frontend-build/.next/server/pages/500.html +1 -1
  48. package/frontend-build/.next/server/server-reference-manifest.js +1 -1
  49. package/frontend-build/.next/server/server-reference-manifest.json +1 -1
  50. package/package.json +4 -2
  51. /package/frontend-build/.next/static/{WjvzoOqaNaXuc-wYMNArI → Yaj8BSKIx7sIJnGFuGLQZ}/_buildManifest.js +0 -0
  52. /package/frontend-build/.next/static/{WjvzoOqaNaXuc-wYMNArI → Yaj8BSKIx7sIJnGFuGLQZ}/_clientMiddlewareManifest.js +0 -0
  53. /package/frontend-build/.next/static/{WjvzoOqaNaXuc-wYMNArI → Yaj8BSKIx7sIJnGFuGLQZ}/_ssgManifest.js +0 -0
@@ -182,12 +182,18 @@ class OrchestrationEngine:
182
182
  # This properly interrupts a stuck session.call_tool() inside
183
183
  # the executor when the step deadline is reached.
184
184
  #
185
- # IMPORTANT: never yield human_input_required inside the
186
- # fail_after scope. Python's GC finalizer runs aclose() on
187
- # abandoned async generators in a NEW asyncio Task; if a
188
- # cancel scope is active at the suspension point, anyio raises
189
- # "cancel scope in a different task". We break out of the scope
190
- # cleanly (in the current task) and yield the event below.
185
+ # IMPORTANT: this scope stays open across the yields below, so
186
+ # this generator must be advanced — and closed — from a single
187
+ # task, or anyio raises "cancel scope in a different task".
188
+ # core.orchestration.runner is what guarantees that; every
189
+ # entry point runs us through it (issue #356). Do not drive
190
+ # the engine directly from a task-per-pull consumer.
191
+ #
192
+ # human_input_required is still yielded outside the scope: the
193
+ # generator is abandoned at that point, and Python's GC
194
+ # finalizer runs aclose() on abandoned async generators in a
195
+ # NEW asyncio Task. We break out of the scope cleanly (in the
196
+ # current task) and yield the event below.
191
197
  human_input_event: dict | None = None
192
198
  try:
193
199
  with anyio.fail_after(step_timeout):
@@ -0,0 +1,70 @@
1
+ """Single-task runner for orchestration engine streams.
2
+
3
+ The engine holds anyio cancel scopes across `yield`s (the per-step timeout at
4
+ `engine._execute_loop`). anyio binds a cancel scope to the task that entered
5
+ it, so the engine's generator must be advanced -- and closed -- from ONE task,
6
+ whatever the HTTP layer does with the events. A consumer that pulls each event
7
+ in a fresh task raises "Attempted to exit cancel scope in a different task"
8
+ (issue #356).
9
+
10
+ Every orchestration entry point goes through here, so the launch path can no
11
+ longer change how the engine is driven.
12
+ """
13
+ import asyncio
14
+ from typing import AsyncGenerator
15
+
16
+ # Live in-process runs, keyed by run_id. The cancel endpoint cancels the task
17
+ # to interrupt an in-progress await; the run-status endpoint consults it to
18
+ # report a run that is still inside its first step (no checkpoint yet).
19
+ _active_tasks: dict[str, asyncio.Task] = {}
20
+
21
+ # Queue marker meaning "the engine generator is exhausted".
22
+ SENTINEL = object()
23
+
24
+
25
+ def spawn_engine_run(agen, run_id: str) -> tuple[asyncio.Task, asyncio.Queue]:
26
+ """Advance `agen` in one dedicated task, returning (task, queue).
27
+
28
+ The task is detached: it runs to completion even if nobody drains the
29
+ queue, so a client disconnect never kills a run. Only the cancel endpoint
30
+ stops it.
31
+ """
32
+ queue: asyncio.Queue = asyncio.Queue()
33
+
34
+ async def _pump():
35
+ try:
36
+ async for event in agen:
37
+ await queue.put(event)
38
+ # Yield so the SSE consumer can dequeue and flush this event
39
+ # to the HTTP response before the next one is enqueued.
40
+ await asyncio.sleep(0)
41
+ except asyncio.CancelledError:
42
+ await queue.put({"type": "orchestration_error", "error": "Cancelled"})
43
+ except FileNotFoundError:
44
+ await queue.put({"type": "orchestration_error", "error": "Run not found"})
45
+ except Exception as e:
46
+ await queue.put({"type": "orchestration_error", "error": str(e)})
47
+ finally:
48
+ _active_tasks.pop(run_id, None)
49
+ print(f"DEBUG SSE QUEUE: sentinel sent for '{run_id}', stream closing", flush=True)
50
+ await queue.put(SENTINEL)
51
+
52
+ task = asyncio.create_task(_pump())
53
+ _active_tasks[run_id] = task
54
+ return task, queue
55
+
56
+
57
+ async def stream_engine_events(agen, run_id: str) -> AsyncGenerator[dict, None]:
58
+ """Yield the engine's event dicts while a dedicated task advances it.
59
+
60
+ Deliberately does NOT cancel that task on close: the run is detached, so
61
+ abandoning this generator (client disconnect, GC finalization) must leave
62
+ it running -- the run still checkpoints and stays cancellable and
63
+ resumable.
64
+ """
65
+ _task, queue = spawn_engine_run(agen, run_id)
66
+ while True:
67
+ event = await queue.get()
68
+ if event is SENTINEL:
69
+ return
70
+ yield event
@@ -4,6 +4,7 @@ Yields structured event dicts that callers can handle differently
4
4
  (collect for sync response, or stream as SSE).
5
5
  """
6
6
  import asyncio
7
+ import contextlib
7
8
  import json
8
9
  import sys
9
10
  import time
@@ -47,50 +48,74 @@ MAX_TURNS = 30
47
48
  # Heartbeats are yielded as the raw SSE string, so endpoints can pass them
48
49
  # straight through; real events remain dicts (`isinstance(item, str)`
49
50
  # distinguishes them).
51
+ #
52
+ # The source is advanced by a single dedicated pump task rather than a fresh
53
+ # task per pull. anyio binds a cancel scope to the task that entered it, so a
54
+ # source holding a scope across a yield (the orchestration engine's per-step
55
+ # timeout) must be advanced — and closed — from one task. See issue #356.
50
56
  # ──────────────────────────────────────────────────────────────────────────────
51
57
  SSE_HEARTBEAT = ": ping\n\n"
52
58
 
53
59
 
60
+ class _StreamEnd:
61
+ """Sentinel handing the source's terminal state back to the consumer."""
62
+ __slots__ = ("error",)
63
+
64
+ def __init__(self, error):
65
+ self.error = error
66
+
67
+
54
68
  async def iter_with_heartbeat(agen, interval: float = 10.0):
55
69
  """Yield items from `agen`, injecting `SSE_HEARTBEAT` whenever no item
56
70
  arrives within `interval` seconds.
57
71
 
58
72
  The underlying generator is never cancelled on a heartbeat timeout — only
59
73
  when the consumer stops early (e.g. a client disconnect closes this
60
- generator), in which case the pending pull is cancelled and the source is
61
- closed in the finally block.
74
+ generator), in which case the pump is cancelled and closes the source.
62
75
  """
63
- ait = agen.__aiter__()
64
- pending = None
76
+ queue: asyncio.Queue = asyncio.Queue(maxsize=1) # maxsize=1 keeps backpressure
77
+
78
+ async def _pump():
79
+ # Advances `agen` in THIS single task, so a cancel scope the source
80
+ # holds across a yield is never exited from another task — and
81
+ # aclose() below runs in that scope's host task too.
82
+ try:
83
+ async for item in agen:
84
+ await queue.put(item)
85
+ await queue.put(_StreamEnd(None))
86
+ except asyncio.CancelledError:
87
+ raise
88
+ except BaseException as exc:
89
+ with contextlib.suppress(BaseException):
90
+ await queue.put(_StreamEnd(exc))
91
+ finally:
92
+ aclose = getattr(agen, "aclose", None)
93
+ if aclose is not None:
94
+ with contextlib.suppress(BaseException):
95
+ await aclose()
96
+
97
+ pump = asyncio.ensure_future(_pump())
65
98
  try:
66
99
  while True:
67
- if pending is None:
68
- pending = asyncio.ensure_future(ait.__anext__())
69
- done, _ = await asyncio.wait({pending}, timeout=interval)
70
- if not done:
100
+ try:
101
+ item = await asyncio.wait_for(queue.get(), timeout=interval)
102
+ except asyncio.TimeoutError:
103
+ if pump.done():
104
+ # Source ended without a sentinel — it was cancelled.
105
+ return
71
106
  # No event within the interval — keep the stream warm.
72
107
  yield SSE_HEARTBEAT
73
108
  continue
74
- task = pending
75
- pending = None
76
- try:
77
- item = task.result()
78
- except StopAsyncIteration:
109
+ if isinstance(item, _StreamEnd):
110
+ if item.error is not None:
111
+ raise item.error
79
112
  return
80
113
  yield item
81
114
  finally:
82
- if pending is not None:
83
- pending.cancel()
84
- try:
85
- await pending
86
- except BaseException:
87
- pass
88
- aclose = getattr(agen, "aclose", None)
89
- if aclose is not None:
90
- try:
91
- await aclose()
92
- except BaseException:
93
- pass
115
+ if not pump.done():
116
+ pump.cancel()
117
+ with contextlib.suppress(BaseException):
118
+ await pump
94
119
 
95
120
 
96
121
  async def drain_queue_with_heartbeat(queue, sentinel, interval: float = 10.0):
@@ -1591,6 +1616,7 @@ async def run_react_loop(request, server_module):
1591
1616
  from core.routes.orchestrations import load_orchestrations
1592
1617
  from core.models_orchestration import Orchestration
1593
1618
  from core.orchestration.engine import OrchestrationEngine
1619
+ from core.orchestration.runner import stream_engine_events
1594
1620
 
1595
1621
  orchs = load_orchestrations()
1596
1622
  orch_data = next((o for o in orchs if o["id"] == orch_id), None)
@@ -1598,7 +1624,14 @@ async def run_react_loop(request, server_module):
1598
1624
  orch = Orchestration.model_validate(orch_data)
1599
1625
  engine = OrchestrationEngine(orch, server_module)
1600
1626
  run_id = f"run_{orch_id}_{int(time.time() * 1000)}"
1601
- async for event in engine.run(user_message, run_id, session_id=session_id):
1627
+ # Route through the shared runner, exactly as the editor's
1628
+ # run endpoint does: the engine holds anyio cancel scopes
1629
+ # across yields, so it must be advanced from a single task
1630
+ # no matter how the SSE layer consumes us (issue #356).
1631
+ # It also registers the run for cancel/reconnect.
1632
+ async for event in stream_engine_events(
1633
+ engine.run(user_message, run_id, session_id=session_id), run_id
1634
+ ):
1602
1635
  yield event
1603
1636
  return
1604
1637
  else:
@@ -113,8 +113,9 @@ async def v1_chat(body: V1ChatRequest, key_record: dict = Depends(require_api_ke
113
113
  etype = event.get("type", "")
114
114
  if etype == "final":
115
115
  final_event = event
116
- elif etype == "error":
117
- error_msg = event.get("message", "Unknown error")
116
+ elif etype in ("error", "orchestration_error"):
117
+ # orchestration_error is what an orchestrator agent fails with.
118
+ error_msg = event.get("message") or event.get("error") or "Unknown error"
118
119
  except Exception as exc:
119
120
  log.exception("[v1/chat] Unhandled error for session=%s", chat_request.session_id)
120
121
  raise HTTPException(status_code=500, detail="An internal error occurred. Check server logs for details.")
@@ -204,8 +205,10 @@ async def v1_chat_stream(body: V1ChatRequest, key_record: dict = Depends(require
204
205
  })
205
206
  yield _format_sse_event({"type": "done"})
206
207
 
207
- elif etype == "error":
208
- log.error("[v1/chat/stream] Agent error session=%s: %s", chat_request.session_id, event.get("message"))
208
+ elif etype in ("error", "orchestration_error"):
209
+ # orchestration_error is what an orchestrator agent fails with.
210
+ log.error("[v1/chat/stream] Agent error session=%s: %s", chat_request.session_id,
211
+ event.get("message") or event.get("error"))
209
212
  yield _format_sse_event({"type": "error", "message": "The agent encountered an error processing your request."})
210
213
 
211
214
  await asyncio.sleep(0)
@@ -314,8 +317,11 @@ async def v1_orchestration_run_stream(
314
317
 
315
318
  async def event_stream():
316
319
  from core.react_engine import iter_with_heartbeat
320
+ from core.orchestration.runner import stream_engine_events
317
321
  try:
318
- async for event in iter_with_heartbeat(engine.run(body.message, run_id)):
322
+ async for event in iter_with_heartbeat(
323
+ stream_engine_events(engine.run(body.message, run_id), run_id)
324
+ ):
319
325
  # Heartbeat comments are yielded as raw SSE strings — pass through.
320
326
  if isinstance(event, str):
321
327
  yield event
@@ -436,8 +442,13 @@ async def v1_orchestration_resume_stream(
436
442
 
437
443
  async def event_stream():
438
444
  from core.react_engine import iter_with_heartbeat
445
+ from core.orchestration.runner import stream_engine_events
439
446
  try:
440
- async for event in iter_with_heartbeat(OrchestrationEngine.resume(run_id, human_response, _server)):
447
+ async for event in iter_with_heartbeat(
448
+ stream_engine_events(
449
+ OrchestrationEngine.resume(run_id, human_response, _server), run_id
450
+ )
451
+ ):
441
452
  # Heartbeat comments are yielded as raw SSE strings — pass through.
442
453
  if isinstance(event, str):
443
454
  yield event
@@ -39,6 +39,9 @@ async def chat(request: ChatRequest):
39
39
  final_event = event
40
40
  elif event["type"] == "error":
41
41
  return ChatResponse(response=event["message"], intent="chat", data=None, tool_name=None)
42
+ elif event["type"] == "orchestration_error":
43
+ # An orchestrator agent failed — same outcome, different event shape.
44
+ return ChatResponse(response=event["error"], intent="chat", data=None, tool_name=None)
42
45
 
43
46
  if not final_event:
44
47
  return ChatResponse(response="I completed the requested actions.", intent="chat", data=None, tool_name=None)
@@ -1,7 +1,6 @@
1
1
  """
2
2
  Orchestration management endpoints: CRUD, run, human-input, cancel.
3
3
  """
4
- import asyncio
5
4
  import os
6
5
  import json
7
6
  import time
@@ -13,14 +12,14 @@ from core.models_orchestration import Orchestration
13
12
  from core.config import DATA_DIR
14
13
  from core.json_store import JsonStore
15
14
  from core.react_engine import drain_queue_with_heartbeat
15
+ # Registry of active run tasks (for mid-step cancellation), owned by the runner
16
+ # so non-route callers can register runs too.
17
+ from core.orchestration.runner import SENTINEL, _active_tasks, spawn_engine_run
16
18
 
17
19
  router = APIRouter()
18
20
 
19
21
  _orch_store = JsonStore(os.path.join(DATA_DIR, "orchestrations.json"), cache_ttl=2.0)
20
22
 
21
- # In-memory registry of active run tasks (for mid-step cancellation)
22
- _active_tasks: dict[str, asyncio.Task] = {}
23
-
24
23
 
25
24
  def load_orchestrations() -> list[dict]:
26
25
  return _orch_store.load()
@@ -153,36 +152,14 @@ async def run_orchestration(orch_id: str, request: Request):
153
152
  from core.orchestration.engine import OrchestrationEngine
154
153
  engine = OrchestrationEngine(orch, server_module)
155
154
 
156
- queue: asyncio.Queue = asyncio.Queue()
157
- _SENTINEL = object()
158
-
159
- async def _run_engine():
160
- try:
161
- async for event in engine.run(user_input, run_id):
162
- etype = event.get("type", "")
163
- if etype not in ("chunk", "thinking", "token_usage"):
164
- print(f"DEBUG SSE QUEUE: → {etype} step={event.get('orch_step_id', '')}", flush=True)
165
- await queue.put(event)
166
- # Yield so event_stream() can dequeue and flush this event
167
- # to the HTTP response before the next event is enqueued.
168
- await asyncio.sleep(0)
169
- except asyncio.CancelledError:
170
- await queue.put({"type": "orchestration_error", "error": "Cancelled"})
171
- except Exception as e:
172
- await queue.put({"type": "orchestration_error", "error": str(e)})
173
- finally:
174
- _active_tasks.pop(run_id, None)
175
- print(f"DEBUG SSE QUEUE: sentinel sent, stream closing", flush=True)
176
- await queue.put(_SENTINEL)
177
-
178
- # Engine runs independently of SSE consumer; store task for cancellation
179
- task = asyncio.create_task(_run_engine())
180
- _active_tasks[run_id] = task
155
+ # Engine runs in its own task, independently of the SSE consumer, and is
156
+ # registered for cancellation.
157
+ _task, queue = spawn_engine_run(engine.run(user_input, run_id), run_id)
181
158
 
182
159
  async def event_stream():
183
160
  # Heartbeats keep the stream warm during long LLM/compaction steps so a
184
161
  # busy-but-alive run is not mistaken for a dropped connection.
185
- async for chunk in drain_queue_with_heartbeat(queue, _SENTINEL):
162
+ async for chunk in drain_queue_with_heartbeat(queue, SENTINEL):
186
163
  yield chunk
187
164
  yield "data: {\"type\": \"done\"}\n\n"
188
165
 
@@ -222,29 +199,14 @@ async def resume_failed_run(run_id: str, request: Request):
222
199
 
223
200
  from core.orchestration.engine import OrchestrationEngine
224
201
 
225
- queue: asyncio.Queue = asyncio.Queue()
226
- _SENTINEL = object()
227
-
228
- async def _run_engine():
229
- try:
230
- async for event in OrchestrationEngine.resume_failed(run_id, server_module):
231
- await queue.put(event)
232
- await asyncio.sleep(0)
233
- except FileNotFoundError:
234
- await queue.put({"type": "orchestration_error", "error": "Run not found"})
235
- except Exception as e:
236
- await queue.put({"type": "orchestration_error", "error": str(e)})
237
- finally:
238
- _active_tasks.pop(run_id, None)
239
- await queue.put(_SENTINEL)
240
-
241
- task = asyncio.create_task(_run_engine())
242
- _active_tasks[run_id] = task
202
+ _task, queue = spawn_engine_run(
203
+ OrchestrationEngine.resume_failed(run_id, server_module), run_id
204
+ )
243
205
 
244
206
  async def event_stream():
245
207
  # Heartbeats keep the stream warm during long LLM/compaction steps so a
246
208
  # busy-but-alive run is not mistaken for a dropped connection.
247
- async for chunk in drain_queue_with_heartbeat(queue, _SENTINEL):
209
+ async for chunk in drain_queue_with_heartbeat(queue, SENTINEL):
248
210
  yield chunk
249
211
  yield "data: {\"type\": \"done\"}\n\n"
250
212
 
@@ -341,27 +303,14 @@ async def submit_human_input(run_id: str, request: Request):
341
303
  # --- V1 in-process path (unchanged) ---
342
304
  from core.orchestration.engine import OrchestrationEngine
343
305
 
344
- queue: asyncio.Queue = asyncio.Queue()
345
- _SENTINEL = object()
346
-
347
- async def _run_engine():
348
- try:
349
- async for event in OrchestrationEngine.resume(run_id, human_response, server_module):
350
- await queue.put(event)
351
- await asyncio.sleep(0)
352
- except FileNotFoundError:
353
- await queue.put({"type": "orchestration_error", "error": "Run not found"})
354
- except Exception as e:
355
- await queue.put({"type": "orchestration_error", "error": str(e)})
356
- finally:
357
- await queue.put(_SENTINEL)
358
-
359
- asyncio.create_task(_run_engine())
306
+ _task, queue = spawn_engine_run(
307
+ OrchestrationEngine.resume(run_id, human_response, server_module), run_id
308
+ )
360
309
 
361
310
  async def event_stream():
362
311
  # Heartbeats keep the stream warm during long LLM/compaction steps so a
363
312
  # busy-but-alive run is not mistaken for a dropped connection.
364
- async for chunk in drain_queue_with_heartbeat(queue, _SENTINEL):
313
+ async for chunk in drain_queue_with_heartbeat(queue, SENTINEL):
365
314
  yield chunk
366
315
  yield "data: {\"type\": \"done\"}\n\n"
367
316
 
@@ -1,4 +1,6 @@
1
- psycopg[binary]
2
- psycopg_pool
1
+ # Coding-agent extras (code indexing). Installed alongside requirements.txt.
2
+ # Upper bounds are mandatory here too — see the header of requirements.txt.
3
+ psycopg[binary]>=3.0,<4
4
+ psycopg_pool>=3.0,<4
3
5
  cocoindex>=0.3.30,<1.0
4
- numpy
6
+ numpy>=1.26,<3
@@ -0,0 +1,167 @@
1
+ # This file was autogenerated by uv via the following command:
2
+ # uv pip compile --universal --python-version 3.11 requirements-messaging.txt --constraint requirements.lock -o requirements-messaging.lock
3
+ aiohappyeyeballs==2.7.1
4
+ # via
5
+ # -c requirements.lock
6
+ # aiohttp
7
+ aiohttp==3.14.3
8
+ # via
9
+ # -c requirements.lock
10
+ # aiohttp-retry
11
+ # botbuilder-integration-aiohttp
12
+ # discord-py
13
+ # twilio
14
+ aiohttp-retry==2.9.1
15
+ # via twilio
16
+ aiosignal==1.4.0
17
+ # via
18
+ # -c requirements.lock
19
+ # aiohttp
20
+ anyio==4.14.2
21
+ # via
22
+ # -c requirements.lock
23
+ # httpx
24
+ attrs==26.1.0
25
+ # via
26
+ # -c requirements.lock
27
+ # aiohttp
28
+ audioop-lts==0.2.2 ; python_full_version >= '3.13'
29
+ # via discord-py
30
+ azure-core==1.41.0
31
+ # via msrest
32
+ botbuilder-core==4.17.1
33
+ # via botbuilder-integration-aiohttp
34
+ botbuilder-integration-aiohttp==4.17.1
35
+ # via -r requirements-messaging.txt
36
+ botbuilder-schema==4.17.1
37
+ # via
38
+ # botbuilder-core
39
+ # botbuilder-integration-aiohttp
40
+ # botframework-connector
41
+ # botframework-streaming
42
+ botframework-connector==4.17.1
43
+ # via
44
+ # -r requirements-messaging.txt
45
+ # botbuilder-core
46
+ # botbuilder-integration-aiohttp
47
+ # botframework-streaming
48
+ botframework-streaming==4.17.1
49
+ # via botbuilder-core
50
+ certifi==2026.7.22
51
+ # via
52
+ # -c requirements.lock
53
+ # httpcore
54
+ # httpx
55
+ # msrest
56
+ # requests
57
+ cffi==2.1.1 ; platform_python_implementation != 'PyPy'
58
+ # via
59
+ # -c requirements.lock
60
+ # cryptography
61
+ charset-normalizer==3.4.9
62
+ # via
63
+ # -c requirements.lock
64
+ # requests
65
+ cryptography==50.0.0
66
+ # via
67
+ # -c requirements.lock
68
+ # msal
69
+ # pyjwt
70
+ discord-py==2.7.1
71
+ # via -r requirements-messaging.txt
72
+ frozenlist==1.8.0
73
+ # via
74
+ # -c requirements.lock
75
+ # aiohttp
76
+ # aiosignal
77
+ h11==0.16.0
78
+ # via
79
+ # -c requirements.lock
80
+ # httpcore
81
+ httpcore==1.0.9
82
+ # via
83
+ # -c requirements.lock
84
+ # httpx
85
+ # python-telegram-bot
86
+ httpx==0.28.1
87
+ # via
88
+ # -c requirements.lock
89
+ # python-telegram-bot
90
+ idna==3.18
91
+ # via
92
+ # -c requirements.lock
93
+ # anyio
94
+ # httpx
95
+ # requests
96
+ # yarl
97
+ isodate==0.7.2
98
+ # via msrest
99
+ jsonpickle==3.4.2
100
+ # via botbuilder-core
101
+ msal==1.37.0
102
+ # via botframework-connector
103
+ msrest==0.7.1
104
+ # via
105
+ # botbuilder-schema
106
+ # botframework-connector
107
+ multidict==6.7.1
108
+ # via
109
+ # -c requirements.lock
110
+ # aiohttp
111
+ # yarl
112
+ oauthlib==3.3.1
113
+ # via
114
+ # -c requirements.lock
115
+ # requests-oauthlib
116
+ propcache==0.5.2
117
+ # via
118
+ # -c requirements.lock
119
+ # aiohttp
120
+ # yarl
121
+ pycparser==3.0 ; implementation_name != 'PyPy' and platform_python_implementation != 'PyPy'
122
+ # via
123
+ # -c requirements.lock
124
+ # cffi
125
+ pyjwt==2.13.0
126
+ # via
127
+ # -c requirements.lock
128
+ # botframework-connector
129
+ # msal
130
+ # twilio
131
+ python-telegram-bot==22.8
132
+ # via -r requirements-messaging.txt
133
+ requests==2.34.2
134
+ # via
135
+ # -c requirements.lock
136
+ # azure-core
137
+ # msal
138
+ # msrest
139
+ # requests-oauthlib
140
+ # twilio
141
+ requests-oauthlib==2.0.0
142
+ # via
143
+ # -c requirements.lock
144
+ # msrest
145
+ slack-bolt==1.30.0
146
+ # via -r requirements-messaging.txt
147
+ slack-sdk==3.43.0
148
+ # via slack-bolt
149
+ twilio==9.10.9
150
+ # via -r requirements-messaging.txt
151
+ typing-extensions==4.16.0
152
+ # via
153
+ # -c requirements.lock
154
+ # aiohttp
155
+ # aiosignal
156
+ # anyio
157
+ # azure-core
158
+ urllib3==2.7.0
159
+ # via
160
+ # -c requirements.lock
161
+ # botbuilder-schema
162
+ # requests
163
+ yarl==1.24.5
164
+ # via
165
+ # -c requirements.lock
166
+ # aiohttp
167
+ # botbuilder-integration-aiohttp
@@ -1,6 +1,8 @@
1
- python-telegram-bot>=20.0
2
- discord.py>=2.3
3
- slack-bolt>=1.18
4
- botframework-connector>=4.15
5
- botbuilder-integration-aiohttp>=4.15
6
- twilio>=8.0
1
+ # Messaging-channel extras (Telegram / Discord / Slack / Teams / Twilio).
2
+ # Upper bounds are mandatory here too — see the header of requirements.txt.
3
+ python-telegram-bot>=20.0,<23
4
+ discord.py>=2.3,<3
5
+ slack-bolt>=1.18,<2
6
+ botframework-connector>=4.15,<5
7
+ botbuilder-integration-aiohttp>=4.15,<5
8
+ twilio>=8.0,<10