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.
Files changed (41) hide show
  1. meshagent/cli/agent.py +15 -11
  2. meshagent/cli/api_keys.py +4 -4
  3. meshagent/cli/async_typer.py +52 -4
  4. meshagent/cli/call.py +12 -8
  5. meshagent/cli/chatbot.py +1007 -129
  6. meshagent/cli/cli.py +21 -20
  7. meshagent/cli/cli_mcp.py +92 -28
  8. meshagent/cli/cli_secrets.py +10 -10
  9. meshagent/cli/common_options.py +19 -4
  10. meshagent/cli/containers.py +164 -16
  11. meshagent/cli/database.py +997 -0
  12. meshagent/cli/developer.py +3 -3
  13. meshagent/cli/exec.py +22 -6
  14. meshagent/cli/helper.py +62 -11
  15. meshagent/cli/helpers.py +66 -9
  16. meshagent/cli/host.py +37 -0
  17. meshagent/cli/mailbot.py +1004 -40
  18. meshagent/cli/mailboxes.py +223 -0
  19. meshagent/cli/meeting_transcriber.py +10 -4
  20. meshagent/cli/messaging.py +7 -7
  21. meshagent/cli/multi.py +402 -0
  22. meshagent/cli/oauth2.py +44 -21
  23. meshagent/cli/participant_token.py +5 -3
  24. meshagent/cli/port.py +70 -0
  25. meshagent/cli/queue.py +2 -2
  26. meshagent/cli/room.py +20 -212
  27. meshagent/cli/rooms.py +214 -0
  28. meshagent/cli/services.py +32 -23
  29. meshagent/cli/sessions.py +5 -5
  30. meshagent/cli/storage.py +5 -5
  31. meshagent/cli/task_runner.py +770 -0
  32. meshagent/cli/version.py +1 -1
  33. meshagent/cli/voicebot.py +502 -76
  34. meshagent/cli/webhook.py +7 -7
  35. meshagent/cli/worker.py +1327 -0
  36. {meshagent_cli-0.7.0.dist-info → meshagent_cli-0.21.0.dist-info}/METADATA +13 -13
  37. meshagent_cli-0.21.0.dist-info/RECORD +44 -0
  38. meshagent_cli-0.7.0.dist-info/RECORD +0 -36
  39. {meshagent_cli-0.7.0.dist-info → meshagent_cli-0.21.0.dist-info}/WHEEL +0 -0
  40. {meshagent_cli-0.7.0.dist-info → meshagent_cli-0.21.0.dist-info}/entry_points.txt +0 -0
  41. {meshagent_cli-0.7.0.dist-info → meshagent_cli-0.21.0.dist-info}/top_level.txt +0 -0
meshagent/cli/mailbot.py CHANGED
@@ -1,6 +1,8 @@
1
1
  import typer
2
- from meshagent.api import ParticipantToken
2
+ from meshagent.cli import async_typer
3
3
  from rich import print
4
+
5
+ from meshagent.api import ParticipantToken
4
6
  from typing import Annotated, Optional
