fastworkflow 3.2.0__tar.gz → 3.3.0__tar.gz

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 (263) hide show
  1. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/PKG-INFO +4 -1
  2. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/README.md +3 -0
  3. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/__init__.py +28 -1
  4. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/_workflows/command_metadata_extraction/intent_detection.py +334 -8
  5. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/_workflows/command_metadata_extraction/parameter_extraction.py +132 -0
  6. fastworkflow-3.3.0/fastworkflow/benchmark/__init__.py +1 -0
  7. fastworkflow-3.3.0/fastworkflow/benchmark/catalog.py +387 -0
  8. fastworkflow-3.3.0/fastworkflow/benchmark/setup.py +243 -0
  9. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/checkpoint_store.py +1 -1
  10. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/cli.py +7 -0
  11. fastworkflow-3.3.0/fastworkflow/command_executor.py +499 -0
  12. fastworkflow-3.3.0/fastworkflow/experiment/__init__.py +37 -0
  13. fastworkflow-3.3.0/fastworkflow/experiment/readiness.py +193 -0
  14. fastworkflow-3.3.0/fastworkflow/experiment/runner.py +1202 -0
  15. fastworkflow-3.3.0/fastworkflow/experiment/setup.py +271 -0
  16. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/model_pipeline_training.py +124 -29
  17. fastworkflow-3.3.0/fastworkflow/observability/__init__.py +1 -0
  18. fastworkflow-3.3.0/fastworkflow/observability/capture_policy.py +598 -0
  19. fastworkflow-3.3.0/fastworkflow/observability/decision_signals.py +518 -0
  20. fastworkflow-3.3.0/fastworkflow/observability/evidence_run.py +451 -0
  21. fastworkflow-3.3.0/fastworkflow/observability/execution_recorder.py +118 -0
  22. fastworkflow-3.3.0/fastworkflow/observability/provenance.py +797 -0
  23. fastworkflow-3.3.0/fastworkflow/observability/store.py +6318 -0
  24. fastworkflow-3.3.0/fastworkflow/observability/workspace.py +799 -0
  25. fastworkflow-3.3.0/fastworkflow/review/__init__.py +1 -0
  26. fastworkflow-3.3.0/fastworkflow/review/adapters.py +335 -0
  27. fastworkflow-3.3.0/fastworkflow/review/sidecar.py +1032 -0
  28. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/run/__main__.py +28 -1
  29. fastworkflow-3.3.0/fastworkflow/run_chatbot/navigation.py +153 -0
  30. fastworkflow-3.3.0/fastworkflow/run_chatbot/server.py +3788 -0
  31. fastworkflow-3.3.0/fastworkflow/run_chatbot/static/index.html +6049 -0
  32. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/run_fastapi_mcp/__main__.py +134 -7
  33. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/run_fastapi_mcp/checkpoint.py +22 -1
  34. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/run_fastapi_mcp/utils.py +297 -10
  35. fastworkflow-3.3.0/fastworkflow/runtime_manifest.py +1265 -0
  36. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/session_state_store.py +3 -1
  37. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/skills_for_coding_fastworkflows/build-task-benchmarks/SKILL.md +16 -1
  38. fastworkflow-3.3.0/fastworkflow/skills_for_coding_fastworkflows/create-workflow-benchmarks/SKILL.md +138 -0
  39. fastworkflow-3.3.0/fastworkflow/skills_for_coding_fastworkflows/create-workflow-benchmarks/runner-reference.md +131 -0
  40. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/skills_for_coding_fastworkflows/debug-workflow-conversations/SKILL.md +53 -35
  41. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/skills_for_coding_fastworkflows/debug-workflow-conversations/reference.md +74 -59
  42. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/skills_for_coding_fastworkflows/evaluate-intent-routing/SKILL.md +9 -0
  43. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/skills_for_coding_fastworkflows/integrate-chat-agent/SKILL.md +14 -0
  44. fastworkflow-3.3.0/fastworkflow/skills_for_coding_fastworkflows/optimize-workflow-with-feedback/SKILL.md +139 -0
  45. fastworkflow-3.3.0/fastworkflow/tracing.py +1018 -0
  46. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/train/metrics_persistence.py +2 -1
  47. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/train/training_report.py +35 -21
  48. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/turn.py +200 -3
  49. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/utils/dspy_cache_utils.py +8 -4
  50. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/utils/dspy_logger.py +18 -0
  51. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/utils/dspy_utils.py +12 -0
  52. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/utils/react.py +1 -1
  53. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/workflow_agent.py +77 -9
  54. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/workflow_execution_context.py +308 -23
  55. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/pyproject.toml +1 -1
  56. fastworkflow-3.2.0/fastworkflow/command_executor.py +0 -288
  57. fastworkflow-3.2.0/fastworkflow/observability_store.py +0 -1862
  58. fastworkflow-3.2.0/fastworkflow/run_chatbot/server.py +0 -1297
  59. fastworkflow-3.2.0/fastworkflow/run_chatbot/static/index.html +0 -2820
  60. fastworkflow-3.2.0/fastworkflow/tracing.py +0 -417
  61. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/LICENSE +0 -0
  62. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/.DS_Store +0 -0
  63. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/_commands/.gitkeep +0 -0
  64. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/_workflows/__init__.py +0 -0
  65. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/_workflows/command_metadata_extraction/__init__.py +0 -0
  66. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/_workflows/command_metadata_extraction/_commands/ErrorCorrection/abort.py +0 -0
  67. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/_workflows/command_metadata_extraction/_commands/ErrorCorrection/you_misunderstood.py +0 -0
  68. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/_workflows/command_metadata_extraction/_commands/IntentDetection/go_up.py +0 -0
  69. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/_workflows/command_metadata_extraction/_commands/IntentDetection/reset_context.py +0 -0
  70. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/_workflows/command_metadata_extraction/_commands/IntentDetection/what_can_i_do.py +0 -0
  71. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/_workflows/command_metadata_extraction/_commands/IntentDetection/what_is_current_context.py +0 -0
  72. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/_workflows/command_metadata_extraction/_commands/__init__.py +0 -0
  73. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/_workflows/command_metadata_extraction/_commands/wildcard.py +0 -0
  74. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/_workflows/command_metadata_extraction/command_context_model.json +0 -0
  75. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/active_workflow.py +0 -0
  76. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/build/__main__.py +0 -0
  77. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/build/ast_class_extractor.py +0 -0
  78. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/build/class_analysis_structures.py +0 -0
  79. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/build/cli_specification.md +0 -0
  80. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/build/command_dependency_resolver.py +0 -0
  81. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/build/command_file_generator.py +0 -0
  82. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/build/command_file_template.py +0 -0
  83. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/build/command_import_utils.py +0 -0
  84. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/build/command_stub_generator.py +0 -0
  85. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/build/context_folder_generator.py +0 -0
  86. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/build/context_model_generator.py +0 -0
  87. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/build/dependency_manager.py +0 -0
  88. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/build/dir_scanner.py +0 -0
  89. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/build/documentation_generator.py +0 -0
  90. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/build/genai_postprocessor.py +0 -0
  91. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/build/inheritance_block_regenerator.py +0 -0
  92. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/build/libcst_transformers.py +0 -0
  93. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/build/navigator_stub_generator.py +0 -0
  94. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/build/pydantic_model_generator.py +0 -0
  95. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/build/utterance_generator.py +0 -0
  96. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/cache_matching.py +0 -0
  97. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/chat_session.py +0 -0
  98. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/command_context_model.py +0 -0
  99. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/command_directory.py +0 -0
  100. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/command_interfaces.py +0 -0
  101. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/command_metadata_api.py +0 -0
  102. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/command_routing.py +0 -0
  103. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/conversation_history_io.py +0 -0
  104. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/conversation_labeling.py +0 -0
  105. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/distillation.py +0 -0
  106. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/extended_workflow_example/README.md +0 -0
  107. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/extended_workflow_example/_commands/WorkItem/get_status.py +0 -0
  108. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/extended_workflow_example/_commands/generate_report.py +0 -0
  109. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/extended_workflow_example/_commands/startup.py +0 -0
  110. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/extended_workflow_example/simple_workflow_template.json +0 -0
  111. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/extended_workflow_example/workflow_inheritance_model.json +0 -0
  112. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/fastworkflow.env +0 -0
  113. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/fastworkflow.passwords.env +0 -0
  114. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/hello_world/_commands/README.md +0 -0
  115. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/hello_world/_commands/add_two_numbers.py +0 -0
  116. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/hello_world/_commands/context_inheritance_model.json +0 -0
  117. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/hello_world/application/add_two_numbers.py +0 -0
  118. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/messaging_app_1/_commands/context_inheritance_model.json +0 -0
  119. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/messaging_app_1/_commands/send_message.py +0 -0
  120. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/messaging_app_1/application/send_message.py +0 -0
  121. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/messaging_app_2/_commands/User/_User.py +0 -0
  122. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/messaging_app_2/_commands/User/send_message.py +0 -0
  123. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/messaging_app_2/_commands/context_inheritance_model.json +0 -0
  124. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/messaging_app_2/_commands/startup.py +0 -0
  125. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/messaging_app_2/application/user.py +0 -0
  126. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/messaging_app_3/_commands/PremiumUser/_PremiumUser.py +0 -0
  127. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/messaging_app_3/_commands/PremiumUser/send_priority_message.py +0 -0
  128. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/messaging_app_3/_commands/User/_User.py +0 -0
  129. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/messaging_app_3/_commands/User/send_message.py +0 -0
  130. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/messaging_app_3/_commands/context_inheritance_model.json +0 -0
  131. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/messaging_app_3/_commands/initialize_user.py +0 -0
  132. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/messaging_app_3/application/user.py +0 -0
  133. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/messaging_app_4/_commands/ChatRoom/_ChatRoom.py +0 -0
  134. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/messaging_app_4/_commands/ChatRoom/add_user.py +0 -0
  135. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/messaging_app_4/_commands/ChatRoom/broadcast_message.py +0 -0
  136. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/messaging_app_4/_commands/ChatRoom/get_current_user.py +0 -0
  137. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/messaging_app_4/_commands/ChatRoom/list_users.py +0 -0
  138. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/messaging_app_4/_commands/ChatRoom/set_current_user.py +0 -0
  139. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/messaging_app_4/_commands/PremiumUser/_PremiumUser.py +0 -0
  140. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/messaging_app_4/_commands/PremiumUser/send_priority_message.py +0 -0
  141. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/messaging_app_4/_commands/User/_User.py +0 -0
  142. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/messaging_app_4/_commands/User/send_message.py +0 -0
  143. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/messaging_app_4/_commands/context_inheritance_model.json +0 -0
  144. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/messaging_app_4/_commands/set_root_context.py +0 -0
  145. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/messaging_app_4/application/chatroom.py +0 -0
  146. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/messaging_app_4/application/user.py +0 -0
  147. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/messaging_app_4/context_hierarchy_model.json +0 -0
  148. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/messaging_app_4/startup_action.json +0 -0
  149. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/_commands/calculate.py +0 -0
  150. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/_commands/cancel_pending_order.py +0 -0
  151. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/_commands/exchange_delivered_order_items.py +0 -0
  152. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/_commands/find_user_id_by_email.py +0 -0
  153. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/_commands/find_user_id_by_name_zip.py +0 -0
  154. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/_commands/get_order_details.py +0 -0
  155. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/_commands/get_product_details.py +0 -0
  156. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/_commands/get_user_details.py +0 -0
  157. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/_commands/list_all_product_types.py +0 -0
  158. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/_commands/modify_pending_order_address.py +0 -0
  159. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/_commands/modify_pending_order_items.py +0 -0
  160. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/_commands/modify_pending_order_payment.py +0 -0
  161. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/_commands/modify_user_address.py +0 -0
  162. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/_commands/return_delivered_order_items.py +0 -0
  163. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/_commands/transfer_to_human_agents.py +0 -0
  164. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/context_inheritance_model.json +0 -0
  165. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/retail_data/__init__.py +0 -0
  166. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/retail_data/orders.json +0 -0
  167. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/retail_data/products.json +0 -0
  168. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/retail_data/users.json +0 -0
  169. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/tools/calculate.py +0 -0
  170. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/tools/cancel_pending_order.py +0 -0
  171. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/tools/exchange_delivered_order_items.py +0 -0
  172. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/tools/find_user_id_by_email.py +0 -0
  173. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/tools/find_user_id_by_name_zip.py +0 -0
  174. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/tools/get_order_details.py +0 -0
  175. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/tools/get_product_details.py +0 -0
  176. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/tools/get_user_details.py +0 -0
  177. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/tools/list_all_product_types.py +0 -0
  178. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/tools/modify_pending_order_address.py +0 -0
  179. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/tools/modify_pending_order_items.py +0 -0
  180. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/tools/modify_pending_order_payment.py +0 -0
  181. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/tools/modify_user_address.py +0 -0
  182. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/tools/return_delivered_order_items.py +0 -0
  183. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/tools/think.py +0 -0
  184. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/tools/tool.py +0 -0
  185. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/tools/transfer_to_human_agents.py +0 -0
  186. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/retail_workflow/workflow_description.txt +0 -0
  187. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/simple_workflow_template/__init__.py +0 -0
  188. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/_WorkItem.py +0 -0
  189. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/add_child_workitem.py +0 -0
  190. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/get_status.py +0 -0
  191. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/go_to_workitem.py +0 -0
  192. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/mark_as_complete.py +0 -0
  193. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/move_to_first_child_workitem.py +0 -0
  194. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/move_to_last_child_workitem.py +0 -0
  195. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/move_to_next_workitem.py +0 -0
  196. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/move_to_previous_workitem.py +0 -0
  197. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/remove_all_child_workitems.py +0 -0
  198. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/remove_child_workitem.py +0 -0
  199. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/show_schema.py +0 -0
  200. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/simple_workflow_template/_commands/startup.py +0 -0
  201. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/simple_workflow_template/application/__init__.py +0 -0
  202. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/simple_workflow_template/application/workitem.py +0 -0
  203. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/simple_workflow_template/simple_workflow_template.json +0 -0
  204. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/examples/simple_workflow_template/startup_action.json +0 -0
  205. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/intent_clarification_agent.py +0 -0
  206. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/kvstore.py +0 -0
  207. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/mcp_server.py +0 -0
  208. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/metrics.py +0 -0
  209. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/nlu_labels.py +0 -0
  210. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/refine/__main__.py +0 -0
  211. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/run/__init__.py +0 -0
  212. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/run_chatbot/__init__.py +0 -0
  213. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/run_chatbot/launcher.py +0 -0
  214. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/run_fastapi_mcp/README.md +0 -0
  215. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/run_fastapi_mcp/__init__.py +0 -0
  216. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/run_fastapi_mcp/jwt_manager.py +0 -0
  217. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/run_fastapi_mcp/mcp_specific.py +0 -0
  218. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/run_fastapi_mcp/redoc_2_standalone_html.py +0 -0
  219. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/run_fastapi_mcp/server_memory.py +0 -0
  220. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/run_fastapi_mcp/turns.py +0 -0
  221. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/serialization_hooks.py +0 -0
  222. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/skills_for_coding_fastworkflows/build-task-benchmarks/reference.md +0 -0
  223. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/skills_for_coding_fastworkflows/declare-parameter-producers/SKILL.md +0 -0
  224. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/skills_for_coding_fastworkflows/design-context-models/SKILL.md +0 -0
  225. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/skills_for_coding_fastworkflows/design-context-models/reference.md +0 -0
  226. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/skills_for_coding_fastworkflows/detect-duplicate-capabilities/SKILL.md +0 -0
  227. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/skills_for_coding_fastworkflows/integrate-chat-agent/reference.md +0 -0
  228. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/skills_for_coding_fastworkflows/resolve-parameter-values/SKILL.md +0 -0
  229. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/skills_for_coding_fastworkflows/supply-training-personas/SKILL.md +0 -0
  230. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/skills_for_coding_fastworkflows/train-and-publish-models/SKILL.md +0 -0
  231. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/skills_for_coding_fastworkflows/train-and-publish-models/reference.md +0 -0
  232. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/skills_for_coding_fastworkflows/validate-command-parameters/SKILL.md +0 -0
  233. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/state_paths.py +0 -0
  234. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/state_serialization.py +0 -0
  235. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/storage_keys.py +0 -0
  236. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/train/__init__.py +0 -0
  237. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/train/__main__.py +0 -0
  238. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/train/artifact_versioning.py +0 -0
  239. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/train/class_balance.py +0 -0
  240. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/train/determinism.py +0 -0
  241. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/train/duplicate_detection.py +0 -0
  242. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/train/generate_synthetic.py +0 -0
  243. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/train/heldout_evaluation.py +0 -0
  244. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/train/param_example_cache.py +0 -0
  245. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/train/personas.py +0 -0
  246. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/train/selective_training.py +0 -0
  247. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/train/utterance_cache.py +0 -0
  248. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/user_message_queues.py +0 -0
  249. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/utils/__init__.py +0 -0
  250. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/utils/chat_adapter.py +0 -0
  251. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/utils/context_utils.py +0 -0
  252. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/utils/env.py +0 -0
  253. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/utils/fuzzy_match.py +0 -0
  254. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/utils/generate_param_examples.py +0 -0
  255. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/utils/insights_loader.py +0 -0
  256. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/utils/logging.py +0 -0
  257. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/utils/parameterize_func_decorator.py +0 -0
  258. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/utils/pydantic_model_2_dspy_signature_class.py +0 -0
  259. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/utils/python_utils.py +0 -0
  260. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/utils/signatures.py +0 -0
  261. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/utils/startup_progress.py +0 -0
  262. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/workflow.py +0 -0
  263. {fastworkflow-3.2.0 → fastworkflow-3.3.0}/fastworkflow/workflow_inheritance_model.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastworkflow
3
- Version: 3.2.0
3
+ Version: 3.3.0
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
@@ -433,6 +433,8 @@ Use **Clear conversations** in Debug mode for an explicit, confirmed reset. It r
433
433
 
434
434
  To turn recording off entirely, set `FW_OBSERVABILITY=0`.
435
435
 
436
+ Phase 0 capture/tracing overhead (bead `fix-49m.2`, not a CI gate) was measured with a stubbed LM on hello-world `add_two_numbers` turns, observability on, no network: command-gap p50 **+0.7%** and turn-wall p50 **+1.6%** vs fork `9904df5` (change 4 `5b1e85e` in between). Reports and method: https://gist.github.com/dharrawal/2e123360ace2e948a138c52ce9f00601
437
+
436
438
  ---
437
439
 
438
440
  ## Production deployment
@@ -723,6 +725,7 @@ Two files per workflow (templates ship with `fastworkflow examples fetch`).
723
725
  | `FW_OBS_RETENTION_DAYS` | Age beyond which the automatic prune (run at recorder startup) drops spans/artifacts (turn records are exempt) | Optional | `30` |
724
726
  | `FW_OBS_DB_MAX_BYTES` | Size cap; the automatic prune evicts oldest spans first while over it | Optional | `1073741824` (1 GiB) |
725
727
  | `FW_OBS_CAPTURE_TRACEBACKS` | Persist exception tracebacks as artifacts. Off by default because tracebacks can carry sensitive values | Optional | `0` |
728
+ | `FW_OBS_SUPPRESS_PRUNE` | Withhold the automatic prune. **This is the cross-process contract for a measured run**: the in-process `suppress_pruning()` helper only covers the process that calls it, so when a harness drives a separate server the variable must be in the *server's* environment **before it starts** — the recorder prunes once on construction, so setting it afterwards is too late. Confirm the value took hold with `GET /probes/readyz?observability=true`, which reports `pruning_suppressed`; the server also logs the regime at startup | Optional | `0` |
726
729
 
727
730
  ### `fastworkflow.passwords.env`
728
731
 
@@ -379,6 +379,8 @@ Use **Clear conversations** in Debug mode for an explicit, confirmed reset. It r
379
379
 
380
380
  To turn recording off entirely, set `FW_OBSERVABILITY=0`.
381
381
 
382
+ Phase 0 capture/tracing overhead (bead `fix-49m.2`, not a CI gate) was measured with a stubbed LM on hello-world `add_two_numbers` turns, observability on, no network: command-gap p50 **+0.7%** and turn-wall p50 **+1.6%** vs fork `9904df5` (change 4 `5b1e85e` in between). Reports and method: https://gist.github.com/dharrawal/2e123360ace2e948a138c52ce9f00601
383
+
382
384
  ---
