fastworkflow 2.15.9__py3-none-any.whl → 2.15.11__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.

Potentially problematic release.


This version of fastworkflow might be problematic. Click here for more details.

@@ -130,6 +130,13 @@ class ResponseGenerator:
130
130
  workflow_context["NLU_Pipeline_Stage"] = NLUPipelineStage.PARAMETER_EXTRACTION
131
131
  workflow.context = workflow_context
132
132
 
133
+ if nlu_pipeline_stage == NLUPipelineStage.PARAMETER_EXTRACTION:
134
+ cnp_output.command_name = workflow.context["command_name"]
135
+ else:
136
+ workflow_context = workflow.context
137
+ workflow_context["command_name"] = cnp_output.command_name
138
+ workflow.context = workflow_context
139
+
133
140
  command_name = cnp_output.command_name
134
141
  extractor = ParameterExtraction(workflow, app_workflow, command_name, command)
135
142
  pe_output = extractor.extract()
@@ -96,8 +96,8 @@ class CommandNamePrediction:
96
96
  ].plain_utterances
97
97
  }
98
98
 
99
- # See if the command starts with a command name followed by a space
100
- tentative_command_name = command.split(" ", 1)[0]
99
+ # See if the command starts with a command name followed by a space or a '('
100
+ tentative_command_name = command.split(" ", 1)[0].split("(", 1)[0]
101
101
  normalized_command_name = tentative_command_name.lower()
102
102
  command_name = None
103
103
  if normalized_command_name in command_name_dict:
@@ -65,12 +65,24 @@ class ParameterExtraction:
65
65
  self.command_name,
66
66
  app_workflow_folderpath)
67
67
 
68
- self._store_parameters(self.cme_workflow, new_params)
69
-
70
- is_valid, error_msg, suggestions = input_for_param_extraction.validate_parameters(
68
+ is_valid, error_msg, suggestions, missing_invalid_fields = \
69
+ input_for_param_extraction.validate_parameters(
71
70
  self.app_workflow, self.command_name, new_params
72
71
  )
73
72
 
73
+ # Set all the missing and invalid fields to None before storing
74
+ current_values = {
75
+ field_name: getattr(new_params, field_name, None)
76
+ for field_name in list(type(new_params).model_fields.keys())
77
+ }
78
+ for field_name in missing_invalid_fields:
79
+ if field_name in current_values:
80
+ current_values[field_name] = NOT_FOUND
81
+ # Reconstruct the model instance without validation
82
+ new_params = new_params.__class__.model_construct(**current_values)
83
+
84
+ self._store_parameters(self.cme_workflow, new_params)
85
+
74
86
  if not is_valid:
75
87
  if params_str := self._format_parameters_for_display(new_params):
76
88
  error_msg = f"Extracted parameters so far:\n{params_str}\n\n{error_msg}"
@@ -345,9 +345,16 @@ class ChatSession:
345
345
  ) and self._status != SessionStatus.STOPPING:
346
346
  try:
347
347
  message = self.user_message_queue.get()
348
+
349
+ if ((
350
+ "NLU_Pipeline_Stage" not in self._cme_workflow.context or
351
+ self._cme_workflow.context["NLU_Pipeline_Stage"] == fastworkflow.NLUPipelineStage.INTENT_DETECTION) and
352
+ message.startswith('/')
353
+ ):
354
+ self._cme_workflow.context["is_assistant_mode_command"] = True
348
355
 
349
356
  # Route based on mode and message type
350
- if self._run_as_agent and not message.startswith('/'):
357
+ if self._run_as_agent and "is_assistant_mode_command" not in self._cme_workflow.context:
351
358
  # In agent mode, use workflow tool agent for processing
352
359
  last_output = self._process_agent_message(message)
353
360
  # elif self._is_mcp_tool_call(message):
@@ -45,7 +45,12 @@ class CommandExecutor(CommandExecutorInterface):
45
45
  command = command)
46
46
  )
47
47
 
48
- if command_output.command_handled or not command_output.success:
48
+ if command_output.command_handled:
49
+ # important to clear the current command mode from the workflow context
50
+ if "is_assistant_mode_command" in chat_session.cme_workflow._context:
51
+ del chat_session.cme_workflow._context["is_assistant_mode_command"]
52
+ return command_output
53
+ elif not command_output.success:
49
54
  return command_output
50
55
 
51
56
  command_name = command_output.command_responses[0].artifacts["command_name"]
@@ -54,7 +59,7 @@ class CommandExecutor(CommandExecutorInterface):
54
59
  workflow = ChatSession.get_active_workflow()
55
60
  workflow_name = workflow.folderpath.split('/')[-1]
56
61
  context = workflow.current_command_context_displayname
57
-
62
+
58
63
  command_routing_definition = fastworkflow.RoutingRegistry.get_definition(
59
64
  workflow.folderpath
60
65
  )
