fastworkflow 2.15.6__py3-none-any.whl → 2.15.8__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.
fastworkflow/cli.py CHANGED
@@ -12,7 +12,6 @@ import time
12
12
  from pathlib import Path
13
13
  import importlib.resources
14
14
  from rich import print as rprint
15
- from rich.console import Console
16
15
  from rich.live import Live
17
16
  from rich.spinner import Spinner
18
17
 
@@ -175,72 +174,6 @@ def fetch_example(args):
175
174
  traceback.print_exc(file=sys.stderr)
176
175
  sys.exit(1)
177
176
 
178
- def train_example(args):
179
- # sourcery skip: extract-duplicate-method, extract-method, remove-redundant-fstring, split-or-ifs
180
- """Train an existing example workflow."""
181
- # Create a spinner for the initial check
182
- spinner = Spinner("dots", text="[bold green]Preparing to train example...[/bold green]")
183
-
184
- with Live(spinner, refresh_per_second=10):
185
- # Check if example exists in the local examples directory
186
- local_examples_dir = Path("./examples")
187
- workflow_path = local_examples_dir / args.name
188
-
189
- if workflow_path.is_dir():
190
- # Get the appropriate env files for this example workflow
191
- env_file_path, passwords_file_path = find_default_env_files(local_examples_dir)
192
-
193
- # Check if the files exist
194
- env_file = Path(env_file_path)
195
- passwords_file = Path(passwords_file_path)
196
-
197
- # Create args object for train_main
198
- train_args = argparse.Namespace(
199
- workflow_folderpath=str(workflow_path),
200
- env_file_path=str(env_file),
201
- passwords_file_path=str(passwords_file)
202
- )
203
-
204
- # After the spinner, handle any errors or proceed with training
205
- if not workflow_path.is_dir():
206
- rprint(f"[bold red]Error:[/bold red] Example '{args.name}' not found in '{local_examples_dir}'.")
207
- rprint(f"Use 'fastworkflow examples fetch {args.name}' to fetch the example first.")
208
- rprint("Or use 'fastworkflow examples list' to see available examples.")
209
- sys.exit(1)
210
-
211
- if not env_file.exists() or not passwords_file.exists():
212
- rprint(f"[bold red]Error:[/bold red] Required environment files not found:")
213
- if not env_file.exists():
214
- rprint(f" - {env_file} (not found)")
215
- if not passwords_file.exists():
216
- rprint(f" - {passwords_file} (not found)")
217
- rprint("\nPlease run the following command to fetch the example and its environment files:")
218
- rprint(f" fastworkflow examples fetch {args.name}")
219
- rprint("\nAfter fetching, edit the passwords file to add your API keys:")
220
- rprint(f" {local_examples_dir}/fastworkflow.passwords.env")
221
- sys.exit(1)
222
-
223
- rprint(f"[bold green]Training example[/bold green] '{args.name}' in '{workflow_path}'...")
224
-
225
- try:
226
- # Lazy import train_main only when actually training
227
- from .train.__main__ import train_main as _train_main
228
- # Call train_main directly instead of using subprocess
229
- result = _train_main(train_args)
230
-
231
- if result is None or result == 0:
232
- rprint(f"\n✅ Successfully trained example '{args.name}'.")
233
- rprint(f"You can now run it with:\nfastworkflow examples run {args.name}")
234
- else:
235
- rprint(f"\n❌ Training failed.")
236
- sys.exit(1)
237
-
238
- except Exception as e:
239
- rprint(f"[bold red]An unexpected error occurred during training:[/bold red] {e}")
240
- import traceback
241
- traceback.print_exc(file=sys.stderr)
242
- sys.exit(1)
243
-
244
177
  def find_default_env_files(workflow_path):