383
385
 
384
386
  ## Production deployment
@@ -669,6 +671,7 @@ Two files per workflow (templates ship with `fastworkflow examples fetch`).
669
671
  | `FW_OBS_RETENTION_DAYS` | Age beyond which the automatic prune (run at recorder startup) drops spans/artifacts (turn records are exempt) | Optional | `30` |
670
672
  | `FW_OBS_DB_MAX_BYTES` | Size cap; the automatic prune evicts oldest spans first while over it | Optional | `1073741824` (1 GiB) |
671
673
  | `FW_OBS_CAPTURE_TRACEBACKS` | Persist exception tracebacks as artifacts. Off by default because tracebacks can carry sensitive values | Optional | `0` |
674
+ | `FW_OBS_SUPPRESS_PRUNE` | Withhold the automatic prune. **This is the cross-process contract for a measured run**: the in-process `suppress_pruning()` helper only covers the process that calls it, so when a harness drives a separate server the variable must be in the *server's* environment **before it starts** — the recorder prunes once on construction, so setting it afterwards is too late. Confirm the value took hold with `GET /probes/readyz?observability=true`, which reports `pruning_suppressed`; the server also logs the regime at startup | Optional | `0` |
672
675
 
673
676
  ### `fastworkflow.passwords.env`
674
677
 
@@ -93,6 +93,19 @@ class CommandOutput(BaseModel):
93
93
  command_parameters: Any = None # typed model in memory; dict in records [A10]
94
94
  started_at: Optional[datetime] = None
95
95
  duration_ms: Optional[int] = None
96
+ # Joins this outcome to the span that produced it (arch §12.0 delta 1).
97
+ # Optional with a default because §12.2 requires CommandOutput stay
98
+ # compatible: every existing constructor call and every already-serialized
99
+ # record must keep validating, and both do — an absent key reads as None.
100
+ # None means "not dispatched through a call-id-stamping path", which is the
101
+ # honest answer for a hand-built CommandOutput; it never means "no span".
102
+ command_call_id: Optional[str] = None
103
+ # [A7] structural marker for an ask_user exchange. Optional with a None
104
+ # default for the same §12.2 reason as command_call_id: every existing
105
+ # constructor call and every already-serialized record must keep
106
+ # validating. None means "written before this field existed" and defers to
107
+ # the name comparison in `is_ask_user`; True and False are authoritative.
108
+ ask_user_entry: Optional[bool] = None
96
109
 
97
110
  @model_validator(mode="before")
98
111
  @classmethod
@@ -112,7 +125,21 @@ class CommandOutput(BaseModel):
112
125
 
113
126
  @property
114
127
  def is_ask_user(self) -> bool:
115
- """True if this entry is an ask_user clarification exchange. [A7]"""
128
+ """True if this entry is an ask_user clarification exchange. [A7]
129
+
130
+ `ask_user_entry` is authoritative when set; the name comparison is the
131
+ fallback for records serialized before the field existed. It cannot be
132
+ the primary test any more: since fix-ajv.16 a FAILURE output carries the
133
+ real routed command name with `success=False`, which is byte-identical
134
+ to an unanswered question, and root-context command names are
135
+ UNQUALIFIED — so a workflow defining a root command called `ask_user`
136
+ would have its failures collected by `complete_ask_user_entry`, which
137
+ would overwrite the error with the user's answer and mark it successful.
138
+ The guarantee now rests on a field the framework sets, not on no
139
+ workflow ever choosing a name. fix-ajv.17.
140
+ """
141
+ if self.ask_user_entry is not None:
142
+ return self.ask_user_entry
116
143
  return self.command_name == "ask_user"
117
144
 
118
145
  @property
@@ -9,11 +9,21 @@ import fastworkflow
9
9
  from fastworkflow.utils.logging import logger