@@ -77,13 +82,17 @@ class CommandExecutor(CommandExecutorInterface):
77
82
  command_output = response_generation_object(workflow, command, input_obj)
78
83
  else:
79
84
  command_output = response_generation_object(workflow, command)
80
-
85
+
81
86
  # Set the additional attributes
82
87
  command_output.workflow_name = workflow_name
83
88
  command_output.context = context
84
89
  command_output.command_name = command_name
85
90
  command_output.command_parameters = input_obj or None
86
91
 
92
+ # important to clear the current command mode from the workflow context
93
+ if "is_assistant_mode_command" in chat_session.cme_workflow._context:
94
+ del chat_session.cme_workflow._context["is_assistant_mode_command"]
95
+
87
96
  return command_output
88
97
 
89
98
  @classmethod
@@ -134,7 +143,7 @@ class CommandExecutor(CommandExecutorInterface):
134
143
  input_obj = command_parameters_class(**action.parameters)
135
144
 
136
145
  input_for_param_extraction = InputForParamExtraction(command=action.command)
137
- is_valid, error_msg, _ = input_for_param_extraction.validate_parameters(
146
+ is_valid, error_msg, _, _ = input_for_param_extraction.validate_parameters(
138
147
  workflow, action.command_name, input_obj
139
148
  )
140
149
  if not is_valid:
@@ -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
  )
@@ -1,4 +1,5 @@
1
1
  import sys
2
+ import ast
2
3
  import dspy
3
4
  import os
4
5
  from contextlib import suppress
@@ -156,7 +157,7 @@ Today's date is {today}.
156
157
  def create_signature_from_pydantic_model(
157
158
  pydantic_model: Type[BaseModel]
158
159
  ) -> Type[dspy.Signature]:
159
- """
160
+ """
160
161
  Create a DSPy Signature class from a Pydantic model with type annotations.
161
162
 
162
163
  Args:
@@ -165,79 +166,76 @@ Today's date is {today}.
165
166
  Returns:
166
167
  A DSPy Signature class
167
168
  """
168
- signature_components = {
169
- "command": (str, dspy.InputField(desc="User's request"))
170
- }
169
+ signature_components = {
170
+ "statement": (str, dspy.InputField(desc="Statement according to Dhar"))
171
+ }
171
172
 
172
- steps = ["query: The original user query (Always include this)."]
173
- field_num = 1
173
+ steps = []
174
+ field_num = 1
174
175
 
175
- for attribute_name, attribute_metadata in pydantic_model.model_fields.items():
176
- is_optional = False
177
- attribute_type = attribute_metadata.annotation
176
+ for attribute_name, attribute_metadata in pydantic_model.model_fields.items():
177
+ is_optional = False
178
+ attribute_type = attribute_metadata.annotation
178
179
 
179
- if hasattr(attribute_type, "__origin__") and attribute_type.__origin__ is Union:
180
- union_elements = get_args(attribute_type)
181
- if type(None) in union_elements:
182
- is_optional = True
183
- attribute_type = next((elem for elem in union_elements if elem is not type(None)), str)
184
-
185
- NOT_FOUND = fastworkflow.get_env_var("NOT_FOUND")
186
- if attribute_type is str:
187
- default_value = NOT_FOUND
188
- elif attribute_type is int:
189
- default_value = INVALID_INT_VALUE
190
- elif attribute_type is float:
191
- default_value = -sys.float_info.max
192
- else:
193
- default_value = None
180
+ if hasattr(attribute_type, "__origin__") and attribute_type.__origin__ is Union:
181
+ union_elements = get_args(attribute_type)
182
+ if type(None) in union_elements:
183
+ is_optional = True
184
+ attribute_type = next((elem for elem in union_elements if elem is not type(None)), str)
194
185
 
195
- if (
196
- attribute_metadata.default is not PydanticUndefined and
197
- attribute_metadata.default is not None and
198
- attribute_metadata.default != Ellipsis
199
- ):
200
- default_value = attribute_metadata.default
186
+ NOT_FOUND = fastworkflow.get_env_var("NOT_FOUND")
187
+ if attribute_type is str:
188
+ default_value = NOT_FOUND
189
+ elif attribute_type is int:
190
+ default_value = INVALID_INT_VALUE
191
+ elif attribute_type is float:
192
+ default_value = -sys.float_info.max
193
+ else:
194
+ default_value = None
195
+
196
+ if (
197
+ attribute_metadata.default is not PydanticUndefined and
198
+ attribute_metadata.default is not None and
199
+ attribute_metadata.default != Ellipsis
200
+ ):
201
+ default_value = attribute_metadata.default
201
202
 
202
- info_text = attribute_metadata.description or f"The {attribute_name}"
203
+ info_text = attribute_metadata.description or f"The {attribute_name}"
203
204
 
204
- if attribute_name != "query":
205
- steps.append(f"Step {field_num}: Identify the {attribute_name} ({info_text}).")
206
- field_num += 1
205
+ if attribute_name != "query":
206
+ steps.append(f"Step {field_num}: Identify the {attribute_name} ({info_text}).")
207
+ field_num += 1
207
208
 
208
- if isinstance(attribute_type, type) and issubclass(attribute_type, Enum):
209
- possible_values = [f"'{option.value}'" for option in attribute_type]
210
- info_text += f". Valid values: {', '.join(possible_values)}"
209
+ if isinstance(attribute_type, type) and issubclass(attribute_type, Enum):
210
+ possible_values = [f"'{option.value}'" for option in attribute_type]
211
+ info_text += f". Valid values: {', '.join(possible_values)}"
211
212
 
212
- if attribute_metadata.examples:
213
- sample_values = ", ".join([f"'{sample}'" for sample in attribute_metadata.examples])
214
- info_text += f". Examples: {sample_values}"
213
+ if attribute_metadata.examples:
214
+ sample_values = ", ".join([f"'{sample}'" for sample in attribute_metadata.examples])
215
+ info_text += f". Examples: {sample_values}"
215
216
 
216
- requirement_status = "Optional" if is_optional else "Required"
217
- info_text += f". This field is {requirement_status}."
217
+ requirement_status = "Optional" if is_optional else "Required"
218
+ info_text += f". This field is {requirement_status}."
218
219
 
219
- if is_optional:
220
- info_text += f" If not mentioned in the query, use: '{default_value or 'None'}'."
221
- elif default_value is not None:
222
- info_text += f" Default value: '{default_value}'."
220
+ if is_optional:
221
+ info_text += f" If not mentioned in the query, use: '{default_value or 'None'}'."
222
+ elif default_value is not None:
223
+ info_text += f" Default value: '{default_value}'."
223
224
 
224
- field_definition = dspy.OutputField(desc=info_text, default=default_value)
225
- signature_components[attribute_name] = (attribute_metadata.annotation, field_definition)
225
+ field_definition = dspy.OutputField(desc=info_text, default=default_value)
226
+ signature_components[attribute_name] = (attribute_metadata.annotation, field_definition)
226
227
 
227
228
  steps.extend((
228
- f"Step {field_num}: Check for any missing details.",
229
- "Return the default value for the parameters for which default value is specified.",
230
- "For parameters specified as enums, return the default value if the parameter value is not explicitly specified in the query",
231
- "Return None for the parameter value which is missing in the query",
232
- "Always return the query in the output.",
229
+ f"Step {field_num}: ",
230
+ "For missing parameter values - return the default if it is specified otherwise return None",
233
231
  ))
234
- generated_docstring = f"""Extract structured parameters from a user query using step-by-step reasoning. Today's date is {date.today()}.
235
-
236
- {chr(10).join(steps)}
237
- """
238
- instructions = generated_docstring
232
+
233
+ generated_docstring = (
234
+ f"Extract parameter values from the statement according to Dhar. Today's date is {date.today()}.\n"
235
+ f"{chr(10).join(steps)}"
236
+ )
239
237
 
240
- return dspy.Signature(signature_components, instructions)
238
+ return dspy.Signature(signature_components, generated_docstring)
241
239
 
242
240
  def extract_parameters(self, CommandParameters: Type[BaseModel] = None, subject_command_name: str = None, workflow_folderpath: str = None) -> BaseModel:
243
241
  """
@@ -269,7 +267,7 @@ Today's date is {today}.
269
267
  self.predictor = dspy.ChainOfThought(signature)
270
268
 
271
269
  def forward(self, command=None):
272
- return self.predictor(command=command)
270
+ return self.predictor(statement=command)
273
271
 
274
272
  param_extractor = ParamExtractor(params_signature)
275
273
 
@@ -318,7 +316,7 @@ Today's date is {today}.
318
316
  def validate_parameters(self,
319
317
  app_workflow: fastworkflow.Workflow,
320
318
  subject_command_name: str,
321
- cmd_parameters: BaseModel) -> Tuple[bool, str, Dict[str, List[str]]]:
319
+ cmd_parameters: BaseModel) -> Tuple[bool, str, Dict[str, List[str]], List[str]]:
322
320
  """
323
321
  Check if the parameters are valid in the current context, including database lookups.
324
322
  """
@@ -335,54 +333,224 @@ Today's date is {today}.
335
333
  invalid_fields = []
336
334
  all_suggestions = {}
337
335
 
338
- # Check required fields
339
336
  for field_name, field_info in type(cmd_parameters).model_fields.items():