5
7
  from meshagent.cli.common_options import (
6
8
  ProjectIdOption,
@@ -9,24 +11,52 @@ from meshagent.cli.common_options import (
9
11
  from meshagent.tools import Toolkit
10
12
  from meshagent.api import RoomClient, WebSocketClientProtocol, ApiScope
11
13
  from meshagent.api.helpers import meshagent_base_url, websocket_room_url
12
- from meshagent.cli import async_typer
13
14
  from meshagent.cli.helper import (
14
15
  get_client,
15
16
  resolve_project_id,
16
17
  resolve_room,
17
18
  resolve_key,
19
+ cleanup_args,
18
20
  )
19
21
  from meshagent.openai import OpenAIResponsesAdapter
20
- from meshagent.openai.tools.responses_adapter import ImageGenerationTool, LocalShellTool
21
- from meshagent.api.services import ServiceHost
22
22
 
23
+ from meshagent.agents.config import RulesConfig
23
24
 
24
25
  from typing import List
25
26
  from pathlib import Path
26
27
 
27
- from meshagent.api import RequiredToolkit, RequiredSchema
28
- from meshagent.openai.tools.responses_adapter import WebSearchTool
28
+ from meshagent.api import RequiredToolkit, RequiredSchema, RoomException
29
+
30
+ import logging
31
+
32
+ from meshagent.tools.database import DatabaseToolkitBuilder, DatabaseToolkitConfig
33
+
34
+ from meshagent.tools.storage import StorageToolkit
35
+ from meshagent.tools.datetime import DatetimeToolkit
36
+ from meshagent.tools.uuid import UUIDToolkit
37
+
38
+ from meshagent.openai.tools.responses_adapter import (
39
+ WebSearchTool,
40
+ ShellConfig,
41
+ ApplyPatchConfig,
42
+ ApplyPatchTool,
43
+ ShellTool,
44
+ LocalShellTool,
45
+ ImageGenerationTool,
46
+ )
47
+
48
+ from meshagent.cli.host import get_service, run_services, get_deferred, service_specs
49
+ from meshagent.api.specs.service import AgentSpec, ANNOTATION_AGENT_TYPE
50
+
51
+ import yaml
52
+
53
+ import shlex
54
+ import sys
29
55
 
56
+ from meshagent.api.client import ConflictError
57
+ from meshagent.agents.adapter import MessageStreamLLMAdapter
58
+
59
+ logger = logging.getLogger("mailbot")
30
60
 
31
61
  app = async_typer.AsyncTyper(help="Join a mailbot to a room")
32
62
 
@@ -45,11 +75,36 @@ def build_mailbot(
45
75
  web_search: Annotated[
46
76
  Optional[bool], typer.Option(..., help="Enable web search tool calling")
47
77
  ] = False,
78
+ toolkit_name: Optional[str] = None,
48
79
  queue: str,
49
80
  email_address: str,
81
+ room_rules_paths: list[str],
82
+ whitelist=list[str],
83
+ require_shell: Optional[bool] = None,
84
+ require_apply_patch: Optional[bool] = None,
85
+ require_storage: Optional[str] = None,
86
+ require_read_only_storage: Optional[str] = None,
87
+ require_time: bool = True,
88
+ require_uuid: bool = False,
89
+ require_table_read: bool,
90
+ require_table_write: bool,
91
+ require_computer_use: bool,
92
+ reply_all: bool,
93
+ database_namespace: Optional[list[str]] = None,
94
+ enable_attachments: bool,
95
+ working_directory: Optional[str] = None,
96
+ skill_dirs: Optional[list[str]] = None,
97
+ shell_image: Optional[str] = None,
98
+ llm_participant: Optional[str] = None,
99
+ log_llm_requests: Optional[bool] = None,
50
100
  ):
51
101
  from meshagent.agents.mail import MailWorker
52
102
 
103
+ if (require_storage or require_read_only_storage) and len(whitelist) == 0:
104
+ logger.warning(
105
+ "you have enabled storage tools without a whilelist, anyone who can send to this mailbox will be able to ask it about files"
106
+ )
107
+
53
108
  requirements = []
54
109
 
55
110
  toolkits = []
@@ -68,10 +123,34 @@ def build_mailbot(
68
123
  print(f"[yellow]rules file not found at {rules_file}[/yellow]")
69
124
 
70
125
  BaseClass = MailWorker
71
- if computer_use:
72
- raise ValueError("computer use is not yet supported for the mail agent")
126
+ if llm_participant:
127
+ llm_adapter = MessageStreamLLMAdapter(
128
+ participant_name=llm_participant,
129
+ )
73
130
  else:
74
- llm_adapter = OpenAIResponsesAdapter(model=model)
131
+ if computer_use or require_computer_use:
132
+ llm_adapter = OpenAIResponsesAdapter(
133
+ model=model,
134
+ response_options={
135
+ "reasoning": {"summary": "concise"},
136
+ "truncation": "auto",
137
+ },
138
+ log_requests=log_llm_requests,
139
+ )
140
+
141
+ else:
142
+ llm_adapter = OpenAIResponsesAdapter(
143
+ model=model,
144
+ log_requests=log_llm_requests,
145
+ )
146
+
147
+ parsed_whitelist = []
148
+ if len(whitelist) > 0:
149
+ for w in whitelist:
150
+ for s in w.split(","):
151
+ s = s.strip()
152
+ if len(s) > 0:
153
+ parsed_whitelist.append(s)
75
154
 
76
155
  class CustomMailbot(BaseClass):
77
156
  def __init__(self):
@@ -82,14 +161,65 @@ def build_mailbot(
82
161
  toolkits=toolkits,
83
162
  queue=queue,
84
163
  email_address=email_address,
164
+ toolkit_name=toolkit_name,
85
165
  rules=rule if len(rule) > 0 else None,
166
+ whitelist=parsed_whitelist if len(parsed_whitelist) > 0 else None,
167
+ reply_all=reply_all,
168
+ enable_attachments=enable_attachments,
169
+ skill_dirs=skill_dirs,
86
170
  )
87
171
 
88
172
  async def start(self, *, room: RoomClient):
89
173
  print(
90
174
  "[bold green]Configure and send an email interact with your mailbot[/bold green]"
91
175
  )
92
- return await super().start(room=room)
176
+ await super().start(room=room)
177
+ if room_rules_paths is not None:
178
+ for p in room_rules_paths:
179
+ await self._load_room_rules(path=p)
180
+
181
+ async def get_rules(self):
182
+ rules = [*await super().get_rules()]
183
+ if room_rules_paths is not None:
184
+ for p in room_rules_paths:
185
+ rules.extend(await self._load_room_rules(path=p))
186
+
187
+ return rules
188
+
189
+ async def _load_room_rules(
190
+ self,
191
+ *,
192
+ path: str,
193
+ ):
194
+ rules = []
195
+ try:
196
+ room_rules = await self.room.storage.download(path=path)
197
+
198
+ rules_txt = room_rules.data.decode()
199
+
200
+ rules_config = RulesConfig.parse(rules_txt)
201
+
202
+ if rules_config.rules is not None:
203
+ rules.extend(rules_config.rules)
204
+
205
+ except RoomException:
206
+ try:
207
+ logger.info("attempting to initialize rules file")
208
+ handle = await self.room.storage.open(path=path, overwrite=False)
209
+ await self.room.storage.write(
210
+ handle=handle,
211
+ data="# Add rules to this file to customize your agent's behavior, lines starting with # will be ignored.\n\n".encode(),
212
+ )
213
+ await self.room.storage.close(handle=handle)
214
+
215
+ except RoomException:
216
+ pass
217
+ logger.info(
218
+ f"unable to load rules from {path}, continuing with default rules"
219
+ )
220
+ pass
221
+
222
+ return rules
93
223
 
94
224
  async def get_thread_toolkits(self, *, thread_context):
95
225
  toolkits = await super().get_thread_toolkits(thread_context=thread_context)
@@ -101,6 +231,22 @@ def build_mailbot(
101
231
  LocalShellTool(thread_context=thread_context)
102
232
  )
103
233
 
234
+ if require_shell:
235
+ thread_toolkit.tools.append(
236
+ ShellTool(
237
+ working_directory=working_directory,
238
+ config=ShellConfig(name="shell"),
239
+ image=shell_image or "python:3.13",
240
+ )
241
+ )
242
+
243
+ if require_apply_patch:
244
+ thread_toolkit.tools.append(
245
+ ApplyPatchTool(
246
+ config=ApplyPatchConfig(name="apply_patch"),
247
+ )
248
+ )
249
+
104
250
  if image_generation is not None:
105
251
  print("adding openai image gen to thread", flush=True)
106
252
  thread_toolkit.tools.append(
@@ -114,6 +260,55 @@ def build_mailbot(
114
260
  if web_search:
115
261
  thread_toolkit.tools.append(WebSearchTool())
116
262
 
263
+ if require_storage:
264
+ thread_toolkit.tools.extend(StorageToolkit().tools)
265
+
266
+ if require_read_only_storage:
267
+ thread_toolkit.tools.extend(StorageToolkit(read_only=True).tools)
268
+
269
+ if len(require_table_read) > 0:
270
+ thread_toolkit.tools.extend(
271
+ (
272
+ await DatabaseToolkitBuilder().make(
273
+ room=self.room,
274
+ model=model,
275
+ config=DatabaseToolkitConfig(
276
+ tables=require_table_read,
277
+ read_only=True,
278
+ namespace=database_namespace,
279
+ ),
280
+ )
281
+ ).tools
282
+ )
283
+
284
+ if len(require_table_write) > 0:
285
+ thread_toolkit.tools.extend(
286
+ (
287
+ await DatabaseToolkitBuilder().make(
288
+ room=self.room,
289
+ model=model,
290
+ config=DatabaseToolkitConfig(
291
+ tables=require_table_write,
292
+ read_only=False,
293
+ namespace=database_namespace,
294
+ ),
295
+ )
296
+ ).tools
297
+ )
298
+
299
+ if require_time:
300
+ thread_toolkit.tools.extend(DatetimeToolkit().tools)
301
+
302
+ if require_uuid:
303
+ thread_toolkit.tools.extend(UUIDToolkit().tools)
304
+
305
+ if require_computer_use:
306
+ from meshagent.computers.agent import ComputerToolkit
307
+
308
+ computer_toolkit = ComputerToolkit(room=self.room, render_screen=None)
309
+
310
+ toolkits.append(computer_toolkit)
311
+
117
312
  toolkits.append(thread_toolkit)
118
313
  return toolkits
119
314
 
@@ -123,12 +318,24 @@ def build_mailbot(
123
318
  @app.async_command("join")
124
319
  async def make_call(
125
320
  *,
126
- project_id: ProjectIdOption = None,
321
+ project_id: ProjectIdOption,
127
322
  room: RoomOption,
128
323
  role: str = "agent",
129
324
  agent_name: Annotated[str, typer.Option(..., help="Name of the agent to call")],
130
325
  rule: Annotated[List[str], typer.Option("--rule", "-r", help="a system rule")] = [],
131
326
  rules_file: Optional[str] = None,
327
+ require_toolkit: Annotated[
328
+ List[str],
329
+ typer.Option(
330
+ "--require-toolkit", "-rt", help="the name or url of a required toolkit"
331
+ ),
332
+ ] = [],
333
+ require_schema: Annotated[
334
+ List[str],
335
+ typer.Option(
336
+ "--require-schema", "-rs", help="the name or url of a required schema"
337
+ ),
338
+ ] = [],
132
339
  toolkit: Annotated[
133
340
  List[str],
134
341
  typer.Option("--toolkit", "-t", help="the name or url of a required toolkit"),
@@ -139,13 +346,21 @@ async def make_call(
139
346
  ] = [],
140
347
  model: Annotated[
141
348
  str, typer.Option(..., help="Name of the LLM model to use for the chatbot")
142
- ] = "gpt-5",
143
- local_shell: Annotated[
349
+ ] = "gpt-5.2",
350
+ require_shell: Annotated[
351
+ Optional[bool],
352
+ typer.Option(..., help="Enable function shell tool calling"),
353
+ ] = False,
354
+ require_local_shell: Annotated[
144
355
  Optional[bool], typer.Option(..., help="Enable local shell tool calling")
145
356
  ] = False,
146
- web_search: Annotated[
357
+ require_web_search: Annotated[
147
358
  Optional[bool], typer.Option(..., help="Enable web search tool calling")
148
359
  ] = False,
360
+ require_apply_patch: Annotated[
361
+ Optional[bool],
362
+ typer.Option(..., help="Enable apply patch tool calling"),
363
+ ] = False,
149
364
  key: Annotated[
150
365
  str,
151
366
  typer.Option("--key", help="an api key to sign the token with"),
@@ -154,6 +369,92 @@ async def make_call(
154
369
  email_address: Annotated[
155
370
  str, typer.Option(..., help="the email address of the agent")
156
371
  ],
372
+ toolkit_name: Annotated[
373
+ Optional[str],
374
+ typer.Option(..., help="the name of a toolkit to expose mail operations"),
375
+ ] = None,
376
+ room_rules: Annotated[
377
+ List[str],
378
+ typer.Option(
379
+ "--room-rules",
380
+ "-rr",
381
+ help="a path to a rules file within the room that can be used to customize the agent's behavior",
382
+ ),
383
+ ] = [],
384
+ whitelist: Annotated[
385
+ List[str],
386
+ typer.Option(
387
+ "--whitelist",
388
+ help="an email to whitelist",
389
+ ),
390
+ ] = [],
391
+ require_storage: Annotated[
392
+ Optional[bool], typer.Option(..., help="Enable storage toolkit")
393
+ ] = False,
394
+ require_read_only_storage: Annotated[
395
+ Optional[bool],
396
+ typer.Option(..., help="Enable read only storage toolkit"),
397
+ ] = False,
398
+ require_time: Annotated[
399
+ bool,
400
+ typer.Option(
401
+ ...,
402
+ help="Enable time/datetime tools",
403
+ ),
404
+ ] = True,
405
+ require_uuid: Annotated[
406
+ bool,
407
+ typer.Option(
408
+ ...,
409
+ help="Enable UUID generation tools",
410
+ ),
411
+ ] = False,
412
+ database_namespace: Annotated[
413
+ Optional[str],
414
+ typer.Option(..., help="Use a specific database namespace"),
415
+ ] = None,
416
+ require_table_read: Annotated[
417
+ list[str],
418
+ typer.Option(..., help="Enable table read tools for a specific table"),
419
+ ] = [],
420
+ require_table_write: Annotated[
421
+ list[str],
422
+ typer.Option(..., help="Enable table write tools for a specific table"),
423
+ ] = [],
424
+ require_computer_use: Annotated[
425
+ Optional[bool],
426
+ typer.Option(
427
+ ...,
428
+ help="Enable computer use (requires computer-use-preview model)",
429
+ hidden=True,
430
+ ),
431
+ ] = False,
432
+ reply_all: Annotated[
433
+ bool, typer.Option(help="Reply-all when responding to emails")
434
+ ] = False,
435
+ enable_attachments: Annotated[
436
+ bool, typer.Option(help="Allow downloading and processing email attachments")
437
+ ] = False,
438
+ working_directory: Annotated[
439
+ Optional[str],
440
+ typer.Option(..., help="The default working directory for shell commands"),
441
+ ] = None,
442
+ skill_dir: Annotated[
443
+ list[str],
444
+ typer.Option(..., help="an agent skills directory"),
445
+ ] = [],
446
+ llm_participant: Annotated[
447
+ Optional[str],
448
+ typer.Option(..., help="Delegate LLM interactions to a remote participant"),
449
+ ] = None,
450
+ shell_image: Annotated[
451
+ Optional[str],
452
+ typer.Option(..., help="an image tag to use to run shell commands in"),
453
+ ] = None,
454
+ log_llm_requests: Annotated[
455
+ Optional[bool],
456
+ typer.Option(..., help="log all requests to the llm"),
457
+ ] = False,
157
458
  ):
158
459
  key = await resolve_key(project_id=project_id, key=key)
159
460
 
@@ -167,7 +468,7 @@ async def make_call(
167
468
  name=agent_name,
168
469
  )
169
470
 
170
- token.add_api_grant(ApiScope.agent_default())
471
+ token.add_api_grant(ApiScope.agent_default(tunnels=require_computer_use))
171
472
 
172
473
  token.add_role_grant(role=role)
173
474
  token.add_room_grant(room)
@@ -181,27 +482,39 @@ async def make_call(
181
482
  token=jwt,
182
483
  )
183
484
  ) as client:
184
- requirements = []
185
-
186
- for t in toolkit:
187
- requirements.append(RequiredToolkit(name=t))
188
-
189
- for t in schema:
190
- requirements.append(RequiredSchema(name=t))
191
-
192
485
  CustomMailbot = build_mailbot(
193
486
  computer_use=None,
194
487
  model=model,
195
- local_shell=local_shell,
488
+ local_shell=require_local_shell,
196
489
  agent_name=agent_name,
197
490
  rule=rule,
198
- toolkit=toolkit,
199
- schema=schema,
491
+ schema=require_schema + schema,
492
+ toolkit=require_toolkit + toolkit,
200
493
  image_generation=None,
201
- web_search=web_search,
494
+ web_search=require_web_search,
202
495
  rules_file=rules_file,
203
496
  queue=queue,
204
497
  email_address=email_address,
498
+ toolkit_name=toolkit_name,
499
+ room_rules_paths=room_rules,
500
+ whitelist=whitelist,
501
+ require_shell=require_shell,
502
+ require_apply_patch=require_apply_patch,
503
+ require_storage=require_storage,
504
+ require_read_only_storage=require_read_only_storage,
505
+ require_time=require_time,
506
+ require_uuid=require_uuid,
507
+ require_table_read=require_table_read,
508
+ require_table_write=require_table_write,
509
+ require_computer_use=require_computer_use,
510
+ reply_all=reply_all,
511
+ database_namespace=database_namespace,
512
+ enable_attachments=enable_attachments,
513
+ working_directory=working_directory,
514
+ skill_dirs=skill_dir,
515
+ shell_image=shell_image,
516
+ llm_participant=llm_participant,
517
+ log_llm_requests=log_llm_requests,
205
518
  )
206
519
 
207
520
  bot = CustomMailbot()
@@ -225,46 +538,697 @@ async def service(
225
538
  agent_name: Annotated[str, typer.Option(..., help="Name of the agent to call")],
226
539
  rule: Annotated[List[str], typer.Option("--rule", "-r", help="a system rule")] = [],
227
540
  rules_file: Optional[str] = None,
541
+ require_toolkit: Annotated[
542
+ List[str],
543
+ typer.Option(
544
+ "--require-toolkit", "-rt", help="the name or url of a required toolkit"
545
+ ),
546
+ ] = [],
547
+ require_schema: Annotated[
548
+ List[str],
549
+ typer.Option(
550
+ "--require-schema", "-rs", help="the name or url of a required schema"
551
+ ),
552
+ ] = [],
228
553
  toolkit: Annotated[
229
554
  List[str],
230
- typer.Option("--toolkit", "-t", help="the name or url of a required toolkit"),
555
+ typer.Option(
556
+ "--toolkit", "-t", help="the name or url of a required toolkit", hidden=True
557
+ ),
231
558
  ] = [],
232
559
  schema: Annotated[
233
560
  List[str],
234
- typer.Option("--schema", "-s", help="the name or url of a required schema"),
561
+ typer.Option(
562
+ "--schema", "-s", help="the name or url of a required schema", hidden=True
563
+ ),
564
+ ] = [],
565
+ model: Annotated[
566
+ str, typer.Option(..., help="Name of the LLM model to use for the chatbot")
567
+ ] = "gpt-5.2",
568
+ require_shell: Annotated[
569
+ Optional[bool],
570
+ typer.Option(..., help="Enable function shell tool calling"),
571
+ ] = False,
572
+ require_local_shell: Annotated[
573
+ Optional[bool], typer.Option(..., help="Enable local shell tool calling")
574
+ ] = False,
575
+ require_web_search: Annotated[
576
+ Optional[bool], typer.Option(..., help="Enable web search tool calling")
577
+ ] = False,
578
+ require_apply_patch: Annotated[
579
+ Optional[bool],
580
+ typer.Option(..., help="Enable apply patch tool calling"),
581
+ ] = False,
582
+ host: Annotated[
583
+ Optional[str], typer.Option(help="Host to bind the service on")
584
+ ] = None,
585
+ port: Annotated[
586
+ Optional[int], typer.Option(help="Port to bind the service on")
587
+ ] = None,
588
+ path: Annotated[
589
+ Optional[str], typer.Option(help="HTTP path to mount the service at")
590
+ ] = None,
591
+ queue: Annotated[str, typer.Option(..., help="the name of the mail queue")],
592
+ email_address: Annotated[
593
+ str, typer.Option(..., help="the email address of the agent")
594
+ ],
595
+ toolkit_name: Annotated[
596
+ Optional[str],
597
+ typer.Option(..., help="the name of a toolkit to expose mail operations"),
598
+ ] = None,
599
+ room_rules: Annotated[
600
+ List[str],
601
+ typer.Option(
602
+ "--room-rules",
603
+ "-rr",
604
+ help="a path to a rules file within the room that can be used to customize the agent's behavior",
605
+ ),
606
+ ] = [],
607
+ whitelist: Annotated[
608
+ List[str],
609
+ typer.Option(
610
+ "--whitelist",
611
+ help="an email to whitelist",
612
+ ),
613
+ ] = [],
614
+ require_storage: Annotated[
615
+ Optional[bool], typer.Option(..., help="Enable storage toolkit")
616
+ ] = False,
617
+ require_read_only_storage: Annotated[
618
+ Optional[bool],
619
+ typer.Option(..., help="Enable read only storage toolkit"),
620
+ ] = False,
621
+ require_time: Annotated[
622
+ bool,
623
+ typer.Option(
624
+ ...,
625
+ help="Enable time/datetime tools",
626
+ ),
627
+ ] = True,
628
+ require_uuid: Annotated[
629
+ bool,
630
+ typer.Option(
631
+ ...,
632
+ help="Enable UUID generation tools",
633
+ ),
634
+ ] = False,
635
+ database_namespace: Annotated[
636
+ Optional[str],
637
+ typer.Option(..., help="Use a specific database namespace"),
638
+ ] = None,
639
+ require_table_read: Annotated[
640
+ list[str],
641
+ typer.Option(..., help="Enable table read tools for a specific table"),
642
+ ] = [],
643
+ require_table_write: Annotated[
644
+ list[str],
645
+ typer.Option(..., help="Enable table write tools for a specific table"),
646
+ ] = [],
647
+ require_computer_use: Annotated[
648
+ Optional[bool],
649
+ typer.Option(
650
+ ...,
651
+ help="Enable computer use (requires computer-use-preview model)",
652
+ hidden=True,
653
+ ),
654
+ ] = False,
655
+ reply_all: Annotated[
656
+ bool, typer.Option(help="Reply-all when responding to emails")
657
+ ] = False,
658
+ enable_attachments: Annotated[
659
+ bool, typer.Option(help="Allow downloading and processing email attachments")
660
+ ] = False,
661
+ working_directory: Annotated[
662
+ Optional[str],
663
+ typer.Option(..., help="The default working directory for shell commands"),
664
+ ] = None,
665
+ skill_dir: Annotated[
666
+ list[str],
667
+ typer.Option(..., help="an agent skills directory"),
668
+ ] = [],
669
+ llm_participant: Annotated[
670
+ Optional[str],
671
+ typer.Option(..., help="Delegate LLM interactions to a remote participant"),
672
+ ] = None,
673
+ shell_image: Annotated[
674
+ Optional[str],
675
+ typer.Option(..., help="an image tag to use to run shell commands in"),
676
+ ] = None,
677
+ log_llm_requests: Annotated[
678
+ Optional[bool],
679
+ typer.Option(..., help="log all requests to the llm"),
680
+ ] = False,
681
+ ):
682
+ service = get_service(host=host, port=port)
683
+ if path is None:
684
+ path = "/agent"
685
+ i = 0
686
+ while service.has_path(path):
687
+ i += 1
688
+ path = f"/agent{i}"
689
+
690
+ service.agents.append(
691
+ AgentSpec(name=agent_name, annotations={ANNOTATION_AGENT_TYPE: "ChatBot"})
692
+ )
693
+
694
+ service.add_path(
695
+ identity=agent_name,
696
+ path=path,
697
+ cls=build_mailbot(
698
+ queue=queue,
699
+ computer_use=None,
700
+ model=model,
701
+ local_shell=require_local_shell,
702
+ web_search=require_web_search,
703
+ agent_name=agent_name,
704
+ rule=rule,
705
+ schema=require_schema + schema,
706
+ toolkit=require_toolkit + toolkit,
707
+ image_generation=None,
708
+ rules_file=rules_file,
709
+ email_address=email_address,
710
+ toolkit_name=toolkit_name,
711
+ room_rules_paths=room_rules,
712
+ whitelist=whitelist,
713
+ require_shell=require_shell,
714
+ require_apply_patch=require_apply_patch,
715
+ require_storage=require_storage,
716
+ require_read_only_storage=require_read_only_storage,
717
+ require_time=require_time,
718
+ require_uuid=require_uuid,
719
+ require_table_read=require_table_read,
720
+ require_table_write=require_table_write,
721
+ require_computer_use=require_computer_use,
722
+ reply_all=reply_all,
723
+ database_namespace=database_namespace,
724
+ enable_attachments=enable_attachments,
725
+ working_directory=working_directory,
726
+ skill_dirs=skill_dir,
727
+ shell_image=shell_image,
728
+ llm_participant=llm_participant,
729
+ log_llm_requests=log_llm_requests,
730
+ ),
731
+ )
732
+
733
+ if not get_deferred():
734
+ await run_services()
735
+
736
+
737
+ @app.async_command("spec")
738
+ async def spec(
739
+ *,
740
+ service_name: Annotated[str, typer.Option("--service-name", help="service name")],
741
+ service_description: Annotated[
742
+ Optional[str], typer.Option("--service-description", help="service description")
743
+ ] = None,
744
+ service_title: Annotated[
745
+ Optional[str],
746
+ typer.Option("--service-title", help="a display name for the service"),
747
+ ] = None,
748
+ agent_name: Annotated[str, typer.Option(..., help="Name of the agent to call")],
749
+ rule: Annotated[List[str], typer.Option("--rule", "-r", help="a system rule")] = [],
750
+ rules_file: Optional[str] = None,
751
+ require_toolkit: Annotated[
752
+ List[str],
753
+ typer.Option(
754
+ "--require-toolkit", "-rt", help="the name or url of a required toolkit"
755
+ ),
756
+ ] = [],
757
+ require_schema: Annotated[
758
+ List[str],
759
+ typer.Option(
760
+ "--require-schema", "-rs", help="the name or url of a required schema"
761
+ ),
762
+ ] = [],
763
+ toolkit: Annotated[
764
+ List[str],
765
+ typer.Option(
766
+ "--toolkit", "-t", help="the name or url of a required toolkit", hidden=True
767
+ ),
768
+ ] = [],
769
+ schema: Annotated[
770
+ List[str],
771
+ typer.Option(
772
+ "--schema", "-s", help="the name or url of a required schema", hidden=True
773
+ ),
235
774
  ] = [],
236
775
  model: Annotated[
237
776
  str, typer.Option(..., help="Name of the LLM model to use for the chatbot")
238
- ] = "gpt-5",
239
- local_shell: Annotated[
777
+ ] = "gpt-5.2",
778
+ require_shell: Annotated[
779
+ Optional[bool],
780
+ typer.Option(..., help="Enable function shell tool calling"),
781
+ ] = False,
782
+ require_local_shell: Annotated[
240
783
  Optional[bool], typer.Option(..., help="Enable local shell tool calling")
241
784
  ] = False,
242
- host: Annotated[Optional[str], typer.Option()] = None,
243
- port: Annotated[Optional[int], typer.Option()] = None,
244
- path: Annotated[str, typer.Option()] = "/agent",
785
+ require_web_search: Annotated[
786
+ Optional[bool], typer.Option(..., help="Enable web search tool calling")
787
+ ] = False,
788
+ require_apply_patch: Annotated[
789
+ Optional[bool],
790
+ typer.Option(..., help="Enable apply patch tool calling"),
791
+ ] = False,
792
+ host: Annotated[
793
+ Optional[str], typer.Option(help="Host to bind the service on")
794
+ ] = None,
795
+ port: Annotated[
796
+ Optional[int], typer.Option(help="Port to bind the service on")
797
+ ] = None,
798
+ path: Annotated[
799
+ Optional[str], typer.Option(help="HTTP path to mount the service at")
800
+ ] = None,
245
801
  queue: Annotated[str, typer.Option(..., help="the name of the mail queue")],
246
802
  email_address: Annotated[
247
803
  str, typer.Option(..., help="the email address of the agent")
248
804
  ],
805
+ toolkit_name: Annotated[
806
+ Optional[str],
807
+ typer.Option(..., help="the name of a toolkit to expose mail operations"),
808
+ ] = None,
809
+ room_rules: Annotated[
810
+ List[str],
811
+ typer.Option(
812
+ "--room-rules",
813
+ "-rr",
814
+ help="a path to a rules file within the room that can be used to customize the agent's behavior",
815
+ ),
816
+ ] = [],
817
+ whitelist: Annotated[
818
+ List[str],
819
+ typer.Option(
820
+ "--whitelist",
821
+ help="an email to whitelist",
822
+ ),
823
+ ] = [],
824
+ require_storage: Annotated[
825
+ Optional[bool], typer.Option(..., help="Enable storage toolkit")
826
+ ] = False,
827
+ require_read_only_storage: Annotated[
828
+ Optional[bool],
829
+ typer.Option(..., help="Enable read only storage toolkit"),
830
+ ] = False,
831
+ require_time: Annotated[
832
+ bool,
833
+ typer.Option(
834
+ ...,
835
+ help="Enable time/datetime tools",
836
+ ),
837
+ ] = True,
838
+ require_uuid: Annotated[
839
+ bool,
840
+ typer.Option(
841
+ ...,
842
+ help="Enable UUID generation tools",
843
+ ),
844
+ ] = False,
845
+ database_namespace: Annotated[
846
+ Optional[str],
847
+ typer.Option(..., help="Use a specific database namespace"),
848
+ ] = None,
849
+ require_table_read: Annotated[
850
+ list[str],
851
+ typer.Option(..., help="Enable table read tools for a specific table"),
852
+ ] = [],
853
+ require_table_write: Annotated[
854
+ list[str],
855
+ typer.Option(..., help="Enable table write tools for a specific table"),
856
+ ] = [],
857
+ require_computer_use: Annotated[
858
+ Optional[bool],
859
+ typer.Option(
860
+ ...,
861
+ help="Enable computer use (requires computer-use-preview model)",
862
+ hidden=True,
863
+ ),
864
+ ] = False,
865
+ reply_all: Annotated[
866
+ bool, typer.Option(help="Reply-all when responding to emails")
867
+ ] = False,
868
+ enable_attachments: Annotated[
869
+ bool, typer.Option(help="Allow downloading and processing email attachments")
870
+ ] = False,
871
+ working_directory: Annotated[
872
+ Optional[str],
873
+ typer.Option(..., help="The default working directory for shell commands"),
874
+ ] = None,
875
+ skill_dir: Annotated[
876
+ list[str],
877
+ typer.Option(..., help="an agent skills directory"),
878
+ ] = [],
879
+ llm_participant: Annotated[
880
+ Optional[str],
881
+ typer.Option(..., help="Delegate LLM interactions to a remote participant"),
882
+ ] = None,
883
+ shell_image: Annotated[
884
+ Optional[str],
885
+ typer.Option(..., help="an image tag to use to run shell commands in"),
886
+ ] = None,
887
+ log_llm_requests: Annotated[
888
+ Optional[bool],
889
+ typer.Option(..., help="log all requests to the llm"),
890
+ ] = False,
249
891
  ):
250
- print("[bold green]Connecting to room...[/bold green]", flush=True)
892
+ service = get_service(host=host, port=port)
893
+ if path is None:
894
+ path = "/agent"
895
+ i = 0
896
+ while service.has_path(path):
897
+ i += 1
898
+ path = f"/agent{i}"
899
+
900
+ service.agents.append(
901
+ AgentSpec(name=agent_name, annotations={ANNOTATION_AGENT_TYPE: "ChatBot"})
902
+ )
251
903
 
252
- service = ServiceHost(host=host, port=port)
253
904
  service.add_path(
905
+ identity=agent_name,
254
906
  path=path,
255
907
  cls=build_mailbot(
256
908
  queue=queue,
257
909
  computer_use=None,
258
910
  model=model,
259
- local_shell=local_shell,
911
+ local_shell=require_local_shell,
912
+ web_search=require_web_search,
260
913
  agent_name=agent_name,
261
914
  rule=rule,
262
- toolkit=toolkit,
263
- schema=schema,
915
+ schema=require_schema + schema,
916
+ toolkit=require_toolkit + toolkit,
264
917
  image_generation=None,
265
918
  rules_file=rules_file,
266
919
  email_address=email_address,
920
+ toolkit_name=toolkit_name,
921
+ room_rules_paths=room_rules,
922
+ whitelist=whitelist,
923
+ require_shell=require_shell,
924
+ require_apply_patch=require_apply_patch,
925
+ require_storage=require_storage,
926
+ require_read_only_storage=require_read_only_storage,
927
+ require_time=require_time,
928
+ require_uuid=require_uuid,
929
+ require_table_read=require_table_read,
930
+ require_table_write=require_table_write,
931
+ require_computer_use=require_computer_use,
932
+ reply_all=reply_all,
933
+ database_namespace=database_namespace,
934
+ enable_attachments=enable_attachments,
935
+ working_directory=working_directory,
936
+ skill_dirs=skill_dir,
937
+ shell_image=shell_image,
938
+ llm_participant=llm_participant,
939
+ log_llm_requests=log_llm_requests,
267
940
  ),
268
941
  )
269
942
 
270
- await service.run()
943
+ spec = service_specs()[0]
944
+ spec.metadata.annotations = {
945
+ "meshagent.service.id": service_name,
946
+ }
947
+
948
+ spec.metadata.name = service_name
949
+ spec.metadata.description = service_description
950
+ spec.container.image = (
951
+ "us-central1-docker.pkg.dev/meshagent-public/images/cli:{SERVER_VERSION}-esgz"
952
+ )
953
+ spec.container.command = shlex.join(
954
+ ["meshagent", "mailbot", "service", *cleanup_args(sys.argv[2:])]
955
+ )
956
+
957
+ print(yaml.dump(spec.model_dump(mode="json", exclude_none=True), sort_keys=False))
958
+
959
+
960
+ @app.async_command("deploy")
961
+ async def deploy(
962
+ *,
963
+ service_name: Annotated[str, typer.Option("--service-name", help="service name")],
964
+ service_description: Annotated[
965
+ Optional[str], typer.Option("--service-description", help="service description")
966
+ ] = None,
967
+ service_title: Annotated[
968
+ Optional[str],
969
+ typer.Option("--service-title", help="a display name for the service"),
970
+ ] = None,
971
+ agent_name: Annotated[str, typer.Option(..., help="Name of the agent to call")],
972
+ rule: Annotated[List[str], typer.Option("--rule", "-r", help="a system rule")] = [],
973
+ rules_file: Optional[str] = None,
974
+ require_toolkit: Annotated[
975
+ List[str],
976
+ typer.Option(
977
+ "--require-toolkit", "-rt", help="the name or url of a required toolkit"
978
+ ),
979
+ ] = [],
980
+ require_schema: Annotated[
981
+ List[str],
982
+ typer.Option(
983
+ "--require-schema", "-rs", help="the name or url of a required schema"
984
+ ),
985
+ ] = [],
986
+ toolkit: Annotated[
987
+ List[str],
988
+ typer.Option(
989
+ "--toolkit", "-t", help="the name or url of a required toolkit", hidden=True
990
+ ),
991
+ ] = [],
992
+ schema: Annotated[
993
+ List[str],
994
+ typer.Option(
995
+ "--schema", "-s", help="the name or url of a required schema", hidden=True
996
+ ),
997
+ ] = [],
998
+ model: Annotated[
999
+ str, typer.Option(..., help="Name of the LLM model to use for the chatbot")
1000
+ ] = "gpt-5.2",
1001
+ require_shell: Annotated[
1002
+ Optional[bool],
1003
+ typer.Option(..., help="Enable function shell tool calling"),
1004
+ ] = False,
1005
+ require_local_shell: Annotated[
1006
+ Optional[bool], typer.Option(..., help="Enable local shell tool calling")
1007
+ ] = False,
1008
+ require_web_search: Annotated[
1009
+ Optional[bool], typer.Option(..., help="Enable web search tool calling")
1010
+ ] = False,
1011
+ require_apply_patch: Annotated[
1012
+ Optional[bool],
1013
+ typer.Option(..., help="Enable apply patch tool calling"),
1014
+ ] = False,
1015
+ host: Annotated[
1016
+ Optional[str], typer.Option(help="Host to bind the service on")
1017
+ ] = None,
1018
+ port: Annotated[
1019
+ Optional[int], typer.Option(help="Port to bind the service on")
1020
+ ] = None,
1021
+ path: Annotated[
1022
+ Optional[str], typer.Option(help="HTTP path to mount the service at")
1023
+ ] = None,
1024
+ queue: Annotated[str, typer.Option(..., help="the name of the mail queue")],
1025
+ email_address: Annotated[
1026
+ str, typer.Option(..., help="the email address of the agent")
1027
+ ],
1028
+ toolkit_name: Annotated[
1029
+ Optional[str],
1030
+ typer.Option(..., help="the name of a toolkit to expose mail operations"),
1031
+ ] = None,
1032
+ room_rules: Annotated[
1033
+ List[str],
1034
+ typer.Option(
1035
+ "--room-rules",
1036
+ "-rr",
1037
+ help="a path to a rules file within the room that can be used to customize the agent's behavior",
1038
+ ),
1039
+ ] = [],
1040
+ whitelist: Annotated[
1041
+ List[str],
1042
+ typer.Option(
1043
+ "--whitelist",
1044
+ help="an email to whitelist",
1045
+ ),
1046
+ ] = [],
1047
+ require_storage: Annotated[
1048
+ Optional[bool], typer.Option(..., help="Enable storage toolkit")
1049
+ ] = False,
1050
+ require_read_only_storage: Annotated[
1051
+ Optional[bool],
1052
+ typer.Option(..., help="Enable read only storage toolkit"),
1053
+ ] = False,
1054
+ require_time: Annotated[
1055
+ bool,
1056
+ typer.Option(
1057
+ ...,
1058
+ help="Enable time/datetime tools",
1059
+ ),
1060
+ ] = True,
1061
+ require_uuid: Annotated[
1062
+ bool,
1063
+ typer.Option(
1064
+ ...,
1065
+ help="Enable UUID generation tools",
1066
+ ),
1067
+ ] = False,
1068
+ database_namespace: Annotated[
1069
+ Optional[str],
1070
+ typer.Option(..., help="Use a specific database namespace"),
1071
+ ] = None,
1072
+ require_table_read: Annotated[
1073
+ list[str],
1074
+ typer.Option(..., help="Enable table read tools for a specific table"),
1075
+ ] = [],
1076
+ require_table_write: Annotated[
1077
+ list[str],
1078
+ typer.Option(..., help="Enable table write tools for a specific table"),
1079
+ ] = [],
1080
+ require_computer_use: Annotated[
1081
+ Optional[bool],
1082
+ typer.Option(
1083
+ ...,
1084
+ help="Enable computer use (requires computer-use-preview model)",
1085
+ hidden=True,
1086
+ ),
1087
+ ] = False,
1088
+ reply_all: Annotated[
1089
+ bool, typer.Option(help="Reply-all when responding to emails")
1090
+ ] = False,
1091
+ enable_attachments: Annotated[
1092
+ bool, typer.Option(help="Allow downloading and processing email attachments")
1093
+ ] = False,
1094
+ working_directory: Annotated[
1095
+ Optional[str],
1096
+ typer.Option(..., help="The default working directory for shell commands"),
1097
+ ] = None,
1098
+ skill_dir: Annotated[
1099
+ list[str],
1100
+ typer.Option(..., help="an agent skills directory"),
1101
+ ] = [],
1102
+ llm_participant: Annotated[
1103
+ Optional[str],
1104
+ typer.Option(..., help="Delegate LLM interactions to a remote participant"),
1105
+ ] = None,
1106
+ shell_image: Annotated[
1107
+ Optional[str],
1108
+ typer.Option(..., help="an image tag to use to run shell commands in"),
1109
+ ] = None,
1110
+ log_llm_requests: Annotated[
1111
+ Optional[bool],
1112
+ typer.Option(..., help="log all requests to the llm"),
1113
+ ] = False,
1114
+ project_id: ProjectIdOption,
1115
+ room: Annotated[
1116
+ Optional[str],
1117
+ typer.Option("--room", help="The name of a room to create the service for"),
1118
+ ] = None,
1119
+ ):
1120
+ project_id = await resolve_project_id(project_id=project_id)
1121
+
1122
+ service = get_service(host=host, port=port)
1123
+ if path is None:
1124
+ path = "/agent"
1125
+ i = 0
1126
+ while service.has_path(path):
1127
+ i += 1
1128
+ path = f"/agent{i}"
1129
+
1130
+ service.agents.append(
1131
+ AgentSpec(name=agent_name, annotations={ANNOTATION_AGENT_TYPE: "ChatBot"})
1132
+ )
1133
+
1134
+ service.add_path(
1135
+ identity=agent_name,
1136
+ path=path,
1137
+ cls=build_mailbot(
1138
+ queue=queue,
1139
+ computer_use=None,
1140
+ model=model,
1141
+ local_shell=require_local_shell,
1142
+ web_search=require_web_search,
1143
+ agent_name=agent_name,
1144
+ rule=rule,
1145
+ schema=require_schema + schema,
1146
+ toolkit=require_toolkit + toolkit,
1147
+ image_generation=None,
1148
+ rules_file=rules_file,
1149
+ email_address=email_address,
1150
+ toolkit_name=toolkit_name,
1151
+ room_rules_paths=room_rules,
1152
+ whitelist=whitelist,
1153
+ require_shell=require_shell,
1154
+ require_apply_patch=require_apply_patch,
1155
+ require_storage=require_storage,
1156
+ require_read_only_storage=require_read_only_storage,
1157
+ require_time=require_time,
1158
+ require_uuid=require_uuid,
1159
+ require_table_read=require_table_read,
1160
+ require_table_write=require_table_write,
1161
+ require_computer_use=require_computer_use,
1162
+ reply_all=reply_all,
1163
+ database_namespace=database_namespace,
1164
+ enable_attachments=enable_attachments,
1165
+ working_directory=working_directory,
1166
+ skill_dirs=skill_dir,
1167
+ shell_image=shell_image,
1168
+ llm_participant=llm_participant,
1169
+ log_llm_requests=log_llm_requests,
1170
+ ),
1171
+ )
1172
+
1173
+ spec = service_specs()[0]
1174
+ spec.metadata.annotations = {
1175
+ "meshagent.service.id": service_name,
1176
+ }
1177
+
1178
+ spec.metadata.name = service_name
1179
+ spec.metadata.description = service_description
1180
+ spec.container.image = (
1181
+ "us-central1-docker.pkg.dev/meshagent-public/images/cli:{SERVER_VERSION}-esgz"
1182
+ )
1183
+ spec.container.command = shlex.join(
1184
+ ["meshagent", "mailbot", *cleanup_args(sys.argv[:2])]
1185
+ )
1186
+
1187
+ client = await get_client()
1188
+ try:
1189
+ id = None
1190
+ try:
1191
+ if id is None:
1192
+ if room is None:
1193
+ services = await client.list_services(project_id=project_id)
1194
+ else:
1195
+ services = await client.list_room_services(
1196
+ project_id=project_id, room_name=room
1197
+ )
1198
+
1199
+ for s in services:
1200
+ if s.metadata.name == spec.metadata.name:
1201
+ id = s.id
1202
+
1203
+ if id is None:
1204
+ if room is None:
1205
+ id = await client.create_service(
1206
+ project_id=project_id, service=spec
1207
+ )
1208
+ else:
1209
+ id = await client.create_room_service(
1210
+ project_id=project_id, service=spec, room_name=room
1211
+ )
1212
+
1213
+ else:
1214
+ spec.id = id
1215
+ if room is None:
1216
+ await client.update_service(
1217
+ project_id=project_id, service_id=id, service=spec
1218
+ )
1219
+ else:
1220
+ await client.update_room_service(
1221
+ project_id=project_id,
1222
+ service_id=id,
1223
+ service=spec,
1224
+ room_name=room,
1225
+ )
1226
+
1227
+ except ConflictError:
1228
+ print(f"[red]Service name already in use: {spec.metadata.name}[/red]")
1229
+ raise typer.Exit(code=1)
1230
+ else:
1231
+ print(f"[green]Deployed service:[/] {id}")
1232
+
1233
+ finally:
1234
+ await client.close()