fastworkflow 2.15.5__py3-none-any.whl → 2.17.13__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 (42) hide show
  1. fastworkflow/_workflows/command_metadata_extraction/_commands/ErrorCorrection/you_misunderstood.py +1 -1
  2. fastworkflow/_workflows/command_metadata_extraction/_commands/IntentDetection/what_can_i_do.py +16 -2
  3. fastworkflow/_workflows/command_metadata_extraction/_commands/wildcard.py +27 -570
  4. fastworkflow/_workflows/command_metadata_extraction/intent_detection.py +360 -0
  5. fastworkflow/_workflows/command_metadata_extraction/parameter_extraction.py +411 -0
  6. fastworkflow/chat_session.py +379 -206
  7. fastworkflow/cli.py +80 -165
  8. fastworkflow/command_context_model.py +73 -7
  9. fastworkflow/command_executor.py +14 -5
  10. fastworkflow/command_metadata_api.py +106 -6
  11. fastworkflow/examples/fastworkflow.env +2 -1
  12. fastworkflow/examples/fastworkflow.passwords.env +2 -1
  13. fastworkflow/examples/retail_workflow/_commands/exchange_delivered_order_items.py +32 -3
  14. fastworkflow/examples/retail_workflow/_commands/find_user_id_by_email.py +6 -5
  15. fastworkflow/examples/retail_workflow/_commands/modify_pending_order_items.py +32 -3
  16. fastworkflow/examples/retail_workflow/_commands/return_delivered_order_items.py +13 -2
  17. fastworkflow/examples/retail_workflow/_commands/transfer_to_human_agents.py +1 -1
  18. fastworkflow/intent_clarification_agent.py +131 -0
  19. fastworkflow/mcp_server.py +3 -3
  20. fastworkflow/run/__main__.py +33 -40
  21. fastworkflow/run_fastapi_mcp/README.md +373 -0
  22. fastworkflow/run_fastapi_mcp/__main__.py +1300 -0
  23. fastworkflow/run_fastapi_mcp/conversation_store.py +391 -0
  24. fastworkflow/run_fastapi_mcp/jwt_manager.py +341 -0
  25. fastworkflow/run_fastapi_mcp/mcp_specific.py +103 -0
  26. fastworkflow/run_fastapi_mcp/redoc_2_standalone_html.py +40 -0
  27. fastworkflow/run_fastapi_mcp/utils.py +517 -0
  28. fastworkflow/train/__main__.py +1 -1
  29. fastworkflow/utils/chat_adapter.py +99 -0
  30. fastworkflow/utils/python_utils.py +4 -4
  31. fastworkflow/utils/react.py +258 -0
  32. fastworkflow/utils/signatures.py +338 -139
  33. fastworkflow/workflow.py +1 -5
  34. fastworkflow/workflow_agent.py +185 -133
  35. {fastworkflow-2.15.5.dist-info → fastworkflow-2.17.13.dist-info}/METADATA +16 -18
  36. {fastworkflow-2.15.5.dist-info → fastworkflow-2.17.13.dist-info}/RECORD +40 -30
  37. fastworkflow/run_agent/__main__.py +0 -294
  38. fastworkflow/run_agent/agent_module.py +0 -194
  39. /fastworkflow/{run_agent → run_fastapi_mcp}/__init__.py +0 -0
  40. {fastworkflow-2.15.5.dist-info → fastworkflow-2.17.13.dist-info}/LICENSE +0 -0
  41. {fastworkflow-2.15.5.dist-info → fastworkflow-2.17.13.dist-info}/WHEEL +0 -0
  42. {fastworkflow-2.15.5.dist-info → fastworkflow-2.17.13.dist-info}/entry_points.txt +0 -0
@@ -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:
@@ -11,16 +11,17 @@ from ..tools.find_user_id_by_email import FindUserIdByEmail
11
11
 
12
12
 
13
13
  class Signature:
14
- """Find user id by email"""
14
+ """
15
+ Find user id by email.
16
+ If email is not available, use `find_user_id_by_name_zip` instead.
17
+ As a last resort transfer to a human agent
18
+ """
15
19
  class Input(BaseModel):
16
20
  """Parameters taken from user utterance."""
17
21
 