340
- field_value = getattr(cmd_parameters, field_name, None)
337
+ field_value = getattr(cmd_parameters, field_name, None)
338
+
339
+ if field_value not in [NOT_FOUND, None, INVALID_INT_VALUE, INVALID_FLOAT_VALUE]:
340
+ annotation = field_info.annotation
341
+
342
+ # Build list of candidate concrete types (exclude NoneType from Union)
343
+ candidate_types: List[Type[Any]] = []
344
+ if hasattr(annotation, "__origin__") and annotation.__origin__ is Union:
345
+ for t in get_args(annotation):
346
+ if t is not type(None): # noqa: E721
347
+ candidate_types.append(t) # type: ignore[arg-type]
348
+ else:
349
+ candidate_types = [annotation] # type: ignore[list-item]
350
+
351
+ def build_type_suggestion() -> List[str]:
352
+ examples = getattr(field_info, "examples", []) or []
353
+ example = examples[0] if examples else None
354
+ # Enum suggestions list valid values
355
+ enum_types = [t for t in candidate_types if isinstance(t, type) and issubclass(t, Enum)]
356
+ if enum_types:
357
+ opts = [f"'{opt.value}'" for t in enum_types for opt in t]
358
+ return [f"Please provide a value matching the expected type/format. Valid values: {', '.join(opts)}"]
359
+ # List suggestions
360
+ def _is_list_type(tt):
361
+ try:
362
+ return hasattr(tt, "__origin__") and tt.__origin__ in (list, List)
363
+ except Exception:
364
+ return False
365
+ list_types = [t for t in candidate_types if _is_list_type(t)]
366
+ if list_types:
367
+ inner_args = get_args(list_types[0])
368
+ inner = inner_args[0] if inner_args else str
369
+ inner_name = inner.__name__ if isinstance(inner, type) else str(inner)
370
+ hint = (
371
+ f"Please provide a list of {inner_name} values. Accepted formats: "
372
+ f"JSON list (e.g., [\"a\", \"b\"]), Python list (e.g., ['a', 'b']), "
373
+ f"or comma-separated (e.g., a,b)."
374
+ )
375
+ return [hint]
376
+ # Fallback: show expected type names (handles unions)
377
+ name_list: List[str] = []
378
+ for t in candidate_types:
379
+ if isinstance(t, type):
380
+ name_list.append(t.__name__)
381
+ else:
382
+ name_list.append(str(t))
383
+ base = f"Please provide a value matching the expected type/format: {' or '.join(name_list)}"
384
+ if example is not None:
385
+ base = f"{base} (e.g., {example})"
386
+ return [base]
387
+
388
+ valid_by_type = False
389
+ corrected_value: Optional[Any] = None
390
+ def _is_list_type(tt):
391
+ try:
392
+ return hasattr(tt, "__origin__") and tt.__origin__ in (list, List)
393
+ except Exception:
394
+ return False
395
+
396
+ def _parse_list_like_string(s: str) -> Optional[list]:
397
+ if not isinstance(s, str):
398
+ return None
399
+ text = s.strip()
400
+ if text.startswith("[") and text.endswith("]"):
401
+ with suppress(Exception):
402
+ parsed = json.loads(text)
403
+ if isinstance(parsed, list):
404
+ return parsed
405
+ # Try Python literal list
406
+ with suppress(Exception):
407
+ parsed = ast.literal_eval(text)
408
+ if isinstance(parsed, list):
409
+ return parsed
410
+ # Fallback: comma-separated
411
+ if "," in text:
412
+ parts = [p.strip() for p in text.split(",")]
413
+ cleaned = [
414
+ (p[1:-1] if len(p) >= 2 and ((p[0] == p[-1] == '"') or (p[0] == p[-1] == "'")) else p)
415
+ for p in parts
416
+ ]
417
+ return cleaned
418
+ return None
419
+
420
+ def _coerce_scalar(expected_type: Type[Any], val: Any) -> Tuple[bool, Optional[Any]]:
421
+ # str
422
+ if expected_type is str:
423
+ return True, str(val)
424
+ # bool
425
+ if expected_type is bool:
426
+ if isinstance(val, bool):
427
+ return True, val
428
+ elif isinstance(val, str):
429
+ lower_val = val.lower().strip()
430
+ if lower_val in ('true', 'false'):
431
+ return True, lower_val == 'true'
432
+ # Also handle string representations of integers
433
+ elif lower_val in ('0', '1'):
434
+ return True, lower_val == '1'
435
+ elif isinstance(val, int):
436
+ return True, bool(val)
437
+ return False, None
438
+ # int
439
+ if expected_type is int:
440
+ if isinstance(val, bool):
441
+ return False, None
442
+ if isinstance(val, int):
443
+ return True, val
444
+ if isinstance(val, str):
445
+ with suppress(Exception):
446
+ return True, int(val.strip())
447
+ return False, None
448
+ # float
449
+ if expected_type is float:
450
+ if isinstance(val, (int, float)) and not isinstance(val, bool):
451
+ return True, float(val)
452
+ if isinstance(val, str):
453
+ with suppress(Exception):
454
+ return True, float(val.strip())
455
+ return False, None
456
+ # Enum
457
+ if isinstance(expected_type, type) and issubclass(expected_type, Enum):
458
+ ok, enum_val = _try_coerce_enum(expected_type, val)
459
+ return (ok, enum_val if ok else None)
460
+ # Unknown: accept if already instance
461
+ return (True, val) if isinstance(val, expected_type) else (False, None)
462
+
463
+ def _try_coerce_list(list_type: Any, value: Any) -> Tuple[bool, Optional[list]]:
464
+ inner_args = get_args(list_type)
465
+ inner_type = inner_args[0] if inner_args else str
466
+ raw_list: Optional[list] = None
467
+ if isinstance(value, list):
468
+ raw_list = value
469
+ elif isinstance(value, str):
470
+ raw_list = _parse_list_like_string(value)
471
+ if raw_list is None:
472
+ return False, None
473
+ coerced_list = []
474
+ for item in raw_list:
475
+ ok, coerced = _coerce_scalar(inner_type, item)
476
+ if not ok:
477
+ return False, None
478
+ coerced_list.append(coerced)
479
+ return True, coerced_list
480
+
481
+ def _try_coerce_enum(enum_cls: Type[Enum], val: Any) -> Tuple[bool, Optional[Enum]]:
482
+ if isinstance(val, enum_cls):
483
+ return True, val
484
+ if isinstance(val, str):
485
+ for member in enum_cls:
486
+ if val == member.value or val.lower() == str(member.name).lower():
487
+ return True, member
488
+ return False, None
489
+
490
+ for t in candidate_types:
491
+ # list[...] and typing.List[...] support
492
+ if _is_list_type(t):
493
+ ok, coerced_list = _try_coerce_list(t, field_value)
494
+ if ok:
495
+ corrected_value = coerced_list
496
+ valid_by_type = True
497
+ break
498
+ # str, bool, int, float - use _coerce_scalar for consistency
499
+ if t in (str, bool, int, float):
500
+ ok, coerced = _coerce_scalar(t, field_value)
501
+ if ok:
502
+ corrected_value = coerced
503
+ valid_by_type = True
504
+ break # Enum
505
+ if isinstance(t, type) and issubclass(t, Enum):
506
+ ok, enum_val = _try_coerce_enum(t, field_value)
507
+ if ok:
508
+ corrected_value = enum_val
509
+ valid_by_type = True
510
+ break
511
+
512
+ if valid_by_type:
513
+ if corrected_value is not None:
514
+ setattr(cmd_parameters, field_name, corrected_value)
515
+ else:
516
+ invalid_fields.append(f"{field_name} '{field_value}'")
517
+ all_suggestions[field_name] = build_type_suggestion()
518
+ is_valid = False
341
519
 
