aethergraph 0.1.0a1__py3-none-any.whl

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 (182) hide show
  1. aethergraph/__init__.py +49 -0
  2. aethergraph/config/__init__.py +0 -0
  3. aethergraph/config/config.py +121 -0
  4. aethergraph/config/context.py +16 -0
  5. aethergraph/config/llm.py +26 -0
  6. aethergraph/config/loader.py +60 -0
  7. aethergraph/config/runtime.py +9 -0
  8. aethergraph/contracts/errors/errors.py +44 -0
  9. aethergraph/contracts/services/artifacts.py +142 -0
  10. aethergraph/contracts/services/channel.py +72 -0
  11. aethergraph/contracts/services/continuations.py +23 -0
  12. aethergraph/contracts/services/eventbus.py +12 -0
  13. aethergraph/contracts/services/kv.py +24 -0
  14. aethergraph/contracts/services/llm.py +17 -0
  15. aethergraph/contracts/services/mcp.py +22 -0
  16. aethergraph/contracts/services/memory.py +108 -0
  17. aethergraph/contracts/services/resume.py +28 -0
  18. aethergraph/contracts/services/state_stores.py +33 -0
  19. aethergraph/contracts/services/wakeup.py +28 -0
  20. aethergraph/core/execution/base_scheduler.py +77 -0
  21. aethergraph/core/execution/forward_scheduler.py +777 -0
  22. aethergraph/core/execution/global_scheduler.py +634 -0
  23. aethergraph/core/execution/retry_policy.py +22 -0
  24. aethergraph/core/execution/step_forward.py +411 -0
  25. aethergraph/core/execution/step_result.py +18 -0
  26. aethergraph/core/execution/wait_types.py +72 -0
  27. aethergraph/core/graph/graph_builder.py +192 -0
  28. aethergraph/core/graph/graph_fn.py +219 -0
  29. aethergraph/core/graph/graph_io.py +67 -0
  30. aethergraph/core/graph/graph_refs.py +154 -0
  31. aethergraph/core/graph/graph_spec.py +115 -0
  32. aethergraph/core/graph/graph_state.py +59 -0
  33. aethergraph/core/graph/graphify.py +128 -0
  34. aethergraph/core/graph/interpreter.py +145 -0
  35. aethergraph/core/graph/node_handle.py +33 -0
  36. aethergraph/core/graph/node_spec.py +46 -0
  37. aethergraph/core/graph/node_state.py +63 -0
  38. aethergraph/core/graph/task_graph.py +747 -0
  39. aethergraph/core/graph/task_node.py +82 -0
  40. aethergraph/core/graph/utils.py +37 -0
  41. aethergraph/core/graph/visualize.py +239 -0
  42. aethergraph/core/runtime/ad_hoc_context.py +61 -0
  43. aethergraph/core/runtime/base_service.py +153 -0
  44. aethergraph/core/runtime/bind_adapter.py +42 -0
  45. aethergraph/core/runtime/bound_memory.py +69 -0
  46. aethergraph/core/runtime/execution_context.py +220 -0
  47. aethergraph/core/runtime/graph_runner.py +349 -0
  48. aethergraph/core/runtime/lifecycle.py +26 -0
  49. aethergraph/core/runtime/node_context.py +203 -0
  50. aethergraph/core/runtime/node_services.py +30 -0
  51. aethergraph/core/runtime/recovery.py +159 -0
  52. aethergraph/core/runtime/run_registration.py +33 -0
  53. aethergraph/core/runtime/runtime_env.py +157 -0
  54. aethergraph/core/runtime/runtime_registry.py +32 -0
  55. aethergraph/core/runtime/runtime_services.py +224 -0
  56. aethergraph/core/runtime/wakeup_watcher.py +40 -0
  57. aethergraph/core/tools/__init__.py +10 -0
  58. aethergraph/core/tools/builtins/channel_tools.py +194 -0
  59. aethergraph/core/tools/builtins/toolset.py +134 -0
  60. aethergraph/core/tools/toolkit.py +510 -0
  61. aethergraph/core/tools/waitable.py +109 -0
  62. aethergraph/plugins/channel/__init__.py +0 -0
  63. aethergraph/plugins/channel/adapters/__init__.py +0 -0
  64. aethergraph/plugins/channel/adapters/console.py +106 -0
  65. aethergraph/plugins/channel/adapters/file.py +102 -0
  66. aethergraph/plugins/channel/adapters/slack.py +285 -0
  67. aethergraph/plugins/channel/adapters/telegram.py +302 -0
  68. aethergraph/plugins/channel/adapters/webhook.py +104 -0
  69. aethergraph/plugins/channel/adapters/webui.py +134 -0
  70. aethergraph/plugins/channel/routes/__init__.py +0 -0
  71. aethergraph/plugins/channel/routes/console_routes.py +86 -0
  72. aethergraph/plugins/channel/routes/slack_routes.py +49 -0
  73. aethergraph/plugins/channel/routes/telegram_routes.py +26 -0
  74. aethergraph/plugins/channel/routes/webui_routes.py +136 -0
  75. aethergraph/plugins/channel/utils/__init__.py +0 -0
  76. aethergraph/plugins/channel/utils/slack_utils.py +278 -0
  77. aethergraph/plugins/channel/utils/telegram_utils.py +324 -0
  78. aethergraph/plugins/channel/websockets/slack_ws.py +68 -0
  79. aethergraph/plugins/channel/websockets/telegram_polling.py +151 -0
  80. aethergraph/plugins/mcp/fs_server.py +128 -0
  81. aethergraph/plugins/mcp/http_server.py +101 -0
  82. aethergraph/plugins/mcp/ws_server.py +180 -0
  83. aethergraph/plugins/net/http.py +10 -0
  84. aethergraph/plugins/utils/data_io.py +359 -0
  85. aethergraph/runner/__init__.py +5 -0
  86. aethergraph/runtime/__init__.py +62 -0
  87. aethergraph/server/__init__.py +3 -0
  88. aethergraph/server/app_factory.py +84 -0
  89. aethergraph/server/start.py +122 -0
  90. aethergraph/services/__init__.py +10 -0
  91. aethergraph/services/artifacts/facade.py +284 -0
  92. aethergraph/services/artifacts/factory.py +35 -0
  93. aethergraph/services/artifacts/fs_store.py +656 -0
  94. aethergraph/services/artifacts/jsonl_index.py +123 -0
  95. aethergraph/services/artifacts/paths.py +23 -0
  96. aethergraph/services/artifacts/sqlite_index.py +209 -0
  97. aethergraph/services/artifacts/utils.py +124 -0
  98. aethergraph/services/auth/dev.py +16 -0
  99. aethergraph/services/channel/channel_bus.py +293 -0
  100. aethergraph/services/channel/factory.py +44 -0
  101. aethergraph/services/channel/session.py +511 -0
  102. aethergraph/services/channel/wait_helpers.py +57 -0
  103. aethergraph/services/clock/clock.py +9 -0
  104. aethergraph/services/container/default_container.py +320 -0
  105. aethergraph/services/continuations/continuation.py +56 -0
  106. aethergraph/services/continuations/factory.py +34 -0
  107. aethergraph/services/continuations/stores/fs_store.py +264 -0
  108. aethergraph/services/continuations/stores/inmem_store.py +95 -0
  109. aethergraph/services/eventbus/inmem.py +21 -0
  110. aethergraph/services/features/static.py +10 -0
  111. aethergraph/services/kv/ephemeral.py +90 -0
  112. aethergraph/services/kv/factory.py +27 -0
  113. aethergraph/services/kv/layered.py +41 -0
  114. aethergraph/services/kv/sqlite_kv.py +128 -0
  115. aethergraph/services/llm/factory.py +157 -0
  116. aethergraph/services/llm/generic_client.py +542 -0
  117. aethergraph/services/llm/providers.py +3 -0
  118. aethergraph/services/llm/service.py +105 -0
  119. aethergraph/services/logger/base.py +36 -0
  120. aethergraph/services/logger/compat.py +50 -0
  121. aethergraph/services/logger/formatters.py +106 -0
  122. aethergraph/services/logger/std.py +203 -0
  123. aethergraph/services/mcp/helpers.py +23 -0
  124. aethergraph/services/mcp/http_client.py +70 -0
  125. aethergraph/services/mcp/mcp_tools.py +21 -0
  126. aethergraph/services/mcp/registry.py +14 -0
  127. aethergraph/services/mcp/service.py +100 -0
  128. aethergraph/services/mcp/stdio_client.py +70 -0
  129. aethergraph/services/mcp/ws_client.py +115 -0
  130. aethergraph/services/memory/bound.py +106 -0
  131. aethergraph/services/memory/distillers/episode.py +116 -0
  132. aethergraph/services/memory/distillers/rolling.py +74 -0
  133. aethergraph/services/memory/facade.py +633 -0
  134. aethergraph/services/memory/factory.py +78 -0
  135. aethergraph/services/memory/hotlog_kv.py +27 -0
  136. aethergraph/services/memory/indices.py +74 -0
  137. aethergraph/services/memory/io_helpers.py +72 -0
  138. aethergraph/services/memory/persist_fs.py +40 -0
  139. aethergraph/services/memory/resolver.py +152 -0
  140. aethergraph/services/metering/noop.py +4 -0
  141. aethergraph/services/prompts/file_store.py +41 -0
  142. aethergraph/services/rag/chunker.py +29 -0
  143. aethergraph/services/rag/facade.py +593 -0
  144. aethergraph/services/rag/index/base.py +27 -0
  145. aethergraph/services/rag/index/faiss_index.py +121 -0
  146. aethergraph/services/rag/index/sqlite_index.py +134 -0
  147. aethergraph/services/rag/index_factory.py +52 -0
  148. aethergraph/services/rag/parsers/md.py +7 -0
  149. aethergraph/services/rag/parsers/pdf.py +14 -0
  150. aethergraph/services/rag/parsers/txt.py +7 -0
  151. aethergraph/services/rag/utils/hybrid.py +39 -0
  152. aethergraph/services/rag/utils/make_fs_key.py +62 -0
  153. aethergraph/services/redactor/simple.py +16 -0
  154. aethergraph/services/registry/key_parsing.py +44 -0
  155. aethergraph/services/registry/registry_key.py +19 -0
  156. aethergraph/services/registry/unified_registry.py +185 -0
  157. aethergraph/services/resume/multi_scheduler_resume_bus.py +65 -0
  158. aethergraph/services/resume/router.py +73 -0
  159. aethergraph/services/schedulers/registry.py +41 -0
  160. aethergraph/services/secrets/base.py +7 -0
  161. aethergraph/services/secrets/env.py +8 -0
  162. aethergraph/services/state_stores/externalize.py +135 -0
  163. aethergraph/services/state_stores/graph_observer.py +131 -0
  164. aethergraph/services/state_stores/json_store.py +67 -0
  165. aethergraph/services/state_stores/resume_policy.py +119 -0
  166. aethergraph/services/state_stores/serialize.py +249 -0
  167. aethergraph/services/state_stores/utils.py +91 -0
  168. aethergraph/services/state_stores/validate.py +78 -0
  169. aethergraph/services/tracing/noop.py +18 -0
  170. aethergraph/services/waits/wait_registry.py +91 -0
  171. aethergraph/services/wakeup/memory_queue.py +57 -0
  172. aethergraph/services/wakeup/scanner_producer.py +56 -0
  173. aethergraph/services/wakeup/worker.py +31 -0
  174. aethergraph/tools/__init__.py +25 -0
  175. aethergraph/utils/optdeps.py +8 -0
  176. aethergraph-0.1.0a1.dist-info/METADATA +410 -0
  177. aethergraph-0.1.0a1.dist-info/RECORD +182 -0
  178. aethergraph-0.1.0a1.dist-info/WHEEL +5 -0
  179. aethergraph-0.1.0a1.dist-info/entry_points.txt +2 -0
  180. aethergraph-0.1.0a1.dist-info/licenses/LICENSE +176 -0
  181. aethergraph-0.1.0a1.dist-info/licenses/NOTICE +31 -0
  182. aethergraph-0.1.0a1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,293 @@