18
22
  email: str = Field(
19
23
  default="NOT_FOUND",
20
- description=(
21
- "The email address to search for. If email is not available, "
22
- "use `find_user_id_by_name_zip` instead. As a last resort transfer to a human agent"
23
- ),
24
+ description="The email address to search for",
24
25
  pattern=r"^(NOT_FOUND|[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$",
25
26
  examples=["user@example.com"],
26
27
  )
@@ -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",
@@ -0,0 +1,131 @@
1
+ """
2
+ Intent detection error state agent module for fastWorkflow.
3
+ Specialized agent for handling intent detection errors.
4
+ """
5
+
6
+ import json
7
+ import dspy
8
+
9
+ import fastworkflow
10
+ from fastworkflow.utils.react import fastWorkflowReAct
11
+ from fastworkflow.command_metadata_api import CommandMetadataAPI
12
+
13
+
14
+ class IntentClarificationAgentSignature(dspy.Signature):
15
+ """
16
+ Handle intent detection errors by clarifying user intent.
17
+ You are provided with:
18
+ 1. The workflow agent's inputs and trajectory - showing what the agent has been trying to do
19
+ 2. Suggested commands metadata (for intent ambiguity) or empty (for intent misunderstanding - use show_available_commands tool)
20
+
21
+ Review the agent trajectory to understand the context and what led to this error.
22
+ If suggested_commands_metadata is provided, review it carefully to understand each command's purpose, parameters, and usage.
23
+ If suggested_commands_metadata is empty, use the show_available_commands tool to get the full list of available commands.
24
+ IMPORTANT: When clarifying intent, preserve ALL parameters from the original command.
25
+ Use available tools to resolve ambiguous or misunderstood commands. Use the ask_user tool ONLY as a last resort. Return the complete clarified command with the correct name and all original parameters.
26
+ """
27
+ original_command = dspy.InputField(desc="The original command with all parameters that caused the error.")
28
+ error_message = dspy.InputField(desc="The intent detection error message from the workflow.")
29
+ agent_inputs = dspy.InputField(desc="The original inputs to the workflow agent.")
30
+ agent_trajectory = dspy.InputField(desc="The workflow agent's trajectory showing all actions taken so far leading to this error.")
31
+ clarified_command = dspy.OutputField(desc="The complete command with correct command name AND all original parameters preserved.")
32
+
33
+
34
+ # def _show_available_commands(chat_session: fastworkflow.ChatSession) -> str:
35
+ # """
36
+ # Show available commands to help resolve intent detection errors.
37
+
38
+ # Args:
39
+ # chat_session: The chat session instance
40
+
41
+ # Returns:
42
+ # List of available commands
43
+ # """
44
+
45
+ # current_workflow = chat_session.get_active_workflow()
46
+ # return CommandMetadataAPI.get_command_display_text(
47
+ # subject_workflow_path=current_workflow.folderpath,
48
+ # cme_workflow_path=fastworkflow.get_internal_workflow_path("command_metadata_extraction"),
49
+ # active_context_name=current_workflow.current_command_context_name,
50
+ # )
51
+
52
+
53
+ def _ask_user_for_clarification(
54
+ clarification_request: str,
55
+ chat_session: fastworkflow.ChatSession
56
+ ) -> str:
57
+ """
58
+ Ask user for clarification when intent is unclear.
59
+
60
+ Args:
61
+ clarification_request: The question to ask the user
62
+ chat_session: The chat session instance
63
+
64
+ Returns:
65
+ User's response
66
+ """
67
+ command_output = fastworkflow.CommandOutput(
68
+ command_responses=[fastworkflow.CommandResponse(response=clarification_request)],
69
+ workflow_name=chat_session.get_active_workflow().folderpath.split('/')[-1]
70
+ )
71
+ chat_session.command_output_queue.put(command_output)
72
+
73
+ user_response = chat_session.user_message_queue.get()
74
+
75
+ # Log to action.jsonl (shared with main agent)
76
+ with open("action.jsonl", "a", encoding="utf-8") as f:
77
+ agent_user_dialog = {
78
+ "intent_clarification_agent": True,
79
+ "agent_query": clarification_request,
80
+ "user_response": user_response
81
+ }
82
+ f.write(json.dumps(agent_user_dialog, ensure_ascii=False) + "\n")
83
+
84
+ return user_response
85
+
86
+
87
+ def initialize_intent_clarification_agent(
88
+ chat_session: fastworkflow.ChatSession,
89
+ max_iters: int = 20
90
+ ):
91
+ """
92
+ Initialize a specialized agent for handling intent detection errors.
93
+ This agent has a limited tool set and shares traces with the main execution agent.
94
+
95
+ Args:
96
+ chat_session: The chat session instance
97
+ max_iters: Maximum iterations for the agent (default: 10)
98
+
99
+ Returns:
100
+ DSPy ReAct agent configured for intent detection error handling
101
+ """
102
+ if not chat_session:
103
+ raise ValueError("chat_session cannot be null")
104
+
105
+ # def show_available_commands() -> str:
106
+ # """
107
+ # Show all available commands to help resolve intent ambiguity.
108
+ # """
109
+ # return _show_available_commands(chat_session)
110
+
111
+ def ask_user(clarification_request: str) -> str:
112
+ """
113
+ Ask the user for clarification when the intent is unclear.
114
+ Use this as a last resort when you cannot determine the correct command.
115
+
116
+ Args:
117
+ clarification_request: Clear question to ask the user
118
+ """
119
+ return _ask_user_for_clarification(clarification_request, chat_session)
120
+
121
+ # Limited tool set for intent detection errors
122
+ tools = [
123
+ # show_available_commands,
124
+ ask_user,
125
+ ]
126
+
127
+ return fastWorkflowReAct(
128
+ IntentClarificationAgentSignature,
129
+ tools=tools,
130
+ max_iters=max_iters,
131
+ )
@@ -41,7 +41,7 @@ class FastWorkflowMCPServer:
41
41
  NOT_FOUND = fastworkflow.get_env_var('NOT_FOUND')
