meshagent-cli 0.7.0__py3-none-any.whl → 0.21.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.
- meshagent/cli/agent.py +15 -11
- meshagent/cli/api_keys.py +4 -4
- meshagent/cli/async_typer.py +52 -4
- meshagent/cli/call.py +12 -8
- meshagent/cli/chatbot.py +1007 -129
- meshagent/cli/cli.py +21 -20
- meshagent/cli/cli_mcp.py +92 -28
- meshagent/cli/cli_secrets.py +10 -10
- meshagent/cli/common_options.py +19 -4
- meshagent/cli/containers.py +164 -16
- meshagent/cli/database.py +997 -0
- meshagent/cli/developer.py +3 -3
- meshagent/cli/exec.py +22 -6
- meshagent/cli/helper.py +62 -11
- meshagent/cli/helpers.py +66 -9
- meshagent/cli/host.py +37 -0
- meshagent/cli/mailbot.py +1004 -40
- meshagent/cli/mailboxes.py +223 -0
- meshagent/cli/meeting_transcriber.py +10 -4
- meshagent/cli/messaging.py +7 -7
- meshagent/cli/multi.py +402 -0
- meshagent/cli/oauth2.py +44 -21
- meshagent/cli/participant_token.py +5 -3
- meshagent/cli/port.py +70 -0
- meshagent/cli/queue.py +2 -2
- meshagent/cli/room.py +20 -212
- meshagent/cli/rooms.py +214 -0
- meshagent/cli/services.py +32 -23
- meshagent/cli/sessions.py +5 -5
- meshagent/cli/storage.py +5 -5
- meshagent/cli/task_runner.py +770 -0
- meshagent/cli/version.py +1 -1
- meshagent/cli/voicebot.py +502 -76
- meshagent/cli/webhook.py +7 -7
- meshagent/cli/worker.py +1327 -0
- {meshagent_cli-0.7.0.dist-info → meshagent_cli-0.21.0.dist-info}/METADATA +13 -13
- meshagent_cli-0.21.0.dist-info/RECORD +44 -0
- meshagent_cli-0.7.0.dist-info/RECORD +0 -36
- {meshagent_cli-0.7.0.dist-info → meshagent_cli-0.21.0.dist-info}/WHEEL +0 -0
- {meshagent_cli-0.7.0.dist-info → meshagent_cli-0.21.0.dist-info}/entry_points.txt +0 -0
- {meshagent_cli-0.7.0.dist-info → meshagent_cli-0.21.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# meshagent/cli/mailboxes.py
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated, Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from aiohttp import ClientResponseError
|
|
9
|
+
from rich import print
|
|
10
|
+
|
|
11
|
+
from meshagent.cli import async_typer
|
|
12
|
+
from meshagent.cli.common_options import ProjectIdOption, OutputFormatOption
|
|
13
|
+
from meshagent.cli.helper import get_client, print_json_table, resolve_project_id
|
|
14
|
+
|
|
15
|
+
app = async_typer.AsyncTyper(help="Manage mailboxes for your project")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.async_command("create")
|
|
19
|
+
async def mailbox_create(
|
|
20
|
+
*,
|
|
21
|
+
project_id: ProjectIdOption,
|
|
22
|
+
address: Annotated[
|
|
23
|
+
str,
|
|
24
|
+
typer.Option(
|
|
25
|
+
"--address",
|
|
26
|
+
"-a",
|
|
27
|
+
help="Mailbox email address (unique per project)",
|
|
28
|
+
),
|
|
29
|
+
],
|
|
30
|
+
room: Annotated[
|
|
31
|
+
str,
|
|
32
|
+
typer.Option(
|
|
33
|
+
"--room",
|
|
34
|
+
"-r",
|
|
35
|
+
help="Room name to route inbound mail into",
|
|
36
|
+
),
|
|
37
|
+
],
|
|
38
|
+
queue: Annotated[
|
|
39
|
+
str,
|
|
40
|
+
typer.Option(
|
|
41
|
+
"--queue",
|
|
42
|
+
"-q",
|
|
43
|
+
help="Queue name to deliver inbound messages to",
|
|
44
|
+
),
|
|
45
|
+
],
|
|
46
|
+
public: Annotated[
|
|
47
|
+
bool,
|
|
48
|
+
typer.Option(
|
|
49
|
+
"--public",
|
|
50
|
+
help="Queue name to deliver inbound messages to",
|
|
51
|
+
),
|
|
52
|
+
] = False,
|
|
53
|
+
):
|
|
54
|
+
"""Create a mailbox attached to the project."""
|
|
55
|
+
client = await get_client()
|
|
56
|
+
try:
|
|
57
|
+
project_id = await resolve_project_id(project_id)
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
await client.create_mailbox(
|
|
61
|
+
project_id=project_id,
|
|
62
|
+
address=address,
|
|
63
|
+
room=room,
|
|
64
|
+
queue=queue,
|
|
65
|
+
public=public,
|
|
66
|
+
)
|
|
67
|
+
except ClientResponseError as exc:
|
|
68
|
+
# Common patterns: 409 conflict on duplicate address, 400 validation, etc.
|
|
69
|
+
if exc.status == 409:
|
|
70
|
+
print(f"[red]Mailbox address already in use:[/] {address}")
|
|
71
|
+
raise typer.Exit(code=1)
|
|
72
|
+
raise
|
|
73
|
+
else:
|
|
74
|
+
print(f"[green]Created mailbox:[/] {address}")
|
|
75
|
+
finally:
|
|
76
|
+
await client.close()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@app.async_command("update")
|
|
80
|
+
async def mailbox_update(
|
|
81
|
+
*,
|
|
82
|
+
project_id: ProjectIdOption,
|
|
83
|
+
address: Annotated[
|
|
84
|
+
str,
|
|
85
|
+
typer.Argument(help="Mailbox email address to update"),
|
|
86
|
+
],
|
|
87
|
+
room: Annotated[
|
|
88
|
+
Optional[str],
|
|
89
|
+
typer.Option(
|
|
90
|
+
"--room",
|
|
91
|
+
"-r",
|
|
92
|
+
help="Room name to route inbound mail into",
|
|
93
|
+
),
|
|
94
|
+
] = None,
|
|
95
|
+
queue: Annotated[
|
|
96
|
+
Optional[str],
|
|
97
|
+
typer.Option(
|
|
98
|
+
"--queue",
|
|
99
|
+
"-q",
|
|
100
|
+
help="Queue name to deliver inbound messages to",
|
|
101
|
+
),
|
|
102
|
+
] = None,
|
|
103
|
+
public: Annotated[
|
|
104
|
+
bool,
|
|
105
|
+
typer.Option(
|
|
106
|
+
"--public",
|
|
107
|
+
help="Queue name to deliver inbound messages to",
|
|
108
|
+
),
|
|
109
|
+
] = False,
|
|
110
|
+
):
|
|
111
|
+
"""Update a mailbox routing configuration."""
|
|
112
|
+
client = await get_client()
|
|
113
|
+
try:
|
|
114
|
+
project_id = await resolve_project_id(project_id)
|
|
115
|
+
|
|
116
|
+
# Keep parity with other CLIs: allow partial update by reading existing first
|
|
117
|
+
if room is None or queue is None:
|
|
118
|
+
try:
|
|
119
|
+
mb = await client.get_mailbox(project_id=project_id, address=address)
|
|
120
|
+
except ClientResponseError as exc:
|
|
121
|
+
if exc.status == 404:
|
|
122
|
+
print(f"[red]Mailbox not found:[/] {address}")
|
|
123
|
+
raise typer.Exit(code=1)
|
|
124
|
+
raise
|
|
125
|
+
room = room or mb.room
|
|
126
|
+
queue = queue or mb.queue
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
await client.update_mailbox(
|
|
130
|
+
project_id=project_id,
|
|
131
|
+
address=address,
|
|
132
|
+
room=room,
|
|
133
|
+
queue=queue,
|
|
134
|
+
public=public,
|
|
135
|
+
)
|
|
136
|
+
except ClientResponseError as exc:
|
|
137
|
+
if exc.status == 404:
|
|
138
|
+
print(f"[red]Mailbox not found:[/] {address}")
|
|
139
|
+
raise typer.Exit(code=1)
|
|
140
|
+
raise
|
|
141
|
+
else:
|
|
142
|
+
print(f"[green]Updated mailbox:[/] {address}")
|
|
143
|
+
finally:
|
|
144
|
+
await client.close()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@app.async_command("show")
|
|
148
|
+
async def mailbox_show(
|
|
149
|
+
*,
|
|
150
|
+
project_id: ProjectIdOption,
|
|
151
|
+
address: Annotated[str, typer.Argument(help="Mailbox address to show")],
|
|
152
|
+
):
|
|
153
|
+
"""Show mailbox details."""
|
|
154
|
+
client = await get_client()
|
|
155
|
+
try:
|
|
156
|
+
project_id = await resolve_project_id(project_id)
|
|
157
|
+
try:
|
|
158
|
+
mb = await client.get_mailbox(project_id=project_id, address=address)
|
|
159
|
+
except ClientResponseError as exc:
|
|
160
|
+
if exc.status == 404:
|
|
161
|
+
print(f"[red]Mailbox not found:[/] {address}")
|
|
162
|
+
raise typer.Exit(code=1)
|
|
163
|
+
raise
|
|
164
|
+
print(mb.model_dump(mode="json"))
|
|
165
|
+
finally:
|
|
166
|
+
await client.close()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@app.async_command("list")
|
|
170
|
+
async def mailbox_list(
|
|
171
|
+
*,
|
|
172
|
+
project_id: ProjectIdOption,
|
|
173
|
+
o: OutputFormatOption = "table",
|
|
174
|
+
):
|
|
175
|
+
"""List mailboxes for the project."""
|
|
176
|
+
client = await get_client()
|
|
177
|
+
try:
|
|
178
|
+
project_id = await resolve_project_id(project_id)
|
|
179
|
+
mailboxes = await client.list_mailboxes(project_id=project_id)
|
|
180
|
+
|
|
181
|
+
if o == "json":
|
|
182
|
+
# Keep your existing conventions: wrap in an object.
|
|
183
|
+
print({"mailboxes": [mb.model_dump(mode="json") for mb in mailboxes]})
|
|
184
|
+
else:
|
|
185
|
+
print_json_table(
|
|
186
|
+
[
|
|
187
|
+
{
|
|
188
|
+
"address": mb.address,
|
|
189
|
+
"room": mb.room,
|
|
190
|
+
"queue": mb.queue,
|
|
191
|
+
"public": mb.public,
|
|
192
|
+
}
|
|
193
|
+
for mb in mailboxes
|
|
194
|
+
],
|
|
195
|
+
"address",
|
|
196
|
+
"room",
|
|
197
|
+
"queue",
|
|
198
|
+
)
|
|
199
|
+
finally:
|
|
200
|
+
await client.close()
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@app.async_command("delete")
|
|
204
|
+
async def mailbox_delete(
|
|
205
|
+
*,
|
|
206
|
+
project_id: ProjectIdOption,
|
|
207
|
+
address: Annotated[str, typer.Argument(help="Mailbox address to delete")],
|
|
208
|
+
):
|
|
209
|
+
"""Delete a mailbox."""
|
|
210
|
+
client = await get_client()
|
|
211
|
+
try:
|
|
212
|
+
project_id = await resolve_project_id(project_id)
|
|
213
|
+
try:
|
|
214
|
+
await client.delete_mailbox(project_id=project_id, address=address)
|
|
215
|
+
except ClientResponseError as exc:
|
|
216
|
+
if exc.status == 404:
|
|
217
|
+
print(f"[red]Mailbox not found:[/] {address}")
|
|
218
|
+
raise typer.Exit(code=1)
|
|
219
|
+
raise
|
|
220
|
+
else:
|
|
221
|
+
print(f"[green]Mailbox deleted:[/] {address}")
|
|
222
|
+
finally:
|
|
223
|
+
await client.close()
|
|
@@ -22,7 +22,7 @@ app = async_typer.AsyncTyper(help="Join a meeting transcriber to a room")
|
|
|
22
22
|
@app.async_command("join")
|
|
23
23
|
async def join(
|
|
24
24
|
*,
|
|
25
|
-
project_id: ProjectIdOption
|
|
25
|
+
project_id: ProjectIdOption,
|
|
26
26
|
room: RoomOption,
|
|
27
27
|
agent_name: Annotated[str, typer.Option(..., help="Name of the agent")],
|
|
28
28
|
key: Annotated[
|
|
@@ -93,9 +93,15 @@ async def join(
|
|
|
93
93
|
async def service(
|
|
94
94
|
*,
|
|
95
95
|
agent_name: Annotated[str, typer.Option(..., help="Name of the agent")],
|
|
96
|
-
host: Annotated[
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
host: Annotated[
|
|
97
|
+
Optional[str], typer.Option(help="Host to bind the service on")
|
|
98
|
+
] = None,
|
|
99
|
+
port: Annotated[
|
|
100
|
+
Optional[int], typer.Option(help="Port to bind the service on")
|
|
101
|
+
] = None,
|
|
102
|
+
path: Annotated[
|
|
103
|
+
str, typer.Option(help="HTTP path to mount the service at")
|
|
104
|
+
] = "/agent",
|
|
99
105
|
):
|
|
100
106
|
try:
|
|
101
107
|
from meshagent.livekit.agents.meeting_transcriber import MeetingTranscriber
|
meshagent/cli/messaging.py
CHANGED
|
@@ -16,13 +16,13 @@ from meshagent.cli.helper import (
|
|
|
16
16
|
resolve_room,
|
|
17
17
|
)
|
|
18
18
|
|
|
19
|
-
app = async_typer.AsyncTyper()
|
|
19
|
+
app = async_typer.AsyncTyper(help="Send and receive messages in a room")
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
@app.async_command("list-participants")
|
|
22
|
+
@app.async_command("list-participants", help="List messaging-enabled participants")
|
|
23
23
|
async def messaging_list_participants_command(
|
|
24
24
|
*,
|
|
25
|
-
project_id: ProjectIdOption
|
|
25
|
+
project_id: ProjectIdOption,
|
|
26
26
|
room: RoomOption,
|
|
27
27
|
):
|
|
28
28
|
"""
|
|
@@ -61,10 +61,10 @@ async def messaging_list_participants_command(
|
|
|
61
61
|
await account_client.close()
|
|
62
62
|
|
|
63
63
|
|
|
64
|
-
@app.async_command("send")
|
|
64
|
+
@app.async_command("send", help="Send a direct message to a participant")
|
|
65
65
|
async def messaging_send_command(
|
|
66
66
|
*,
|
|
67
|
-
project_id: ProjectIdOption
|
|
67
|
+
project_id: ProjectIdOption,
|
|
68
68
|
room: RoomOption,
|
|
69
69
|
to_participant_id: Annotated[
|
|
70
70
|
str, typer.Option(..., help="Participant ID to send a message to")
|
|
@@ -120,10 +120,10 @@ async def messaging_send_command(
|
|
|
120
120
|
await account_client.close()
|
|
121
121
|
|
|
122
122
|
|
|
123
|
-
@app.async_command("broadcast")
|
|
123
|
+
@app.async_command("broadcast", help="Broadcast a message to all participants")
|
|
124
124
|
async def messaging_broadcast_command(
|
|
125
125
|
*,
|
|
126
|
-
project_id: ProjectIdOption
|
|
126
|
+
project_id: ProjectIdOption,
|
|
127
127
|
room: RoomOption,
|
|
128
128
|
data: Annotated[str, typer.Option(..., help="JSON message to broadcast")],
|
|
129
129
|
):
|
meshagent/cli/multi.py
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from meshagent.cli import async_typer
|
|
3
|
+
from meshagent.cli.host import run_services, set_deferred, service_specs, get_service
|
|
4
|
+
from meshagent.cli.common_options import ProjectIdOption
|
|
5
|
+
from typing import Annotated, Optional
|
|
6
|
+
|
|
7
|
+
import importlib.util
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import click
|
|
10
|
+
import shlex
|
|
11
|
+
|
|
12
|
+
from rich import print
|
|
13
|
+
from meshagent.cli.call import _make_call
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
from typer.main import get_command
|
|
17
|
+
|
|
18
|
+
from meshagent.cli.helper import (
|
|
19
|
+
get_client,
|
|
20
|
+
resolve_project_id,
|
|
21
|
+
resolve_room,
|
|
22
|
+
resolve_key,
|
|
23
|
+
)
|
|
24
|
+
from meshagent.api import (
|
|
25
|
+
ParticipantToken,
|
|
26
|
+
ApiScope,
|
|
27
|
+
)
|
|
28
|
+
from aiohttp import ClientResponseError
|
|
29
|
+
import asyncio
|
|
30
|
+
|
|
31
|
+
from meshagent.api.keys import parse_api_key
|
|
32
|
+
|
|
33
|
+
from meshagent.cli.chatbot import service as chatbot_service
|
|
34
|
+
from meshagent.cli.worker import service as worker_service
|
|
35
|
+
from meshagent.cli.mailbot import service as mailbot_service
|
|
36
|
+
from meshagent.cli.voicebot import service as voicebot_service
|
|
37
|
+
|
|
38
|
+
import yaml
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
app = async_typer.AsyncTyper(help="Connect agents and tools to a room")
|
|
42
|
+
|
|
43
|
+
cli = async_typer.AsyncTyper(help="Add agents to a team")
|
|
44
|
+
|
|
45
|
+
cli.command("chatbot")(chatbot_service)
|
|
46
|
+
cli.command("worker")(worker_service)
|
|
47
|
+
cli.command("mailbot")(mailbot_service)
|
|
48
|
+
cli.command("voicebot")(voicebot_service)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@cli.async_command("python")
|
|
52
|
+
async def python(
|
|
53
|
+
*,
|
|
54
|
+
module: str,
|
|
55
|
+
host: Annotated[
|
|
56
|
+
Optional[str], typer.Option(help="Host to bind the service on")
|
|
57
|
+
] = None,
|
|
58
|
+
port: Annotated[
|
|
59
|
+
Optional[int], typer.Option(help="Port to bind the service on")
|
|
60
|
+
] = None,
|
|
61
|
+
path: Annotated[
|
|
62
|
+
Optional[str],
|
|
63
|
+
typer.Option(help="A path to add the service at"),
|
|
64
|
+
] = None,
|
|
65
|
+
identity: Annotated[
|
|
66
|
+
Optional[str],
|
|
67
|
+
typer.Option(help="The desired identity for the service"),
|
|
68
|
+
] = None,
|
|
69
|
+
name: Annotated[
|
|
70
|
+
str, typer.Option(help="Entry-point name in the Python module")
|
|
71
|
+
] = "main",
|
|
72
|
+
):
|
|
73
|
+
service = get_service(host=host, port=port)
|
|
74
|
+
|
|
75
|
+
if path is None:
|
|
76
|
+
path = "/agent"
|
|
77
|
+
i = 0
|
|
78
|
+
while service.has_path(path):
|
|
79
|
+
i += 1
|
|
80
|
+
path = f"/agent{i}"
|
|
81
|
+
|
|
82
|
+
module = import_from_path(module)
|
|
83
|
+
service.add_path(path=path, identity=identity, cls=getattr(module, name or "main"))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def execute_via_root(app, line: str, *, prog_name="meshagent") -> int:
|
|
87
|
+
cmd = get_command(app)
|
|
88
|
+
try:
|
|
89
|
+
cmd.main(args=shlex.split(line), prog_name=prog_name, standalone_mode=False)
|
|
90
|
+
return 0
|
|
91
|
+
except click.ClickException as e:
|
|
92
|
+
e.show()
|
|
93
|
+
return e.exit_code
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
subcommand_help = """a list of sub commands to run, seperated by semicolons
|
|
97
|
+
|
|
98
|
+
available sub commands:
|
|
99
|
+
|
|
100
|
+
chatbot ...;
|
|
101
|
+
mailbot ...;
|
|
102
|
+
worker ...;
|
|
103
|
+
voicebot ...;
|
|
104
|
+
python path-to-python-file.py --name=NameOfModule;
|
|
105
|
+
|
|
106
|
+
chatbot, worker, and mailbot command arguments mirror those of the respective meshagent chatbot service, meshagent mailbot service, meshagent voicebot service, and meshagent worker service commands.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def build_spec(
|
|
111
|
+
*,
|
|
112
|
+
command: Annotated[str, typer.Option("-c", help=subcommand_help)],
|
|
113
|
+
service_name: Annotated[str, typer.Option("--service-name", help="service name")],
|
|
114
|
+
service_description: Annotated[
|
|
115
|
+
Optional[str], typer.Option("--service-description", help="service description")
|
|
116
|
+
] = None,
|
|
117
|
+
service_title: Annotated[
|
|
118
|
+
Optional[str],
|
|
119
|
+
typer.Option("--service-title", help="a display name for the service"),
|
|
120
|
+
] = None,
|
|
121
|
+
):
|
|
122
|
+
for c in command.split(";"):
|
|
123
|
+
if execute_via_root(cli, c, prog_name="meshagent") != 0:
|
|
124
|
+
print(f"[red]{c} failed[/red]")
|
|
125
|
+
raise typer.Exit(1)
|
|
126
|
+
|
|
127
|
+
specs = service_specs()
|
|
128
|
+
if len(specs) == 0:
|
|
129
|
+
print("[red]found no services, specify at least one agent or tool to run[/red]")
|
|
130
|
+
raise typer.Exit(1)
|
|
131
|
+
|
|
132
|
+
if len(specs) > 1:
|
|
133
|
+
print(
|
|
134
|
+
"[red]found multiple services leave host and port empty or use the same port for each command[/red]"
|
|
135
|
+
)
|
|
136
|
+
raise typer.Exit(1)
|
|
137
|
+
|
|
138
|
+
spec = specs[0]
|
|
139
|
+
spec.metadata.annotations = {
|
|
140
|
+
"meshagent.service.id": service_name,
|
|
141
|
+
}
|
|
142
|
+
for port in spec.ports:
|
|
143
|
+
port.num = "*"
|
|
144
|
+
|
|
145
|
+
spec.metadata.name = service_name
|
|
146
|
+
spec.metadata.description = service_description
|
|
147
|
+
spec.container.image = (
|
|
148
|
+
"us-central1-docker.pkg.dev/meshagent-public/images/cli:{SERVER_VERSION}-esgz"
|
|
149
|
+
)
|
|
150
|
+
spec.container.command = (
|
|
151
|
+
f'meshagent multi service -c "{command.replace('"', '\\"')}"'
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@app.async_command("spec")
|
|
156
|
+
async def spec(
|
|
157
|
+
command: Annotated[str, typer.Option("-c", help=subcommand_help)],
|
|
158
|
+
service_name: Annotated[str, typer.Option("--service-name", help="service name")],
|
|
159
|
+
service_description: Annotated[
|
|
160
|
+
Optional[str], typer.Option("--service-description", help="service description")
|
|
161
|
+
] = None,
|
|
162
|
+
service_title: Annotated[
|
|
163
|
+
Optional[str],
|
|
164
|
+
typer.Option("--service-title", help="a display name for the service"),
|
|
165
|
+
] = None,
|
|
166
|
+
):
|
|
167
|
+
set_deferred(True)
|
|
168
|
+
|
|
169
|
+
spec = build_spec(
|
|
170
|
+
command=command,
|
|
171
|
+
service_name=service_name,
|
|
172
|
+
service_description=service_description,
|
|
173
|
+
service_title=service_title,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
print(yaml.dump(spec.model_dump(mode="json", exclude_none=True), sort_keys=False))
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@app.async_command("deploy")
|
|
180
|
+
async def deploy(
|
|
181
|
+
project_id: ProjectIdOption,
|
|
182
|
+
command: Annotated[str, typer.Option("-c", help=subcommand_help)],
|
|
183
|
+
service_name: Annotated[str, typer.Option("--service-name", help="service name")],
|
|
184
|
+
service_description: Annotated[
|
|
185
|
+
Optional[str], typer.Option("--service-description", help="service description")
|
|
186
|
+
] = None,
|
|
187
|
+
service_title: Annotated[
|
|
188
|
+
Optional[str],
|
|
189
|
+
typer.Option("--service-title", help="a display name for the service"),
|
|
190
|
+
] = None,
|
|
191
|
+
room: Annotated[
|
|
192
|
+
Optional[str],
|
|
193
|
+
typer.Option("--room", help="The name of a room to create the service for"),
|
|
194
|
+
] = None,
|
|
195
|
+
):
|
|
196
|
+
project_id = await resolve_project_id(project_id)
|
|
197
|
+
|
|
198
|
+
client = await get_client()
|
|
199
|
+
try:
|
|
200
|
+
set_deferred(True)
|
|
201
|
+
|
|
202
|
+
spec = build_spec(
|
|
203
|
+
command=command,
|
|
204
|
+
service_name=service_name,
|
|
205
|
+
service_description=service_description,
|
|
206
|
+
service_title=service_title,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
spec.container.secrets = []
|
|
210
|
+
|
|
211
|
+
id = None
|
|
212
|
+
try:
|
|
213
|
+
if id is None:
|
|
214
|
+
if room is None:
|
|
215
|
+
services = await client.list_services(project_id=project_id)
|
|
216
|
+
else:
|
|
217
|
+
services = await client.list_room_services(
|
|
218
|
+
project_id=project_id, room_name=room
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
for s in services:
|
|
222
|
+
if s.metadata.name == spec.metadata.name:
|
|
223
|
+
id = s.id
|
|
224
|
+
|
|
225
|
+
if id is None:
|
|
226
|
+
if room is None:
|
|
227
|
+
id = await client.create_service(
|
|
228
|
+
project_id=project_id, service=spec
|
|
229
|
+
)
|
|
230
|
+
else:
|
|
231
|
+
id = await client.create_room_service(
|
|
232
|
+
project_id=project_id, service=spec, room_name=room
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
else:
|
|
236
|
+
spec.id = id
|
|
237
|
+
if room is None:
|
|
238
|
+
await client.update_service(
|
|
239
|
+
project_id=project_id, service_id=id, service=spec
|
|
240
|
+
)
|
|
241
|
+
else:
|
|
242
|
+
await client.update_room_service(
|
|
243
|
+
project_id=project_id,
|
|
244
|
+
service_id=id,
|
|
245
|
+
service=spec,
|
|
246
|
+
room_name=room,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
except ClientResponseError as exc:
|
|
250
|
+
if exc.status == 409:
|
|
251
|
+
print(f"[red]Service name already in use: {spec.metadata.name}[/red]")
|
|
252
|
+
raise typer.Exit(code=1)
|
|
253
|
+
raise
|
|
254
|
+
else:
|
|
255
|
+
print(f"[green]Deployed service:[/] {id}")
|
|
256
|
+
|
|
257
|
+
finally:
|
|
258
|
+
await client.close()
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@app.async_command("service")
|
|
262
|
+
async def host(
|
|
263
|
+
host: Annotated[
|
|
264
|
+
Optional[str], typer.Option(help="Host to bind the service on")
|
|
265
|
+
] = None,
|
|
266
|
+
port: Annotated[
|
|
267
|
+
Optional[int], typer.Option(help="Port to bind the service on")
|
|
268
|
+
] = None,
|
|
269
|
+
command: Annotated[str, typer.Option("-c", help=subcommand_help)] = [],
|
|
270
|
+
):
|
|
271
|
+
set_deferred(True)
|
|
272
|
+
|
|
273
|
+
for c in command.split(";"):
|
|
274
|
+
if execute_via_root(cli, c, prog_name="meshagent") != 0:
|
|
275
|
+
print(f"[red]{c} failed[/red]")
|
|
276
|
+
raise typer.Exit(1)
|
|
277
|
+
|
|
278
|
+
await run_services()
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def import_from_path(path: str, module_name: str | None = None):
|
|
282
|
+
path = Path(path)
|
|
283
|
+
module_name = module_name or path.stem
|
|
284
|
+
|
|
285
|
+
spec = importlib.util.spec_from_file_location(module_name, path)
|
|
286
|
+
if spec is None or spec.loader is None:
|
|
287
|
+
raise ImportError(f"Cannot load spec for {path}")
|
|
288
|
+
|
|
289
|
+
module = importlib.util.module_from_spec(spec)
|
|
290
|
+
spec.loader.exec_module(module)
|
|
291
|
+
return module
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@app.async_command("join")
|
|
295
|
+
async def join(
|
|
296
|
+
*,
|
|
297
|
+
project_id: ProjectIdOption,
|
|
298
|
+
command: Annotated[str, typer.Option("-c", help=subcommand_help)] = [],
|
|
299
|
+
port: Annotated[
|
|
300
|
+
int,
|
|
301
|
+
typer.Option(
|
|
302
|
+
"--port",
|
|
303
|
+
"-p",
|
|
304
|
+
help=(
|
|
305
|
+
"a port number to run the agent on (will set MESHAGENT_PORT environment variable when launching the service)"
|
|
306
|
+
),
|
|
307
|
+
),
|
|
308
|
+
] = None,
|
|
309
|
+
room: Annotated[
|
|
310
|
+
Optional[str],
|
|
311
|
+
typer.Option(
|
|
312
|
+
help="A room name to test the service in (must not be currently running)"
|
|
313
|
+
),
|
|
314
|
+
] = None,
|
|
315
|
+
key: Annotated[
|
|
316
|
+
str,
|
|
317
|
+
typer.Option("--key", help="an api key to sign the token with"),
|
|
318
|
+
] = None,
|
|
319
|
+
):
|
|
320
|
+
set_deferred(True)
|
|
321
|
+
|
|
322
|
+
for c in command.split(";"):
|
|
323
|
+
if execute_via_root(cli, c, prog_name="meshagent") != 0:
|
|
324
|
+
print(f"[red]{c} failed[/red]")
|
|
325
|
+
raise typer.Exit(1)
|
|
326
|
+
|
|
327
|
+
services_task = asyncio.create_task(run_services())
|
|
328
|
+
|
|
329
|
+
key = await resolve_key(project_id=project_id, key=key)
|
|
330
|
+
|
|
331
|
+
if port is None:
|
|
332
|
+
import socket
|
|
333
|
+
|
|
334
|
+
def find_free_port():
|
|
335
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
336
|
+
s.bind(("", 0)) # Bind to a free port provided by the host.
|
|
337
|
+
s.listen(1)
|
|
338
|
+
return s.getsockname()[1]
|
|
339
|
+
|
|
340
|
+
port = find_free_port()
|
|
341
|
+
|
|
342
|
+
my_client = await get_client()
|
|
343
|
+
try:
|
|
344
|
+
project_id = await resolve_project_id(project_id)
|
|
345
|
+
room = resolve_room(room)
|
|
346
|
+
|
|
347
|
+
if room is None:
|
|
348
|
+
print("[bold red]Room was not set[/bold red]")
|
|
349
|
+
raise typer.Exit(1)
|
|
350
|
+
|
|
351
|
+
try:
|
|
352
|
+
parsed_key = parse_api_key(key)
|
|
353
|
+
token = ParticipantToken(
|
|
354
|
+
name="cli", project_id=project_id, api_key_id=parsed_key.id
|
|
355
|
+
)
|
|
356
|
+
token.add_api_grant(ApiScope.agent_default())
|
|
357
|
+
token.add_role_grant("user")
|
|
358
|
+
token.add_room_grant(room)
|
|
359
|
+
|
|
360
|
+
print("[bold green]Connecting to room...[/bold green]")
|
|
361
|
+
|
|
362
|
+
run_tasks = []
|
|
363
|
+
|
|
364
|
+
for spec in service_specs():
|
|
365
|
+
sys.stdout.write("\n")
|
|
366
|
+
|
|
367
|
+
for p in spec.ports:
|
|
368
|
+
print(f"[bold green]Connecting port {p.num}...[/bold green]")
|
|
369
|
+
|
|
370
|
+
for endpoint in p.endpoints:
|
|
371
|
+
print(
|
|
372
|
+
f"[bold green]Connecting endpoint {endpoint.path}...[/bold green]"
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
run_tasks.append(
|
|
376
|
+
asyncio.create_task(
|
|
377
|
+
_make_call(
|
|
378
|
+
room=room,
|
|
379
|
+
project_id=project_id,
|
|
380
|
+
participant_name=endpoint.meshagent.identity,
|
|
381
|
+
url=f"http://localhost:{p.num}{endpoint.path}",
|
|
382
|
+
arguments={},
|
|
383
|
+
key=key,
|
|
384
|
+
permissions=endpoint.meshagent.api,
|
|
385
|
+
)
|
|
386
|
+
)
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
await asyncio.gather(*run_tasks, services_task)
|
|
390
|
+
|
|
391
|
+
except ClientResponseError as exc:
|
|
392
|
+
if exc.status == 409:
|
|
393
|
+
print(f"[red]Room already in use: {room}[/red]")
|
|
394
|
+
raise typer.Exit(code=1)
|
|
395
|
+
raise
|
|
396
|
+
|
|
397
|
+
except Exception as e:
|
|
398
|
+
print(f"[red]{e}[/red]")
|
|
399
|
+
raise typer.Exit(code=1)
|
|
400
|
+
|
|
401
|
+
finally:
|
|
402
|
+
await my_client.close()
|