fastworkflow 3.0.1__tar.gz → 3.1.2__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 (216) hide show
  1. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/PKG-INFO +3 -2
  2. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/README.md +1 -0
  3. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/_workflows/command_metadata_extraction/intent_detection.py +18 -1
  4. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/docs/integrate-chat-agent/reference.md +5 -1
  5. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/fastworkflow.env +16 -0
  6. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/run_fastapi_mcp/README.md +10 -0
  7. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/run_fastapi_mcp/__main__.py +205 -37
  8. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/run_fastapi_mcp/conversation_store.py +238 -9
  9. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/run_fastapi_mcp/turns.py +126 -4
  10. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/run_fastapi_mcp/utils.py +224 -4
  11. fastworkflow-3.1.2/fastworkflow/utils/fuzzy_match.py +170 -0
  12. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/utils/signatures.py +43 -6
  13. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/workflow_execution_context.py +23 -2
  14. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/pyproject.toml +2 -2
  15. fastworkflow-3.0.1/fastworkflow/utils/fuzzy_match.py +0 -70
  16. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/LICENSE +0 -0
  17. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/.DS_Store +0 -0
  18. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/__init__.py +0 -0
  19. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/_commands/.gitkeep +0 -0
  20. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/_workflows/__init__.py +0 -0
  21. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/_workflows/command_metadata_extraction/__init__.py +0 -0
  22. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/_workflows/command_metadata_extraction/_commands/ErrorCorrection/abort.py +0 -0
  23. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/_workflows/command_metadata_extraction/_commands/ErrorCorrection/you_misunderstood.py +0 -0
  24. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/_workflows/command_metadata_extraction/_commands/IntentDetection/go_up.py +0 -0
  25. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/_workflows/command_metadata_extraction/_commands/IntentDetection/reset_context.py +0 -0
  26. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/_workflows/command_metadata_extraction/_commands/IntentDetection/what_can_i_do.py +0 -0
  27. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/_workflows/command_metadata_extraction/_commands/IntentDetection/what_is_current_context.py +0 -0
  28. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/_workflows/command_metadata_extraction/_commands/__init__.py +0 -0
  29. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/_workflows/command_metadata_extraction/_commands/wildcard.py +0 -0
  30. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/_workflows/command_metadata_extraction/command_context_model.json +0 -0
  31. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/_workflows/command_metadata_extraction/parameter_extraction.py +0 -0
  32. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/active_workflow.py +0 -0
  33. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/__main__.py +0 -0
  34. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/ast_class_extractor.py +0 -0
  35. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/class_analysis_structures.py +0 -0
  36. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/cli_specification.md +0 -0
  37. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/command_dependency_resolver.py +0 -0
  38. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/command_file_generator.py +0 -0
  39. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/command_file_template.py +0 -0
  40. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/command_import_utils.py +0 -0
  41. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/command_stub_generator.py +0 -0
  42. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/context_folder_generator.py +0 -0
  43. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/context_model_generator.py +0 -0
  44. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/dependency_manager.py +0 -0
  45. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/dir_scanner.py +0 -0
  46. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/documentation_generator.py +0 -0
  47. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/genai_postprocessor.py +0 -0
  48. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/inheritance_block_regenerator.py +0 -0
  49. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/libcst_transformers.py +0 -0
  50. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/navigator_stub_generator.py +0 -0
  51. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/pydantic_model_generator.py +0 -0
  52. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/utterance_generator.py +0 -0
  53. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/cache_matching.py +0 -0
  54. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/chat_session.py +0 -0
  55. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/checkpoint_store.py +0 -0
  56. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/cli.py +0 -0
  57. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/command_context_model.py +0 -0
  58. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/command_directory.py +0 -0
  59. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/command_executor.py +0 -0
  60. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/command_interfaces.py +0 -0
  61. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/command_metadata_api.py +0 -0
  62. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/command_routing.py +0 -0
  63. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/conversation_history_io.py +0 -0
  64. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/distillation.py +0 -0
  65. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/docs/context_modules_prd.txt +0 -0
  66. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/docs/integrate-chat-agent/SKILL.md +0 -0
  67. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/extended_workflow_example/README.md +0 -0
  68. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/extended_workflow_example/_commands/WorkItem/get_status.py +0 -0
  69. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/extended_workflow_example/_commands/generate_report.py +0 -0
  70. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/extended_workflow_example/_commands/startup.py +0 -0
  71. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/extended_workflow_example/simple_workflow_template.json +0 -0
  72. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/extended_workflow_example/workflow_inheritance_model.json +0 -0
  73. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/fastworkflow.passwords.env +0 -0
  74. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/hello_world/_commands/README.md +0 -0
  75. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/hello_world/_commands/add_two_numbers.py +0 -0
  76. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/hello_world/_commands/context_inheritance_model.json +0 -0
  77. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/hello_world/application/add_two_numbers.py +0 -0
  78. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_1/_commands/context_inheritance_model.json +0 -0
  79. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_1/_commands/send_message.py +0 -0
  80. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_1/application/send_message.py +0 -0
  81. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_2/_commands/User/_User.py +0 -0
  82. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_2/_commands/User/send_message.py +0 -0
  83. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_2/_commands/context_inheritance_model.json +0 -0
  84. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_2/_commands/startup.py +0 -0
  85. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_2/application/user.py +0 -0
  86. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_3/_commands/PremiumUser/_PremiumUser.py +0 -0
  87. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_3/_commands/PremiumUser/send_priority_message.py +0 -0
  88. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_3/_commands/User/_User.py +0 -0
  89. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_3/_commands/User/send_message.py +0 -0
  90. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_3/_commands/context_inheritance_model.json +0 -0
  91. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_3/_commands/initialize_user.py +0 -0
  92. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_3/application/user.py +0 -0
  93. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_4/_commands/ChatRoom/_ChatRoom.py +0 -0
  94. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_4/_commands/ChatRoom/add_user.py +0 -0
  95. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_4/_commands/ChatRoom/broadcast_message.py +0 -0
  96. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_4/_commands/ChatRoom/get_current_user.py +0 -0
  97. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_4/_commands/ChatRoom/list_users.py +0 -0
  98. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_4/_commands/ChatRoom/set_current_user.py +0 -0
  99. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_4/_commands/PremiumUser/_PremiumUser.py +0 -0
  100. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_4/_commands/PremiumUser/send_priority_message.py +0 -0
  101. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_4/_commands/User/_User.py +0 -0
  102. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_4/_commands/User/send_message.py +0 -0
  103. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_4/_commands/context_inheritance_model.json +0 -0
  104. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_4/_commands/set_root_context.py +0 -0
  105. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_4/application/chatroom.py +0 -0
  106. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_4/application/user.py +0 -0
  107. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_4/context_hierarchy_model.json +0 -0
  108. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_4/startup_action.json +0 -0
  109. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/_commands/calculate.py +0 -0
  110. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/_commands/cancel_pending_order.py +0 -0
  111. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/_commands/exchange_delivered_order_items.py +0 -0
  112. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/_commands/find_user_id_by_email.py +0 -0
  113. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/_commands/find_user_id_by_name_zip.py +0 -0
  114. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/_commands/get_order_details.py +0 -0
  115. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/_commands/get_product_details.py +0 -0
  116. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/_commands/get_user_details.py +0 -0
  117. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/_commands/list_all_product_types.py +0 -0
  118. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/_commands/modify_pending_order_address.py +0 -0
  119. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/_commands/modify_pending_order_items.py +0 -0
  120. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/_commands/modify_pending_order_payment.py +0 -0
  121. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/_commands/modify_user_address.py +0 -0
  122. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/_commands/return_delivered_order_items.py +0 -0
  123. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/_commands/transfer_to_human_agents.py +0 -0
  124. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/context_inheritance_model.json +0 -0
  125. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/retail_data/__init__.py +0 -0
  126. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/retail_data/orders.json +0 -0
  127. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/retail_data/products.json +0 -0
  128. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/retail_data/users.json +0 -0
  129. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/tools/calculate.py +0 -0
  130. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/tools/cancel_pending_order.py +0 -0
  131. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/tools/exchange_delivered_order_items.py +0 -0
  132. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/tools/find_user_id_by_email.py +0 -0
  133. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/tools/find_user_id_by_name_zip.py +0 -0
  134. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/tools/get_order_details.py +0 -0
  135. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/tools/get_product_details.py +0 -0
  136. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/tools/get_user_details.py +0 -0
  137. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/tools/list_all_product_types.py +0 -0
  138. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/tools/modify_pending_order_address.py +0 -0
  139. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/tools/modify_pending_order_items.py +0 -0
  140. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/tools/modify_pending_order_payment.py +0 -0
  141. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/tools/modify_user_address.py +0 -0
  142. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/tools/return_delivered_order_items.py +0 -0
  143. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/tools/think.py +0 -0
  144. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/tools/tool.py +0 -0
  145. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/tools/transfer_to_human_agents.py +0 -0
  146. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/workflow_description.txt +0 -0
  147. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/__init__.py +0 -0
  148. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/_WorkItem.py +0 -0
  149. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/add_child_workitem.py +0 -0
  150. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/get_status.py +0 -0
  151. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/go_to_workitem.py +0 -0
  152. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/mark_as_complete.py +0 -0
  153. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/move_to_first_child_workitem.py +0 -0
  154. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/move_to_last_child_workitem.py +0 -0
  155. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/move_to_next_workitem.py +0 -0
  156. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/move_to_previous_workitem.py +0 -0
  157. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/remove_all_child_workitems.py +0 -0
  158. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/remove_child_workitem.py +0 -0
  159. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/show_schema.py +0 -0
  160. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/_commands/startup.py +0 -0
  161. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/application/__init__.py +0 -0
  162. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/application/workitem.py +0 -0
  163. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/simple_workflow_template.json +0 -0
  164. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/startup_action.json +0 -0
  165. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/intent_clarification_agent.py +0 -0
  166. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/kvstore.py +0 -0
  167. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/mcp_server.py +0 -0
  168. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/model_pipeline_training.py +0 -0
  169. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/nlu_labels.py +0 -0
  170. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/refine/__main__.py +0 -0
  171. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/run/__init__.py +0 -0
  172. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/run/__main__.py +0 -0
  173. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/run_fastapi_mcp/__init__.py +0 -0
  174. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/run_fastapi_mcp/checkpoint.py +0 -0
  175. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/run_fastapi_mcp/jwt_manager.py +0 -0
  176. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/run_fastapi_mcp/mcp_specific.py +0 -0
  177. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/run_fastapi_mcp/redoc_2_standalone_html.py +0 -0
  178. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/run_fastapi_mcp/server_memory.py +0 -0
  179. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/serialization_hooks.py +0 -0
  180. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/session_state_store.py +0 -0
  181. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/state_paths.py +0 -0
  182. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/state_serialization.py +0 -0
  183. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/storage_keys.py +0 -0
  184. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/train/__init__.py +0 -0
  185. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/train/__main__.py +0 -0
  186. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/train/artifact_versioning.py +0 -0
  187. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/train/class_balance.py +0 -0
  188. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/train/determinism.py +0 -0
  189. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/train/duplicate_detection.py +0 -0
  190. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/train/generate_synthetic.py +0 -0
  191. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/train/heldout_evaluation.py +0 -0
  192. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/train/param_example_cache.py +0 -0
  193. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/train/personas.py +0 -0
  194. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/train/selective_training.py +0 -0
  195. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/train/training_report.py +0 -0
  196. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/train/utterance_cache.py +0 -0
  197. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/turn.py +0 -0
  198. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/user_message_queues.py +0 -0
  199. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/utils/__init__.py +0 -0
  200. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/utils/chat_adapter.py +0 -0
  201. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/utils/context_utils.py +0 -0
  202. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/utils/dspy_cache_utils.py +0 -0
  203. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/utils/dspy_logger.py +0 -0
  204. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/utils/dspy_utils.py +0 -0
  205. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/utils/env.py +0 -0
  206. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/utils/generate_param_examples.py +0 -0
  207. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/utils/insights_loader.py +0 -0
  208. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/utils/logging.py +0 -0
  209. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/utils/parameterize_func_decorator.py +0 -0
  210. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/utils/pydantic_model_2_dspy_signature_class.py +0 -0
  211. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/utils/python_utils.py +0 -0
  212. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/utils/react.py +0 -0
  213. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/utils/startup_progress.py +0 -0
  214. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/workflow.py +0 -0
  215. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/workflow_agent.py +0 -0
  216. {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/workflow_inheritance_model.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastworkflow
3
- Version: 3.0.1
3
+ Version: 3.1.2
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
@@ -15,7 +15,7 @@ Provides-Extra: training
15
15
  Requires-Dist: aiohttp (>=3.14.3)
16
16
  Requires-Dist: colorama (>=0.4.6,<0.5.0)
17
17
  Requires-Dist: cryptography (>=50.0.0)
18
- Requires-Dist: datasets (>=4.0.0,<5.0.0) ; extra == "training"
18
+ Requires-Dist: datasets (>=5.0.1,<6.0.0) ; extra == "training"
19
19
  Requires-Dist: dspy (>=3.0.1,<4.0.0)
20
20
  Requires-Dist: fastapi (>=0.120.1) ; extra == "server" or extra == "fastapi"
21
21
  Requires-Dist: fastapi-mcp (>=0.4.0,<0.5.0) ; extra == "server" or extra == "fastapi"
@@ -626,6 +626,7 @@ Two files per workflow (templates ship with `fastworkflow examples fetch`).
626
626
  | `LLM_PLANNER` | Model for the agent's task planner | `run` (agent) | `mistral/mistral-small-latest` |
627
627
  | `LLM_AGENT` | Model for the DSPy agent | `run` (agent) | `mistral/mistral-small-latest` |
628
628
  | `LLM_CONVERSATION_STORE` | Model for conversation topic/summary | FastAPI service | `mistral/mistral-small-latest` |
629
+ | `LLM_CONVERSATION_STORE_TIMEOUT_SECONDS` | Per-attempt client-side deadline for the topic/summary call (2 attempts max) | Optional | `12` |
629
630
  | `LITELLM_PROXY_API_BASE` | LiteLLM Proxy URL | with `litellm_proxy/` models | *not set* |
630
631
  | `INTENT_DETECTION_TINY_MODEL` | HF id for the small intent model | `train` (optional) | `google/bert_uncased_L-4_H-128_A-2` |
631
632
  | `INTENT_DETECTION_LARGE_MODEL` | HF id for the large intent model | `train` (optional) | `distilbert-base-uncased` |
@@ -572,6 +572,7 @@ Two files per workflow (templates ship with `fastworkflow examples fetch`).
572
572
  | `LLM_PLANNER` | Model for the agent's task planner | `run` (agent) | `mistral/mistral-small-latest` |
573
573
  | `LLM_AGENT` | Model for the DSPy agent | `run` (agent) | `mistral/mistral-small-latest` |
574
574
  | `LLM_CONVERSATION_STORE` | Model for conversation topic/summary | FastAPI service | `mistral/mistral-small-latest` |
575
+ | `LLM_CONVERSATION_STORE_TIMEOUT_SECONDS` | Per-attempt client-side deadline for the topic/summary call (2 attempts max) | Optional | `12` |
575
576
  | `LITELLM_PROXY_API_BASE` | LiteLLM Proxy URL | with `litellm_proxy/` models | *not set* |
576
577
  | `INTENT_DETECTION_TINY_MODEL` | HF id for the small intent model | `train` (optional) | `google/bert_uncased_L-4_H-128_A-2` |
577
578
  | `INTENT_DETECTION_LARGE_MODEL` | HF id for the large intent model | `train` (optional) | `distilbert-base-uncased` |
@@ -111,12 +111,29 @@ class CommandNamePrediction:
111
111
  command = command.replace(f"{tentative_command_name}", "").strip().replace(" ", " ")
112
112
  else:
113
113
  # Use Levenshtein distance for fuzzy matching with the full command part after @
114
+ # No match is ([], None), never (None, None) — len() here is safe.
114
115
  best_matched_commands, _ = find_best_matches(
115
116
  command.replace(" ", "_"),
116
117
  command_name_dict.keys(),
117
118
  threshold=0.3 # Adjust threshold as needed
118
119
  )
119
- if best_matched_commands:
120
+ if (
121
+ len(best_matched_commands) > 1
122
+ and nlu_pipeline_stage == NLUPipelineStage.INTENT_DETECTION
123
+ ):
124
+ # Commands sharing a prefix tie at distance 0, because scoring
125
+ # compares only the leading len(input) characters, and
126
+ # command_name_dict iterates a set — so picking [0] would choose
127
+ # nondeterministically between them across processes. Leave the
128
+ # name unset so the classifier and its ambiguity prompt decide.
129
+ # The clarification stages are deliberately excluded: they have
130
+ # no classifier to fall back to.
131
+ logger.warning(
132
+ f"Fuzzy pre-match tied across {best_matched_commands} for "
133
+ f"utterance '{command}' in context '{command_context_name}'. "
134
+ "Deferring to the classifier instead of picking one."
135
+ )
136
+ elif best_matched_commands:
120
137
  command_name = best_matched_commands[0]
121
138
 
122
139
  if nlu_pipeline_stage == NLUPipelineStage.INTENT_DETECTION:
@@ -168,7 +168,11 @@ class ResponseGenerator:
168
168
  ```
169
169
  - Use `default="NOT_FOUND"` for parameters so missing values are detected, not hallucinated.
170
170
  - Use `Field` `description` + `examples` (and `pattern`/`min_length` where helpful) to drive accurate parameter extraction.
171
- - Optional hooks: `db_lookup(workflow_snapshot, command)`, `process_extracted_parameters(...)`.
171
+ - Optional hooks: `db_lookup(workflow, field_name, field_value) -> tuple[bool, str | None, list[str]]`,
172
+ `process_extracted_parameters(...)`. `db_lookup` runs for fields marked
173
+ `json_schema_extra={'db_lookup': True}` and has three return states: `(True, value, [])`
174
+ overwrites the field, `(False, None, [suggestions])` fails validation, and
175
+ `(False, None, [])` does neither — use that last one for a value your hook does not own.
172
176
  - For LLM-generated responses, use `fastworkflow.utils.dspy_utils.dspySignature(Signature.Input, Signature.Output)` with `dspy.Predict`.
173
177
 
174
178
  ### Context model — `_commands/context_inheritance_model.json`
@@ -9,6 +9,22 @@ LLM_PLANNER=mistral/mistral-small-latest
9
9
  LLM_AGENT=mistral/mistral-small-latest
10
10
  LLM_CONVERSATION_STORE=mistral/mistral-small-latest
11
11
 
12
+ # Client-side deadline, in seconds, for ONE LLM_CONVERSATION_STORE attempt
13
+ # (conversation topic/summary generation). Defaults to 12.
14
+ #
15
+ # This is a deadline on the request, not on whoever is awaiting it, and that is
16
+ # the point: the call runs in a worker thread, and a timeout on the await would
17
+ # cancel the wait while the thread kept running - which at process exit is
18
+ # joined for up to 300 s. Bounding the request is what lets the thread end by
19
+ # itself.
20
+ #
21
+ # The call gets at most two attempts (one retry), so the worst case is about
22
+ # twice this value. Keep the product inside the server's 30 s shutdown drain, or
23
+ # a rotate in flight when the process stops can outlive it. Do not set this at
24
+ # or below the normal latency of a topic+summary generation (a few seconds):
25
+ # every merely-slow /new_conversation then fails with HTTP 500.
26
+ # LLM_CONVERSATION_STORE_TIMEOUT_SECONDS=12
27
+
12
28
  # ============================================================================
13
29
  # LiteLLM Proxy Configuration (Optional)
14
30
  # ============================================================================
@@ -48,12 +48,22 @@ Configure in your environment (loaded at process startup via CLI args or env loa
48
48
  |----------|-------------|----------|---------|
49
49
  | `FASTWORKFLOW_STATE_ROOT` | Absolute root for all persistent state (conversations, suspended sessions, checkpoints) | No | `~/.local/state/fastworkflow` |
50
50
  | `FASTWORKFLOW_WORKFLOW_ID` | Overrides the per-workflow state namespace (defaults to the workflow folder name) | No | *workflow folder name* |
51
+ | `LLM_CONVERSATION_STORE_TIMEOUT_SECONDS` | Per-attempt client-side deadline for the topic/summary call, in seconds. At most 2 attempts, so the wall-clock worst case is about twice this plus backoff — kept under the 30s shutdown drain on purpose | No | `12` |
51
52
  | `--expect_encrypted_jwt` | Enable full JWT signature verification (pass flag to require signed tokens) | No | False (no verification by default) |
52
53
 
54
+ Set these in the workflow's `fastworkflow.env`, not as shell exports: values are read from the env files loaded at startup.
55
+
53
56
  Notes:
54
57
  - Conversation DBs are stored under `FASTWORKFLOW_STATE_ROOT/workflows/<workflow-id>/conversations` (directory is auto-created).
55
58
  - `/conversations` now accepts a `limit` query parameter (default `20`).
56
59
  - Shutdown waits up to 30 seconds for active turns (hard-coded).
60
+ - Shutdown does **not** generate conversation topics or summaries. It makes any
61
+ unsaved turns durable and leaves the placeholder blank topic. A conversation is
62
+ labeled when it is used as a chat instead: on its first completed chat turn, on
63
+ `POST /activate_conversation` if still blank, and on `POST /new_conversation`.
64
+ Labels refresh as a thread grows, at turn counts 1, 4, 16, 64, … A blank `topic`
65
+ in `GET /conversations` means "no successful title yet" — a conversation that
66
+ only ever ran `/initialize` or `/perform_action` keeps it, by design.
57
67
 
58
68
  ## Auth Modes
59
69
 
@@ -62,10 +62,13 @@ from .mcp_specific import setup_mcp
62
62
  from .utils import (
63
63
  get_channelconversations_dir,
64
64
  CHECKPOINT_REAP_INTERVAL_SECONDS,
65
+ SHUTDOWN_DRAIN_SECONDS,
65
66
  ChannelSessionManager,
66
67
  MAX_CONVERSATION_TURNS_IN_MEMORY,
68
+ ensure_topic_and_summary,
67
69
  save_conversation_incremental,
68
70
  save_last_turn_feedback,
71
+ try_ensure_topic_and_summary,
69
72
  InitializationRequest,
70
73
  TokenResponse,
71
74
  InitializeResponse,
@@ -105,6 +108,10 @@ from .conversation_store import (
105
108
  ConversationStore,
106
109
  ConversationSummary,
107
110
  generate_topic_and_summary,
111
+ resolve_topic_generation_timeout,
112
+ topic_generation_timeout_seconds,
113
+ TOPIC_GENERATION_MAX_RETRIES,
114
+ TOPIC_GENERATION_TIMEOUT_ENV_VAR,
108
115
  extract_turns_from_history,
109
116
  restore_history_from_turns
110
117
  )
@@ -360,6 +367,24 @@ async def lifespan(_app: FastAPI):
360
367
  resolved, source = session_manager.configure_max_live_sessions()
361
368
  logger.info(f"max_live_sessions={resolved} (source={source})")
362
369
 
370
+ # Same reasoning for the topic/summary deadline: resolved once here so a
371
+ # malformed value rejects startup instead of failing every rotate at
372
+ # runtime, and logged with its source so an operator can see whether
373
+ # their override was actually picked up.
374
+ timeout, timeout_source = resolve_topic_generation_timeout()
375
+ attempts = TOPIC_GENERATION_MAX_RETRIES + 1
376
+ logger.info(
377
+ f"{TOPIC_GENERATION_TIMEOUT_ENV_VAR}={timeout} "
378
+ f"(source={timeout_source}), attempts={attempts}"
379
+ )
380
+ if timeout * attempts >= SHUTDOWN_DRAIN_SECONDS:
381
+ logger.warning(
382
+ f"{TOPIC_GENERATION_TIMEOUT_ENV_VAR}={timeout} over {attempts} "
383
+ f"attempt(s) can exceed the {SHUTDOWN_DRAIN_SECONDS}s shutdown "
384
+ "drain, so a conversation rotate in flight when this process is "
385
+ "asked to stop may be killed part-way through"
386
+ )
387
+
363
388
  # Mark FastWorkflow as initialized for readiness probe
364
389
  readiness_state.set_initialized(True)
365
390
 
@@ -405,7 +430,28 @@ async def lifespan(_app: FastAPI):
405
430
  return remaining
406
431
 
407
432
  async def finalize_conversations_on_shutdown(skip_channel_ids: list[str]) -> None:
408
- logger.info("Finalizing conversations with topic and summary...")
433
+ """Make each quiescent channel's unsaved turns durable. Never label them.
434
+
435
+ Shutdown generates no topics or summaries. That was one synchronous
436
+ LLM_CONVERSATION_STORE call per live channel, untimed and unbudgeted, in
437
+ front of the checkpoints stop_all_chat_sessions writes — so under a 30 s
438
+ termination grace period the process was killed part-way through
439
+ generating titles and lost everything each channel had accumulated since
440
+ its last eviction. Conversations are left with the blank placeholder
441
+ topic and summary the store already writes for them.
442
+
443
+ What is worth doing here is the durable save. A turn persists from inside
444
+ _run_turn's own try, so a turn whose work raised leaves in-memory history
445
+ that was never appended — including on a channel that never got as far as
446
+ reserving a conversation id. save_conversation_incremental is the same
447
+ chokepoint the turn path uses: it reserves that id when there is none,
448
+ appends only turns above the high-water mark, and creates the record with
449
+ the blank placeholders rather than inventing a topic to store.
450
+ """
451
+ logger.info(
452
+ "Persisting unsaved conversation turns at shutdown; conversations are "
453
+ "left with placeholder topic and summary (no topic generation)"
454
+ )
409
455
  skip = set(skip_channel_ids)
410
456
  for channel_id in list(session_manager._sessions.keys()):
411
457
  if channel_id in skip:
@@ -413,20 +459,17 @@ async def lifespan(_app: FastAPI):
413
459
  runtime = await session_manager.get_session(channel_id)
414
460
  if not runtime:
415
461
  continue
416
- if turns := _conversation_turns_for_summary(runtime):
417
- try:
418
- topic, summary = generate_topic_and_summary(turns)
419
- if runtime.active_conversation_id > 0:
420
- runtime.conversation_store.update_conversation_topic_summary(
421
- runtime.active_conversation_id, topic, summary
422
- )
423
- logger.info(f"Finalized conversation {runtime.active_conversation_id} for user {channel_id} during shutdown")
424
- else:
425
- logger.warning(f"Conversation history exists but no active_conversation_id for user {channel_id} during shutdown")
426
- conv_id = runtime.conversation_store.save_conversation(topic, summary, turns)
427
- logger.info(f"Created conversation {conv_id} for user {channel_id} during shutdown")
428
- except Exception as e:
429
- logger.error(f"Failed to finalize conversation for user {channel_id} during shutdown: {e}")
462
+ try:
463
+ if appended := save_conversation_incremental(
464
+ runtime, extract_turns_from_history, logger
465
+ ):
466
+ logger.info(
467
+ f"Persisted {appended} unsaved turn(s) to conversation "
468
+ f"{runtime.active_conversation_id} for user {channel_id} "
469
+ "during shutdown"
470
+ )
471
+ except Exception as e:
472
+ logger.error(f"Failed to finalize conversation for user {channel_id} during shutdown: {e}")
430
473
 
431
474
  async def reap_checkpoints_periodically() -> None:
432
475
  """Drive checkpoint and pending-state retention. Neither store has a timer.
@@ -537,7 +580,14 @@ async def lifespan(_app: FastAPI):
537
580
  reaper.cancel()
538
581
  with contextlib.suppress(asyncio.CancelledError):
539
582
  await reaper
540
- still_busy = await wait_for_active_turns_to_complete(max_wait_seconds=30)
583
+ still_busy = await wait_for_active_turns_to_complete(
584
+ max_wait_seconds=SHUTDOWN_DRAIN_SECONDS
585
+ )
586
+ # Order between these two is no longer safety-critical: with topic
587
+ # generation gone both are bounded local SQLite writes, so neither can
588
+ # consume the termination grace period and leave the other unrun. It
589
+ # stays as it is because finalize reads the execution contexts that
590
+ # stop_all_chat_sessions closes.
541
591
  await finalize_conversations_on_shutdown(still_busy)
542
592
  await stop_all_chat_sessions(still_busy)
543
593
  logger.info("FastWorkflow FastAPI service shutdown complete")
@@ -1244,10 +1294,15 @@ async def invoke_agent_stream(
1244
1294
  return turn_output
1245
1295
 
1246
1296
  async def owned_turn(execn) -> None:
1247
- try:
1248
- await run_owned_turn(
1249
- runtime, turn_registry, execn, streaming_work, session_manager
1250
- )
1297
+ async def finish_stream() -> None:
1298
+ """Deliver the terminal event, then end the body.
1299
+
1300
+ Handed to run_owned_turn as its on_done so the response completes
1301
+ as soon as the turn does, instead of waiting on the post-turn
1302
+ conversation labeling. The error event has to go out ahead of the
1303
+ sentinel: the body's drain loop stops at the sentinel, so anything
1304
+ emitted after it is never read.
1305
+ """
1251
1306
  if execn.error is not None:
1252
1307
  await emit(
1253
1308
  "error",
@@ -1258,7 +1313,23 @@ async def invoke_agent_stream(
1258
1313
  )
1259
1314
  },
1260
1315
  )
1316
+ await events.put(None)
1317
+
1318
+ try:
1319
+ await run_owned_turn(
1320
+ runtime,
1321
+ turn_registry,
1322
+ execn,
1323
+ streaming_work,
1324
+ session_manager,
1325
+ on_done=finish_stream,
1326
+ )
1261
1327
  finally:
1328
+ # Kept as the guarantee, not the normal path: finish_stream has
1329
+ # already ended the body. A second sentinel is harmless (the drain
1330
+ # loop stopped at the first and never reads it), whereas a missing
1331
+ # one - run_owned_turn raising before its finally, or finish_stream
1332
+ # itself failing - would hang the client forever.
1262
1333
  await events.put(None)
1263
1334
 
1264
1335
  try:
@@ -1497,8 +1568,15 @@ async def new_conversation(
1497
1568
  ) -> dict[str, str]:
1498
1569
  """
1499
1570
  Persist the current conversation and start a new one.
1500
- Generates topic and summary synchronously; on failure, does not rotate.
1501
-
1571
+ Generates topic and summary off the event loop; on failure, does not rotate.
1572
+
1573
+ Rotate stays strict (fix-dzs.4). It is an explicit user action, and archiving
1574
+ a thread the user can no longer find - /activate_conversation looks
1575
+ conversations up by topic - is worse than telling them it did not happen.
1576
+ What changes is that the failure is now prompt: the generation is bounded at
1577
+ the LLM client, so this answers within that bound instead of hanging on an
1578
+ unresponsive provider.
1579
+
1502
1580
  Requires a valid JWT access token in the Authorization header (Bearer token format).
1503
1581
  """
1504
1582
  channel_id = session.channel_id
@@ -1513,20 +1591,86 @@ async def new_conversation(
1513
1591
  # Serialize ctx mutation with turns; reject if a turn is active (§3.4).
1514
1592
  _reject_if_busy(channel_id)
1515
1593
  async with runtime.lock:
1594
+ # Persist before labeling (fix-dzs.9). A rotate clears the
1595
+ # in-memory history, so anything not yet durable at that moment is
1596
+ # gone - and a turn persists from inside _run_turn's own try, so a
1597
+ # store failure there leaves history holding turns with no record
1598
+ # and, if a previous rotate had already reserved an id, no way to
1599
+ # write one. Labeling that state raised ValueError out of
1600
+ # update_conversation_topic_summary AFTER paying for the
1601
+ # generation, so the rotate that would have cleaned the state up
1602
+ # was the one thing the user could not do. This is the same
1603
+ # chokepoint shutdown uses: it reserves the id when there is none,
1604
+ # appends only what is missing, and creates the record - which is
1605
+ # what makes the label write below well-defined.
1606
+ save_conversation_incremental(
1607
+ runtime, extract_turns_from_history, logger
1608
+ )
1609
+
1516
1610
  # Summarize the whole conversation, not just the in-memory window
1517
1611
  if turns := _conversation_turns_for_summary(runtime):
1518
- # Generate topic and summary synchronously (turns already saved incrementally)
1519
- topic, summary = generate_topic_and_summary(turns)
1520
-
1521
- # Update topic/summary for the conversation (turns already persisted)
1612
+ # Through the shared helper, so a rotate and a lazy fill
1613
+ # cannot both be generating for this channel at once and both
1614
+ # inherit the blank-topic policy. force=True because a rotate
1615
+ # is an explicit user action: it labels whether or not the
1616
+ # refresh schedule says the label is due, and the turns are
1617
+ # passed in because this call site is the only one that can
1618
+ # summarize a conversation with no durable record yet.
1619
+ #
1620
+ # The generation happens in a worker thread inside the helper.
1621
+ # DSPy is synchronous, so calling it here stalled the whole
1622
+ # event loop - every other channel's request included - for
1623
+ # the length of an LLM round trip. There is deliberately no
1624
+ # asyncio.wait_for around it; see the helper's docstring.
1625
+ #
1626
+ # Still inside runtime.lock: this serializes conversation-state
1627
+ # mutation, and the rotate below depends on it.
1628
+ try:
1629
+ topic, summary = await ensure_topic_and_summary(
1630
+ runtime,
1631
+ generate_topic_and_summary,
1632
+ turns=turns,
1633
+ force=True,
1634
+ )
1635
+ except Exception as e:
1636
+ # "Labeling", not "generation": this covers the generation
1637
+ # AND the write that follows it, and saying the wrong one
1638
+ # sends an operator to the wrong place. The turns really are
1639
+ # safe here - the incremental save above ran before any of
1640
+ # this - so that half of the message is now a fact rather
1641
+ # than a hope.
1642
+ logger.error(
1643
+ f"Could not label the conversation for session {channel_id}, "
1644
+ f"not rotating: {type(e).__name__}: {e}"
1645
+ )
1646
+ raise HTTPException(
1647
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1648
+ detail=(
1649
+ f"Could not label the conversation for channel_id: "
1650
+ f"{channel_id}; it was NOT rotated. Its turns are "
1651
+ f"durably recorded. Generation is given "
1652
+ f"{TOPIC_GENERATION_MAX_RETRIES + 1} attempt(s) of "
1653
+ f"{TOPIC_GENERATION_TIMEOUT_ENV_VAR}="
1654
+ f"{topic_generation_timeout_seconds()}s each "
1655
+ f"(LLM_CONVERSATION_STORE). "
1656
+ f"Cause: {type(e).__name__}: {e}"
1657
+ ),
1658
+ ) from e
1659
+
1660
+ # Topic/summary written by the helper above for the
1661
+ # conversation (turns already persisted)
1522
1662
  if runtime.active_conversation_id > 0:
1523
1663
  conv_id = runtime.active_conversation_id
1524
- runtime.conversation_store.update_conversation_topic_summary(
1525
- conv_id, topic, summary
1526
- )
1527
1664
  logger.info(f"Finalized conversation {conv_id} with topic and summary for session {channel_id}")
1528
1665
  else:
1529
- # Edge case: conversation history exists but no active ID (shouldn't happen with incremental saves)
1666
+ # Unreachable since the incremental save moved above this
1667
+ # (fix-dzs.9), and kept only as a belt-and-braces: reaching
1668
+ # `turns` non-empty requires either in-memory turns, in
1669
+ # which case that save reserved an id and wrote a record, or
1670
+ # a durable record, which needs an id already. Left in place
1671
+ # rather than deleted because it is the one branch that
1672
+ # would still persist turns if that reasoning is ever
1673
+ # invalidated, and the cost of keeping it is a log line.
1530
1674
  logger.warning(f"Conversation history exists but no active_conversation_id for session {channel_id}")
1531
1675
  conv_id = runtime.conversation_store.save_conversation(topic, summary, turns)
1532
1676
  logger.info(f"Created conversation {conv_id} for session {channel_id}")
@@ -1681,7 +1825,18 @@ async def activate_conversation(
1681
1825
  ) -> dict[str, str]:
1682
1826
  """
1683
1827
  Activate a conversation by its conversation_id.
1684
-
1828
+
1829
+ A conversation that is still unlabeled is labeled here, from its stored turns.
1830
+ This is the trigger that covers a conversation whose turns were recorded by a
1831
+ process that died before labeling anything: opening it is the moment a title
1832
+ becomes useful, since /activate_conversation is also how conversations are
1833
+ looked up by topic. A conversation that already has a title is not
1834
+ relabeled - that would spend an LLM call to re-describe the same turns.
1835
+
1836
+ Labeling never fails the activation: on a generation failure the conversation
1837
+ is activated anyway and keeps its blank topic, which is the sentinel that
1838
+ makes the next eligible trigger retry.
1839
+
1685
1840
  Requires a valid JWT access token in the Authorization header (Bearer token format).
1686
1841
  """
1687
1842
  channel_id = session.channel_id
@@ -1693,8 +1848,11 @@ async def activate_conversation(
1693
1848
  detail=f"User session not found: {channel_id}"
1694
1849
  )
1695
1850
 
1696
- # Get conversation by ID
1697
- conv = runtime.conversation_store.get_conversation(request.conversation_id)
1851
+ # Get conversation by ID, carrying only the window that is about to be
1852
+ # restored: the rest of it would be deserialized and then dropped.
1853
+ conv = runtime.conversation_store.get_conversation_window(
1854
+ request.conversation_id, MAX_CONVERSATION_TURNS_IN_MEMORY
1855
+ )
1698
1856
  if not conv:
1699
1857
  raise HTTPException(
1700
1858
  status_code=status.HTTP_404_NOT_FOUND,
@@ -1709,14 +1867,24 @@ async def activate_conversation(
1709
1867
  # Restore the newest window of the conversation; the rest stays in
1710
1868
  # the durable record. Everything restored is already recorded, so
1711
1869
  # the high-water mark moves with it and the next incremental save
1712
- # does not append the conversation a second time.
1713
- restored_history = restore_history_from_turns(
1714
- conv["turns"][-MAX_CONVERSATION_TURNS_IN_MEMORY:]
1715
- )
1870
+ # does not append the conversation a second time. The window came
1871
+ # from the read above, so there is nothing left to slice here.
1872
+ restored_history = restore_history_from_turns(conv["turns"])
1716
1873
  runtime.execution_context._conversation_history = restored_history
1717
1874
  runtime.durable_turn_count = len(restored_history.messages)
1718
1875
  logger.info(f"Activated conversation {request.conversation_id} for session {channel_id}")
1719
1876
 
1877
+ # After the activation, so the helper labels the conversation the
1878
+ # user just opened, and inside runtime.lock for the same reason
1879
+ # the rotate is: this serializes conversation-state mutation. The
1880
+ # helper reads the whole durable conversation itself rather than
1881
+ # reusing `conv` above, which now carries only the newest window -
1882
+ # summarizing a window would silently produce "topic of the last
1883
+ # N turns".
1884
+ await try_ensure_topic_and_summary(
1885
+ runtime, generate_topic_and_summary
1886
+ )
1887
+
1720
1888
  return {"status": "ok"}
1721
1889
 
1722
1890
  except HTTPException: