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
@@ -0,0 +1,411 @@
1
+ import contextlib
2
+ import sys
3
+ import re
4
+ from typing import Dict, List, Optional
5
+
6
+ from pydantic import BaseModel
7
+ from pydantic_core import PydanticUndefined
8
+
9
+ import fastworkflow
10
+ from fastworkflow.utils.logging import logger
11
+ from fastworkflow import ModuleType
12
+
13
+ from fastworkflow.utils.signatures import InputForParamExtraction
14
+
15
+
16
+ INVALID_INT_VALUE = -sys.maxsize
17
+ INVALID_FLOAT_VALUE = -sys.float_info.max
18
+
19
+ MISSING_INFORMATION_ERRMSG = fastworkflow.get_env_var("MISSING_INFORMATION_ERRMSG")
20
+ INVALID_INFORMATION_ERRMSG = fastworkflow.get_env_var("INVALID_INFORMATION_ERRMSG")
21
+
22
+ NOT_FOUND = fastworkflow.get_env_var("NOT_FOUND")
23
+ INVALID = fastworkflow.get_env_var("INVALID")
24
+ PARAMETER_EXTRACTION_ERROR_MSG = None
25
+
26
+
27
+ class ParameterExtraction:
28
+ class Output(BaseModel):
29
+ parameters_are_valid: bool
30
+ cmd_parameters: Optional[BaseModel] = None
31
+ error_msg: Optional[str] = None
32
+ suggestions: Optional[Dict[str, List[str]]] = None
33
+
34
+ def __init__(self, cme_workflow: fastworkflow.Workflow, app_workflow: fastworkflow.Workflow, command_name: str, command: str):
35
+ self.cme_workflow = cme_workflow
36
+ self.app_workflow = app_workflow
37
+ self.command_name = command_name
38
+ self.command = command
39
+
40
+ def extract(self) -> "ParameterExtraction.Output":
41
+ app_workflow_folderpath = self.app_workflow.folderpath
42
+ app_command_routing_definition = fastworkflow.RoutingRegistry.get_definition(app_workflow_folderpath)
43
+
44
+ command_parameters_class = (
45
+ app_command_routing_definition.get_command_class(
46
+ self.command_name, ModuleType.COMMAND_PARAMETERS_CLASS
47
+ )
48
+ )
49
+ if not command_parameters_class:
50
+ return self.Output(parameters_are_valid=True)
51
+
52
+ stored_params = self._get_stored_parameters(self.cme_workflow)
53
+
54
+ self.command = self.command.replace(self.command_name, "").strip()
55
+
56
+ input_for_param_extraction = InputForParamExtraction.create(
57
+ self.app_workflow, self.command_name,
58
+ self.command)
59
+
60
+ # If we have missing fields (in parameter extraction error state), try to apply the command directly
61
+ if stored_params:
62
+ new_params = self._extract_and_merge_missing_parameters(stored_params, self.command)
63
+ else:
64
+ # Check if we're in agentic mode (not assistant mode command)
65
+ is_agentic_mode = (
66
+ "is_assistant_mode_command" not in self.cme_workflow.context
67
+ and "run_as_agent" in self.app_workflow.context
68
+ and self.app_workflow.context["run_as_agent"]
69
+ )
70
+
71
+ if is_agentic_mode:
72
+ # Try regex-based extraction first in agentic mode
73
+ new_params = self._extract_parameters_from_xml(self.command, command_parameters_class)
74
+
75
+ # If regex extraction fails, fall back to LLM-based extraction
76
+ if new_params is None:
77
+ new_params = input_for_param_extraction.extract_parameters(
78
+ command_parameters_class,
79
+ self.command_name,
80
+ app_workflow_folderpath)
81
+ else:
82
+ # Use LLM-based extraction for assistant mode
83
+ new_params = input_for_param_extraction.extract_parameters(
84
+ command_parameters_class,
85
+ self.command_name,
86
+ app_workflow_folderpath)
87
+
88
+ is_valid, error_msg, suggestions, missing_invalid_fields = \
89
+ input_for_param_extraction.validate_parameters(
90
+ self.app_workflow, self.command_name, new_params
91
+ )
92
+
93
+ # Set all the missing and invalid fields to None before storing
94
+ current_values = {
95
+ field_name: getattr(new_params, field_name, None)
96
+ for field_name in list(type(new_params).model_fields.keys())
97
+ }
98
+ for field_name in missing_invalid_fields:
99
+ if field_name in current_values:
100
+ current_values[field_name] = NOT_FOUND
101
+ # Reconstruct the model instance without validation
102
+ new_params = new_params.__class__.model_construct(**current_values)
103
+
104
+ self._store_parameters(self.cme_workflow, new_params)
105
+
106
+ if not is_valid:
107
+ if params_str := self._format_parameters_for_display(new_params):
108
+ error_msg = f"Extracted parameters so far:\n{params_str}\n\n{error_msg}"
109
+
110
+ if "run_as_agent" not in self.app_workflow.context:
111
+ error_msg += "\nEnter 'abort' to get out of this error state and/or execute a different command."
112
+ error_msg += "\nEnter 'you misunderstood' if the wrong command was executed."
113
+ else:
114
+ error_msg += "\nCheck your command name if the wrong command was executed."
115
+ return self.Output(
116
+ parameters_are_valid=False,
117
+ error_msg=error_msg,
118
+ cmd_parameters=new_params,
119
+ suggestions=suggestions)
120
+
121
+ self._clear_parameters(self.cme_workflow)
122
+ return self.Output(
123
+ parameters_are_valid=True,
124
+ cmd_parameters=new_params)
125
+
126
+ @staticmethod
127
+ def _get_stored_parameters(cme_workflow: fastworkflow.Workflow):
128
+ return cme_workflow.context.get("stored_parameters")
129
+
130
+ @staticmethod
131
+ def _store_parameters(cme_workflow: fastworkflow.Workflow, parameters):
132
+ cme_workflow.context["stored_parameters"] = parameters
133
+
134
+ @staticmethod
135
+ def _clear_parameters(cme_workflow: fastworkflow.Workflow):
136
+ if "stored_parameters" in cme_workflow.context:
137
+ del cme_workflow.context["stored_parameters"]
138
+
139
+ @staticmethod
140
+ def _extract_missing_fields(input_for_param_extraction, sws, command_name, stored_params):
141
+ stored_missing_fields = []
142
+ is_valid, error_msg, _ = input_for_param_extraction.validate_parameters(
143
+ sws, command_name, stored_params
144
+ )
145
+
146
+ if not is_valid:
147
+ if MISSING_INFORMATION_ERRMSG in error_msg:
148
+ missing_fields_str = error_msg.split(f"{MISSING_INFORMATION_ERRMSG}")[1].split("\n")[0]
149
+ stored_missing_fields = [f.strip() for f in missing_fields_str.split(",")]
150
+ if INVALID_INFORMATION_ERRMSG in error_msg:
151
+ invalid_section = error_msg.split(f"{INVALID_INFORMATION_ERRMSG}")[1]
152
+ if "\n" in invalid_section:
153
+ invalid_fields_str = invalid_section.split("\n")[0]
154
+ stored_missing_fields.extend(
155
+ invalid_field.split(" '")[0].strip()
156
+ for invalid_field in invalid_fields_str.split(", ")
157
+ )
158
+ return stored_missing_fields
159
+
160
+ @staticmethod
161
+ def _merge_parameters(old_params, new_params, missing_fields):
162
+ """
163
+ Merge new parameters with old parameters, prioritizing new values when appropriate.
164
+ """
165
+ merged_data = {
166
+ field_name: getattr(old_params, field_name, None)
167
+ for field_name in list(type(old_params).model_fields.keys())
168
+ }
169
+
170
+ # all_fields = list(old_params.model_fields.keys())
171
+ missing_fields = missing_fields or []
172
+
173
+ for field_name in missing_fields:
174
+ merged_data[field_name] = getattr(new_params, field_name)
175
+
176
+ # Construct the model instance without validation
177
+ return old_params.__class__.model_construct(**merged_data)
178
+
179
+ # if hasattr(new_params, field_name):
180
+ # new_value = getattr(new_params, field_name)
181
+ # old_value = merged_data.get(field_name)
182
+
183
+ # if new_value is not None and new_value != NOT_FOUND:
184
+ # if isinstance(old_value, str) and INVALID in old_value and INVALID not in new_value:
185
+ # merged_data[field_name] = new_value
186
+
187
+ # elif old_value is None or old_value == NOT_FOUND:
188
+ # merged_data[field_name] = new_value
189
+
190
+ # elif isinstance(old_value, int) and old_value == INVALID_INT_VALUE:
191
+ # with contextlib.suppress(ValueError, TypeError):
192
+ # merged_data[field_name] = int(new_value)
193
+
194
+ # elif isinstance(old_value, float) and old_value == INVALID_FLOAT_VALUE:
195
+ # with contextlib.suppress(ValueError, TypeError):
196
+ # merged_data[field_name] = float(new_value)
197
+
198
+ # elif (field_name in missing_fields and
199
+ # hasattr(old_params.model_fields.get(field_name), "json_schema_extra") and
200
+ # old_params.model_fields.get(field_name).json_schema_extra and
201
+ # "db_lookup" in old_params.model_fields.get(field_name).json_schema_extra):
202
+ # merged_data[field_name] = new_value
203
+
204
+ # elif field_name in missing_fields:
205
+ # field_info = old_params.model_fields.get(field_name)
206
+ # has_pattern = hasattr(field_info, "pattern") and field_info.pattern is not None
207
+
208
+ # if not has_pattern:
209
+ # for meta in getattr(field_info, "metadata", []):
210
+ # if hasattr(meta, "pattern"):
211
+ # has_pattern = True
212
+ # break
213
+
214
+ # if not has_pattern and hasattr(field_info, "json_schema_extra") and field_info.json_schema_extra:
215
+ # has_pattern = "pattern" in field_info.json_schema_extra
216
+
217
+ # if has_pattern:
218
+ # merged_data[field_name] = new_value
219
+
220
+ @staticmethod
221
+ def _format_parameters_for_display(params):
222
+ """
223
+ Format parameters for display in the error message.
224
+ """
225
+ if not params:
226
+ return ""
227
+
228
+ lines = []
229
+
230
+ all_fields = list(type(params).model_fields.keys())
231
+
232
+ for field_name in all_fields:
233
+ value = getattr(params, field_name, None)
234
+
235
+ if value in [
236
+ NOT_FOUND,
237
+ None,
238
+ INVALID_INT_VALUE,
239
+ INVALID_FLOAT_VALUE
240
+ ]:
241
+ continue
242
+
243
+ display_name = " ".join(word.capitalize() for word in field_name.split('_'))
244
+
245
+ # Format fields appropriately based on type
246
+ if (
247
+ isinstance(value, bool)
248
+ or not hasattr(value, 'value')
249
+ and isinstance(value, (int, float))
250
+ or not hasattr(value, 'value')
251
+ and isinstance(value, str)
252
+ or not hasattr(value, 'value')
253
+ ):
254
+ lines.append(f"{display_name}: {value}")
255
+ else: # Handle enum types
256
+ lines.append(f"{display_name}: {value.value}")
257
+ return "\n".join(lines)
258
+
259
+ @staticmethod
260
+ def _apply_missing_fields(command: str, default_params: BaseModel, missing_fields: list):
261
+ global PARAMETER_EXTRACTION_ERROR_MSG
262
+ if not PARAMETER_EXTRACTION_ERROR_MSG:
263
+ PARAMETER_EXTRACTION_ERROR_MSG = fastworkflow.get_env_var("PARAMETER_EXTRACTION_ERROR_MSG")
264
+
265
+ # Work on plain dict to avoid validation during assignment
266
+ params_data = {
267
+ field_name: getattr(default_params, field_name, None)
268
+ for field_name in list(type(default_params).model_fields.keys())
269
+ }
270
+
271
+ if "," in command:
272
+ parts = [part.strip() for part in command.split(",")]
273
+
274
+ if (
275
+ len(parts) == len(missing_fields) == 1
276
+ or len(parts) != len(missing_fields)
277
+ and parts
278
+ and missing_fields
279
+ ):
280
+ field = missing_fields[0]
281
+ if field in params_data:
282
+ params_data[field] = parts[0]
283
+ elif len(parts) == len(missing_fields) and len(missing_fields) > 1:
284
+ for i, field in enumerate(missing_fields):
285
+ if i < len(parts) and field in params_data:
286
+ params_data[field] = parts[i]
287
+ elif missing_fields:
288
+ field = missing_fields[0]
289
+ if field in params_data:
290
+ params_data[field] = command.strip()
291
+
292
+ # Construct model without validation
293
+ return default_params.__class__.model_construct(**params_data)
294
+
295
+ @staticmethod
296
+ def _extract_parameters_from_xml(command: str, command_parameters_class: type[BaseModel]) -> Optional[BaseModel]:
297
+ """
298
+ Extract parameters from XML-formatted command using regex.
299
+
300
+ Returns:
301
+ BaseModel instance with extracted parameters, or None if parsing fails
302
+ """
303
+ field_names = list(command_parameters_class.model_fields.keys())
304
+
305
+ # If no parameters are defined, return empty model immediately
306
+ if not field_names:
307
+ return command_parameters_class.model_construct()
308
+
309
+ extracted_data = {}
310
+
311
+ # Try to extract each parameter using XML tags
312
+ if len(field_names) == 1:
313
+ # If there's only one field, extract content from first XML tag
314
+ pattern = r'<[^>]+>(.+?)</[^>]+>'
315
+ if match := re.search(pattern, command, re.DOTALL):
316
+ parameter_value = match[1].strip()
317
+ extracted_data[field_names[0]] = parameter_value
318
+ else:
319
+ # Try to extract each parameter using XML tags
320
+ for field_name in field_names:
321
+ # Look for <field_name>value</field_name> pattern
322
+ pattern = rf'<{re.escape(field_name)}>(.+?)</{re.escape(field_name)}>'
323
+ if match := re.search(pattern, command, re.DOTALL):
324
+ parameter_value = match[1].strip()
325
+ extracted_data[field_name] = parameter_value
326
+
327
+ # Check if we extracted values for ALL fields (safest criteria for LLM fallback)
328
+ all_fields_extracted = len(extracted_data) == len(field_names)
329
+
330
+ # Check if agent used example values
331
+ if all_fields_extracted:
332
+ for field_name, extracted_value in extracted_data.items():
333
+ field_info = command_parameters_class.model_fields[field_name]
334
+ examples = getattr(field_info, "examples", None)
335
+ if examples and extracted_value in examples:
336
+ all_fields_extracted = False
337
+ break
338
+
339
+ if all_fields_extracted:
340
+ # Initialize all fields with their default values (if they exist) or None
341
+ params_data = {}
342
+ for field_name in field_names:
343
+ field_info = command_parameters_class.model_fields[field_name]
344
+ if field_info.default is not PydanticUndefined:
345
+ params_data[field_name] = field_info.default
346
+ elif field_info.default_factory is not None:
347
+ params_data[field_name] = field_info.default_factory()
348
+ else:
349
+ params_data[field_name] = None
350
+
351
+ # Update with extracted values
352
+ params_data |= extracted_data
353
+
354
+ # Construct model without validation
355
+ return command_parameters_class.model_construct(**params_data)
356
+
357
+ return None
358
+
359
+ @staticmethod
360
+ def _extract_and_merge_missing_parameters(stored_params: BaseModel, command: str):
361
+ """
362
+ Identify fields to fill by scanning for sentinel values and merge values
363
+ parsed from the command string into a new params instance. This preserves
364
+ existing behavior for token/field count mismatches and leaves values as
365
+ strings (no type coercion).
366
+ """
367
+ # Initialize with existing values to avoid triggering validation
368
+ field_names = list(type(stored_params).model_fields.keys())
369
+ params_data = {
370
+ field_name: getattr(stored_params, field_name, None)
371
+ for field_name in field_names
372
+ }
373
+
374
+ # Determine which fields still need user-provided input based on sentinels
375
+ fields_to_fill = []
376
+ for field_name in field_names:
377
+ value = getattr(stored_params, field_name, None)
378
+ if value in [
379
+ NOT_FOUND,
380
+ None,
381
+ INVALID_INT_VALUE,
382
+ INVALID_FLOAT_VALUE,
383
+ ]:
384
+ fields_to_fill.append(field_name)
385
+
386
+ if not fields_to_fill:
387
+ return stored_params
388
+
389
+ # Preserve existing mismatch handling and keep all values as strings
390
+ if "," in command:
391
+ parts = [part.strip() for part in command.split(",")]
392
+
393
+ if (
394
+ len(parts) == len(fields_to_fill) == 1
395
+ or len(parts) != len(fields_to_fill)
396
+ and parts
397
+ ):
398
+ field = fields_to_fill[0]
399
+ if field in params_data:
400
+ params_data[field] = parts[0]
401
+ elif len(parts) == len(fields_to_fill) and len(fields_to_fill) > 1:
402
+ for i, field in enumerate(fields_to_fill):
403
+ if i < len(parts) and field in params_data:
404
+ params_data[field] = parts[i]
405
+ else:
406
+ field = fields_to_fill[0]
407
+ if field in params_data:
408
+ params_data[field] = command.strip()
409
+
410
+ # Return a new instance without validation
411
+ return stored_params.__class__.model_construct(**params_data)