342
- is_optional = False
343
- attribute_type = field_info.annotation
344
- if hasattr(attribute_type, "__origin__") and attribute_type.__origin__ is Union:
345
- union_elements = get_args(attribute_type)
346
- if type(None) in union_elements:
347
- is_optional = True
520
+ is_optional = False
521
+ attribute_type = field_info.annotation
522
+ if hasattr(attribute_type, "__origin__") and attribute_type.__origin__ is Union:
523
+ union_elements = get_args(attribute_type)
524
+ if type(None) in union_elements:
525
+ is_optional = True
348
526
 
349
- is_required=True
350
- if is_optional:
351
- is_required=False
527
+ is_required=True
528
+ if is_optional:
529
+ is_required=False
530
+
531
+ # Only add to missing fields if it's required AND has no value
532
+ if is_required and \
533
+ field_value in [
534
+ NOT_FOUND,
535
+ None,
536
+ INVALID_INT_VALUE,
537
+ INVALID_FLOAT_VALUE
538
+ ]:
539
+ missing_fields.append(field_name)
540
+ is_valid = False
352
541
 
353
- # Only add to missing fields if it's required AND has no value
354
- if is_required and \
355
- field_value in [
356
- NOT_FOUND,
542
+ pattern = next(
543
+ (meta.pattern
544
+ for meta in getattr(field_info, "metadata", [])
545
+ if hasattr(meta, "pattern")),
357
546
  None,
358
- INVALID_INT_VALUE,
359
- INVALID_FLOAT_VALUE
360
- ]:
361
- missing_fields.append(field_name)
362
- is_valid = False
363
-
364
- pattern = next(
365
- (meta.pattern
366
- for meta in getattr(field_info, "metadata", [])
367
- if hasattr(meta, "pattern")),
368
- None,
369
- )
370
- if pattern and field_value is not None and field_value != NOT_FOUND:
371
- invalid_value = None
372
- if hasattr(field_info, "json_schema_extra") and field_info.json_schema_extra:
373
- invalid_value = field_info.json_schema_extra.get("invalid_value")
374
-
375
- if invalid_value and field_value == invalid_value:
376
- invalid_fields.append(f"{field_name} '{field_value}'")
377
- pattern_str = str(pattern)
378
- examples = getattr(field_info, "examples", [])
379
- example = examples[0] if examples else ""
380
- all_suggestions[field_name] = [f"Please use the format matching pattern {pattern_str} (e.g., {example})"]
381
- is_valid = False
547
+ )
548
+ if pattern and field_value is not None and field_value != NOT_FOUND:
549
+ invalid_value = None
550
+ if hasattr(field_info, "json_schema_extra") and field_info.json_schema_extra:
551
+ invalid_value = field_info.json_schema_extra.get("invalid_value")
382
552
 