42
42
 
43
43
  # Get available commands from workflow
44
- workflow = fastworkflow.ChatSession.get_active_workflow()
44
+ workflow = fastworkflow.chat_session.get_active_workflow()
45
45
  workflow_folderpath = workflow.folderpath
46
46
  # Use cached routing definition instead of rebuilding every time
47
47
  routing = RoutingRegistry.get_definition(workflow_folderpath)
@@ -147,7 +147,7 @@ class FastWorkflowMCPServer:
147
147
  arguments=arguments
148
148
  )
149
149
 
150
- workflow = fastworkflow.ChatSession.get_active_workflow()
150
+ workflow = fastworkflow.chat_session.get_active_workflow()
151
151
  # Execute using MCP-compliant method
152
152
  return CommandExecutor.perform_mcp_tool_call(
153
153
  workflow,
@@ -203,7 +203,7 @@ class FastWorkflowMCPServer:
203
203
 
204
204
  Falls back to the first available path if the active context is none.
205
205
  """
206
- workflow = fastworkflow.ChatSession.get_active_workflow()
206
+ workflow = fastworkflow.chat_session.get_active_workflow()
207
207
  return workflow.current_command_context_name
208
208
 
209
209
 
@@ -19,14 +19,13 @@ def run_main(args):
19
19
  from rich.table import Table
20
20
  from rich.text import Text
21
21
  from rich.console import Group
22
- from rich.live import Live
23
22
  from rich.spinner import Spinner
24
23
  from prompt_toolkit import PromptSession
25
24
  from prompt_toolkit.patch_stdout import patch_stdout
25
+ from prompt_toolkit.formatted_text import HTML
26
26
 
27
27
  import fastworkflow
28
28
  from fastworkflow.utils.logging import logger
29
- from fastworkflow.command_executor import CommandExecutor
30
29
 
31
30
  # Progress bar helper
32
31
  from fastworkflow.utils.startup_progress import StartupProgress
@@ -34,7 +33,7 @@ def run_main(args):
34
33
  # Instantiate a global console for consistent styling
35
34
  global console
36
35
  console = Console()
37
- prompt_session = PromptSession("User > ")
36
+ prompt_session = PromptSession(HTML('<b>User ></b> '))
38
37
 
39
38
  def _build_artifact_table(artifacts: dict[str, str]) -> Table:
40
39
  """Return a rich.Table representation for artifact key-value pairs."""
@@ -60,7 +59,7 @@ def run_main(args):
60
59
  if command_output.command_name:
61
60
  command_info_table.add_row("Command:", Text(command_output.command_name, style="yellow"))
62
61
  if command_output.command_parameters:
63
- command_info_table.add_row("Parameters:", Text(command_output.command_parameters, style="yellow"))
62
+ command_info_table.add_row("Parameters:", Text(str(command_output.command_parameters.model_dump()), style="yellow"))
64
63
 
65
64
  # Add command info section if we have any rows
66
65
  if command_info_table.row_count > 0:
@@ -138,15 +137,13 @@ def run_main(args):
138
137
  raise ValueError("Cannot provide both startup_command and startup_action")
139
138
 
140
139
  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.")
140
+ console.print(
141
+ "[bold green]Tips:[/bold green] Type '//exit' to quit the application. Type '//new' to start a new conversation. "
142
+ "[bold green]Tips:[/bold green] Prefix natural language commands with a single '/' to execute them in deterministic (non-agentic) mode")
142
143
 
143
144
  # ------------------------------------------------------------------
144
145
  # Startup progress bar ------------------------------------------------
145
146
  # ------------------------------------------------------------------
146
- command_info_root = os.path.join(args.workflow_path, "___command_info")
147
- subdir_count = 0
148
- if os.path.isdir(command_info_root):
149
- subdir_count = len([d for d in os.listdir(command_info_root) if os.path.isdir(os.path.join(command_info_root, d))])
150
147
 
151
148
  # 3 coarse CLI steps + per-directory warm-up (handled inside ChatSession) + 1 global warm-up
152
149
  StartupProgress.begin(total=3)
@@ -168,9 +165,9 @@ def run_main(args):
168
165
  with open(args.context_file_path, 'r') as file:
169
166
  context_dict = json.load(file)
170
167
 
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)
168
+ # Create the chat session in agent mode always
169
+ # run_as_agent = args.run_as_agent if hasattr(args, 'run_as_agent') else False
170
+ fastworkflow.chat_session = fastworkflow.ChatSession(run_as_agent=True)
174
171
 
175
172
  # Start the workflow within the chat session
176
173
  fastworkflow.chat_session.start_workflow(
@@ -193,8 +190,12 @@ def run_main(args):
193
190
  while not fastworkflow.chat_session.workflow_is_complete or args.keep_alive:
194
191
  with patch_stdout():
195
192
  user_command = prompt_session.prompt()
196
- if user_command == "exit":
193
+ if user_command.startswith("//exit"):
197
194
  break
195
+ if user_command.startswith("//new"):
196
+ fastworkflow.chat_session.clear_conversation_history()
197
+ console.print("[bold]Agent >[/bold] New conversation started!\n", end="")
198
+ user_command = prompt_session.prompt()
198
199
 
199
200
  fastworkflow.chat_session.user_message_queue.put(user_command)
200
201
 
@@ -219,28 +220,25 @@ def run_main(args):
219
220
  with console.status("[bold cyan]Processing command...[/bold cyan]", spinner="dots") as status:
220
221
  counter = 0
221
222
  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"
223
+ # Always show agent traces (run mode is always agentic)
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"
233
233
 
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))
234
+ if evt.direction == fastworkflow.CommandTraceEventDirection.AGENT_TO_WORKFLOW:
235
+ console.print(f'[bold]Agent >[/bold] {evt.raw_command}', style=info_style)
236
+ else:
237
+ # command info (dim yellow or dim orange3)
238
+ info = f"[bold]Workflow >[/bold] {evt.command_name or ''}, {evt.parameters}: "
239
+ console.print(info, style=info_style, end="")
240
+ # response (dim green or dim orange3)
241
+ console.print(f'[bold]Workflow >[/bold] {evt.response_text}', style=resp_style)
244
242
 
245
243
  time.sleep(0.5)
246
244
  counter += 1
@@ -273,11 +271,6 @@ if __name__ == "__main__":
273
271
  parser.add_argument(
274
272
  "--project_folderpath", help="Optional path to project folder containing application code", default=None
275
273
  )
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
- )
274
+ # run mode is always agentic; deterministic NL execution can be forced by prefixing '/' to a command
282
275
  args = parser.parse_args()
283
276
  run_main(args)