10
10
  from fastworkflow import NLUPipelineStage, tracing
11
11
  from fastworkflow.cache_matching import cache_match, store_utterance_cache
12
+ from fastworkflow.observability.decision_signals import (
13
+ DecisionUncertainty,
14
+ UncertaintySignal,
15
+ ambiguity_set_size,
16
+ classifier_confidence,
17
+ classifier_topk_margin,
18
+ fuzzy_distance,
19
+ )
12
20
  from fastworkflow.kvstore import KVStore
13
21
  from fastworkflow.model_pipeline_training import (
14
- CommandRouter
22
+ CommandRouter,
23
+ GLOBAL_CONTEXT_FOLDER,
15
24
  )
16
25
  from fastworkflow.nlu_labels import is_escalation, is_non_routable
26
+ from fastworkflow.train.artifact_versioning import VERSIONS_DIRNAME
17
27
 
18
28
  from fastworkflow.utils.fuzzy_match import find_best_matches
19
29
 
@@ -23,6 +33,250 @@ from fastworkflow.utils.fuzzy_match import find_best_matches
23
33
  # was discarded. This is product behaviour, not workflow configuration.
24
34
 
25
35
 
36
+ # What became of an escalation signal on this prediction. FW-REQ-021 clause 1 asks
37
+ # for a first-class escalation outcome rather than only the list of labels that were
38
+ # thrown away: "no escalation label was predicted" and "we never ran the classifier
39
+ # that could predict one" are different facts, and `escalation_labels_discarded`
40
+ # being absent cannot tell them apart.
41
+ #
42
+ # Only the classifier can produce an escalation label, so every other matching layer
43
+ # reports NOT_EVALUATED rather than ABSENT.
44
+ ESCALATION_OUTCOME_NOT_EVALUATED = "not_evaluated"
45
+ ESCALATION_OUTCOME_ABSENT = "absent"
46
+ # The escalation label won outright: `resolve_fully_qualified_command_name` maps it
47
+ # to None and the CME wildcard command walks the parent chain, which is the signal
48
+ # being acted on.
49
+ ESCALATION_OUTCOME_HONORED = "honored"
50
+ # The classifier ranked an escalation label alongside local candidates and was not
51
+ # confident. The user is prompted with the local candidates only (the ambiguity
52
+ # message filters non-routable labels) and the "try my parent" signal is dropped —
53
+ # GAP-18, recorded here as an outcome instead of inferred from a log line.
54
+ ESCALATION_OUTCOME_SUPPRESSED_BY_AMBIGUITY = "suppressed_by_ambiguity"
55
+
56
+ ESCALATION_OUTCOMES: frozenset[str] = frozenset({
57
+ ESCALATION_OUTCOME_NOT_EVALUATED,
58
+ ESCALATION_OUTCOME_ABSENT,
59
+ ESCALATION_OUTCOME_HONORED,
60
+ ESCALATION_OUTCOME_SUPPRESSED_BY_AMBIGUITY,
61
+ })
62
+
63
+ # Maximum normalized Levenshtein distance the fuzzy pre-match will accept. Named so
64
+ # the span can record what a distance was compared against — a distance with no
65
+ # threshold beside it cannot be binned by a calibration report. The value is
66
+ # unchanged.
67
+ _FUZZY_PREMATCH_MAX_DISTANCE = 0.3 # Adjust threshold as needed
68
+
69
+ # Identifies the matcher that produced a fuzzy distance, so a curve drawn from one
70
+ # matcher is not silently continued by another. The name carries the two properties
71
+ # that fix the units: a normalized Levenshtein distance, and `best_window=False`,
72
+ # which scores only the candidate's LEADING len(input) characters. Turning
73
+ # `best_window` on can only lower distances, so it would shift every bin without
74
+ # changing the field name.
75
+ _FUZZY_MATCHER_VERSION = "levenshtein-leading-window/1"
76
+
77
+ # Reported when the classifier artifacts are not under the R4 versioned layout. A
78
+ # tree that has never been trained under versioning has no version to report, and
79
+ # saying so is better than inventing one that would look comparable across runs.
80
+ _UNVERSIONED_ARTIFACT = "unversioned"
81
+
82
+ # Reported when the router did not say which model answered. Named rather than
83
+ # defaulted to "tiny", because a signal_version that claims the wrong tier is worse
84
+ # than one that admits it does not know.
85
+ _UNKNOWN_MODEL_TIER = "unknown-tier"
86
+
87
+
88
+ def escalation_outcome_of(predictions: list[str]) -> str:
89
+ """What became of an escalation signal in *predictions*.
90
+
91
+ The classifier returns one label when it was confident and its top-k when it was
92
+ not (``CommandRouter.predict_with_details``), so a lone escalation label is one
93
+ the runtime acted on — it resolves to ``command_name=None`` and the CME wildcard
94
+ command walks the parent chain. An escalation label ranked among several is the
95
+ GAP-18 case: the ambiguity message filters non-routable labels out, so the user
96
+ sees the local candidates and the "try my parent" signal goes nowhere.
97
+
98
+ Reports an outcome, never a decision: the caller stores the result on the span
99
+ and the resolution path never reads it back.
100
+ """
101
+ if not any(is_escalation(prediction) for prediction in predictions):
102
+ return ESCALATION_OUTCOME_ABSENT
103
+ if len(predictions) == 1:
104
+ return ESCALATION_OUTCOME_HONORED
105
+ return ESCALATION_OUTCOME_SUPPRESSED_BY_AMBIGUITY
106
+
107
+
108
+ def _classifier_signal_version(model_artifact_path: str, model_tier: str) -> str:
109
+ """Identity of the trained artifact behind a classifier signal.
110
+
111
+ `signal_version` exists so that a retrained classifier is distinguishable: a
112
+ confidence of 0.8 from one artifact is not the same measurement as a
113
+ confidence of 0.8 from the next, and FW-REQ-021 clause 13 requires thresholds
114
+ be re-validated when the producing artifact changes.
115
+
116
+ Under R4 versioning the per-context entry in ``___command_info`` is a
117
+ compatibility link into ``___command_info/versions/<version>/<context>``, so one
118
+ ``realpath`` recovers the published version without importing the trainer's
119
+ resolver or re-reading its pointer file on every prediction. The ``*`` to
120
+ ``global`` mapping mirrors ``CommandRouter.__init__``, which does the same
121
+ substitution before opening the artifacts.
122
+ """
123
+ resolved = os.path.realpath(
124
+ model_artifact_path.replace('*', GLOBAL_CONTEXT_FOLDER)
125
+ )
126
+ versions_parent = os.path.dirname(os.path.dirname(resolved))
127
+ version = (
128
+ os.path.basename(os.path.dirname(resolved))
129
+ if os.path.basename(versions_parent) == VERSIONS_DIRNAME
130
+ else _UNVERSIONED_ARTIFACT
131
+ )
132
+ return f"intent-classifier/{version}/{os.path.basename(resolved)}/{model_tier}"
133
+
134
+
135
+ def _topk_margin_signals(
136
+ classifier_details: dict, signal_version: str
137
+ ) -> list[UncertaintySignal]:
138
+ """The gap between the top two label probabilities, as a 0- or 1-item list.
139
+
140
+ A list rather than an Optional so the caller has nothing to test. That is not
141
+ style: `nlu_trace["classifier"]` belongs to `CommandRouter`, and the test
142
+ doubles that inject labels return it EMPTY on purpose, so the emitter has to
143
+ tolerate a router that reports no top-k exactly as it tolerates one that
144
+ reports no `model_tier`. Pairing the kept scores with their own tail yields one
145
+ pair for two scores and no pair at all for fewer, which gets that tolerance
146
+ without comparing a captured measurement against anything (arch §17.3).
147
+
148
+ The scores are positionally aligned with `topk_labels` and come from the single
149
+ forward pass `predict_batch` already made, so this is a projection of a number
150
+ that was computed and thrown away, not a new measurement.
151
+ """
152
+ top_two = list(classifier_details.get("topk_scores") or ())[:2]
153
+ return [
154
+ classifier_topk_margin(
155
+ float(first) - float(second), signal_version=signal_version
156
+ )
157
+ for first, second in zip(top_two, top_two[1:])
158
+ ]
159
+
160
+
161
+ def command_identity_uncertainty(nlu_trace: dict) -> DecisionUncertainty:
162
+ """The §6.6.1 record for the command-identity decision *nlu_trace* describes.
163
+
164
+ A pure function of the facts ``_predict_impl`` recorded, and deliberately
165
+ one-way: it reads the capture bag and writes nothing back, so the resolution
166
+ path cannot come to depend on what is being measured about it. That is the
167
+ EXP-003 exit criterion and the architecture §17.3 stop condition — capture
168
+ only, no threshold, no branch.
169
+
170
+ It reads ``matcher_layer``, which is the name of the branch that has already
171
+ run, not a measurement of it. No confidence, distance, count, or assembled
172
+ record is read by anything other than ``tracing.end_span``.
173
+ """
174
+ matcher_layer = nlu_trace.get("matcher_layer")
175
+ signals: list[UncertaintySignal] = []
176
+ signals_absent_reason = None
177
+ # Every tier that enumerates candidates records how many; the tiers below that
178
+ # do not enumerate resolved to exactly one command, or to none at all.
179
+ candidate_count = nlu_trace.get("candidate_count", 1)
180
+
181
+ if matcher_layer == "exact_prefix":
182
+ # An exact command-name match has nothing to be unsure about. Emitting
183
+ # confidence 1.0 here would enter a calibration curve as a real
184
+ # measurement of a classifier that never ran.
185
+ signals_absent_reason = "deterministic-resolution"
186
+ elif matcher_layer == "clarification_default":
187
+ # 'what can i do?' is a constant the code substitutes when no matcher
188
+ # claimed the utterance in a clarification stage. Nothing was measured and
189
+ # nothing could have been, so this is deterministic in the same sense as an
190
+ # exact match rather than an uncaptured measurement.
191
+ signals_absent_reason = "deterministic-resolution"
192
+ elif matcher_layer == "fuzzy_prematch":
193
+ signals.extend([
194
+ fuzzy_distance(
195
+ nlu_trace["fuzzy_distance"], signal_version=_FUZZY_MATCHER_VERSION
196
+ ),
197
+ ambiguity_set_size(candidate_count, signal_version=_FUZZY_MATCHER_VERSION),
198
+ ])
199
+ elif matcher_layer == "embedding_cache":
200
+ # The cosine similarity that decided this is on the span as
201
+ # `cache_similarity`, but §6.6.1's SignalKind enum has no member for an
202
+ # embedding similarity, so the structured record genuinely cannot carry it.
203
+ # "not-instrumented" is the honest report of a decision whose measurement
204
+ # the contract cannot represent yet.
205
+ signals_absent_reason = "not-instrumented"
206
+ elif matcher_layer == "classifier":
207
+ signal_version = nlu_trace["classifier_signal_version"]
208
+ signals.append(
209
+ classifier_confidence(
210
+ nlu_trace["classifier"]["confidence"], signal_version=signal_version
211
+ )
212
+ )
213
+ # classifier-topk-margin is missing on purpose: `predict_with_details`
214
+ # returns the winning label's probability and the top-k label NAMES, and
215
+ # `predict_single_sentence` drops `top_k_scores` from what `predict_batch`
216
+ # computed. There is no second-best probability to subtract, and inventing
217
+ # one is worse than its absence.
218
+ #
219
+ # Amendment (fix-ajv.12): it no longer is. `predict_single_sentence` now
220
+ # carries `top_k_scores` through to `predict_with_details`, so the
221
+ # second-best probability is a fact the same forward pass already produced.
222
+ # It stays absent when the details dict does not carry it — a stubbed
223
+ # router, or a record written before this — which is why the helper returns
224
+ # a list instead of raising on a missing key.
225
+ signals.extend(
226
+ _topk_margin_signals(nlu_trace["classifier"], signal_version)
227
+ )
228
+ signals.append(
229
+ ambiguity_set_size(candidate_count, signal_version=signal_version)
230
+ )
231
+ else:
232
+ # No matcher claimed the utterance. The fuzzy pre-match did run and compared
233
+ # a distance against the threshold, but `find_best_matches` returns
234
+ # ``([], None)`` above the threshold and discards the value, so the one
235
+ # measurement taken here is not recoverable at this call site.
236
+ signals_absent_reason = "not-instrumented"
237
+ candidate_count = 0
238
+
239
+ return DecisionUncertainty(
240
+ decision_kind="command-identity",
241
+ signals=tuple(signals),
242
+ candidate_count=candidate_count,
243
+ reducible=nlu_trace.get("reducible"),
244
+ signals_absent_reason=signals_absent_reason,
245
+ )
246
+
247
+
248
+ def _recorded_command_identity_uncertainty(
249
+ span, nlu_trace: dict
250
+ ) -> Optional[dict]:
251
+ """``command_identity_uncertainty`` in span-attribute form, never raising.
252
+
253
+ Skipped entirely when *span* is None. ``start_span`` declines without a sink
254
+ or an open turn and ``end_span`` then discards its attributes, so assembling a
255
+ record nobody will store is pure overhead against this slice's declared
256
+ budget — and it keeps the promise the test doubles in
257
+ ``test_intent_detection_fuzzy_tie.py`` were written against, that a stubbed
258
+ classifier's details reach nothing but a span that no-ops.
259
+
260
+ ``tracing`` wraps every one of its own calls because a broken recorder must
261
+ degrade to a log line rather than a failed turn, and this assembly runs just
262
+ outside that guard, in the caller's frame. Capture that can lose a user's turn
263
+ is precisely the behavior change Phase 0 exists to avoid.
264
+
265
+ A record that violates its own contract is a bug in the recorder, so it is
266
+ logged loudly and the attribute says what happened rather than going quietly
267
+ absent — which would read as "this span predates the capture" — or null, which
268
+ on the parameter-extraction side means "no decision". The pure function above
269
+ still raises, which is what the tests exercise.
270
+ """
271
+ if span is None:
272
+ return None
273
+ try:
274
+ return command_identity_uncertainty(nlu_trace).model_dump(mode="json")
275
+ except Exception as exc:
276
+ logger.warning(f"command-identity uncertainty capture failed: {exc!r}")
277
+ return {"capture_error": type(exc).__name__}
278
+
279
+
26
280
  class CommandNamePrediction:
27
281
  class Output(BaseModel):
28
282
  command_name: Optional[str] = None
@@ -37,7 +291,7 @@ class CommandNamePrediction:
37
291
 
38
292
  self.convo_path = os.path.join(self.app_workflow_folderpath, "___convo_info")
39
293
  self.cache_path = self._get_cache_path(self.app_workflow_id, self.convo_path)
40
- self.path = self._get_cache_path_cache(self.convo_path)
294
+ self.path = self._get_cache_path_cache(self.convo_path, self.app_workflow_id)
41
295
 
42
296
  def predict(self, command_context_name: str, command: str, nlu_pipeline_stage: NLUPipelineStage) -> "CommandNamePrediction.Output":
43
297
  """Predict, wrapped in a ``fw.nlu.intent`` span (D3 as amended).
@@ -49,6 +303,11 @@ class CommandNamePrediction:
49
303
  classifier), the classifier's confidence and threshold when it ran,
50
304
  and the candidate set on an ambiguity. Emission never affects the
51
305
  prediction: the helpers no-op without a bound host/sink.
306
+
307
+ It also carries the §6.6.1 ``DecisionUncertainty`` for the
308
+ command-identity decision, assembled by ``command_identity_uncertainty``
309
+ from the facts below. That record is written to the span and read by
310
+ nothing else (FW-REQ-021 P0: representation only).
52
311
  """
