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
@@ -60,7 +60,7 @@ class ResponseGenerator:
60
60
  ])
61
61
  )
62
62
  response = (
63
- "Please enter the correct command from the list below:\n"
63
+ "Please select the correct command name from the list below:\n"
64
64
  f"{response}\n\nor type 'abort' to cancel"
65
65
  )
66
66
 
@@ -8,6 +8,7 @@ import json
8
8
 
9
9
  import fastworkflow
10
10
  from fastworkflow.train.generate_synthetic import generate_diverse_utterances
11
+ from fastworkflow.command_context_model import get_workflow_info
11
12
  from fastworkflow.command_metadata_api import CommandMetadataAPI
12
13
 
13
14
  class Signature:
@@ -139,20 +140,33 @@ class ResponseGenerator:
139
140
  with contextlib.suppress(Exception):
140
141
  if fastworkflow.chat_session:
141
142
  is_agent_mode = fastworkflow.chat_session.run_as_agent
142
-
143
+
143
144
  app_workflow = workflow.context["app_workflow"]
144
- response = CommandMetadataAPI.get_command_display_text(
145
+
146
+ # Get workflow definition
147
+ workflow_info = get_workflow_info(app_workflow.folderpath)
148
+ workflow_def_text = CommandMetadataAPI.get_workflow_definition_display_text(workflow_info)
149
+
150
+ # Get available commands in current context
151
+ commands_text = CommandMetadataAPI.get_command_display_text(
145
152
  subject_workflow_path=app_workflow.folderpath,
146
153
  cme_workflow_path=workflow.folderpath,
147
154
  active_context_name=app_workflow.current_command_context_name,
148
155
  for_agents=is_agent_mode,
149
156
  )
157
+
158
+ response = f"{workflow_def_text}\n\n{commands_text}"
150
159
 
160
+ nlu_pipeline_stage = workflow.context.get(
161
+ "NLU_Pipeline_Stage",
162
+ fastworkflow.NLUPipelineStage.INTENT_DETECTION)
163
+ success = nlu_pipeline_stage == fastworkflow.NLUPipelineStage.INTENT_DETECTION
151
164
  return fastworkflow.CommandOutput(
152
165
  workflow_id=workflow.id,
153
166
  command_responses=[
154
167
  fastworkflow.CommandResponse(
155
168
  response=response,
169
+ success=success
156
170
  )
157
171
  ]
158
172
  )
@@ -1,569 +1,9 @@
1
- import contextlib
2
- from enum import Enum
3
- import sys
4
- from typing import Dict, List, Optional, Type, Union
5
- import json
6
- import os
7
-
8
- from pydantic import BaseModel
9
- from pydantic_core import PydanticUndefined
10
- from speedict import Rdict
11
-
12
1
  import fastworkflow
13
- from fastworkflow.utils.logging import logger
14
- from fastworkflow import Action, CommandOutput, CommandResponse, ModuleType, NLUPipelineStage
15
- from fastworkflow.cache_matching import cache_match, store_utterance_cache
2
+ from fastworkflow import Action, CommandOutput, CommandResponse, NLUPipelineStage
16
3
  from fastworkflow.command_executor import CommandExecutor
17
- from fastworkflow.command_routing import RoutingDefinition
18
- import fastworkflow.command_routing
19
- from fastworkflow.model_pipeline_training import (
20
- predict_single_sentence,
21
- get_artifact_path,
22
- CommandRouter
23
- )
24
-
25
- from fastworkflow.train.generate_synthetic import generate_diverse_utterances
26
- from fastworkflow.utils.fuzzy_match import find_best_matches
27
- from fastworkflow.utils.signatures import InputForParamExtraction
28
-
29
-
30
- INVALID_INT_VALUE = -sys.maxsize
31
- INVALID_FLOAT_VALUE = -sys.float_info.max
32
-
33
- MISSING_INFORMATION_ERRMSG = fastworkflow.get_env_var("MISSING_INFORMATION_ERRMSG")
34
- INVALID_INFORMATION_ERRMSG = fastworkflow.get_env_var("INVALID_INFORMATION_ERRMSG")
35
-
36
- NOT_FOUND = fastworkflow.get_env_var("NOT_FOUND")
37
- INVALID = fastworkflow.get_env_var("INVALID")
38
- PARAMETER_EXTRACTION_ERROR_MSG = None
39
-
40
-
41
- class CommandNamePrediction:
42
- class Output(BaseModel):
43
- command_name: Optional[str] = None
44
- error_msg: Optional[str] = None
45
- is_cme_command: bool = False
46
-
47
- def __init__(self, cme_workflow: fastworkflow.Workflow):
48
- self.cme_workflow = cme_workflow
49
- self.app_workflow = cme_workflow.context["app_workflow"]
50
- self.app_workflow_folderpath = self.app_workflow.folderpath
51
- self.app_workflow_id = self.app_workflow.id
52
-
53
- self.convo_path = os.path.join(self.app_workflow_folderpath, "___convo_info")
54
- self.cache_path = self._get_cache_path(self.app_workflow_id, self.convo_path)
55
- self.path = self._get_cache_path_cache(self.convo_path)
56
-
57
- def predict(self, command_context_name: str, command: str, nlu_pipeline_stage: NLUPipelineStage) -> "CommandNamePrediction.Output":
58
- # sourcery skip: extract-duplicate-method
59
-
60
- model_artifact_path = f"{self.app_workflow_folderpath}/___command_info/{command_context_name}"
61
- command_router = CommandRouter(model_artifact_path)
62
-
63
- # Re-use the already-built ModelPipeline attached to the router
64
- # instead of instantiating a fresh one. This avoids reloading HF
65
- # checkpoints and transferring tensors each time we see a new
66
- # message for the same context.
67
- modelpipeline = command_router.modelpipeline
68
-
69
- crd = fastworkflow.RoutingRegistry.get_definition(
70
- self.cme_workflow.folderpath)
71
- cme_command_names = crd.get_command_names('IntentDetection')
72
-
73
- valid_command_names = set()
74
- if nlu_pipeline_stage == NLUPipelineStage.INTENT_AMBIGUITY_CLARIFICATION:
75
- valid_command_names = self._get_suggested_commands(self.path)
76
- elif nlu_pipeline_stage in (
77
- NLUPipelineStage.INTENT_DETECTION, NLUPipelineStage.INTENT_MISUNDERSTANDING_CLARIFICATION):
78
- app_crd = fastworkflow.RoutingRegistry.get_definition(
79
- self.app_workflow_folderpath)
80
- valid_command_names = (
81
- set(cme_command_names) |
82
- set(app_crd.get_command_names(command_context_name))
83
- )
84
-
85
- command_name_dict = {
86
- fully_qualified_command_name.split('/')[-1]: fully_qualified_command_name
87
- for fully_qualified_command_name in valid_command_names
88
- }
89
-
90
- if nlu_pipeline_stage == NLUPipelineStage.INTENT_AMBIGUITY_CLARIFICATION:
91
- # what_can_i_do is special in INTENT_AMBIGUITY_CLARIFICATION
92
- # We will not predict, just match plain utterances with exact or fuzzy match
93
- command_name_dict |= {
94
- plain_utterance: 'IntentDetection/what_can_i_do'
95
- for plain_utterance in crd.command_directory.map_command_2_utterance_metadata[
96
- 'IntentDetection/what_can_i_do'
97
- ].plain_utterances
98
- }
99
-
100
- if nlu_pipeline_stage != NLUPipelineStage.INTENT_DETECTION:
101
- # abort is special.
102
- # We will not predict, just match plain utterances with exact or fuzzy match
103
- command_name_dict |= {
104
- plain_utterance: 'ErrorCorrection/abort'
105
- for plain_utterance in crd.command_directory.map_command_2_utterance_metadata[
106
- 'ErrorCorrection/abort'
107
- ].plain_utterances
108
- }
109
-
110
- if nlu_pipeline_stage != NLUPipelineStage.INTENT_MISUNDERSTANDING_CLARIFICATION:
111
- # you_misunderstood is special.
112
- # We will not predict, just match plain utterances with exact or fuzzy match
113
- command_name_dict |= {
114
- plain_utterance: 'ErrorCorrection/you_misunderstood'
115
- for plain_utterance in crd.command_directory.map_command_2_utterance_metadata[
116
- 'ErrorCorrection/you_misunderstood'
117
- ].plain_utterances
118
- }
119
-
120
- # See if the command starts with a command name followed by a space
121
- tentative_command_name = command.split(" ", 1)[0]
122
- normalized_command_name = tentative_command_name.lower()
123
- command_name = None
124
- if normalized_command_name in command_name_dict:
125
- command_name = normalized_command_name
126
- command = command.replace(f"{tentative_command_name}", "").strip().replace(" ", " ")
127
- else:
128
- # Use Levenshtein distance for fuzzy matching with the full command part after @
129
- best_matched_commands, _ = find_best_matches(
130
- command.replace(" ", "_"),
131
- command_name_dict.keys(),
132
- threshold=0.3 # Adjust threshold as needed
133
- )
134
- if best_matched_commands:
135
- command_name = best_matched_commands[0]
136
-
137
- if nlu_pipeline_stage == NLUPipelineStage.INTENT_DETECTION:
138
- if not command_name:
139
- if cache_result := cache_match(self.path, command, modelpipeline, 0.85):
140
- command_name = cache_result
141
- else:
142
- predictions=command_router.predict(command)
143
-
144
- if len(predictions)==1:
145
- command_name = predictions[0].split('/')[-1]
146
- else:
147
- # If confidence is low, treat as ambiguous command (type 1)
148
- error_msg = self._formulate_ambiguous_command_error_message(predictions)
149
- # Store suggested commands
150
- self._store_suggested_commands(self.path, predictions, 1)
151
- return CommandNamePrediction.Output(error_msg=error_msg)
152
-
153
- elif nlu_pipeline_stage in (
154
- NLUPipelineStage.INTENT_AMBIGUITY_CLARIFICATION,
155
- NLUPipelineStage.INTENT_MISUNDERSTANDING_CLARIFICATION
156
- ) and not command_name:
157
- command_name = "what_can_i_do"
158
-
159
- if not command_name or command_name == "wildcard":
160
- fully_qualified_command_name=None
161
- is_cme_command=False
162
- else:
163
- fully_qualified_command_name = command_name_dict[command_name]
164
- is_cme_command=(
165
- fully_qualified_command_name in cme_command_names or
166
- fully_qualified_command_name in crd.get_command_names('ErrorCorrection')
167
- )
168
-
169
- if (
170
- nlu_pipeline_stage
171
- in (
172
- NLUPipelineStage.INTENT_AMBIGUITY_CLARIFICATION,
173
- NLUPipelineStage.INTENT_MISUNDERSTANDING_CLARIFICATION,
174
- )
175
- and not fully_qualified_command_name.endswith('abort')
176
- and not fully_qualified_command_name.endswith('what_can_i_do')
177
- and not fully_qualified_command_name.endswith('you_misunderstood')
178
- ):
179
- command = self.cme_workflow.context["command"]
180
- store_utterance_cache(self.path, command, command_name, modelpipeline)
181
-
182
- return CommandNamePrediction.Output(
183
- command_name=fully_qualified_command_name,
184
- is_cme_command=is_cme_command
185
- )
186
-
187
- @staticmethod
188
- def _get_cache_path(workflow_id, convo_path):
189
- """
190
- Generate cache file path based on workflow ID
191
- """
192
- base_dir = convo_path
193
- # Create directory if it doesn't exist
194
- os.makedirs(base_dir, exist_ok=True)
195
- return os.path.join(base_dir, f"{workflow_id}.db")
196
-
197
- @staticmethod
198
- def _get_cache_path_cache(convo_path):
199
- """
200
- Generate cache file path based on workflow ID
201
- """
202
- base_dir = convo_path
203
- # Create directory if it doesn't exist
204
- os.makedirs(base_dir, exist_ok=True)
205
- return os.path.join(base_dir, "cache.db")
206
-
207
- # Store the suggested commands with the flag type
208
- @staticmethod
209
- def _store_suggested_commands(cache_path, command_list, flag_type):
210
- """
211
- Store the list of suggested commands for the constrained selection
212
-
213
- Args:
214
- cache_path: Path to the cache database
215
- command_list: List of suggested commands
216
- flag_type: Type of constraint (1=ambiguous, 2=misclassified)
217
- """
218
- db = Rdict(cache_path)
219
- try:
220
- db["suggested_commands"] = command_list
221
- db["flag_type"] = flag_type
222
- finally:
223
- db.close()
224
-
225
- # Get the suggested commands
226
- @staticmethod
227
- def _get_suggested_commands(cache_path):
228
- """
229
- Get the list of suggested commands for the constrained selection
230
- """
231
- db = Rdict(cache_path)
232
- try:
233
- return db.get("suggested_commands", [])
234
- finally:
235
- db.close()
236
-
237
- @staticmethod
238
- def _get_count(cache_path):
239
- db = Rdict(cache_path)
240
- try:
241
- return db.get("utterance_count", 0) # Default to 0 if key doesn't exist
242
- finally:
243
- db.close()
244
-
245
- @staticmethod
246
- def _print_db_contents(cache_path):
247
- db = Rdict(cache_path)
248
- try:
249
- print("All keys in database:", list(db.keys()))
250
- for key in db.keys():
251
- print(f"Key: {key}, Value: {db[key]}")
252
- finally:
253
- db.close()
254
-
255
- @staticmethod
256
- def _store_utterance(cache_path, utterance, label):
257
- """
258
- Store utterance in existing or new database
259
- Returns: The utterance count used
260
- """
261
- # Open the database (creates if doesn't exist)
262
- db = Rdict(cache_path)
263
-
264
- try:
265
- # Get existing counter or initialize to 0
266
- utterance_count = db.get("utterance_count", 0)
267
-
268
- # Create and store the utterance entry
269
- utterance_data = {
270
- "utterance": utterance,
271
- "label": label
272
- }
273
-
274
- db[utterance_count] = utterance_data
275
-
276
- # Increment and store the counter
277
- utterance_count += 1
278
- db["utterance_count"] = utterance_count
279
-
280
- return utterance_count - 1 # Return the count used for this utterance
281
-
282
- finally:
283
- # Always close the database
284
- db.close()
285
-
286
- # Function to read from database
287
- @staticmethod
288
- def _read_utterance(cache_path, utterance_id):
289
- """
290
- Read a specific utterance from the database
291
- """
292
- db = Rdict(cache_path)
293
- try:
294
- return db.get(utterance_id)['utterance']
295
- finally:
296
- db.close()
297
-
298
- @staticmethod
299
- def _formulate_ambiguous_command_error_message(route_choice_list: list[str]) -> str:
300
- command_list = (
301
- "\n".join([
302
- f"{route_choice.split('/')[-1].lower()}"
303
- for route_choice in route_choice_list if route_choice != 'wildcard'
304
- ])
305
- )
306
-
307
- return (
308
- "The command is ambiguous. Please select from these possible options:\n"
309
- f"{command_list}\n\n"
310
- "or type 'what can i do' to see all commands\n"
311
- "or type 'abort' to cancel"
312
- )
313
-
314
- class ParameterExtraction:
315
- class Output(BaseModel):
316
- parameters_are_valid: bool
317
- cmd_parameters: Optional[BaseModel] = None
318
- error_msg: Optional[str] = None
319
- suggestions: Optional[Dict[str, List[str]]] = None
320
-
321
- def __init__(self, cme_workflow: fastworkflow.Workflow, app_workflow: fastworkflow.Workflow, command_name: str, command: str):
322
- self.cme_workflow = cme_workflow
323
- self.app_workflow = app_workflow
324
- self.command_name = command_name
325
- self.command = command
326
-
327
- def extract(self) -> "ParameterExtraction.Output":
328
- app_workflow_folderpath = self.app_workflow.folderpath
329
- app_command_routing_definition = fastworkflow.RoutingRegistry.get_definition(app_workflow_folderpath)
330
-
331
- command_parameters_class = (
332
- app_command_routing_definition.get_command_class(
333
- self.command_name, ModuleType.COMMAND_PARAMETERS_CLASS
334
- )
335
- )
336
- if not command_parameters_class:
337
- return self.Output(parameters_are_valid=True)
338
4
 
339
- stored_params = self._get_stored_parameters(self.cme_workflow)
340
-
341
- self.command = self.command.replace(self.command_name, "").strip()
342
-
343
- input_for_param_extraction = InputForParamExtraction.create(
344
- self.app_workflow, self.command_name,
345
- self.command)
346
-
347
- if stored_params:
348
- _, _, _, stored_missing_fields = self._extract_missing_fields(input_for_param_extraction, self.app_workflow, self.command_name, stored_params)
349
- else:
350
- stored_missing_fields = []
351
-
352
- # If we have missing fields (in parameter extraction error state), try to apply the command directly
353
- if stored_missing_fields:
354
- # Apply the command directly as parameter values
355
- direct_params = self._apply_missing_fields(self.command, stored_params, stored_missing_fields)
356
- new_params = direct_params
357
- else:
358
- # Otherwise use the LLM-based extraction
359
- new_params = input_for_param_extraction.extract_parameters(
360
- command_parameters_class,
361
- self.command_name,
362
- app_workflow_folderpath)
363
-
364
- if stored_params:
365
- merged_params = self._merge_parameters(stored_params, new_params, stored_missing_fields)
366
- else:
367
- merged_params = new_params
368
-
369
- self._store_parameters(self.cme_workflow, merged_params)
370
-
371
- is_valid, error_msg, suggestions = input_for_param_extraction.validate_parameters(
372
- self.app_workflow, self.command_name, merged_params
373
- )
374
-
375
- if not is_valid:
376
- if params_str := self._format_parameters_for_display(merged_params):
377
- error_msg = f"Extracted parameters so far:\n{params_str}\n\n{error_msg}"
378
-
379
- error_msg += "\nEnter 'abort' to get out of this error state and/or execute a different command."
380
- error_msg += "\nEnter 'you misunderstood' if the wrong command was executed."
381
- return self.Output(
382
- parameters_are_valid=False,
383
- error_msg=error_msg,
384
- cmd_parameters=merged_params,
385
- suggestions=suggestions)
386
-
387
- self._clear_parameters(self.cme_workflow)
388
- return self.Output(
389
- parameters_are_valid=True,
390
- cmd_parameters=merged_params)
391
-
392
- @staticmethod
393
- def _get_stored_parameters(cme_workflow: fastworkflow.Workflow):
394
- return cme_workflow.context.get("stored_parameters")
395
-
396
- @staticmethod
397
- def _store_parameters(cme_workflow: fastworkflow.Workflow, parameters):
398
- cme_workflow.context["stored_parameters"] = parameters
399
-
400
- @staticmethod
401
- def _clear_parameters(cme_workflow: fastworkflow.Workflow):
402
- if "stored_parameters" in cme_workflow.context:
403
- del cme_workflow.context["stored_parameters"]
404
-
405
- @staticmethod
406
- def _extract_missing_fields(input_for_param_extraction, sws, command_name, stored_params):
407
- stored_missing_fields = []
408
- is_valid, error_msg, suggestions = input_for_param_extraction.validate_parameters(
409
- sws, command_name, stored_params
410
- )
411
-
412
- if not is_valid:
413
- if MISSING_INFORMATION_ERRMSG in error_msg:
414
- missing_fields_str = error_msg.split(f"{MISSING_INFORMATION_ERRMSG}")[1].split("\n")[0]
415
- stored_missing_fields = [f.strip() for f in missing_fields_str.split(",")]
416
- if INVALID_INFORMATION_ERRMSG in error_msg:
417
- invalid_section = error_msg.split(f"{INVALID_INFORMATION_ERRMSG}")[1]
418
- if "\n" in invalid_section:
419
- invalid_fields_str = invalid_section.split("\n")[0]
420
- stored_missing_fields.extend(
421
- invalid_field.split(" '")[0].strip()
422
- for invalid_field in invalid_fields_str.split(", ")
423
- )
424
- return is_valid, error_msg, suggestions, stored_missing_fields
425
-
426
- @staticmethod
427
- def _merge_parameters(old_params, new_params, missing_fields):
428
- """
429
- Merge new parameters with old parameters, prioritizing new values when appropriate.
430
- """
431
- global PARAMETER_EXTRACTION_ERROR_MSG
432
- if not PARAMETER_EXTRACTION_ERROR_MSG:
433
- PARAMETER_EXTRACTION_ERROR_MSG = fastworkflow.get_env_var("PARAMETER_EXTRACTION_ERROR_MSG")
434
-
435
- merged = old_params.model_copy()
436
-
437
- try:
438
- all_fields = list(old_params.model_fields.keys())
439
- missing_fields = missing_fields or []
440
-
441
- for field_name in all_fields:
442
- if hasattr(new_params, field_name):
443
- new_value = getattr(new_params, field_name)
444
- old_value = getattr(merged, field_name)
445
-
446
- if new_value is not None and new_value != NOT_FOUND:
447
- if isinstance(old_value, str) and INVALID in old_value and INVALID not in new_value:
448
- setattr(merged, field_name, new_value)
449
-
450
- elif old_value is None or old_value == NOT_FOUND:
451
- setattr(merged, field_name, new_value)
452
-
453
- elif isinstance(old_value, int) and old_value == INVALID_INT_VALUE:
454
- with contextlib.suppress(ValueError, TypeError):
455
- setattr(merged, field_name, int(new_value))
456
-
457
- elif isinstance(old_value, float) and old_value == INVALID_FLOAT_VALUE:
458
- with contextlib.suppress(ValueError, TypeError):
459
- setattr(merged, field_name, float(new_value))
460
-
461
- elif (field_name in missing_fields and
462
- hasattr(merged.model_fields.get(field_name), "json_schema_extra") and
463
- merged.model_fields.get(field_name).json_schema_extra and
464
- "db_lookup" in merged.model_fields.get(field_name).json_schema_extra):
465
- setattr(merged, field_name, new_value)
466
-
467
- elif field_name in missing_fields:
468
- field_info = merged.model_fields.get(field_name)
469
- has_pattern = hasattr(field_info, "pattern") and field_info.pattern is not None
470
-
471
- if not has_pattern:
472
- for meta in getattr(field_info, "metadata", []):
473
- if hasattr(meta, "pattern"):
474
- has_pattern = True
475
- break
476
-
477
- if not has_pattern and hasattr(field_info, "json_schema_extra") and field_info.json_schema_extra:
478
- has_pattern = "pattern" in field_info.json_schema_extra
479
-
480
- if has_pattern:
481
- setattr(merged, field_name, new_value)
482
- except Exception as exc:
483
- logger.warning(PARAMETER_EXTRACTION_ERROR_MSG.format(error=exc))
484
-
485
- return merged
486
-
487
- @staticmethod
488
- def _format_parameters_for_display(params):
489
- """
490
- Format parameters for display in the error message.
491
- """
492
- if not params:
493
- return ""
494
-
495
- lines = []
496
-
497
- all_fields = list(params.model_fields.keys())
498
-
499
- for field_name in all_fields:
500
- value = getattr(params, field_name, None)
501
-
502
- if value in [
503
- NOT_FOUND,
504
- None,
505
- INVALID_INT_VALUE,
506
- INVALID_FLOAT_VALUE
507
- ]:
508
- continue
509
-
510
- display_name = " ".join(word.capitalize() for word in field_name.split('_'))
511
-
512
- # Format fields appropriately based on type
513
- if (
514
- isinstance(value, bool)
515
- or not hasattr(value, 'value')
516
- and isinstance(value, (int, float))
517
- or not hasattr(value, 'value')
518
- and isinstance(value, str)
519
- or not hasattr(value, 'value')
520
- ):
521
- lines.append(f"{display_name}: {value}")
522
- else: # Handle enum types
523
- lines.append(f"{display_name}: {value.value}")
524
- return "\n".join(lines)
525
-
526
- @staticmethod
527
- def _apply_missing_fields(command: str, default_params: BaseModel, missing_fields: list):
528
- global PARAMETER_EXTRACTION_ERROR_MSG
529
- if not PARAMETER_EXTRACTION_ERROR_MSG:
530
- PARAMETER_EXTRACTION_ERROR_MSG = fastworkflow.get_env_var("PARAMETER_EXTRACTION_ERROR_MSG")
531
-
532
- params = default_params.model_copy()
533
-
534
- try:
535
- if "," in command:
536
- parts = [part.strip() for part in command.split(",")]
537
-
538
- if len(parts) == len(missing_fields):
539
- if len(missing_fields) == 1:
540
- field = missing_fields[0]
541
- if hasattr(params, field):
542
- setattr(params, field, parts[0])
543
- return params
544
- elif len(missing_fields) > 1:
545
- for i, field in enumerate(missing_fields):
546
- if i < len(parts) and hasattr(params, field):
547
- setattr(params, field, parts[i])
548
- return params
549
- else:
550
- if parts and missing_fields:
551
- field = missing_fields[0]
552
- if hasattr(params, field):
553
- setattr(params, field, parts[0])
554
- return params
555
-
556
- elif missing_fields:
557
- field = missing_fields[0]
558
- if hasattr(params, field):
559
- setattr(params, field, command.strip())
560
- return params
561
-
562
- except Exception as exc:
563
- # logger.warning(PARAMETER_EXTRACTION_ERROR_MSG.format(error=exc))
564
- pass
565
-
566
- return params
5
+ from ..intent_detection import CommandNamePrediction
6
+ from ..parameter_extraction import ParameterExtraction
567
7
 
568
8
 
569
9
  class Signature:
@@ -626,7 +66,11 @@ class ResponseGenerator:
626
66
  workflow_context = workflow.context
627
67
  if cnp_output.command_name == 'ErrorCorrection/you_misunderstood':
628
68
  workflow_context["NLU_Pipeline_Stage"] = NLUPipelineStage.INTENT_MISUNDERSTANDING_CLARIFICATION
629
- else:
69
+ workflow_context["command"] = command
70
+ elif (
71
+ nlu_pipeline_stage == fastworkflow.NLUPipelineStage.INTENT_DETECTION or
72
+ cnp_output.command_name == 'ErrorCorrection/abort'
73
+ ):
630
74
  workflow.end_command_processing()
631
75
  workflow.context = workflow_context
632
76
 
@@ -635,9 +79,13 @@ class ResponseGenerator:
635
79
  command=command,
636
80
  )
