ignite-ember 0.1.0__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.
- ember_code/__init__.py +3 -0
- ember_code/__main__.py +11 -0
- ember_code/backend/__init__.py +5 -0
- ember_code/backend/__main__.py +361 -0
- ember_code/backend/command_handler.py +662 -0
- ember_code/backend/server.py +668 -0
- ember_code/cli.py +279 -0
- ember_code/core/__init__.py +0 -0
- ember_code/core/auth/__init__.py +21 -0
- ember_code/core/auth/client.py +193 -0
- ember_code/core/auth/credentials.py +174 -0
- ember_code/core/config/__init__.py +13 -0
- ember_code/core/config/api_keys.py +33 -0
- ember_code/core/config/defaults.py +145 -0
- ember_code/core/config/models.py +308 -0
- ember_code/core/config/permissions.py +165 -0
- ember_code/core/config/settings.py +271 -0
- ember_code/core/config/tool_permissions.py +303 -0
- ember_code/core/evals/__init__.py +11 -0
- ember_code/core/evals/assertions.py +81 -0
- ember_code/core/evals/loader.py +83 -0
- ember_code/core/evals/reporter.py +76 -0
- ember_code/core/evals/runner.py +279 -0
- ember_code/core/guardrails/__init__.py +16 -0
- ember_code/core/guardrails/base.py +33 -0
- ember_code/core/guardrails/injection.py +52 -0
- ember_code/core/guardrails/moderation.py +23 -0
- ember_code/core/guardrails/pii.py +56 -0
- ember_code/core/guardrails/runner.py +54 -0
- ember_code/core/hooks/__init__.py +14 -0
- ember_code/core/hooks/events.py +18 -0
- ember_code/core/hooks/executor.py +178 -0
- ember_code/core/hooks/loader.py +77 -0
- ember_code/core/hooks/schemas.py +22 -0
- ember_code/core/hooks/tool_hook.py +171 -0
- ember_code/core/init.py +439 -0
- ember_code/core/knowledge/__init__.py +27 -0
- ember_code/core/knowledge/embedder.py +108 -0
- ember_code/core/knowledge/embedder_registry.py +85 -0
- ember_code/core/knowledge/manager.py +107 -0
- ember_code/core/knowledge/models.py +83 -0
- ember_code/core/knowledge/sync.py +179 -0
- ember_code/core/knowledge/vector_store.py +120 -0
- ember_code/core/learn.py +51 -0
- ember_code/core/mcp/__init__.py +15 -0
- ember_code/core/mcp/approval.py +110 -0
- ember_code/core/mcp/client.py +217 -0
- ember_code/core/mcp/config.py +134 -0
- ember_code/core/mcp/tools.py +28 -0
- ember_code/core/mcp/transport.py +52 -0
- ember_code/core/memory/__init__.py +5 -0
- ember_code/core/memory/manager.py +88 -0
- ember_code/core/pool.py +531 -0
- ember_code/core/prompts/__init__.py +14 -0
- ember_code/core/prompts/main_agent.md +250 -0
- ember_code/core/queue_hook.py +140 -0
- ember_code/core/scheduler/__init__.py +6 -0
- ember_code/core/scheduler/models.py +32 -0
- ember_code/core/scheduler/parser.py +145 -0
- ember_code/core/scheduler/runner.py +179 -0
- ember_code/core/scheduler/store.py +124 -0
- ember_code/core/session/__init__.py +17 -0
- ember_code/core/session/commands.py +103 -0
- ember_code/core/session/core.py +778 -0
- ember_code/core/session/ide_context.py +161 -0
- ember_code/core/session/interactive.py +177 -0
- ember_code/core/session/knowledge_ops.py +281 -0
- ember_code/core/session/memory_ops.py +90 -0
- ember_code/core/session/persistence.py +106 -0
- ember_code/core/session/runner.py +81 -0
- ember_code/core/skills/__init__.py +13 -0
- ember_code/core/skills/executor.py +64 -0
- ember_code/core/skills/loader.py +131 -0
- ember_code/core/skills/parser.py +85 -0
- ember_code/core/tools/__init__.py +5 -0
- ember_code/core/tools/codeindex.py +353 -0
- ember_code/core/tools/custom_loader.py +101 -0
- ember_code/core/tools/edit.py +104 -0
- ember_code/core/tools/knowledge.py +122 -0
- ember_code/core/tools/notebook.py +260 -0
- ember_code/core/tools/orchestrate.py +421 -0
- ember_code/core/tools/registry.py +256 -0
- ember_code/core/tools/schedule.py +103 -0
- ember_code/core/tools/search.py +155 -0
- ember_code/core/tools/web.py +78 -0
- ember_code/core/utils/__init__.py +1 -0
- ember_code/core/utils/audit.py +69 -0
- ember_code/core/utils/context.py +146 -0
- ember_code/core/utils/display.py +125 -0
- ember_code/core/utils/media.py +141 -0
- ember_code/core/utils/mentions.py +30 -0
- ember_code/core/utils/response.py +23 -0
- ember_code/core/utils/tips.py +142 -0
- ember_code/core/utils/update_checker.py +163 -0
- ember_code/core/workspace.py +66 -0
- ember_code/core/worktree.py +171 -0
- ember_code/frontend/__init__.py +0 -0
- ember_code/frontend/tui/__init__.py +5 -0
- ember_code/frontend/tui/app.py +1075 -0
- ember_code/frontend/tui/backend_client.py +408 -0
- ember_code/frontend/tui/conversation_view.py +87 -0
- ember_code/frontend/tui/file_index.py +162 -0
- ember_code/frontend/tui/format_helpers.py +3 -0
- ember_code/frontend/tui/hitl_handler.py +152 -0
- ember_code/frontend/tui/input_handler.py +202 -0
- ember_code/frontend/tui/process_manager.py +117 -0
- ember_code/frontend/tui/run_controller.py +818 -0
- ember_code/frontend/tui/session_manager.py +90 -0
- ember_code/frontend/tui/status_tracker.py +91 -0
- ember_code/frontend/tui/widgets/__init__.py +67 -0
- ember_code/frontend/tui/widgets/_activity.py +239 -0
- ember_code/frontend/tui/widgets/_agent_run.py +70 -0
- ember_code/frontend/tui/widgets/_chrome.py +439 -0
- ember_code/frontend/tui/widgets/_constants.py +5 -0
- ember_code/frontend/tui/widgets/_dialogs.py +609 -0
- ember_code/frontend/tui/widgets/_file_picker.py +103 -0
- ember_code/frontend/tui/widgets/_formatting.py +31 -0
- ember_code/frontend/tui/widgets/_help_panel.py +309 -0
- ember_code/frontend/tui/widgets/_input.py +123 -0
- ember_code/frontend/tui/widgets/_mcp_panel.py +292 -0
- ember_code/frontend/tui/widgets/_messages.py +620 -0
- ember_code/frontend/tui/widgets/_task_progress.py +142 -0
- ember_code/frontend/tui/widgets/_tasks.py +267 -0
- ember_code/frontend/tui/widgets/_tokens.py +120 -0
- ember_code/protocol/__init__.py +8 -0
- ember_code/protocol/agno_events.py +317 -0
- ember_code/protocol/messages.py +325 -0
- ember_code/protocol/serializer.py +197 -0
- ember_code/transport/__init__.py +11 -0
- ember_code/transport/base.py +37 -0
- ember_code/transport/in_process.py +69 -0
- ember_code/transport/unix_socket.py +186 -0
- ignite_ember-0.1.0.dist-info/METADATA +184 -0
- ignite_ember-0.1.0.dist-info/RECORD +137 -0
- ignite_ember-0.1.0.dist-info/WHEEL +4 -0
- ignite_ember-0.1.0.dist-info/entry_points.txt +2 -0
- ignite_ember-0.1.0.dist-info/licenses/LICENSE +21 -0
ember_code/__init__.py
ADDED
ember_code/__main__.py
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"""Backend process entry point.
|
|
2
|
+
|
|
3
|
+
Usage: python -m ember_code.backend --socket /tmp/ember-code/<uuid>.sock
|
|
4
|
+
|
|
5
|
+
Starts a BackendServer, listens on the given Unix socket, and
|
|
6
|
+
processes FE messages until shutdown.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import logging
|
|
13
|
+
import signal
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import click
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@click.command()
|
|
23
|
+
@click.option("--socket", "socket_path", required=True, help="Unix socket path")
|
|
24
|
+
@click.option("--project-dir", type=click.Path(exists=True), default=".")
|
|
25
|
+
@click.option("--resume-session", "resume_session_id", default=None)
|
|
26
|
+
@click.option("--additional-dirs", multiple=True, default=())
|
|
27
|
+
@click.option("--debug", is_flag=True, default=False)
|
|
28
|
+
def main(
|
|
29
|
+
socket_path: str,
|
|
30
|
+
project_dir: str,
|
|
31
|
+
resume_session_id: str | None,
|
|
32
|
+
additional_dirs: tuple[str, ...],
|
|
33
|
+
debug: bool,
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Start the Ember Code backend server."""
|
|
36
|
+
if debug:
|
|
37
|
+
log_path = Path.home() / ".ember" / "debug.log"
|
|
38
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
logging.basicConfig(
|
|
40
|
+
filename=str(log_path),
|
|
41
|
+
level=logging.DEBUG,
|
|
42
|
+
format="%(asctime)s %(name)s %(levelname)s %(message)s",
|
|
43
|
+
force=True,
|
|
44
|
+
)
|
|
45
|
+
logging.getLogger("ember_code").setLevel(logging.DEBUG)
|
|
46
|
+
|
|
47
|
+
extra_dirs = [Path(d) for d in additional_dirs] if additional_dirs else None
|
|
48
|
+
asyncio.run(_run(socket_path, Path(project_dir), resume_session_id, extra_dirs))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def _check_update() -> dict | None:
|
|
52
|
+
try:
|
|
53
|
+
from ember_code.core.utils.update_checker import check_for_update
|
|
54
|
+
|
|
55
|
+
info = await check_for_update()
|
|
56
|
+
if info and info.available:
|
|
57
|
+
return {"available": True, "version": info.latest_version, "message": info.message}
|
|
58
|
+
except Exception:
|
|
59
|
+
pass
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ── RPC dispatch table ──────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _build_rpc_table(backend: Any, transport: Any) -> dict[str, Any]:
|
|
67
|
+
"""Build method dispatch for RPCRequest messages."""
|
|
68
|
+
|
|
69
|
+
async def _login(args: dict) -> dict:
|
|
70
|
+
# Wrap on_status callback to push notifications
|
|
71
|
+
async def _on_status(text: str) -> None:
|
|
72
|
+
from ember_code.protocol import messages as msg
|
|
73
|
+
|
|
74
|
+
await transport.send(
|
|
75
|
+
msg.PushNotification(channel="login_status", payload={"text": text})
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
success, result = await backend.login(on_status=_on_status)
|
|
79
|
+
return {"success": success, "result": result}
|
|
80
|
+
|
|
81
|
+
async def _get_skill_definitions(args: dict) -> list[dict]:
|
|
82
|
+
pool = backend.get_skill_pool()
|
|
83
|
+
return [
|
|
84
|
+
{"name": s.name, "description": s.description, "prompt": getattr(s, "prompt", "")}
|
|
85
|
+
for s in pool.list_skills()
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
# Async methods
|
|
90
|
+
"ensure_mcp": lambda args: backend.ensure_mcp(),
|
|
91
|
+
"mcp_connect": lambda args: backend.mcp_connect(args["server_name"]),
|
|
92
|
+
"mcp_disconnect": lambda args: backend.mcp_disconnect(args["server_name"]),
|
|
93
|
+
"compact_if_needed": lambda args: backend.compact_if_needed(
|
|
94
|
+
args["ctx_tokens"], args["max_ctx"]
|
|
95
|
+
),
|
|
96
|
+
"extract_learnings": lambda args: backend.extract_learnings(
|
|
97
|
+
args["user_msg"], args["assistant_msg"]
|
|
98
|
+
),
|
|
99
|
+
"login": _login,
|
|
100
|
+
"fire_session_start_hook": lambda args: backend.fire_session_start_hook(),
|
|
101
|
+
"auto_sync_knowledge": lambda args: backend.auto_sync_knowledge(),
|
|
102
|
+
"shutdown": lambda args: backend.shutdown(),
|
|
103
|
+
"get_chat_history": lambda args: backend.get_chat_history(args["session_id"]),
|
|
104
|
+
"execute_scheduled_task": lambda args: backend.execute_scheduled_task(args["description"]),
|
|
105
|
+
"cancel_scheduled_task": lambda args: backend.cancel_scheduled_task(args["task_id"]),
|
|
106
|
+
"get_scheduled_tasks": lambda args: backend.get_scheduled_tasks(
|
|
107
|
+
args.get("include_done", True)
|
|
108
|
+
),
|
|
109
|
+
"list_sessions": lambda args: backend.list_sessions(),
|
|
110
|
+
"switch_session": lambda args: backend.switch_session(args["session_id"]),
|
|
111
|
+
# Sync accessors
|
|
112
|
+
"get_processing": lambda args: backend.processing,
|
|
113
|
+
"get_session_id": lambda args: backend.session_id,
|
|
114
|
+
"get_run_timeout": lambda args: backend.run_timeout,
|
|
115
|
+
"get_skill_names": lambda args: backend.skill_names,
|
|
116
|
+
"get_mcp_status": lambda args: backend.get_mcp_status(),
|
|
117
|
+
"get_mcp_server_details": lambda args: backend.get_mcp_server_details(),
|
|
118
|
+
"get_mcp_servers": lambda args: backend.get_mcp_servers(),
|
|
119
|
+
"get_status": lambda args: backend.get_status(),
|
|
120
|
+
"switch_model": lambda args: backend.switch_model(args["model_name"]),
|
|
121
|
+
"reload_cloud_credentials": lambda args: backend.reload_cloud_credentials(),
|
|
122
|
+
"clear_cloud_credentials": lambda args: backend.clear_cloud_credentials(),
|
|
123
|
+
"toggle_verbose": lambda args: backend.toggle_verbose(),
|
|
124
|
+
"cancel_run": lambda args: backend.cancel_run(),
|
|
125
|
+
"check_permission": lambda args: backend.check_permission(
|
|
126
|
+
args["tool_name"], args["func_name"], args["tool_args"]
|
|
127
|
+
),
|
|
128
|
+
"save_permission_rule": lambda args: backend.save_permission_rule(
|
|
129
|
+
args["rule"], args["level"]
|
|
130
|
+
),
|
|
131
|
+
"get_display_config": lambda args: (
|
|
132
|
+
backend.settings.display.model_dump()
|
|
133
|
+
if hasattr(backend.settings.display, "model_dump")
|
|
134
|
+
else {}
|
|
135
|
+
),
|
|
136
|
+
"get_model_registry": lambda args: {
|
|
137
|
+
"default": backend.settings.models.default,
|
|
138
|
+
"max_context_window": backend.settings.models.max_context_window,
|
|
139
|
+
"registry": {k: v for k, v in backend.settings.models.registry.items()},
|
|
140
|
+
},
|
|
141
|
+
"check_for_update": lambda args: _check_update(),
|
|
142
|
+
"get_skill_definitions": _get_skill_definitions,
|
|
143
|
+
"start_scheduler": lambda args: _start_scheduler_with_push(backend, transport),
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _start_scheduler_with_push(backend: Any, transport: Any) -> None:
|
|
148
|
+
"""Start the scheduler with push notification callbacks."""
|
|
149
|
+
from ember_code.protocol import messages as msg
|
|
150
|
+
|
|
151
|
+
def on_started(task_id: str, description: str) -> None:
|
|
152
|
+
asyncio.ensure_future(
|
|
153
|
+
transport.send(
|
|
154
|
+
msg.PushNotification(
|
|
155
|
+
channel="scheduler_started",
|
|
156
|
+
payload={"task_id": task_id, "description": description},
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def on_completed(task_id: str, description: str, result: str) -> None:
|
|
162
|
+
asyncio.ensure_future(
|
|
163
|
+
transport.send(
|
|
164
|
+
msg.PushNotification(
|
|
165
|
+
channel="scheduler_completed",
|
|
166
|
+
payload={"task_id": task_id, "description": description, "result": result},
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
backend.start_scheduler(on_task_started=on_started, on_task_completed=on_completed)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# ── Main loop ──────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
async def _run(
|
|
178
|
+
socket_path: str,
|
|
179
|
+
project_dir: Path,
|
|
180
|
+
resume_session_id: str | None,
|
|
181
|
+
additional_dirs: list[Path] | None = None,
|
|
182
|
+
) -> None:
|
|
183
|
+
from ember_code.backend.server import BackendServer
|
|
184
|
+
from ember_code.core.config.settings import load_settings
|
|
185
|
+
from ember_code.protocol import messages as msg
|
|
186
|
+
from ember_code.transport.unix_socket import UnixSocketServerTransport
|
|
187
|
+
|
|
188
|
+
settings = load_settings(project_dir=project_dir)
|
|
189
|
+
|
|
190
|
+
backend = BackendServer(
|
|
191
|
+
settings,
|
|
192
|
+
project_dir=project_dir,
|
|
193
|
+
resume_session_id=resume_session_id,
|
|
194
|
+
additional_dirs=additional_dirs,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
transport = UnixSocketServerTransport(socket_path)
|
|
198
|
+
await transport.start()
|
|
199
|
+
|
|
200
|
+
# Signal ready to FE
|
|
201
|
+
print(f"READY {socket_path}", flush=True)
|
|
202
|
+
|
|
203
|
+
# Handle SIGTERM/SIGINT
|
|
204
|
+
shutdown_event = asyncio.Event()
|
|
205
|
+
|
|
206
|
+
def _signal_handler():
|
|
207
|
+
shutdown_event.set()
|
|
208
|
+
|
|
209
|
+
loop = asyncio.get_event_loop()
|
|
210
|
+
for sig in (signal.SIGTERM, signal.SIGINT):
|
|
211
|
+
loop.add_signal_handler(sig, _signal_handler)
|
|
212
|
+
|
|
213
|
+
# Queue for message injection (replaces wire_queue_hook)
|
|
214
|
+
_queue: list[str] = []
|
|
215
|
+
backend.wire_queue_hook(_queue)
|
|
216
|
+
|
|
217
|
+
# Orchestrate progress → push notification
|
|
218
|
+
def _on_progress(line: str) -> None:
|
|
219
|
+
asyncio.ensure_future(
|
|
220
|
+
transport.send(
|
|
221
|
+
msg.PushNotification(channel="orchestrate_progress", payload={"line": line})
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
backend.wire_orchestrate_progress(_on_progress)
|
|
226
|
+
|
|
227
|
+
rpc_table = _build_rpc_table(backend, transport)
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
await transport.wait_for_connection()
|
|
231
|
+
logger.info("FE connected, processing messages")
|
|
232
|
+
|
|
233
|
+
async for message in transport.receive():
|
|
234
|
+
if isinstance(message, msg.Shutdown):
|
|
235
|
+
break
|
|
236
|
+
|
|
237
|
+
if shutdown_event.is_set():
|
|
238
|
+
break
|
|
239
|
+
|
|
240
|
+
await _handle_message(message, backend, transport, rpc_table, _queue)
|
|
241
|
+
|
|
242
|
+
except Exception as exc:
|
|
243
|
+
logger.error("Backend error: %s", exc, exc_info=True)
|
|
244
|
+
finally:
|
|
245
|
+
await backend.shutdown()
|
|
246
|
+
await transport.close()
|
|
247
|
+
logger.info("Backend shut down")
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
async def _handle_message(
|
|
251
|
+
message: Any,
|
|
252
|
+
backend: Any,
|
|
253
|
+
transport: Any,
|
|
254
|
+
rpc_table: dict,
|
|
255
|
+
queue: list[str],
|
|
256
|
+
) -> None:
|
|
257
|
+
from ember_code.protocol import messages as msg
|
|
258
|
+
from ember_code.protocol.messages import Message
|
|
259
|
+
|
|
260
|
+
req_id = message.id or ""
|
|
261
|
+
|
|
262
|
+
# ── Streaming: run_message ──
|
|
263
|
+
if isinstance(message, msg.UserMessage):
|
|
264
|
+
async for proto in backend.run_message(message.text, media=message.file_contents):
|
|
265
|
+
if req_id:
|
|
266
|
+
proto = proto.model_copy(update={"id": req_id})
|
|
267
|
+
await transport.send(proto)
|
|
268
|
+
await transport.send(msg.StreamEnd(id=req_id))
|
|
269
|
+
|
|
270
|
+
# ── Streaming: resolve_hitl ──
|
|
271
|
+
elif isinstance(message, msg.HITLResponse):
|
|
272
|
+
async for proto in backend.resolve_hitl(
|
|
273
|
+
message.requirement_id, message.action, message.choice
|
|
274
|
+
):
|
|
275
|
+
if req_id:
|
|
276
|
+
proto = proto.model_copy(update={"id": req_id})
|
|
277
|
+
await transport.send(proto)
|
|
278
|
+
await transport.send(msg.StreamEnd(id=req_id))
|
|
279
|
+
|
|
280
|
+
# ── Command ──
|
|
281
|
+
elif isinstance(message, msg.Command):
|
|
282
|
+
result = await backend.handle_command(message.text)
|
|
283
|
+
result = result.model_copy(update={"id": req_id})
|
|
284
|
+
await transport.send(result)
|
|
285
|
+
|
|
286
|
+
# ── Session management (typed messages) ──
|
|
287
|
+
elif isinstance(message, msg.SessionList):
|
|
288
|
+
result = await backend.list_sessions()
|
|
289
|
+
result = result.model_copy(update={"id": req_id})
|
|
290
|
+
await transport.send(result)
|
|
291
|
+
|
|
292
|
+
elif isinstance(message, msg.SessionSwitch):
|
|
293
|
+
result = await backend.switch_session(message.session_id)
|
|
294
|
+
result = result.model_copy(update={"id": req_id})
|
|
295
|
+
await transport.send(result)
|
|
296
|
+
|
|
297
|
+
elif isinstance(message, msg.ModelSwitch):
|
|
298
|
+
result = backend.switch_model(message.model_name)
|
|
299
|
+
result = result.model_copy(update={"id": req_id})
|
|
300
|
+
await transport.send(result)
|
|
301
|
+
|
|
302
|
+
elif isinstance(message, msg.MCPToggle):
|
|
303
|
+
result = await backend.toggle_mcp(message.server_name, message.connect)
|
|
304
|
+
result = result.model_copy(update={"id": req_id})
|
|
305
|
+
await transport.send(result)
|
|
306
|
+
|
|
307
|
+
# ── Queue injection ──
|
|
308
|
+
elif isinstance(message, msg.QueueMessage):
|
|
309
|
+
queue.append(message.text)
|
|
310
|
+
|
|
311
|
+
# ── Cancel ──
|
|
312
|
+
elif isinstance(message, msg.Cancel):
|
|
313
|
+
backend.cancel_run()
|
|
314
|
+
|
|
315
|
+
# ── Generic RPC ──
|
|
316
|
+
elif isinstance(message, msg.RPCRequest):
|
|
317
|
+
handler = rpc_table.get(message.method)
|
|
318
|
+
if handler is None:
|
|
319
|
+
await transport.send(
|
|
320
|
+
msg.RPCResponse(id=req_id, error=f"Unknown RPC method: {message.method}")
|
|
321
|
+
)
|
|
322
|
+
return
|
|
323
|
+
|
|
324
|
+
try:
|
|
325
|
+
result = handler(message.args)
|
|
326
|
+
if asyncio.iscoroutine(result) or asyncio.isfuture(result):
|
|
327
|
+
result = await result
|
|
328
|
+
|
|
329
|
+
# If result is a Message, send it directly with correlation ID
|
|
330
|
+
if isinstance(result, Message):
|
|
331
|
+
result = result.model_copy(update={"id": req_id})
|
|
332
|
+
await transport.send(result)
|
|
333
|
+
else:
|
|
334
|
+
# Wrap in RPCResponse for plain values
|
|
335
|
+
await transport.send(msg.RPCResponse(id=req_id, result=_serialize(result)))
|
|
336
|
+
except Exception as exc:
|
|
337
|
+
logger.error("RPC %s failed: %s", message.method, exc, exc_info=True)
|
|
338
|
+
await transport.send(msg.RPCResponse(id=req_id, error=str(exc)))
|
|
339
|
+
|
|
340
|
+
else:
|
|
341
|
+
logger.warning("Unknown FE message type: %s", type(message).__name__)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _serialize(value: Any) -> Any:
|
|
345
|
+
"""Make a value JSON-serializable."""
|
|
346
|
+
if value is None:
|
|
347
|
+
return None
|
|
348
|
+
if isinstance(value, (str, int, float, bool)):
|
|
349
|
+
return value
|
|
350
|
+
if isinstance(value, (list, tuple)):
|
|
351
|
+
return [_serialize(v) for v in value]
|
|
352
|
+
if isinstance(value, dict):
|
|
353
|
+
return {str(k): _serialize(v) for k, v in value.items()}
|
|
354
|
+
# Pydantic models
|
|
355
|
+
if hasattr(value, "model_dump"):
|
|
356
|
+
return value.model_dump()
|
|
357
|
+
return str(value)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
if __name__ == "__main__":
|
|
361
|
+
main()
|