383
- else:
384
- pattern_regex = re.compile(pattern)
385
- if not pattern_regex.fullmatch(str(field_value)):
553
+ if invalid_value and field_value == invalid_value:
386
554
  invalid_fields.append(f"{field_name} '{field_value}'")
387
555
  pattern_str = str(pattern)
388
556
  examples = getattr(field_info, "examples", [])
@@ -390,6 +558,15 @@ Today's date is {today}.
390
558
  all_suggestions[field_name] = [f"Please use the format matching pattern {pattern_str} (e.g., {example})"]
391
559
  is_valid = False
392
560
 
561
+ else:
562
+ pattern_regex = re.compile(pattern)
563
+ if not pattern_regex.fullmatch(str(field_value)):
564
+ invalid_fields.append(f"{field_name} '{field_value}'")
565
+ pattern_str = str(pattern)
566
+ examples = getattr(field_info, "examples", [])
567
+ example = examples[0] if examples else ""
568
+ all_suggestions[field_name] = [f"Please use the format matching pattern {pattern_str} (e.g., {example})"]
569
+ is_valid = False
393
570
 
394
571
  for field_name, field_info in type(cmd_parameters).model_fields.items():
395
572
  field_value = getattr(cmd_parameters, field_name, None)
@@ -420,20 +597,20 @@ Today's date is {today}.
420
597
  if is_valid:
421
598
  if not (
422
599
  self.input_for_param_extraction_class and \
423
- hasattr(self.input_for_param_extraction_class, 'validate_extracted_parameters')
600
+ hasattr(self.input_for_param_extraction_class, 'validate_extracted_parameters')
424
601
  ):
425
- return (True, "All required parameters are valid.", {})
426
-
602
+ return (True, "All required parameters are valid.", {}, [])
603
+
427
604
  try:
428
605
  is_valid, message = self.input_for_param_extraction_class.validate_extracted_parameters(app_workflow, subject_command_name, cmd_parameters)
429
606
  except Exception as e:
430
607
  message = f"Exception in {subject_command_name}'s validate_extracted_parameters function: {str(e)}"
431
608
  logger.critical(message)
432
- return (False, message, {})
433
-
609
+ return (False, message, {}, [])
610
+
434
611
  if is_valid:
435
- return (True, "All required parameters are valid.", {})
436
- return (False, message, {})
612
+ return (True, "All required parameters are valid.", {}, [])
613
+ return (False, message, {}, [])
437
614
 
438
615
  message = ''
439
616
  if missing_fields:
@@ -474,4 +651,4 @@ Today's date is {today}.
474
651
  if "run_as_agent" not in app_workflow.context:
475
652
  message += "\nFor parameter values that include a comma, provide separately from other values, and one at a time."
476
653
 
477
- return (False, message, all_suggestions)
654
+ return (False, message, all_suggestions, combined_fields)
fastworkflow/workflow.py CHANGED
@@ -288,21 +288,17 @@ class Workflow:
288
288
 
289
289
  def end_command_processing(self) -> None:
290
290
  """Process the end of a command"""
291
- mark_dirty = False
292
291
  # important to clear the current command from the workflow context
293
292
  if "command" in self._context:
294
293
  del self._context["command"]
295
- mark_dirty = True
296
294
 
297
295
  # important to clear parameter extraction error state (if any)
298
296
  if "stored_parameters" in self._context:
299
297
  del self._context["stored_parameters"]
300
- mark_dirty = True
301
298
 
302
299
  self._context["NLU_Pipeline_Stage"] = fastworkflow.NLUPipelineStage.INTENT_DETECTION
303
300
 
304
- if mark_dirty:
305
- self._mark_dirty()
301
+ self._mark_dirty()
306
302
 
307
303
  def close(self) -> bool:
308
304
  """close the session"""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastworkflow
3
- Version: 2.15.9
3
+ Version: 2.15.11
4
4
  Summary: A framework for rapidly building large-scale, deterministic, interactive workflows with a fault-tolerant, conversational UX
5
5
  License: Apache-2.0
6
6
  Keywords: fastworkflow,ai,workflow,llm,openai
@@ -10,10 +10,10 @@ fastworkflow/_workflows/command_metadata_extraction/_commands/IntentDetection/re
10
10
  fastworkflow/_workflows/command_metadata_extraction/_commands/IntentDetection/what_can_i_do.py,sha256=Fw8tsk3wyCujf8nBfUgPDxnTP9c2IE513FzqAWGm8pU,6216
