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.
- aethergraph/__init__.py +49 -0
- aethergraph/config/__init__.py +0 -0
- aethergraph/config/config.py +121 -0
- aethergraph/config/context.py +16 -0
- aethergraph/config/llm.py +26 -0
- aethergraph/config/loader.py +60 -0
- aethergraph/config/runtime.py +9 -0
- aethergraph/contracts/errors/errors.py +44 -0
- aethergraph/contracts/services/artifacts.py +142 -0
- aethergraph/contracts/services/channel.py +72 -0
- aethergraph/contracts/services/continuations.py +23 -0
- aethergraph/contracts/services/eventbus.py +12 -0
- aethergraph/contracts/services/kv.py +24 -0
- aethergraph/contracts/services/llm.py +17 -0
- aethergraph/contracts/services/mcp.py +22 -0
- aethergraph/contracts/services/memory.py +108 -0
- aethergraph/contracts/services/resume.py +28 -0
- aethergraph/contracts/services/state_stores.py +33 -0
- aethergraph/contracts/services/wakeup.py +28 -0
- aethergraph/core/execution/base_scheduler.py +77 -0
- aethergraph/core/execution/forward_scheduler.py +777 -0
- aethergraph/core/execution/global_scheduler.py +634 -0
- aethergraph/core/execution/retry_policy.py +22 -0
- aethergraph/core/execution/step_forward.py +411 -0
- aethergraph/core/execution/step_result.py +18 -0
- aethergraph/core/execution/wait_types.py +72 -0
- aethergraph/core/graph/graph_builder.py +192 -0
- aethergraph/core/graph/graph_fn.py +219 -0
- aethergraph/core/graph/graph_io.py +67 -0
- aethergraph/core/graph/graph_refs.py +154 -0
- aethergraph/core/graph/graph_spec.py +115 -0
- aethergraph/core/graph/graph_state.py +59 -0
- aethergraph/core/graph/graphify.py +128 -0
- aethergraph/core/graph/interpreter.py +145 -0
- aethergraph/core/graph/node_handle.py +33 -0
- aethergraph/core/graph/node_spec.py +46 -0
- aethergraph/core/graph/node_state.py +63 -0
- aethergraph/core/graph/task_graph.py +747 -0
- aethergraph/core/graph/task_node.py +82 -0
- aethergraph/core/graph/utils.py +37 -0
- aethergraph/core/graph/visualize.py +239 -0
- aethergraph/core/runtime/ad_hoc_context.py +61 -0
- aethergraph/core/runtime/base_service.py +153 -0
- aethergraph/core/runtime/bind_adapter.py +42 -0
- aethergraph/core/runtime/bound_memory.py +69 -0
- aethergraph/core/runtime/execution_context.py +220 -0
- aethergraph/core/runtime/graph_runner.py +349 -0
- aethergraph/core/runtime/lifecycle.py +26 -0
- aethergraph/core/runtime/node_context.py +203 -0
- aethergraph/core/runtime/node_services.py +30 -0
- aethergraph/core/runtime/recovery.py +159 -0
- aethergraph/core/runtime/run_registration.py +33 -0
- aethergraph/core/runtime/runtime_env.py +157 -0
- aethergraph/core/runtime/runtime_registry.py +32 -0
- aethergraph/core/runtime/runtime_services.py +224 -0
- aethergraph/core/runtime/wakeup_watcher.py +40 -0
- aethergraph/core/tools/__init__.py +10 -0
- aethergraph/core/tools/builtins/channel_tools.py +194 -0
- aethergraph/core/tools/builtins/toolset.py +134 -0
- aethergraph/core/tools/toolkit.py +510 -0
- aethergraph/core/tools/waitable.py +109 -0
- aethergraph/plugins/channel/__init__.py +0 -0
- aethergraph/plugins/channel/adapters/__init__.py +0 -0
- aethergraph/plugins/channel/adapters/console.py +106 -0
- aethergraph/plugins/channel/adapters/file.py +102 -0
- aethergraph/plugins/channel/adapters/slack.py +285 -0
- aethergraph/plugins/channel/adapters/telegram.py +302 -0
- aethergraph/plugins/channel/adapters/webhook.py +104 -0
- aethergraph/plugins/channel/adapters/webui.py +134 -0
- aethergraph/plugins/channel/routes/__init__.py +0 -0
- aethergraph/plugins/channel/routes/console_routes.py +86 -0
- aethergraph/plugins/channel/routes/slack_routes.py +49 -0
- aethergraph/plugins/channel/routes/telegram_routes.py +26 -0
- aethergraph/plugins/channel/routes/webui_routes.py +136 -0
- aethergraph/plugins/channel/utils/__init__.py +0 -0
- aethergraph/plugins/channel/utils/slack_utils.py +278 -0
- aethergraph/plugins/channel/utils/telegram_utils.py +324 -0
- aethergraph/plugins/channel/websockets/slack_ws.py +68 -0
- aethergraph/plugins/channel/websockets/telegram_polling.py +151 -0
- aethergraph/plugins/mcp/fs_server.py +128 -0
- aethergraph/plugins/mcp/http_server.py +101 -0
- aethergraph/plugins/mcp/ws_server.py +180 -0
- aethergraph/plugins/net/http.py +10 -0
- aethergraph/plugins/utils/data_io.py +359 -0
- aethergraph/runner/__init__.py +5 -0
- aethergraph/runtime/__init__.py +62 -0
- aethergraph/server/__init__.py +3 -0
- aethergraph/server/app_factory.py +84 -0
- aethergraph/server/start.py +122 -0
- aethergraph/services/__init__.py +10 -0
- aethergraph/services/artifacts/facade.py +284 -0
- aethergraph/services/artifacts/factory.py +35 -0
- aethergraph/services/artifacts/fs_store.py +656 -0
- aethergraph/services/artifacts/jsonl_index.py +123 -0
- aethergraph/services/artifacts/paths.py +23 -0
- aethergraph/services/artifacts/sqlite_index.py +209 -0
- aethergraph/services/artifacts/utils.py +124 -0
- aethergraph/services/auth/dev.py +16 -0
- aethergraph/services/channel/channel_bus.py +293 -0
- aethergraph/services/channel/factory.py +44 -0
- aethergraph/services/channel/session.py +511 -0
- aethergraph/services/channel/wait_helpers.py +57 -0
- aethergraph/services/clock/clock.py +9 -0
- aethergraph/services/container/default_container.py +320 -0
- aethergraph/services/continuations/continuation.py +56 -0
- aethergraph/services/continuations/factory.py +34 -0
- aethergraph/services/continuations/stores/fs_store.py +264 -0
- aethergraph/services/continuations/stores/inmem_store.py +95 -0
- aethergraph/services/eventbus/inmem.py +21 -0
- aethergraph/services/features/static.py +10 -0
- aethergraph/services/kv/ephemeral.py +90 -0
- aethergraph/services/kv/factory.py +27 -0
- aethergraph/services/kv/layered.py +41 -0
- aethergraph/services/kv/sqlite_kv.py +128 -0
- aethergraph/services/llm/factory.py +157 -0
- aethergraph/services/llm/generic_client.py +542 -0
- aethergraph/services/llm/providers.py +3 -0
- aethergraph/services/llm/service.py +105 -0
- aethergraph/services/logger/base.py +36 -0
- aethergraph/services/logger/compat.py +50 -0
- aethergraph/services/logger/formatters.py +106 -0
- aethergraph/services/logger/std.py +203 -0
- aethergraph/services/mcp/helpers.py +23 -0
- aethergraph/services/mcp/http_client.py +70 -0
- aethergraph/services/mcp/mcp_tools.py +21 -0
- aethergraph/services/mcp/registry.py +14 -0
- aethergraph/services/mcp/service.py +100 -0
- aethergraph/services/mcp/stdio_client.py +70 -0
- aethergraph/services/mcp/ws_client.py +115 -0
- aethergraph/services/memory/bound.py +106 -0
- aethergraph/services/memory/distillers/episode.py +116 -0
- aethergraph/services/memory/distillers/rolling.py +74 -0
- aethergraph/services/memory/facade.py +633 -0
- aethergraph/services/memory/factory.py +78 -0
- aethergraph/services/memory/hotlog_kv.py +27 -0
- aethergraph/services/memory/indices.py +74 -0
- aethergraph/services/memory/io_helpers.py +72 -0
- aethergraph/services/memory/persist_fs.py +40 -0
- aethergraph/services/memory/resolver.py +152 -0
- aethergraph/services/metering/noop.py +4 -0
- aethergraph/services/prompts/file_store.py +41 -0
- aethergraph/services/rag/chunker.py +29 -0
- aethergraph/services/rag/facade.py +593 -0
- aethergraph/services/rag/index/base.py +27 -0
- aethergraph/services/rag/index/faiss_index.py +121 -0
- aethergraph/services/rag/index/sqlite_index.py +134 -0
- aethergraph/services/rag/index_factory.py +52 -0
- aethergraph/services/rag/parsers/md.py +7 -0
- aethergraph/services/rag/parsers/pdf.py +14 -0
- aethergraph/services/rag/parsers/txt.py +7 -0
- aethergraph/services/rag/utils/hybrid.py +39 -0
- aethergraph/services/rag/utils/make_fs_key.py +62 -0
- aethergraph/services/redactor/simple.py +16 -0
- aethergraph/services/registry/key_parsing.py +44 -0
- aethergraph/services/registry/registry_key.py +19 -0
- aethergraph/services/registry/unified_registry.py +185 -0
- aethergraph/services/resume/multi_scheduler_resume_bus.py +65 -0
- aethergraph/services/resume/router.py +73 -0
- aethergraph/services/schedulers/registry.py +41 -0
- aethergraph/services/secrets/base.py +7 -0
- aethergraph/services/secrets/env.py +8 -0
- aethergraph/services/state_stores/externalize.py +135 -0
- aethergraph/services/state_stores/graph_observer.py +131 -0
- aethergraph/services/state_stores/json_store.py +67 -0
- aethergraph/services/state_stores/resume_policy.py +119 -0
- aethergraph/services/state_stores/serialize.py +249 -0
- aethergraph/services/state_stores/utils.py +91 -0
- aethergraph/services/state_stores/validate.py +78 -0
- aethergraph/services/tracing/noop.py +18 -0
- aethergraph/services/waits/wait_registry.py +91 -0
- aethergraph/services/wakeup/memory_queue.py +57 -0
- aethergraph/services/wakeup/scanner_producer.py +56 -0
- aethergraph/services/wakeup/worker.py +31 -0
- aethergraph/tools/__init__.py +25 -0
- aethergraph/utils/optdeps.py +8 -0
- aethergraph-0.1.0a1.dist-info/METADATA +410 -0
- aethergraph-0.1.0a1.dist-info/RECORD +182 -0
- aethergraph-0.1.0a1.dist-info/WHEEL +5 -0
- aethergraph-0.1.0a1.dist-info/entry_points.txt +2 -0
- aethergraph-0.1.0a1.dist-info/licenses/LICENSE +176 -0
- aethergraph-0.1.0a1.dist-info/licenses/NOTICE +31 -0
- 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
|