245
178
  """Find the appropriate default env files based on context.
246
179
 
@@ -338,12 +271,6 @@ def add_run_parser(subparsers):
338
271
  parser_run.add_argument("--startup_action", help="Optional startup action", default="")
339
272
  parser_run.add_argument("--keep_alive", help="Optional keep_alive", default=True)
340
273
  parser_run.add_argument("--project_folderpath", help="Optional path to project folder containing application code", default=None)
341
- parser_run.add_argument(
342
- "--run_as_agent",
343
- help="Run in agent mode (uses DSPy for tool selection)",
344
- action="store_true",
345
- default=False,
346
- )
347
274
  parser_run.set_defaults(func=lambda args: run_with_defaults(args))
348
275
 
349
276
  def train_with_defaults(args): # sourcery skip: extract-duplicate-method
@@ -363,7 +290,7 @@ def train_with_defaults(args): # sourcery skip: extract-duplicate-method
363
290
  example_name = os.path.basename(args.workflow_folderpath)
364
291
  print("\nThis appears to be an example workflow. Please run:")
365
292
  print(f" fastworkflow examples fetch {example_name}")
366
- print(f" fastworkflow examples train {example_name}")
293
+ print(f" fastworkflow train ./examples/{example_name} ./examples/fastworkflow.env ./examples/fastworkflow.passwords.env")
367
294
  else:
368
295
  print("\nPlease ensure this file exists with required environment variables.")
369
296
  print("You can create a basic .env file in your current directory.")
@@ -376,7 +303,7 @@ def train_with_defaults(args): # sourcery skip: extract-duplicate-method
376
303
  example_name = os.path.basename(args.workflow_folderpath)
377
304
  print("\nThis appears to be an example workflow. Please run:")
378
305
  print(f" fastworkflow examples fetch {example_name}")
379
- print(f" fastworkflow examples train {example_name}")
306
+ print(f" fastworkflow train ./examples/{example_name} ./examples/fastworkflow.env ./examples/fastworkflow.passwords.env")
380
307
  else:
381
308
  print("\nPlease ensure this file exists with required API keys.")
382
309
  print("You can create a basic passwords.env file in your current directory.")
@@ -403,7 +330,7 @@ def run_with_defaults(args): # sourcery skip: extract-duplicate-method
403
330
  example_name = os.path.basename(args.workflow_path)
404
331
  print("\nThis appears to be an example workflow. Please run:")
405
332
  print(f" fastworkflow examples fetch {example_name}")
406
- print(f" fastworkflow examples train {example_name}")
333
+ print(f" fastworkflow train ./examples/{example_name} ./examples/fastworkflow.env ./examples/fastworkflow.passwords.env")
407
334
  else:
408
335
  print("\nPlease ensure this file exists with required environment variables.")
409
336
  print("You can create a basic .env file in your current directory.")
@@ -416,7 +343,7 @@ def run_with_defaults(args): # sourcery skip: extract-duplicate-method
416
343
  example_name = os.path.basename(args.workflow_path)
417
344
  print("\nThis appears to be an example workflow. Please run:")
418
345
  print(f" fastworkflow examples fetch {example_name}")
419
- print(f" fastworkflow examples train {example_name}")
346
+ print(f" fastworkflow train ./examples/{example_name} ./examples/fastworkflow.env ./examples/fastworkflow.passwords.env")
420
347
  else:
421
348
  print("\nPlease ensure this file exists with required API keys.")
422
349
  print("You can create a basic passwords.env file in your current directory.")
@@ -426,86 +353,6 @@ def run_with_defaults(args): # sourcery skip: extract-duplicate-method
426
353
  from .run.__main__ import run_main as _run_main
427
354
  return _run_main(args)
428
355
 
429
- def run_example(args):
430
- # sourcery skip: extract-duplicate-method, extract-method, remove-redundant-fstring, split-or-ifs
431
- """Run an existing example workflow."""
432
- # Create a spinner for the initial check
433
- spinner = Spinner("dots", text="[bold green]Preparing to run example...[/bold green]")
434
-
435
- with Live(spinner, refresh_per_second=10):
436
- # Check if example exists in the local examples directory
437
- local_examples_dir = Path("./examples")
438
- workflow_path = local_examples_dir / args.name
439
-
440
- if workflow_path.is_dir():
441
- # Get the appropriate env files for this example workflow
442
- env_file_path, passwords_file_path = find_default_env_files(local_examples_dir)
443
-
444
- # Check if the files exist
445
- env_file = Path(env_file_path)
446
- passwords_file = Path(passwords_file_path)
447
-
448
- # Check if the example has been trained
449
- command_info_dir = workflow_path / "___command_info"
450
-
451
- # After the spinner, handle any errors or proceed with running
452
- if not workflow_path.is_dir():
453
- rprint(f"[bold red]Error:[/bold red] Example '{args.name}' not found in '{local_examples_dir}'.")
454
- rprint(f"Use 'fastworkflow examples fetch {args.name}' to fetch the example first.")
455
- rprint("Or use 'fastworkflow examples list' to see available examples.")
456
- sys.exit(1)
457
-
458
- if not env_file.exists() or not passwords_file.exists():
459
- rprint(f"[bold red]Error:[/bold red] Required environment files not found:")
460
- if not env_file.exists():
461
- rprint(f" - {env_file} (not found)")
462
- if not passwords_file.exists():
463
- rprint(f" - {passwords_file} (not found)")
464
- rprint("\nPlease run the following command to fetch the example and its environment files:")
465
- rprint(f" fastworkflow examples fetch {args.name}")
466
- rprint("\nAfter fetching, edit the passwords file to add your API keys:")
467
- rprint(f" {local_examples_dir}/fastworkflow.passwords.env")
468
- rprint("\nThen train the example before running it:")
469
- rprint(f" fastworkflow examples train {args.name}")
470
- sys.exit(1)
471
-
472
- # Check if the example has been trained
473
- if not command_info_dir.exists() or not any(command_info_dir.iterdir()):
474
- rprint(f"[bold yellow]Warning:[/bold yellow] Example '{args.name}' does not appear to be trained yet.")
475
- rprint(f"Please train the example first with:")
476
- rprint(f" fastworkflow examples train {args.name}")
477
- response = input("Do you want to continue anyway? [y/N] ")
478
- if response.lower() != 'y':
479
- rprint("Operation cancelled.")
480
- sys.exit(0)
481
-
482
- rprint(f"[bold green]Running example[/bold green] '{args.name}'...")
483
-
484
- # For interactive applications, we need to use os.execvp to replace the current process
485
- # This ensures that stdin/stdout/stderr are properly connected for interactive use
486
- cmd = [
487
- sys.executable,
488
- "-m", "fastworkflow.run",
489
- str(workflow_path),
490
- str(env_file),
491
- str(passwords_file)
492
- ]
493
-
494
- # Forward agent mode flag if requested
495
- if getattr(args, "run_as_agent", False):
496
- cmd.append("--run_as_agent")
497
-
498
- try:
499
- rprint(f"[bold green]Starting interactive session...[/bold green]")
500
- # Replace the current process with the run command
501
- # This ensures that the interactive prompt works correctly
502
- os.execvp(sys.executable, cmd)
503
- except Exception as e:
504
- rprint(f"[bold red]An unexpected error occurred while running the example:[/bold red] {e}")
505
- import traceback
506
- traceback.print_exc(file=sys.stderr)
507
- sys.exit(1)
508
-
509
356
  def main():
510
357
  """Main function for the fastworkflow CLI."""
511
358
 
@@ -528,22 +375,6 @@ def main():
528
375
  parser_fetch.add_argument("--force", action="store_true", help="Force overwrite if example already exists")
529
376
  parser_fetch.set_defaults(func=fetch_example)
530
377
 
531
- # 'examples train' command
532
- parser_train_example = examples_subparsers.add_parser("train", help="Train a specific example")
533
- parser_train_example.add_argument("name", help="The name of the example to train")
534
- parser_train_example.set_defaults(func=train_example)
535
-
536
- # 'examples run' command
537
- parser_run_example = examples_subparsers.add_parser("run", help="Run a specific example")
538
- parser_run_example.add_argument("name", help="The name of the example to run")
539
- parser_run_example.add_argument(
540
- "--run_as_agent",
541
- help="Run the example in agent mode (uses DSPy for tool selection)",
542
- action="store_true",
543
- default=False,
544
- )
545
- parser_run_example.set_defaults(func=run_example)
546
-
547
378
  # Add top-level commands
548
379
  add_build_parser(subparsers)
549
380
  add_refine_parser(subparsers)
@@ -1,7 +1,7 @@
1
1
  LLM_SYNDATA_GEN=mistral/mistral-small-latest
2
2
  LLM_PARAM_EXTRACTION=mistral/mistral-small-latest
3
3
  LLM_RESPONSE_GEN=mistral/mistral-small-latest
4
- LLM_PLANNER=openrouter/openai/gpt-oss-20b:free
4
+ LLM_PLANNER=mistral/mistral-small-latest
5
5
  LLM_AGENT=mistral/mistral-small-latest
6
6
 
7
7
  SPEEDDICT_FOLDERNAME=___workflow_contexts
@@ -24,7 +24,7 @@ class Signature:
24
24
  item_ids: List[str] = Field(
25
25
  default_factory=list,
26
26
  description="The item IDs to be exchanged",
27
- examples=["1008292230"],
27
+ examples=["'34742388', '59475928'"],
28
28
  json_schema_extra={
29
29
  "available_from": ["get_order_details"]
30
30
  }
@@ -32,7 +32,7 @@ class Signature:
32
32
  new_item_ids: List[str] = Field(
33
33
  default_factory=list,
34
34
  description="The new item IDs to exchange for",
35
- examples=["1008292230"],
35
+ examples=["'45854949', '68789960'"],
36
36
  json_schema_extra={
37
37
  "available_from": ["get_product_details"]
38
38
  }
@@ -75,7 +75,36 @@ class Signature:
75
75
  ) -> tuple[bool, str]:
76
76
  if not cmd_parameters.order_id.startswith('#'):
77
77
  cmd_parameters.order_id = f'#{cmd_parameters.order_id}'
78
- return (True, '')
78
+
79
+ error_message = ''
80
+ if len(cmd_parameters.item_ids) == 0:
81
+ error_message = 'item ids must be specified\n'
82
+ for item_id in cmd_parameters.item_ids:
83
+ try:
84
+ int(item_id)
85
+ except ValueError:
86
+ error_message += 'item ids must be integers\n'
87
+ break
88
+
89
+ if len(cmd_parameters.new_item_ids) == 0:
90
+ error_message += 'new item ids must be specified\n'
91
+ for item_id in cmd_parameters.new_item_ids:
92
+ try:
93
+ int(item_id)
94
+ except ValueError:
95
+ error_message += 'new item ids must be integers\n'
96
+ break
97
+
98
+ if len(cmd_parameters.new_item_ids) != len(cmd_parameters.item_ids):
99
+ error_message += 'the number of item ids and new item ids must match for a valid exchange\n'
100
+
101
+ if common_items := set(cmd_parameters.item_ids).intersection(
102
+ set(cmd_parameters.new_item_ids)
103
+ ):
104
+ common_items_str = ', '.join(common_items)
105
+ error_message += f'cannot exchange items for themselves: {common_items_str}. new item ids must differ from item ids\n'
106
+
107
+ return (False, error_message) if error_message else (True, '')
79
108
 
80
109
 
81
110
  class ResponseGenerator:
@@ -24,7 +24,7 @@ class Signature:
24
24
  item_ids: List[str] = Field(
25
25
  default_factory=list,
26
26
  description="List of item IDs to be modified",
27
- examples=["1008292230", "2468135790"],
27
+ examples=["'62727828', '76938178'"],
28
28
  json_schema_extra={
29
29
  "available_from": ["get_order_details"]
30
30
  }
@@ -32,7 +32,7 @@ class Signature:
32
32
  new_item_ids: List[str] = Field(
33
33
  default_factory=list,
34
34
  description="List of new item IDs to replace with",
35
- examples=["1008292231", "2468135791"],
35
+ examples=["'2352352', '42674747'"],
36
36
  json_schema_extra={
37
37
  "available_from": ["get_product_details"]
38
38
  }
@@ -75,7 +75,36 @@ class Signature:
75
75
  ) -> tuple[bool, str]:
76
76
  if not cmd_parameters.order_id.startswith('#'):
77
77
  cmd_parameters.order_id = f'#{cmd_parameters.order_id}'
78
- return (True, '')
78
+
79
+ error_message = ''
80
+ if len(cmd_parameters.item_ids) == 0:
81
+ error_message = 'item ids must be specified\n'
82
+ for item_id in cmd_parameters.item_ids:
83
+ try:
84
+ int(item_id)
85
+ except ValueError:
86
+ error_message += 'item ids must be integers\n'
87
+ break
88
+
89
+ if len(cmd_parameters.new_item_ids) == 0:
90
+ error_message += 'new item ids must be specified\n'
91
+ for item_id in cmd_parameters.new_item_ids:
92
+ try:
93
+ int(item_id)
94
+ except ValueError:
95
+ error_message += 'new item ids must be integers\n'
96
+ break
97
+
98
+ if len(cmd_parameters.new_item_ids) != len(cmd_parameters.item_ids):
99
+ error_message += 'the number of item ids and new item ids must match for a valid exchange\n'
100
+
101
+ if common_items := set(cmd_parameters.item_ids).intersection(
102
+ set(cmd_parameters.new_item_ids)
103
+ ):
104
+ common_items_str = ', '.join(common_items)
105
+ error_message += f'cannot modify items for themselves: {common_items_str}. new item ids must differ from item ids\n'
106
+
107
+ return (False, error_message) if error_message else (True, '')
79
108
 
80
109
 
81
110
  class ResponseGenerator:
@@ -24,7 +24,7 @@ class Signature:
24
24
  item_ids: List[str] = Field(
25
25
  default_factory=list,
26
26
  description="List of item IDs to be returned",
27
- examples=["1008292230"],
27
+ examples=["'57347828', '8646987326'"],
28
28
  json_schema_extra={
29
29
  "available_from": ["get_order_details"]
30
30
  }
@@ -69,7 +69,18 @@ class Signature:
69
69
  ) -> tuple[bool, str]:
70
70
  if not cmd_parameters.order_id.startswith('#'):
71
71
  cmd_parameters.order_id = f'#{cmd_parameters.order_id}'
72
- return (True, '')
72
+
73
+ error_message = ''
74
+ if len(cmd_parameters.item_ids) == 0:
75
+ error_message = 'item ids must be specified\n'
76
+ for item_id in cmd_parameters.item_ids:
77
+ try:
78
+ int(item_id)
79
+ except ValueError:
80
+ error_message += 'item ids must be integers\n'
81
+ break
82
+
83
+ return (False, error_message) if error_message else (True, '')
73
84
 
74
85
 
75
86
  class ResponseGenerator:
@@ -10,7 +10,7 @@ from ..tools.transfer_to_human_agents import TransferToHumanAgents
10
10
 
11
11
 
12
12
  class Signature:
13
- """Transfer to a human agent ONLY AS THE LAST RESORT"""
13
+ """Transfer to a human agent ONLY if the user demands it explicitly."""
14
14
  class Input(BaseModel):
15
15
  summary: str = Field(
16
16
  default="NOT_FOUND",
@@ -23,6 +23,7 @@ def run_main(args):
23
23
  from rich.spinner import Spinner
24
24
  from prompt_toolkit import PromptSession
25
25
  from prompt_toolkit.patch_stdout import patch_stdout
26
+ from prompt_toolkit.formatted_text import HTML
26
27
 
27
28
  import fastworkflow
28
29
  from fastworkflow.utils.logging import logger
@@ -34,7 +35,7 @@ def run_main(args):
34
35
  # Instantiate a global console for consistent styling
35
36
  global console
36
37
  console = Console()
37
- prompt_session = PromptSession("User > ")
38
+ prompt_session = PromptSession(HTML('<b>User ></b> '))
38
39
 
39
40
  def _build_artifact_table(artifacts: dict[str, str]) -> Table:
40
41
  """Return a rich.Table representation for artifact key-value pairs."""
@@ -138,7 +139,9 @@ def run_main(args):
138
139
  raise ValueError("Cannot provide both startup_command and startup_action")
139
140
 
140
141
  console.print(Panel(f"Running fastWorkflow: [bold]{args.workflow_path}[/bold]", title="[bold green]fastworkflow[/bold green]", border_style="green"))
141
- console.print("[bold green]Tip:[/bold green] Type 'exit' to quit the application.")
142
+ console.print(
143
+ "[bold green]Tips:[/bold green] Type '//exit' to quit the application. Type '//new' to start a new conversation. "
144
+ "[bold green]Tips:[/bold green] Prefix natural language commands with a single '/' to execute them in deterministic (non-agentic) mode")
142
145
 
143
146
  # ------------------------------------------------------------------
144
147
  # Startup progress bar ------------------------------------------------
@@ -168,9 +171,9 @@ def run_main(args):
168
171
  with open(args.context_file_path, 'r') as file:
169
172
  context_dict = json.load(file)
170
173
 
171
- # Create the chat session with agent mode if specified
172
- run_as_agent = args.run_as_agent if hasattr(args, 'run_as_agent') else False
173
- fastworkflow.chat_session = fastworkflow.ChatSession(run_as_agent=run_as_agent)
174
+ # Create the chat session in agent mode always
175
+ # run_as_agent = args.run_as_agent if hasattr(args, 'run_as_agent') else False
176
+ fastworkflow.chat_session = fastworkflow.ChatSession(run_as_agent=True)
174
177
 
175
178
  # Start the workflow within the chat session
176
179
  fastworkflow.chat_session.start_workflow(
@@ -193,8 +196,12 @@ def run_main(args):
193
196
  while not fastworkflow.chat_session.workflow_is_complete or args.keep_alive:
194
197
  with patch_stdout():
195
198
  user_command = prompt_session.prompt()
196
- if user_command == "exit":
199
+ if user_command.startswith("//exit"):
197
200
  break
201
+ if user_command.startswith("//new"):
202
+ fastworkflow.chat_session.clear_conversation_history()
203
+ console.print("[bold]Agent >[/bold] New conversation started!\n", end="")
204
+ user_command = prompt_session.prompt()
198
205
 
199
206
  fastworkflow.chat_session.user_message_queue.put(user_command)
200
207
 
@@ -219,28 +226,25 @@ def run_main(args):
219
226
  with console.status("[bold cyan]Processing command...[/bold cyan]", spinner="dots") as status:
220
227
  counter = 0
221
228
  while wait_thread.is_alive():
222
- # Check for agent traces if in agent mode
223
- if args.run_as_agent:
224
- while True:
225
- try:
226
- evt = fastworkflow.chat_session.command_trace_queue.get_nowait()
227
- except queue.Empty:
228
- break
229
-
230
- # Choose styles based on success
231
- info_style = "dim orange3" if (evt.success is False) else "dim yellow"
232
- resp_style = "dim orange3" if (evt.success is False) else "dim green"
229
+ # Always show agent traces (run mode is always agentic)
230
+ while True:
231
+ try:
232
+ evt = fastworkflow.chat_session.command_trace_queue.get_nowait()
233
+ except queue.Empty:
234
+ break
235
+
236
+ # Choose styles based on success
237
+ info_style = "dim orange3" if (evt.success is False) else "dim yellow"
238
+ resp_style = "dim orange3" if (evt.success is False) else "dim green"
233
239
 
234
- if evt.direction == fastworkflow.CommandTraceEventDirection.AGENT_TO_WORKFLOW:
235
- console.print(Text("Agent -> Workflow: ", style=info_style), end="")
236
- console.print(Text(str(evt.raw_command or ""), style=info_style))
237
- else:
238
- # command info (dim yellow or dim orange3)
239
- info = f"{evt.command_name or ''}, {evt.parameters}: "
240
- console.print(Text("Workflow -> Agent: ", style=info_style), end="")
241
- console.print(Text(info, style=info_style), end="")
242
- # response (dim green or dim orange3)
243
- console.print(Text(str(evt.response_text or ""), style=resp_style))
240
+ if evt.direction == fastworkflow.CommandTraceEventDirection.AGENT_TO_WORKFLOW:
241
+ console.print(f'[bold]Agent >[/bold] {evt.raw_command}', style=info_style)
242
+ else:
243
+ # command info (dim yellow or dim orange3)
244
+ info = f"[bold]Workflow >[/bold] {evt.command_name or ''}, {evt.parameters}: "
245
+ console.print(info, style=info_style, end="")
246
+ # response (dim green or dim orange3)
247
+ console.print(f'[bold]Workflow >[/bold] {evt.response_text}', style=resp_style)
244
248
 
245
249
  time.sleep(0.5)
246
250
  counter += 1
@@ -273,11 +277,6 @@ if __name__ == "__main__":
273
277
  parser.add_argument(
274
278
  "--project_folderpath", help="Optional path to project folder containing application code", default=None
275
279
  )
276
- parser.add_argument(
277
- "--run_as_agent",
278
- help="Run in agent mode (uses DSPy for tool selection)",
279
- action="store_true",
280
- default=False
281
- )
280
+ # run mode is always agentic; deterministic NL execution can be forced by prefixing '/' to a command
282
281
  args = parser.parse_args()
283
282
  run_main(args)