11
11
  fastworkflow/_workflows/command_metadata_extraction/_commands/IntentDetection/what_is_current_context.py,sha256=S5RQLr62Q2MnKU85nw4IW_ueAK_FXvhcY9gXajFxujg,1464
12
12
  fastworkflow/_workflows/command_metadata_extraction/_commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- fastworkflow/_workflows/command_metadata_extraction/_commands/wildcard.py,sha256=fUOiCMGfzoK3UBan2vMp6yj71OsQKkdyLyknE5jHSg8,6962
13
+ fastworkflow/_workflows/command_metadata_extraction/_commands/wildcard.py,sha256=TphAB_rwR7giCx00hovGZ2p9Qh-q1VAOZQNJF0DjRJY,7287
14
14
  fastworkflow/_workflows/command_metadata_extraction/command_context_model.json,sha256=zGWBweQSmFf7WsfR_F2DE7AJ8S8-q7F9ZbvyccysJJI,117
15
- fastworkflow/_workflows/command_metadata_extraction/intent_detection.py,sha256=6A1q1OsivlW98nbMMfE1b9vjvFuLGRuKpv1Qvm0zHBk,14516
16
- fastworkflow/_workflows/command_metadata_extraction/parameter_extraction.py,sha256=6Sgt3foMFZg2RBPDbnPrxAdlZfnDyW8vPkGDr3PDadw,13117
15
+ fastworkflow/_workflows/command_metadata_extraction/intent_detection.py,sha256=A0vzyHGMtBEWCzfTU_I9tnRx2Byu-ElJgfMugRlXkpA,14542
16
+ fastworkflow/_workflows/command_metadata_extraction/parameter_extraction.py,sha256=MgNkPgA05E1-LSw9pNKDlXdsAphulYNhuDeTTqk5dBY,13686
17
17
  fastworkflow/build/__main__.py,sha256=NtedkZfM56qoEJ5vQECSURbE8AMTfwHN3tAZyZoWabk,15905
18
18
  fastworkflow/build/ast_class_extractor.py,sha256=F9OG4stkp7w3kadKqxMm8h3ZDSp_zg6mwcrKMl_XqdI,13527
19
19
  fastworkflow/build/class_analysis_structures.py,sha256=UWOKcs9pCiNuXc64hNywkTJq5X5KfG1pqdSZwWiZh-c,4053
@@ -35,11 +35,11 @@ fastworkflow/build/navigator_stub_generator.py,sha256=_DSvHC6r1xWQiFHtUgPhI51nQf
35
35
  fastworkflow/build/pydantic_model_generator.py,sha256=oNyoANyUWBpHG-fE3tGL911RNvDzQXjxAm0ssvuXUH4,1854
36
36
  fastworkflow/build/utterance_generator.py,sha256=UrtkF0wyAZ1hiFitHX0g8w7Wh-D0leLCrP1aUACSfHo,299
37
37
  fastworkflow/cache_matching.py,sha256=OoB--1tO6-O4BKCuCrUbB0CkUr76J62K4VAf6MShi-w,7984
38
- fastworkflow/chat_session.py,sha256=MVHSoygLIW4Gh3BRfDYSxmh5RMuBxc96c8E9QRW6ZyU,27563
38
+ fastworkflow/chat_session.py,sha256=NHOMA_MdGh-lKm6gafEDiaRBrnrE7FbbR1g6oxredD4,28000
39
39
  fastworkflow/cli.py,sha256=li9OFT05sxqz4BZJc9byKAeTmomjLfsWMVuy0OiRGSs,18953
40
40
  fastworkflow/command_context_model.py,sha256=nWxLP3TR7WJr3yWCedqcdFOxo_kwae_mS3VRN2cOmK8,13437
41
41
  fastworkflow/command_directory.py,sha256=aJ6UQCwevfF11KbcQB2Qz6mQ7Kj91pZtvHmQY6JFnao,29030
42
- fastworkflow/command_executor.py,sha256=IEcuHEqrqc6I-Hpv451VIHw6z64zkO4o51MpXUFnPGo,7936
42
+ fastworkflow/command_executor.py,sha256=UGM6JpOoZOYR3cbLOOLN3oziwNvUH-Cm-d1XFRzbW7k,8456
43
43
  fastworkflow/command_interfaces.py,sha256=PWIKlcp0G8nmYl0vkrg1o6QzJL0pxXkfrn1joqTa0eU,460
44
44
  fastworkflow/command_metadata_api.py,sha256=KtidE3PM9HYfY-nmEXZ8Y4nnaw2qn23p_gvwFVT3F8Y,39770
45
45
  fastworkflow/command_routing.py,sha256=R7194pcY0d2VHzmCu9ALacm1UvNuIRIvTn8mLp-EZIM,17219