1
+ from __future__ import annotations
2
+
3
+ import warnings
4
+
5
+ from aethergraph.contracts.services.channel import Button, ChannelAdapter, OutEvent
6
+ from aethergraph.services.continuations.continuation import Correlator
7
+
8
+
9
+ class ChannelBus:
10
+ """
11
+ Transport layer:
12
+ - publish(event) : send any OutEvent with smart fallbacks
13
+ - notify(cont) : raise a prompt from a Continuation; inline-resume if adapter can read input
14
+ - peek_correlator(channel_key): ask adapter for a thread hint (optional)
15
+ Optionally aware of:
16
+ - resume_router : used for inline resume (console/local-web)
17
+ - store : used to bind correlator↔token and to mint short resume_key
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ adapters: dict[str, ChannelAdapter],
23
+ *,
24
+ default_channel: str = "console:stdin",
25
+ channel_aliases: dict[str, str] | None = None,
26
+ logger=None,
27
+ resume_router=None,
28
+ store=None,
29
+ ):
30
+ self.adapters = dict(adapters)
31
+ self.default_channel = default_channel
32
+ self.logger = logger
33
+ self.resume_router = resume_router
34
+ self.store = store
35
+ self.channel_aliases: dict[str, str] = dict(channel_aliases or {})
36
+
37
+ # ---- admin ----
38
+ def register_adapter(self, prefix: str, adapter: ChannelAdapter) -> None:
39
+ self.adapters[prefix] = adapter
40
+
41
+ def set_default_channel_key(self, channel_key: str) -> None:
42
+ self.default_channel = channel_key
43
+
44
+ def get_default_channel_key(self) -> str:
45
+ return self.default_channel
46
+
47
+ def register_alias(self, alias: str, target: str) -> None:
48
+ """Register or overwrite a human-friendly alias -> canonical key."""
49
+ if self._prefix(target) not in self.adapters:
50
+ raise RuntimeError(f"Cannot alias to unknown channel prefix: {self._prefix(target)}")
51
+
52
+ self.channel_aliases[alias] = target
53
+
54
+ def resolve_channel_key(self, key: str) -> str:
55
+ """
56
+ Resolve a channel key via the alias map.
57
+ If `key` matches an alias exactly, return the mapped canonical key;
58
+ otherwise return `key` as-is.
59
+ """
60
+ return self.channel_aliases.get(key, key)
61
+
62
+ # ---- internals ----
63
+ def _prefix(self, channel_key: str) -> str:
64
+ return channel_key.split(":", 1)[0]
65
+
66
+ def _pick(self, channel_key: str) -> ChannelAdapter:
67
+ # IMPORTANT: resolve aliases *before* looking up adapter
68
+ resolved_key = self.resolve_channel_key(channel_key)
69
+ prefix = self._prefix(resolved_key)
70
+ if prefix not in self.adapters:
71
+ raise RuntimeError(
72
+ f"No adapter for prefix={prefix}; known: {list(self.adapters.keys())}; Check if you have enabled the required channel service in .env and registered the adapter."
73
+ )
74
+ return self.adapters[prefix]
75
+
76
+ def _warn(self, msg: str) -> None:
77
+ if self.logger:
78
+ self.logger.warning(msg)
79
+ else:
80
+ warnings.warn(msg, stacklevel=2)
81
+
82
+ async def _bind_correlator_if_any(self, event: OutEvent, send_result: dict | None):
83
+ if not self.store or not send_result:
84
+ return
85
+ corr = send_result.get("correlator")
86
+ token = (event.meta or {}).get("token")
87
+ if isinstance(corr, Correlator) and token:
88
+ try:
89
+ await self.store.bind_correlator(token=token, corr=corr)
90
+ except Exception as e:
91
+ self._warn(f"Failed to bind correlator: {e}")
92
+
93
+ def _smart_fallback(self, adapter: ChannelAdapter, event: OutEvent) -> OutEvent | None:
94
+ # Determine required capability for the event type
95
+ need = None
96
+ if event.type in (
97
+ "agent.message",
98
+ "agent.message.update",
99
+ "session.waiting",
100
+ "session.need_input",
101
+ ):
102
+ need = "text"
103
+ elif event.type in ("agent.stream.start", "agent.stream.delta", "agent.stream.end"):
104
+ need = "stream"
105
+ elif event.type in ("session.need_approval", "link.buttons"):
106
+ need = "buttons"
107
+ elif event.type == "file.upload":
108
+ need = "file"
109
+
110
+ caps: set[str] = getattr(adapter, "capabilities", set())
111
+
112
+ # Supported as-is
113
+ if (need is None) or (need in caps):
114
+ return event
115
+
116
+ # buttons → text (numbered list)
117
+ if need == "buttons" and "text" in caps:
118
+ opts = []
119
+ if event.buttons:
120
+ for b in event.buttons:
121
+ lbl = (
122
+ getattr(b, "label", None)
123
+ or str(getattr(b, "value", "") or "").title()
124
+ or "Option"
125
+ )
126
+ val = getattr(b, "value", None) or str(lbl).lower()
127
+ opts.append({"label": str(lbl), "value": str(val)})
128
+ else:
129
+ for o in (event.meta or {}).get("options", []):
130
+ s = str(o)
131
+ opts.append({"label": s, "value": s.lower()})
132
+ if not opts:
133
+ opts = [
134
+ {"label": "Approve", "value": "approve"},
135
+ {"label": "Reject", "value": "reject"},
136
+ ]
137
+ lines = [f"{i + 1}. {o['label']}" for i, o in enumerate(opts)]
138
+ hint = "Reply with the number or the label."
139
+ txt = (event.text or "Choose an option:") + "\n" + "\n".join(lines) + f"\n{hint}"
140
+ meta = dict(event.meta or {})
141
+ meta["options"] = [o["label"] for o in opts]
142
+ meta["options_map"] = {str(i + 1): o["value"] for i, o in enumerate(opts)}
143
+ meta["options_label_to_value"] = {o["label"].lower(): o["value"] for o in opts}
144
+ return OutEvent(type="agent.message", channel=event.channel, text=txt, meta=meta)
145
+
146
+ # stream → text
147
+ if need == "stream" and "text" in caps:
148
+ if event.type == "agent.stream.delta":
149
+ return OutEvent(
150
+ type="agent.message",
151
+ channel=event.channel,
152
+ text=event.text or "",
153
+ meta=event.meta,
154
+ )
155
+ return None # drop start/end
156
+
157
+ # file → text link if available
158
+ if need == "file" and "text" in caps:
159
+ if event.file and "url" in event.file:
160
+ return OutEvent(
161
+ type="agent.message",
162
+ channel=event.channel,
163
+ text=f"[file] {event.file.get('filename', 'file')}: {event.file['url']}",
164
+ meta=event.meta,
165
+ )
166
+ self._warn("Binary file not representable on this adapter.")
167
+ return None
168
+
169
+ self._warn(f"Adapter lacks '{need}', dropping event type={event.type}.")
170
+ return None
171
+
172
+ # ---- core send path ----
173
+ async def publish(self, event: OutEvent) -> dict | None:
174
+ """
175
+ Send any OutEvent; apply smart fallbacks; bind correlator if adapter returns one.
176
+ No inline resume here (use notify for interactions).
177
+ """
178
+ adapter = self._pick(event.channel)
179
+ evt = self._smart_fallback(adapter, event)
180
+ if evt is None:
181
+ return None
182
+ res = await adapter.send(evt)
183
+ await self._bind_correlator_if_any(evt, res)
184
+ return res
185
+
186
+ # ---- continuation-aware notify (used by ChannelSession.ask_*) ----
187
+ async def notify(self, continuation) -> dict | None:
188
+ """
189
+ Present a prompt for a Continuation, returning either:
190
+ - {"payload": {...}} for inline adapters (console/local-web), or
191
+ - {"correlator": Correlator(...)} for push-only adapters (Slack/Telegram).
192
+ Never calls resume_router here; ChannelSession owns the wait/inline short-circuit.
193
+ """
194
+ ch = continuation.channel
195
+ kind = continuation.kind
196
+ prompt = continuation.prompt
197
+
198
+ # Short token for constrained transports
199
+ resume_key = None
200
+ if self.store and hasattr(self.store, "alias_for"):
201
+ try:
202
+ resume_key = await self.store.alias_for(continuation.token)
203
+ except Exception:
204
+ resume_key = None
205
+ if not resume_key:
206
+ resume_key = str(continuation.token)[:24]
207
+
208
+ meta = {
209
+ "run_id": continuation.run_id,
210
+ "node_id": continuation.node_id,
211
+ "token": continuation.token,
212
+ "resume_key": resume_key,
213
+ }
214
+
215
+ # Shape event
216
+ if kind == "user_input":
217
+ silent = False
218
+ if hasattr(continuation, "payload"):
219
+ silent = continuation.payload.get("_silent", False)
220
+
221
+ txt = prompt if isinstance(prompt, str) else None
222
+
223
+ if silent and not txt:
224
+ # Silent wait: don't emit a session.need_input event at all.
225
+ # Just return {} so ChannelSession will rely on the normal wait/resolve path.
226
+ meta["_prompt"] = False
227
+ return {}
228
+
229
+ # Normal ask_text path
230
+ txt = txt or "Please reply."
231
+ meta["_prompt"] = True
232
+ event = OutEvent(type="session.need_input", channel=ch, text=txt, meta=meta)
233
+ needed_cap = "input"
234
+
235
+ elif kind == "approval":
236
+ labels: list[str] = []
237
+ if isinstance(prompt, dict):
238
+ txt = prompt.get("title") or prompt.get("prompt") or "Approve?"
239
+ labels = prompt.get("buttons") or prompt.get("options") or []
240
+ elif isinstance(prompt, str):
241
+ txt = prompt or "Approve?"
242
+ else:
243
+ txt = "Approve?"
244
+ if not labels:
245
+ labels = ["Approve", "Reject"]
246
+ btns = [Button(label=str(lab), value=str(lab).lower()) for lab in labels]
247
+ meta["options"] = labels
248
+ meta["_prompt"] = True
249
+ event = OutEvent(
250
+ type="session.need_approval", channel=ch, text=txt, buttons=btns, meta=meta
251
+ )
252
+ needed_cap = "buttons"
253
+
254
+ elif kind in ("user_files", "user_input_or_files"):
255
+ # Console has no uploads; treat as text input. Other adapters may enhance later.
256
+ txt = prompt if isinstance(prompt, str) else (prompt or "Please reply.")
257
+ meta["_prompt"] = True
258
+ event = OutEvent(type="session.need_input", channel=ch, text=txt, meta=meta)
259
+ needed_cap = "input"
260
+
261
+ else:
262
+ txt = str(prompt) if isinstance(prompt, str) else "Waiting…"
263
+ return await self.publish(
264
+ OutEvent(type="session.waiting", channel=ch, text=txt, meta=meta)
265
+ )
266
+
267
+ # Inline vs push-only
268
+ adapter = self._pick(ch)
269
+ caps = getattr(adapter, "capabilities", set())
270
+
271
+ force_push = False
272
+ if isinstance(prompt, dict):
273
+ force_push = bool(prompt.get("_force_push"))
274
+ if (needed_cap in caps) and not force_push:
275
+ # Inline path
276
+ res = await adapter.send(event)
277
+ await self._bind_correlator_if_any(event, res)
278
+ return res
279
+
280
+ # Push-only path
281
+ return await self.publish(event)
282
+
283
+ # ---- optional: ask adapter for correlator/“thread” without sending ----
284
+ async def peek_correlator(self, channel_key: str) -> Correlator | None:
285
+ adapter = self._pick(channel_key)
286
+ scheme = self._prefix(channel_key)
287
+ thread_ts = None
288
+ if hasattr(adapter, "peek_thread"):
289
+ try:
290
+ thread_ts = await adapter.peek_thread(channel_key)
291
+ except Exception:
292
+ thread_ts = None
293
+ return Correlator(scheme=scheme, channel=channel_key, thread=thread_ts, message=None)
@@ -0,0 +1,44 @@
1
+ # channels/factory.py
2
+ import os
3
+ from typing import Any
4
+
5
+ from aethergraph.config.config import AppSettings
6
+ from aethergraph.plugins.channel.adapters.console import ConsoleChannelAdapter
7
+ from aethergraph.plugins.channel.adapters.file import FileChannelAdapter
8
+ from aethergraph.plugins.channel.adapters.slack import SlackChannelAdapter
9
+ from aethergraph.plugins.channel.adapters.telegram import TelegramChannelAdapter
10
+ from aethergraph.plugins.channel.adapters.webhook import WebhookChannelAdapter
11
+ from aethergraph.services.channel.channel_bus import ChannelBus
12
+
13
+
14
+ def make_channel_adapters_from_env(cfg: AppSettings) -> dict[str, Any]:
15
+ # Always include console adapter
16
+ adapters = {"console": ConsoleChannelAdapter()}
17
+
18
+ # include Slack adapter if enabled
19
+ if cfg.slack.enabled and cfg.slack.bot_token and cfg.slack.signing_secret:
20
+ adapters["slack"] = SlackChannelAdapter(bot_token=cfg.slack.bot_token.get_secret_value())
21
+
22
+ # include Telegram adapter if enabled
23
+ if cfg.telegram.enabled and cfg.telegram.bot_token:
24
+ adapters["tg"] = TelegramChannelAdapter(bot_token=cfg.telegram.bot_token.get_secret_value())
25
+
26
+ # include default file adapter
27
+ file_root = os.path.join(cfg.root, "channel_files")
28
+ adapters["file"] = FileChannelAdapter(root=file_root)
29
+
30
+ # include webhook adapter
31
+ adapters["webhook"] = WebhookChannelAdapter()
32
+ return adapters
33
+
34
+
35
+ def build_bus(
36
+ adapters: dict[str, Any],
37
+ default: str = "console:stdin",
38
+ logger=None,
39
+ resume_router=None,
40
+ cont_store=None,
41
+ ) -> ChannelBus:
42
+ bus = ChannelBus(adapters, logger=logger, resume_router=resume_router, store=cont_store)
43
+ bus.set_default_channel_key(default)
44
+ return bus