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.
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/PKG-INFO +3 -2
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/README.md +1 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/_workflows/command_metadata_extraction/intent_detection.py +18 -1
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/docs/integrate-chat-agent/reference.md +5 -1
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/fastworkflow.env +16 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/run_fastapi_mcp/README.md +10 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/run_fastapi_mcp/__main__.py +205 -37
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/run_fastapi_mcp/conversation_store.py +238 -9
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/run_fastapi_mcp/turns.py +126 -4
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/run_fastapi_mcp/utils.py +224 -4
- fastworkflow-3.1.2/fastworkflow/utils/fuzzy_match.py +170 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/utils/signatures.py +43 -6
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/workflow_execution_context.py +23 -2
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/pyproject.toml +2 -2
- fastworkflow-3.0.1/fastworkflow/utils/fuzzy_match.py +0 -70
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/LICENSE +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/.DS_Store +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/__init__.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/_commands/.gitkeep +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/_workflows/__init__.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/_workflows/command_metadata_extraction/__init__.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/_workflows/command_metadata_extraction/_commands/ErrorCorrection/abort.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/_workflows/command_metadata_extraction/_commands/ErrorCorrection/you_misunderstood.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/_workflows/command_metadata_extraction/_commands/IntentDetection/go_up.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/_workflows/command_metadata_extraction/_commands/IntentDetection/reset_context.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/_workflows/command_metadata_extraction/_commands/IntentDetection/what_can_i_do.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/_workflows/command_metadata_extraction/_commands/IntentDetection/what_is_current_context.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/_workflows/command_metadata_extraction/_commands/__init__.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/_workflows/command_metadata_extraction/_commands/wildcard.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/_workflows/command_metadata_extraction/command_context_model.json +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/_workflows/command_metadata_extraction/parameter_extraction.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/active_workflow.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/__main__.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/ast_class_extractor.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/class_analysis_structures.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/cli_specification.md +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/command_dependency_resolver.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/command_file_generator.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/command_file_template.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/command_import_utils.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/command_stub_generator.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/context_folder_generator.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/context_model_generator.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/dependency_manager.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/dir_scanner.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/documentation_generator.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/genai_postprocessor.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/inheritance_block_regenerator.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/libcst_transformers.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/navigator_stub_generator.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/pydantic_model_generator.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/build/utterance_generator.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/cache_matching.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/chat_session.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/checkpoint_store.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/cli.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/command_context_model.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/command_directory.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/command_executor.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/command_interfaces.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/command_metadata_api.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/command_routing.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/conversation_history_io.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/distillation.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/docs/context_modules_prd.txt +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/docs/integrate-chat-agent/SKILL.md +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/extended_workflow_example/README.md +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/extended_workflow_example/_commands/WorkItem/get_status.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/extended_workflow_example/_commands/generate_report.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/extended_workflow_example/_commands/startup.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/extended_workflow_example/simple_workflow_template.json +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/extended_workflow_example/workflow_inheritance_model.json +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/fastworkflow.passwords.env +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/hello_world/_commands/README.md +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/hello_world/_commands/add_two_numbers.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/hello_world/_commands/context_inheritance_model.json +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/hello_world/application/add_two_numbers.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_1/_commands/context_inheritance_model.json +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_1/_commands/send_message.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_1/application/send_message.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_2/_commands/User/_User.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_2/_commands/User/send_message.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_2/_commands/context_inheritance_model.json +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_2/_commands/startup.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_2/application/user.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_3/_commands/PremiumUser/_PremiumUser.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_3/_commands/PremiumUser/send_priority_message.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_3/_commands/User/_User.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_3/_commands/User/send_message.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_3/_commands/context_inheritance_model.json +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_3/_commands/initialize_user.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_3/application/user.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_4/_commands/ChatRoom/_ChatRoom.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_4/_commands/ChatRoom/add_user.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_4/_commands/ChatRoom/broadcast_message.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_4/_commands/ChatRoom/get_current_user.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_4/_commands/ChatRoom/list_users.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_4/_commands/ChatRoom/set_current_user.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_4/_commands/PremiumUser/_PremiumUser.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_4/_commands/PremiumUser/send_priority_message.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_4/_commands/User/_User.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_4/_commands/User/send_message.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_4/_commands/context_inheritance_model.json +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_4/_commands/set_root_context.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_4/application/chatroom.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_4/application/user.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_4/context_hierarchy_model.json +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/messaging_app_4/startup_action.json +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/_commands/calculate.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/_commands/cancel_pending_order.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/_commands/exchange_delivered_order_items.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/_commands/find_user_id_by_email.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/_commands/find_user_id_by_name_zip.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/_commands/get_order_details.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/_commands/get_product_details.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/_commands/get_user_details.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/_commands/list_all_product_types.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/_commands/modify_pending_order_address.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/_commands/modify_pending_order_items.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/_commands/modify_pending_order_payment.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/_commands/modify_user_address.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/_commands/return_delivered_order_items.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/_commands/transfer_to_human_agents.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/context_inheritance_model.json +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/retail_data/__init__.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/retail_data/orders.json +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/retail_data/products.json +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/retail_data/users.json +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/tools/calculate.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/tools/cancel_pending_order.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/tools/exchange_delivered_order_items.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/tools/find_user_id_by_email.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/tools/find_user_id_by_name_zip.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/tools/get_order_details.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/tools/get_product_details.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/tools/get_user_details.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/tools/list_all_product_types.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/tools/modify_pending_order_address.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/tools/modify_pending_order_items.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/tools/modify_pending_order_payment.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/tools/modify_user_address.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/tools/return_delivered_order_items.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/tools/think.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/tools/tool.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/tools/transfer_to_human_agents.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/retail_workflow/workflow_description.txt +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/__init__.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/_WorkItem.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/add_child_workitem.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/get_status.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/go_to_workitem.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/mark_as_complete.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/move_to_first_child_workitem.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/move_to_last_child_workitem.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/move_to_next_workitem.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/move_to_previous_workitem.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/remove_all_child_workitems.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/remove_child_workitem.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/_commands/WorkItem/show_schema.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/_commands/startup.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/application/__init__.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/application/workitem.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/simple_workflow_template.json +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/examples/simple_workflow_template/startup_action.json +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/intent_clarification_agent.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/kvstore.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/mcp_server.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/model_pipeline_training.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/nlu_labels.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/refine/__main__.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/run/__init__.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/run/__main__.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/run_fastapi_mcp/__init__.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/run_fastapi_mcp/checkpoint.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/run_fastapi_mcp/jwt_manager.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/run_fastapi_mcp/mcp_specific.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/run_fastapi_mcp/redoc_2_standalone_html.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/run_fastapi_mcp/server_memory.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/serialization_hooks.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/session_state_store.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/state_paths.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/state_serialization.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/storage_keys.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/train/__init__.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/train/__main__.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/train/artifact_versioning.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/train/class_balance.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/train/determinism.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/train/duplicate_detection.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/train/generate_synthetic.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/train/heldout_evaluation.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/train/param_example_cache.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/train/personas.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/train/selective_training.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/train/training_report.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/train/utterance_cache.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/turn.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/user_message_queues.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/utils/__init__.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/utils/chat_adapter.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/utils/context_utils.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/utils/dspy_cache_utils.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/utils/dspy_logger.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/utils/dspy_utils.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/utils/env.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/utils/generate_param_examples.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/utils/insights_loader.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/utils/logging.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/utils/parameterize_func_decorator.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/utils/pydantic_model_2_dspy_signature_class.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/utils/python_utils.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/utils/react.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/utils/startup_progress.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/workflow.py +0 -0
- {fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/workflow_agent.py +0 -0
- {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.
|
|
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 (>=
|
|
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
|
|
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:
|
{fastworkflow-3.0.1 → fastworkflow-3.1.2}/fastworkflow/docs/integrate-chat-agent/reference.md
RENAMED
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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(
|
|
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
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
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
|
|
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
|
-
#
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
#
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
1714
|
-
|
|
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:
|