53
312
  host = tracing.current_host()
54
313
  span = tracing.start_span(
@@ -66,6 +325,9 @@ class CommandNamePrediction:
66
325
  command_context_name, command, nlu_pipeline_stage, nlu_trace
67
326
  )
68
327
  except BaseException:
328
+ # No DecisionUncertainty here: the prediction did not complete, so
329
+ # there is no decision to characterise. The raw facts gathered so far
330
+ # still go out.
69
331
  tracing.end_span(
70
332
  host, span, status=tracing.STATUS_ERROR, attributes=nlu_trace
71
333
  )
@@ -76,6 +338,9 @@ class CommandNamePrediction:
76
338
  status=tracing.STATUS_OK,
77
339
  attributes={
78
340
  **nlu_trace,
341
+ "decision_uncertainty": _recorded_command_identity_uncertainty(
342
+ span, nlu_trace
343
+ ),
79
344
  "command_name": output.command_name,
80
345
  "is_cme_command": output.is_cme_command,
81
346
  "ambiguous": output.error_msg is not None,
@@ -96,6 +361,11 @@ class CommandNamePrediction:
96
361
  ) -> "CommandNamePrediction.Output":
97
362
  # sourcery skip: extract-duplicate-method
98
363
 
364
+ # Set before any matching so the attribute is always present: an absent
365
+ # escalation outcome would be indistinguishable from "no escalation", and
366
+ # only the classifier below can produce an escalation label at all.
367
+ nlu_trace["escalation_outcome"] = ESCALATION_OUTCOME_NOT_EVALUATED
368
+
99
369
  model_artifact_path = f"{self.app_workflow_folderpath}/___command_info/{command_context_name}"
100
370
  command_router = CommandRouter(model_artifact_path)
101
371
 
@@ -167,11 +437,20 @@ class CommandNamePrediction:
167
437
  else:
168
438
  # Use Levenshtein distance for fuzzy matching with the full command part after @
169
439
  # No match is ([], None), never (None, None) — len() here is safe.
170
- best_matched_commands, _ = find_best_matches(
440
+ best_matched_commands, best_distance = find_best_matches(
171
441
  command.replace(" ", "_"),
172
442
  command_name_dict.keys(),
173
- threshold=0.3 # Adjust threshold as needed
443
+ threshold=_FUZZY_PREMATCH_MAX_DISTANCE
174
444
  )
445
+ # The distance used to be thrown away with `_` (FW-REQ-021 clause 1:
446
+ # a signal must not be discarded after its threshold comparison). It
447
+ # is a DISTANCE, so lower is a better match; the threshold beside it
448
+ # is a maximum, and `SIGNAL_DOMAINS["fuzzy-score"]` records that
449
+ # polarity for anyone binning these values. It is None above the
450
+ # threshold, because `find_best_matches` returns ([], None) there.
451
+ nlu_trace["fuzzy_distance"] = best_distance
452
+ nlu_trace["fuzzy_threshold"] = _FUZZY_PREMATCH_MAX_DISTANCE
453
+ nlu_trace["candidate_count"] = len(best_matched_commands)
175
454
  if (
176
455
  len(best_matched_commands) > 1
177
456
  and nlu_pipeline_stage == NLUPipelineStage.INTENT_DETECTION
@@ -195,10 +474,23 @@ class CommandNamePrediction:
195
474
 
196
475
  if nlu_pipeline_stage == NLUPipelineStage.INTENT_DETECTION:
197
476
  if not command_name:
198
- if cache_result := cache_match(self.path, command, modelpipeline, 0.85):
199
- command_name = cache_result
477
+ # return_details asks for the similarity alongside the label. The
478
+ # match itself is unchanged — the same threshold, the same winner —
479
+ # but the number it was compared against is no longer discarded
480
+ # (FW-REQ-021 clause 1). A hit is a 2-tuple, a miss is still None,
481
+ # so the walrus test behaves exactly as it did.
482
+ if cache_result := cache_match(
483
+ self.path, command, modelpipeline, 0.85, return_details=True
484
+ ):
485
+ command_name, cache_similarity = cache_result
200
486
  nlu_trace["matcher_layer"] = "embedding_cache"
201
487
  nlu_trace["cache_similarity_threshold"] = 0.85
488
+ # A cosine similarity: HIGHER is a better match, the opposite
489
+ # of the fuzzy distance above, and the threshold beside it is a
490
+ # minimum. Recorded as a plain attribute because §6.6.1's
491
+ # SignalKind enum has no member for it.
492
+ nlu_trace["cache_similarity"] = float(cache_similarity)
493
+ nlu_trace["candidate_count"] = 1
202
494
  else:
203
495
  predictions, classifier_details = (
204
496
  command_router.predict_with_details(command)
@@ -206,6 +498,16 @@ class CommandNamePrediction:
206
498
  # predictions = majority_vote_predictions(command_router, command)
207
499
  nlu_trace["matcher_layer"] = "classifier"
208
500
  nlu_trace["classifier"] = classifier_details
501
+ # `.get`, not `[...]`: the details dict belongs to
502
+ # `CommandRouter`, and a router that reports no tier must not be
503
+ # able to fail a turn from inside the resolution path. The test
504
+ # doubles that inject labels return an empty dict on purpose.
505
+ nlu_trace["classifier_signal_version"] = _classifier_signal_version(
506
+ model_artifact_path,
507
+ classifier_details.get("model_tier", _UNKNOWN_MODEL_TIER),
508
+ )
509
+ nlu_trace["candidate_count"] = len(predictions)
510
+ nlu_trace["escalation_outcome"] = escalation_outcome_of(predictions)
209
511
 
210
512
  if len(predictions)==1:
211
513
  command_name = predictions[0].split('/')[-1]
@@ -230,6 +532,11 @@ class CommandNamePrediction:
230
532
 
231
533
  # Store suggested commands
232
534
  nlu_trace["candidates"] = [str(p) for p in predictions]
535
+ # The runtime is about to ask, and the answer resolves this:
536
+ # requirements §4.14's reducible uncertainty, recorded where
537
+ # the asking happens rather than inferred afterwards from
538
+ # the shape of the record.
539
+ nlu_trace["reducible"] = True
233
540
  self._store_suggested_commands(self.path, predictions, 1)
234
541
  return CommandNamePrediction.Output(error_msg=error_msg)
235
542
 
@@ -239,6 +546,9 @@ class CommandNamePrediction:
239
546
  ) and not command_name:
240
547
  command_name = "what can i do?"
241
548
  nlu_trace["matcher_layer"] = "clarification_default"
549
+ # The fuzzy pre-match above found nothing and left a count of 0; the
550
+ # default that replaces it is one command, chosen by the code.
551
+ nlu_trace["candidate_count"] = 1
242
552
 
243
553
  fully_qualified_command_name = self.resolve_fully_qualified_command_name(
244
554
  command_name, command_name_dict)
@@ -282,13 +592,29 @@ class CommandNamePrediction:
282
592
  return os.path.join(base_dir, f"{workflow_id}.sqlite3")
283
593
 
284
594
  @staticmethod
285
- def _get_cache_path_cache(convo_path):
595
+ def _get_cache_path_cache(convo_path, workflow_id=None):
286
596
  """
287
- Generate cache file path based on workflow ID
597
+ Path to the utterance/clarification cache.
598
+
599
+ Shared across sessions by default, which is the point of the cache: a
600
+ disambiguation learned in one session helps the next.
601
+
602
+ `FW_UTTERANCE_CACHE_SCOPE=workflow` shards it by workflow id instead
603
+ (`fix-bn1` `[XR16]`). A pass^k experiment must not have correlated
604
+ attempts, and this file is read on the runtime turn path
605
+ (`cache_match`) and written on it (`store_utterance_cache`) while being
606
+ keyed on nothing -- so attempt 2 would inherit attempt 1's
607
+ disambiguation decisions, and a treatment arm would inherit the
608
+ baseline arm's, both arms running against the same workflow folder.
609
+ The sibling `_get_cache_path` is already sharded this way; this is the
610
+ same treatment, opt-in so ordinary runs keep their shared cache.
288
611
  """
289
612
  base_dir = convo_path
290
613
  # Create directory if it doesn't exist
291
614
  os.makedirs(base_dir, exist_ok=True)
615
+ scope = fastworkflow.get_env_var("FW_UTTERANCE_CACHE_SCOPE", default="shared")
616
+ if scope == "workflow" and workflow_id is not None:
617
+ return os.path.join(base_dir, f"cache-{workflow_id}.sqlite3")
292
618
  return os.path.join(base_dir, "cache.sqlite3")
293
619
 
294
620
  # Store the suggested commands with the flag type