meshagent-cli 0.6.1__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.
Potentially problematic release.
This version of meshagent-cli might be problematic. Click here for more details.
- meshagent/cli/__init__.py +3 -0
- meshagent/cli/agent.py +263 -0
- meshagent/cli/api_keys.py +102 -0
- meshagent/cli/async_typer.py +31 -0
- meshagent/cli/auth.py +30 -0
- meshagent/cli/auth_async.py +295 -0
- meshagent/cli/call.py +224 -0
- meshagent/cli/chatbot.py +311 -0
- meshagent/cli/cli.py +184 -0
- meshagent/cli/cli_mcp.py +344 -0
- meshagent/cli/cli_secrets.py +414 -0
- meshagent/cli/common_options.py +23 -0
- meshagent/cli/containers.py +577 -0
- meshagent/cli/developer.py +70 -0
- meshagent/cli/exec.py +381 -0
- meshagent/cli/helper.py +147 -0
- meshagent/cli/helpers.py +131 -0
- meshagent/cli/mailbot.py +260 -0
- meshagent/cli/meeting_transcriber.py +124 -0
- meshagent/cli/messaging.py +160 -0
- meshagent/cli/oauth2.py +189 -0
- meshagent/cli/participant_token.py +61 -0
- meshagent/cli/projects.py +105 -0
- meshagent/cli/queue.py +91 -0
- meshagent/cli/services.py +490 -0
- meshagent/cli/sessions.py +26 -0
- meshagent/cli/storage.py +813 -0
- meshagent/cli/version.py +1 -0
- meshagent/cli/voicebot.py +178 -0
- meshagent/cli/webhook.py +100 -0
- meshagent_cli-0.6.1.dist-info/METADATA +47 -0
- meshagent_cli-0.6.1.dist-info/RECORD +35 -0
- meshagent_cli-0.6.1.dist-info/WHEEL +5 -0
- meshagent_cli-0.6.1.dist-info/entry_points.txt +2 -0
- meshagent_cli-0.6.1.dist-info/top_level.txt +1 -0
meshagent/cli/exec.py
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
from meshagent.api.websocket_protocol import WebSocketClientProtocol
|
|
4
|
+
from meshagent.api import RoomClient
|
|
5
|
+
from meshagent.api.helpers import websocket_room_url
|
|
6
|
+
from typing import Annotated, Optional
|
|
7
|
+
from meshagent.cli.common_options import ProjectIdOption, RoomOption
|
|
8
|
+
import asyncio
|
|
9
|
+
import typer
|
|
10
|
+
from rich import print
|
|
11
|
+
import aiohttp
|
|
12
|
+
import struct
|
|
13
|
+
import signal
|
|
14
|
+
import shutil
|
|
15
|
+
import json
|
|
16
|
+
from urllib.parse import quote
|
|
17
|
+
import threading
|
|
18
|
+
import time
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
|
|
23
|
+
from meshagent.cli.helper import (
|
|
24
|
+
get_client,
|
|
25
|
+
resolve_project_id,
|
|
26
|
+
resolve_room,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
if os.name == "nt":
|
|
30
|
+
import msvcrt
|
|
31
|
+
import ctypes
|
|
32
|
+
from ctypes import wintypes
|
|
33
|
+
|
|
34
|
+
_kernel32 = ctypes.windll.kernel32
|
|
35
|
+
_ENABLE_ECHO_INPUT = 0x0004
|
|
36
|
+
_ENABLE_LINE_INPUT = 0x0002
|
|
37
|
+
|
|
38
|
+
def set_raw(f):
|
|
39
|
+
"""Disable line and echo mode for the given file handle."""
|
|
40
|
+
handle = msvcrt.get_osfhandle(f.fileno())
|
|
41
|
+
original_mode = wintypes.DWORD()
|
|
42
|
+
if not _kernel32.GetConsoleMode(handle, ctypes.byref(original_mode)):
|
|
43
|
+
return None
|
|
44
|
+
new_mode = original_mode.value & ~(_ENABLE_ECHO_INPUT | _ENABLE_LINE_INPUT)
|
|
45
|
+
_kernel32.SetConsoleMode(handle, new_mode)
|
|
46
|
+
return handle, original_mode.value
|
|
47
|
+
|
|
48
|
+
def restore(f, state):
|
|
49
|
+
if state is None:
|
|
50
|
+
return None
|
|
51
|
+
handle, mode = state
|
|
52
|
+
_kernel32.SetConsoleMode(handle, mode)
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
else:
|
|
56
|
+
import termios
|
|
57
|
+
import tty as _tty
|
|
58
|
+
|
|
59
|
+
def set_raw(fd):
|
|
60
|
+
old = termios.tcgetattr(fd)
|
|
61
|
+
_tty.setraw(fd)
|
|
62
|
+
return old
|
|
63
|
+
|
|
64
|
+
def restore(fd, old_settings):
|
|
65
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class _StdWriter:
|
|
69
|
+
"""Simple asyncio-friendly wrapper for standard streams on Windows."""
|
|
70
|
+
|
|
71
|
+
def __init__(self, file):
|
|
72
|
+
self._file = file
|
|
73
|
+
|
|
74
|
+
def write(self, data: bytes) -> None:
|
|
75
|
+
self._file.buffer.write(data)
|
|
76
|
+
|
|
77
|
+
async def drain(self) -> None:
|
|
78
|
+
await asyncio.get_running_loop().run_in_executor(None, self._file.flush)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def register(app: typer.Typer):
|
|
82
|
+
@app.async_command("exec")
|
|
83
|
+
async def exec_command(
|
|
84
|
+
*,
|
|
85
|
+
project_id: ProjectIdOption = None,
|
|
86
|
+
room: RoomOption,
|
|
87
|
+
name: Annotated[Optional[str], typer.Option()] = None,
|
|
88
|
+
image: Annotated[Optional[str], typer.Option()] = None,
|
|
89
|
+
command: Annotated[list[str], typer.Argument(...)] = None,
|
|
90
|
+
tty: bool = False,
|
|
91
|
+
room_storage_path: str = "/data",
|
|
92
|
+
):
|
|
93
|
+
"""Open an interactive websocket‑based TTY."""
|
|
94
|
+
client = await get_client()
|
|
95
|
+
try:
|
|
96
|
+
project_id = await resolve_project_id(project_id=project_id)
|
|
97
|
+
room = resolve_room(room)
|
|
98
|
+
|
|
99
|
+
connection = await client.connect_room(project_id=project_id, room=room)
|
|
100
|
+
|
|
101
|
+
ws_url = (
|
|
102
|
+
websocket_room_url(room_name=room) + f"/exec?token={connection.jwt}"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if image:
|
|
106
|
+
ws_url += f"&image={quote(' '.join(image))}"
|
|
107
|
+
|
|
108
|
+
if name:
|
|
109
|
+
ws_url += f"&name={quote(' '.join(name))}"
|
|
110
|
+
|
|
111
|
+
if command and len(command) != 0:
|
|
112
|
+
ws_url += f"&command={quote(' '.join(command))}"
|
|
113
|
+
|
|
114
|
+
if room_storage_path:
|
|
115
|
+
room_storage_path += (
|
|
116
|
+
f"&room_storage_path={quote(' '.join(room_storage_path))}"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if tty:
|
|
120
|
+
if not sys.stdin.isatty():
|
|
121
|
+
print("[red]TTY requested but process is not a TTY[/red]")
|
|
122
|
+
raise typer.Exit(1)
|
|
123
|
+
|
|
124
|
+
ws_url += "&tty=true"
|
|
125
|
+
|
|
126
|
+
else:
|
|
127
|
+
if command is None:
|
|
128
|
+
print("[red]TTY required when not executing a command[/red]")
|
|
129
|
+
raise typer.Exit(1)
|
|
130
|
+
|
|
131
|
+
ws_url += "&tty=false"
|
|
132
|
+
|
|
133
|
+
if tty:
|
|
134
|
+
# Save current terminal settings so we can restore them later.
|
|
135
|
+
old_tty_settings = set_raw(sys.stdin)
|
|
136
|
+
|
|
137
|
+
async with RoomClient(
|
|
138
|
+
protocol=WebSocketClientProtocol(
|
|
139
|
+
url=websocket_room_url(room_name=room),
|
|
140
|
+
token=connection.jwt,
|
|
141
|
+
)
|
|
142
|
+
):
|
|
143
|
+
try:
|
|
144
|
+
async with aiohttp.ClientSession() as session:
|
|
145
|
+
async with session.ws_connect(ws_url) as websocket:
|
|
146
|
+
send_queue = asyncio.Queue[bytes]()
|
|
147
|
+
|
|
148
|
+
loop = asyncio.get_running_loop()
|
|
149
|
+
if os.name == "nt":
|
|
150
|
+
stdout_writer = _StdWriter(sys.stdout)
|
|
151
|
+
stderr_writer = _StdWriter(sys.stderr)
|
|
152
|
+
else:
|
|
153
|
+
(
|
|
154
|
+
stdout_transport,
|
|
155
|
+
stdout_protocol,
|
|
156
|
+
) = await loop.connect_write_pipe(
|
|
157
|
+
asyncio.streams.FlowControlMixin, sys.stdout
|
|
158
|
+
)
|
|
159
|
+
stdout_writer = asyncio.StreamWriter(
|
|
160
|
+
stdout_transport, stdout_protocol, None, loop
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
(
|
|
164
|
+
stderr_transport,
|
|
165
|
+
stderr_protocol,
|
|
166
|
+
) = await loop.connect_write_pipe(
|
|
167
|
+
asyncio.streams.FlowControlMixin, sys.stderr
|
|
168
|
+
)
|
|
169
|
+
stderr_writer = asyncio.StreamWriter(
|
|
170
|
+
stderr_transport, stderr_protocol, None, loop
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
async def recv_from_websocket():
|
|
174
|
+
while True:
|
|
175
|
+
done, pending = await asyncio.wait(
|
|
176
|
+
[asyncio.create_task(websocket.receive())],
|
|
177
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
first = done.pop()
|
|
181
|
+
|
|
182
|
+
if first == read_stdin_task:
|
|
183
|
+
break
|
|
184
|
+
|
|
185
|
+
message = first.result()
|
|
186
|
+
|
|
187
|
+
if websocket.closed:
|
|
188
|
+
break
|
|
189
|
+
|
|
190
|
+
if message.type == aiohttp.WSMsgType.CLOSE:
|
|
191
|
+
break
|
|
192
|
+
|
|
193
|
+
elif message.type == aiohttp.WSMsgType.CLOSING:
|
|
194
|
+
pass
|
|
195
|
+
|
|
196
|
+
elif message.type == aiohttp.WSMsgType.ERROR:
|
|
197
|
+
break
|
|
198
|
+
|
|
199
|
+
if not message.data:
|
|
200
|
+
break
|
|
201
|
+
|
|
202
|
+
data: bytes = message.data
|
|
203
|
+
if len(data) > 0:
|
|
204
|
+
if data[0] == 1:
|
|
205
|
+
stderr_writer.write(data)
|
|
206
|
+
await stderr_writer.drain()
|
|
207
|
+
elif data[0] == 0:
|
|
208
|
+
stdout_writer.write(data)
|
|
209
|
+
await stdout_writer.drain()
|
|
210
|
+
else:
|
|
211
|
+
raise ValueError(
|
|
212
|
+
f"Invalid channel received {data[0]}"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
last_size = None
|
|
216
|
+
|
|
217
|
+
async def send_resize(rows, cols):
|
|
218
|
+
nonlocal last_size
|
|
219
|
+
|
|
220
|
+
size = (cols, rows)
|
|
221
|
+
if size == last_size:
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
last_size = size
|
|
225
|
+
|
|
226
|
+
resize_json = json.dumps(
|
|
227
|
+
{"Width": cols, "Height": rows}
|
|
228
|
+
).encode("utf-8")
|
|
229
|
+
payload = struct.pack("B", 4) + resize_json
|
|
230
|
+
send_queue.put_nowait(payload)
|
|
231
|
+
await asyncio.sleep(5)
|
|
232
|
+
|
|
233
|
+
cols, rows = shutil.get_terminal_size(fallback=(24, 80))
|
|
234
|
+
if tty:
|
|
235
|
+
await send_resize(rows, cols)
|
|
236
|
+
|
|
237
|
+
def on_sigwinch():
|
|
238
|
+
cols, rows = shutil.get_terminal_size(fallback=(24, 80))
|
|
239
|
+
task = asyncio.create_task(send_resize(rows, cols))
|
|
240
|
+
|
|
241
|
+
def on_done(t: asyncio.Task):
|
|
242
|
+
t.result()
|
|
243
|
+
|
|
244
|
+
task.add_done_callback(on_done)
|
|
245
|
+
|
|
246
|
+
if hasattr(signal, "SIGWINCH"):
|
|
247
|
+
loop.add_signal_handler(signal.SIGWINCH, on_sigwinch)
|
|
248
|
+
|
|
249
|
+
async def read_stdin():
|
|
250
|
+
loop = asyncio.get_running_loop()
|
|
251
|
+
|
|
252
|
+
if os.name == "nt":
|
|
253
|
+
queue: asyncio.Queue[bytes] = asyncio.Queue()
|
|
254
|
+
stop_event = threading.Event()
|
|
255
|
+
|
|
256
|
+
if sys.stdin.isatty():
|
|
257
|
+
|
|
258
|
+
def reader() -> None:
|
|
259
|
+
try:
|
|
260
|
+
while not stop_event.is_set():
|
|
261
|
+
if msvcrt.kbhit():
|
|
262
|
+
data = msvcrt.getch()
|
|
263
|
+
loop.call_soon_threadsafe(
|
|
264
|
+
queue.put_nowait, data
|
|
265
|
+
)
|
|
266
|
+
else:
|
|
267
|
+
time.sleep(0.01)
|
|
268
|
+
finally:
|
|
269
|
+
loop.call_soon_threadsafe(
|
|
270
|
+
queue.put_nowait, b""
|
|
271
|
+
)
|
|
272
|
+
else:
|
|
273
|
+
|
|
274
|
+
def reader() -> None:
|
|
275
|
+
try:
|
|
276
|
+
while not stop_event.is_set():
|
|
277
|
+
data = sys.stdin.buffer.read(1)
|
|
278
|
+
loop.call_soon_threadsafe(
|
|
279
|
+
queue.put_nowait, data
|
|
280
|
+
)
|
|
281
|
+
if not data:
|
|
282
|
+
break
|
|
283
|
+
finally:
|
|
284
|
+
loop.call_soon_threadsafe(
|
|
285
|
+
queue.put_nowait, b""
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
thread = threading.Thread(target=reader)
|
|
289
|
+
thread.start()
|
|
290
|
+
|
|
291
|
+
async def reader_task() -> bytes:
|
|
292
|
+
return await queue.get()
|
|
293
|
+
else:
|
|
294
|
+
reader = asyncio.StreamReader()
|
|
295
|
+
protocol = asyncio.StreamReaderProtocol(reader)
|
|
296
|
+
await loop.connect_read_pipe(
|
|
297
|
+
lambda: protocol, sys.stdin
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
async def reader_task():
|
|
301
|
+
return await reader.read(1)
|
|
302
|
+
|
|
303
|
+
try:
|
|
304
|
+
while True:
|
|
305
|
+
# Read one character at a time from stdin without blocking the event loop.
|
|
306
|
+
done, pending = await asyncio.wait(
|
|
307
|
+
[
|
|
308
|
+
asyncio.create_task(reader_task()),
|
|
309
|
+
websocket_recv_task,
|
|
310
|
+
],
|
|
311
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
first = done.pop()
|
|
315
|
+
if first == websocket_recv_task:
|
|
316
|
+
break
|
|
317
|
+
|
|
318
|
+
data = first.result()
|
|
319
|
+
if not data:
|
|
320
|
+
break
|
|
321
|
+
|
|
322
|
+
if websocket.closed:
|
|
323
|
+
break
|
|
324
|
+
|
|
325
|
+
if tty:
|
|
326
|
+
if data == b"\x04":
|
|
327
|
+
break
|
|
328
|
+
|
|
329
|
+
if data:
|
|
330
|
+
send_queue.put_nowait(b"\0" + data)
|
|
331
|
+
else:
|
|
332
|
+
break
|
|
333
|
+
finally:
|
|
334
|
+
if os.name == "nt":
|
|
335
|
+
stop_event.set()
|
|
336
|
+
thread.join()
|
|
337
|
+
|
|
338
|
+
send_queue.put_nowait(b"\0")
|
|
339
|
+
|
|
340
|
+
websocket_recv_task = asyncio.create_task(
|
|
341
|
+
recv_from_websocket()
|
|
342
|
+
)
|
|
343
|
+
read_stdin_task = asyncio.create_task(read_stdin())
|
|
344
|
+
|
|
345
|
+
async def send_to_websocket():
|
|
346
|
+
while True:
|
|
347
|
+
try:
|
|
348
|
+
data = await send_queue.get()
|
|
349
|
+
if websocket.closed:
|
|
350
|
+
break
|
|
351
|
+
|
|
352
|
+
if data is not None:
|
|
353
|
+
await websocket.send_bytes(data)
|
|
354
|
+
|
|
355
|
+
else:
|
|
356
|
+
break
|
|
357
|
+
except asyncio.QueueShutDown:
|
|
358
|
+
break
|
|
359
|
+
|
|
360
|
+
send_to_websocket_task = asyncio.create_task(
|
|
361
|
+
send_to_websocket()
|
|
362
|
+
)
|
|
363
|
+
await asyncio.gather(
|
|
364
|
+
websocket_recv_task,
|
|
365
|
+
read_stdin_task,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
send_queue.shutdown()
|
|
369
|
+
await send_to_websocket_task
|
|
370
|
+
|
|
371
|
+
finally:
|
|
372
|
+
if not sys.stdin.closed and tty:
|
|
373
|
+
# Restore original terminal settings even if the coroutine is cancelled.
|
|
374
|
+
restore(sys.stdin, old_tty_settings)
|
|
375
|
+
|
|
376
|
+
except Exception as e:
|
|
377
|
+
print(f"[red]{e}[/red]")
|
|
378
|
+
logging.error("failed", exc_info=e)
|
|
379
|
+
raise typer.Exit(1)
|
|
380
|
+
finally:
|
|
381
|
+
await client.close()
|
meshagent/cli/helper.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.table import Table
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from meshagent.cli import auth_async
|
|
8
|
+
from meshagent.cli import async_typer
|
|
9
|
+
from meshagent.api.helpers import meshagent_base_url
|
|
10
|
+
from meshagent.api.client import Meshagent
|
|
11
|
+
import os
|
|
12
|
+
from rich import print
|
|
13
|
+
|
|
14
|
+
SETTINGS_FILE = Path.home() / ".meshagent" / "project.json"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _ensure_cache_dir():
|
|
18
|
+
SETTINGS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Settings(BaseModel):
|
|
22
|
+
active_project: Optional[str] = None
|
|
23
|
+
active_api_keys: Optional[dict] = {}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _save_settings(s: Settings):
|
|
27
|
+
_ensure_cache_dir()
|
|
28
|
+
SETTINGS_FILE.write_text(s.model_dump_json())
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _load_settings():
|
|
32
|
+
_ensure_cache_dir()
|
|
33
|
+
if SETTINGS_FILE.exists():
|
|
34
|
+
return Settings.model_validate_json(SETTINGS_FILE.read_text())
|
|
35
|
+
|
|
36
|
+
return Settings()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async def get_active_project():
|
|
40
|
+
settings = _load_settings()
|
|
41
|
+
if settings is None:
|
|
42
|
+
return None
|
|
43
|
+
return settings.active_project
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def set_active_project(project_id: str | None):
|
|
47
|
+
settings = _load_settings()
|
|
48
|
+
settings.active_project = project_id
|
|
49
|
+
_save_settings(settings)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def set_active_api_key(project_id: str, key: str):
|
|
53
|
+
settings = _load_settings()
|
|
54
|
+
settings.active_api_keys[project_id] = key
|
|
55
|
+
_save_settings(settings)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def get_active_api_key(project_id: str):
|
|
59
|
+
settings = _load_settings()
|
|
60
|
+
key: str = settings.active_api_keys.get(project_id)
|
|
61
|
+
# Ignore old keys, API key format changed
|
|
62
|
+
if key is not None and key.startswith("ma-"):
|
|
63
|
+
return key
|
|
64
|
+
else:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
app = async_typer.AsyncTyper()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def get_client():
|
|
72
|
+
key = os.getenv("MESHAGENT_API_KEY")
|
|
73
|
+
if key is not None:
|
|
74
|
+
return Meshagent(
|
|
75
|
+
base_url=meshagent_base_url(),
|
|
76
|
+
token=key,
|
|
77
|
+
)
|
|
78
|
+
else:
|
|
79
|
+
access_token = await auth_async.get_access_token()
|
|
80
|
+
return Meshagent(
|
|
81
|
+
base_url=meshagent_base_url(),
|
|
82
|
+
token=access_token,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def print_json_table(records: list, *cols):
|
|
87
|
+
if not records:
|
|
88
|
+
raise SystemExit("No rows to print")
|
|
89
|
+
|
|
90
|
+
# 2️⃣ --- build the table -------------------------------------------
|
|
91
|
+
table = Table(show_header=True, header_style="bold magenta")
|
|
92
|
+
|
|
93
|
+
if len(cols) > 0:
|
|
94
|
+
# use the keys of the first object as column order
|
|
95
|
+
for col in cols:
|
|
96
|
+
table.add_column(col.title()) # "id" → "Id"
|
|
97
|
+
|
|
98
|
+
for row in records:
|
|
99
|
+
table.add_row(*(str(row.get(col, "")) for col in cols))
|
|
100
|
+
|
|
101
|
+
else:
|
|
102
|
+
# use the keys of the first object as column order
|
|
103
|
+
for col in records[0]:
|
|
104
|
+
table.add_column(col.title()) # "id" → "Id"
|
|
105
|
+
|
|
106
|
+
for row in records:
|
|
107
|
+
table.add_row(*(str(row.get(col, "")) for col in records[0]))
|
|
108
|
+
|
|
109
|
+
# 3️⃣ --- render ------------------------------------------------------
|
|
110
|
+
Console().print(table)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def resolve_room(room_name: Optional[str] = None):
|
|
114
|
+
if room_name is None:
|
|
115
|
+
room_name = os.getenv("MESHAGENT_ROOM")
|
|
116
|
+
|
|
117
|
+
return room_name
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
async def resolve_project_id(project_id: Optional[str] = None):
|
|
121
|
+
if project_id is None:
|
|
122
|
+
project_id = await get_active_project()
|
|
123
|
+
|
|
124
|
+
if project_id is None:
|
|
125
|
+
print(
|
|
126
|
+
"[red]Project ID not specified, activate a project or pass a project on the command line[/red]"
|
|
127
|
+
)
|
|
128
|
+
raise typer.Exit(code=1)
|
|
129
|
+
|
|
130
|
+
return project_id
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def resolve_key(project_id: str | None, key: str | None):
|
|
134
|
+
project_id = await resolve_project_id(project_id=project_id)
|
|
135
|
+
if key is None:
|
|
136
|
+
key = await get_active_api_key(project_id=project_id)
|
|
137
|
+
|
|
138
|
+
if key is None:
|
|
139
|
+
key = os.getenv("MESHAGENT_API_KEY")
|
|
140
|
+
|
|
141
|
+
if key is None:
|
|
142
|
+
print(
|
|
143
|
+
"[red]--key is required if MESHGENT_API_KEY is not set. You can use meshagent api-key create to create a new api key."
|
|
144
|
+
)
|
|
145
|
+
raise typer.Exit(1)
|
|
146
|
+
|
|
147
|
+
return key
|
meshagent/cli/helpers.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from meshagent.cli import async_typer
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from meshagent.api import SchemaRegistry, SchemaRegistration
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
app = async_typer.AsyncTyper(help="Join a mailbot to a room")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.async_command("service")
|
|
13
|
+
async def helpers_service():
|
|
14
|
+
from meshagent.agents.planning import DynamicPlanningResponder, PlanningResponder
|
|
15
|
+
from meshagent.openai.tools import OpenAIResponsesAdapter
|
|
16
|
+
from meshagent.tools.storage import StorageToolkit
|
|
17
|
+
from meshagent.api.services import ServiceHost
|
|
18
|
+
|
|
19
|
+
from meshagent.agents.schemas.gallery import gallery_schema
|
|
20
|
+
from meshagent.agents.schemas.document import document_schema
|
|
21
|
+
from meshagent.agents.schemas.transcript import transcript_schema
|
|
22
|
+
from meshagent.agents.schemas.super_editor_document import (
|
|
23
|
+
super_editor_document_schema,
|
|
24
|
+
)
|
|
25
|
+
from meshagent.agents.schemas.presentation import presentation_schema
|
|
26
|
+
from meshagent.agents import thread_schema
|
|
27
|
+
|
|
28
|
+
logging.getLogger("openai").setLevel(logging.ERROR)
|
|
29
|
+
logging.getLogger("httpx").setLevel(logging.ERROR)
|
|
30
|
+
|
|
31
|
+
service = ServiceHost(port=9000)
|
|
32
|
+
|
|
33
|
+
@service.path("/planner")
|
|
34
|
+
class Planner(PlanningResponder):
|
|
35
|
+
def __init__(self, **kwargs):
|
|
36
|
+
super().__init__(
|
|
37
|
+
name="meshagent.planner",
|
|
38
|
+
title="Generic Task Runner",
|
|
39
|
+
description="an agent that will perform a task with the selected tools",
|
|
40
|
+
llm_adapter=OpenAIResponsesAdapter(model="gpt-4.1"),
|
|
41
|
+
supports_tools=True,
|
|
42
|
+
input_prompt=True,
|
|
43
|
+
output_schema={
|
|
44
|
+
"type": "object",
|
|
45
|
+
"required": ["result"],
|
|
46
|
+
"additionalProperties": False,
|
|
47
|
+
"properties": {"result": {"type": "string"}},
|
|
48
|
+
},
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
@service.path("/schema_planner")
|
|
52
|
+
class DynamicPlanner(DynamicPlanningResponder):
|
|
53
|
+
def __init__(self, **kwargs):
|
|
54
|
+
super().__init__(
|
|
55
|
+
name="meshagent.schema_planner",
|
|
56
|
+
title="Schema Task Runner",
|
|
57
|
+
description="an agent that can produces output that matches a schema",
|
|
58
|
+
llm_adapter=OpenAIResponsesAdapter(model="gpt-4.1"),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
@service.path("/schemas/document")
|
|
62
|
+
class DocumentSchemaRegistry(SchemaRegistry):
|
|
63
|
+
def __init__(self):
|
|
64
|
+
name = "document"
|
|
65
|
+
schema = document_schema
|
|
66
|
+
super().__init__(
|
|
67
|
+
name=f"meshagent.schema.{name}",
|
|
68
|
+
validate_webhook_secret=False,
|
|
69
|
+
schemas=[SchemaRegistration(name=name, schema=schema)],
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
@service.path("/schemas/superdoc")
|
|
73
|
+
class SuperdocDocumentSchemaRegistry(SchemaRegistry):
|
|
74
|
+
def __init__(self):
|
|
75
|
+
name = "superdoc"
|
|
76
|
+
schema = super_editor_document_schema
|
|
77
|
+
super().__init__(
|
|
78
|
+
name=f"meshagent.schema.{name}",
|
|
79
|
+
validate_webhook_secret=False,
|
|
80
|
+
schemas=[SchemaRegistration(name=name, schema=schema)],
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
@service.path("/schemas/gallery")
|
|
84
|
+
class GalleryDocumentSchemaRegistry(SchemaRegistry):
|
|
85
|
+
def __init__(self):
|
|
86
|
+
name = "gallery"
|
|
87
|
+
schema = gallery_schema
|
|
88
|
+
super().__init__(
|
|
89
|
+
name=f"meshagent.schema.{name}",
|
|
90
|
+
validate_webhook_secret=False,
|
|
91
|
+
schemas=[SchemaRegistration(name=name, schema=schema)],
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
@service.path("/schemas/thread")
|
|
95
|
+
class ThreadDocumentSchemaRegistry(SchemaRegistry):
|
|
96
|
+
def __init__(self):
|
|
97
|
+
name = "thread"
|
|
98
|
+
schema = thread_schema
|
|
99
|
+
super().__init__(
|
|
100
|
+
name=f"meshagent.schema.{name}",
|
|
101
|
+
validate_webhook_secret=False,
|
|
102
|
+
schemas=[SchemaRegistration(name=name, schema=schema)],
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
@service.path("/schemas/presentation")
|
|
106
|
+
class PresentationDocumentSchemaRegistry(SchemaRegistry):
|
|
107
|
+
def __init__(presentation):
|
|
108
|
+
name = "presentation"
|
|
109
|
+
schema = presentation_schema
|
|
110
|
+
super().__init__(
|
|
111
|
+
name=f"meshagent.schema.{name}",
|
|
112
|
+
validate_webhook_secret=False,
|
|
113
|
+
schemas=[SchemaRegistration(name=name, schema=schema)],
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
@service.path("/schemas/transcript")
|
|
117
|
+
class TranscriptRegistry(SchemaRegistry):
|
|
118
|
+
def __init__(self):
|
|
119
|
+
name = "transcript"
|
|
120
|
+
schema = transcript_schema
|
|
121
|
+
super().__init__(
|
|
122
|
+
name=f"meshagent.schema.{name}",
|
|
123
|
+
validate_webhook_secret=False,
|
|
124
|
+
schemas=[SchemaRegistration(name=name, schema=schema)],
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
@service.path("/toolkits/storage")
|
|
128
|
+
class HostedStorageToolkit(StorageToolkit):
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
await service.run()
|