@@ -87,7 +87,7 @@ fastworkflow/examples/messaging_app_4/startup_action.json,sha256=HhS0ApuK1wZmX2M
87
87
  fastworkflow/examples/retail_workflow/_commands/calculate.py,sha256=uj-Yg0RSiSPkK7Y0AZN1fgDdL0GWIw33g9ARAPGFFVU,2285
88
88
  fastworkflow/examples/retail_workflow/_commands/cancel_pending_order.py,sha256=npU7sERB915WJyqjuku6L63sxvXtrWGkjoTrU0nNhEU,4466
89
89
  fastworkflow/examples/retail_workflow/_commands/exchange_delivered_order_items.py,sha256=6g04D3eHUQdIBu3wqPbhHMjJUK_91uHxaZrlDggnFS0,5349
90
- fastworkflow/examples/retail_workflow/_commands/find_user_id_by_email.py,sha256=EhI1bNcmWETGllyb5Z91v0mRm8Ex4eQ44IQHZ244Syc,2793
90
+ fastworkflow/examples/retail_workflow/_commands/find_user_id_by_email.py,sha256=EUGj_YJCIceebQghrlC3uDdJsxsAg2hxS3jFcaN3mX8,2761
91
91
  fastworkflow/examples/retail_workflow/_commands/find_user_id_by_name_zip.py,sha256=lA9UfEkBNjCXozM4fALSZZuS0DgO3W1qQ2cg0dv1XUA,3340
92
92
  fastworkflow/examples/retail_workflow/_commands/get_order_details.py,sha256=X5tcMfx0lBm5vot7Ssn8Uzg0UaTPJU7GTn0C8fGnGxM,3584
93
93
  fastworkflow/examples/retail_workflow/_commands/get_product_details.py,sha256=Qfzaz3hHNia9GJvR9Lhdv8qJWdy-GZ6VbzTCYyD4Hh8,2842
@@ -166,13 +166,13 @@ fastworkflow/utils/parameterize_func_decorator.py,sha256=V6YJnishWRCdwiBQW6P17hm
166
166
  fastworkflow/utils/pydantic_model_2_dspy_signature_class.py,sha256=w1pvl8rJq48ulFwaAtBgfXYn_SBIDBgq1aLMUg1zJn8,12875
167
167
  fastworkflow/utils/python_utils.py,sha256=OzSf-bGve1401SHM3QXXFauBOBrlGQzPNgvvGJPavX0,8200
168
168
  fastworkflow/utils/react.py,sha256=HubwmM4H9UzLaLaeIkJseKCNMjyrOXvMZz-8sw4ycCE,11224
169
- fastworkflow/utils/signatures.py,sha256=beDgLw54AN_IZra3d5ZhpDO7u1i5FJzVQeLhs6G_DRA,20951
169
+ fastworkflow/utils/signatures.py,sha256=QOLX3j-AJkRWIkDhogbhxQo8MIt668xIKwd4SWiS2LY,31734
170
170
  fastworkflow/utils/startup_progress.py,sha256=9icSdnpFAxzIq0sUliGpNaH0Efvrt5lDtGfURV5BD98,3539
171
- fastworkflow/workflow.py,sha256=F7kGoNQbAMwy71zT1V_KF8PbRTCY4Dz-16Zv4ApK8m8,18939
171
+ fastworkflow/workflow.py,sha256=37gn7e3ct-gdGw43zS6Ab_ADoJJBO4eJW2PywfUpjEg,18825
172
172
  fastworkflow/workflow_agent.py,sha256=iUcaE1fKLTiRNyDaochPpRHdijCiQBYaCWR8DdslO9Y,16028
173
173
  fastworkflow/workflow_inheritance_model.py,sha256=Pp-qSrQISgPfPjJVUfW84pc7HLmL2evuq0UVIYR51K0,7974
174
- fastworkflow-2.15.9.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
175
- fastworkflow-2.15.9.dist-info/METADATA,sha256=o0zFSQTjmQVkWTTRwjRTKE94k_-6MDLE5kSUHW5GwQY,30065
176
- fastworkflow-2.15.9.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
177
- fastworkflow-2.15.9.dist-info/entry_points.txt,sha256=m8HqoPzCyaZLAx-V5X8MJgw3Lx3GiPDlxNEZ7K-Gb-U,54
178
- fastworkflow-2.15.9.dist-info/RECORD,,
174
+ fastworkflow-2.15.11.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
175
+ fastworkflow-2.15.11.dist-info/METADATA,sha256=Wye3szQ8uZSJktp5hs4odNvQTP7k88YPiAzjXfWc_80,30066
176
+ fastworkflow-2.15.11.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
177
+ fastworkflow-2.15.11.dist-info/entry_points.txt,sha256=m8HqoPzCyaZLAx-V5X8MJgw3Lx3GiPDlxNEZ7K-Gb-U,54
178
+ fastworkflow-2.15.11.dist-info/RECORD,,