637
81
  command_output = CommandExecutor.perform_action(workflow, startup_action)
638
- command_output.command_responses[0].artifacts["command_handled"] = True
639
- # Set the additional attributes
640
- command_output.command_name = cnp_output.command_name
82
+ if (
83
+ nlu_pipeline_stage == fastworkflow.NLUPipelineStage.INTENT_DETECTION or
84
+ cnp_output.command_name == 'ErrorCorrection/abort'
85
+ ):
86
+ command_output.command_responses[0].artifacts["command_handled"] = True
87
+ # Set the additional attributes
88
+ command_output.command_name = cnp_output.command_name
641
89
  return command_output
642
90
 
643
91
  if nlu_pipeline_stage in {
@@ -664,6 +112,7 @@ class ResponseGenerator:
664
112
  workflow_context = workflow.context
665
113
  workflow_context["NLU_Pipeline_Stage"] = \
666
114
  NLUPipelineStage.INTENT_MISUNDERSTANDING_CLARIFICATION
115
+ workflow_context["command"] = command
667
116
  workflow.context = workflow_context
668
117
 
669
118
  startup_action = Action(
@@ -686,11 +135,19 @@ class ResponseGenerator:
686
135
  # move to the parameter extraction stage
687
136
  workflow_context = workflow.context
688
137
  workflow_context["NLU_Pipeline_Stage"] = NLUPipelineStage.PARAMETER_EXTRACTION
138
+ workflow.context = workflow_context
139
+
140
+ if nlu_pipeline_stage == NLUPipelineStage.PARAMETER_EXTRACTION:
141
+ cnp_output.command_name = workflow.context["command_name"]
142
+ else:
143
+ workflow_context = workflow.context
689
144
  workflow_context["command_name"] = cnp_output.command_name
690
145
  workflow.context = workflow_context
691
146
 
692
- command_name = workflow.context["command_name"]
693
- extractor = ParameterExtraction(workflow, app_workflow, command_name, command)
147
+ command_name = cnp_output.command_name
148
+ # Use the preserved original command (with parameters) if available
149
+ preserved_command = f'{command_name}: {workflow.context.get("command", command)}'
150
+ extractor = ParameterExtraction(workflow, app_workflow, command_name, preserved_command)
694
151
  pe_output = extractor.extract()
695
152
  if not pe_output.parameters_are_valid:
696
153
  return CommandOutput(
@@ -713,7 +170,7 @@ class ResponseGenerator:
713
170
  CommandResponse(
714
171
  response="",
715
172
  artifacts={
716
- "command": command,
173
+ "command": preserved_command,
717
174
  "command_name": command_name,
718
175
  "cmd_parameters": pe_output.cmd_parameters,
719
176
  },