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.
- fastworkflow/_workflows/command_metadata_extraction/_commands/wildcard.py +7 -0
- fastworkflow/_workflows/command_metadata_extraction/intent_detection.py +2 -2
- fastworkflow/_workflows/command_metadata_extraction/parameter_extraction.py +15 -3
- fastworkflow/chat_session.py +8 -1
- fastworkflow/command_executor.py +13 -4
- fastworkflow/examples/retail_workflow/_commands/find_user_id_by_email.py +6 -5
- fastworkflow/utils/signatures.py +288 -111
- fastworkflow/workflow.py +1 -5
- {fastworkflow-2.15.9.dist-info → fastworkflow-2.15.11.dist-info}/METADATA +1 -1
- {fastworkflow-2.15.9.dist-info → fastworkflow-2.15.11.dist-info}/RECORD +13 -13
- {fastworkflow-2.15.9.dist-info → fastworkflow-2.15.11.dist-info}/LICENSE +0 -0
- {fastworkflow-2.15.9.dist-info → fastworkflow-2.15.11.dist-info}/WHEEL +0 -0
- {fastworkflow-2.15.9.dist-info → fastworkflow-2.15.11.dist-info}/entry_points.txt +0 -0
|
@@ -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
|
-
|
|
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}"
|
fastworkflow/chat_session.py
CHANGED
|
@@ -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
|
|
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):
|
fastworkflow/command_executor.py
CHANGED
|
@@ -45,7 +45,12 @@ class CommandExecutor(CommandExecutorInterface):
|
|
|
45
45
|
command = command)
|
|
46
46
|
)
|
|
47
47
|
|
|
48
|
-
if command_output.command_handled
|
|
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
|
-
"""
|
|
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
|
)
|
fastworkflow/utils/signatures.py
CHANGED
|
@@ -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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
signature_components = {
|
|
170
|
+
"statement": (str, dspy.InputField(desc="Statement according to Dhar"))
|
|
171
|
+
}
|
|
171
172
|
|
|
172
|
-
|
|
173
|
-
|
|
173
|
+
steps = []
|
|
174
|
+
field_num = 1
|
|
174
175
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
203
|
+
info_text = attribute_metadata.description or f"The {attribute_name}"
|
|
203
204
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
217
|
-
|
|
217
|
+
requirement_status = "Optional" if is_optional else "Required"
|
|
218
|
+
info_text += f". This field is {requirement_status}."
|
|
218
219
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
225
|
-
|
|
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}:
|
|
229
|
-
"
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
542
|
+
pattern = next(
|
|
543
|
+
(meta.pattern
|
|
544
|
+
for meta in getattr(field_info, "metadata", [])
|
|
545
|
+
if hasattr(meta, "pattern")),
|
|
357
546
|
None,
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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=
|
|
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=
|
|
16
|
-
fastworkflow/_workflows/command_metadata_extraction/parameter_extraction.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
169
|
+
fastworkflow/utils/signatures.py,sha256=QOLX3j-AJkRWIkDhogbhxQo8MIt668xIKwd4SWiS2LY,31734
|
|
170
170
|
fastworkflow/utils/startup_progress.py,sha256=9icSdnpFAxzIq0sUliGpNaH0Efvrt5lDtGfURV5BD98,3539
|
|
171
|
-
fastworkflow/workflow.py,sha256=
|
|
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.
|
|
175
|
-
fastworkflow-2.15.
|
|
176
|
-
fastworkflow-2.15.
|
|
177
|
-
fastworkflow-2.15.
|
|
178
|
-
fastworkflow-2.15.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|