fastworkflow 2.17.18__py3-none-any.whl → 2.17.20__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastworkflow/cli.py +1 -3
- fastworkflow/refine/__main__.py +3 -38
- fastworkflow/train/__main__.py +26 -0
- fastworkflow/train/generate_synthetic.py +17 -1
- fastworkflow/utils/python_utils.py +9 -2
- fastworkflow/utils/signatures.py +0 -1
- {fastworkflow-2.17.18.dist-info → fastworkflow-2.17.20.dist-info}/METADATA +6 -3
- {fastworkflow-2.17.18.dist-info → fastworkflow-2.17.20.dist-info}/RECORD +11 -12
- fastworkflow/utils/command_dependency_graph.py +0 -364
- {fastworkflow-2.17.18.dist-info → fastworkflow-2.17.20.dist-info}/LICENSE +0 -0
- {fastworkflow-2.17.18.dist-info → fastworkflow-2.17.20.dist-info}/WHEEL +0 -0
- {fastworkflow-2.17.18.dist-info → fastworkflow-2.17.20.dist-info}/entry_points.txt +0 -0
fastworkflow/cli.py
CHANGED
|
@@ -214,12 +214,10 @@ def add_refine_parser(subparsers):
|
|
|
214
214
|
"""Add subparser for the 'refine' command."""
|
|
215
215
|
parser_refine = subparsers.add_parser(
|
|
216
216
|
"refine",
|
|
217
|
-
help="Refine generated commands
|
|
217
|
+
help="Refine generated commands by enhancing metadata.",
|
|
218
218
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
219
219
|
)
|
|
220
220
|
parser_refine.add_argument('--workflow-folderpath', '-w', required=True, help='Path to the workflow folder to refine')
|
|
221
|
-
parser_refine.add_argument('--semantic-threshold', type=float, default=0.85, help='Semantic match threshold for dependency graph')
|
|
222
|
-
parser_refine.add_argument('--exact-only', action='store_true', help='Use exact name/type matching only for dependency graph')
|
|
223
221
|
|
|
224
222
|
# Lazy-import refine_main only if the user actually invokes the command
|
|
225
223
|
def _refine_main_wrapper(args):
|
fastworkflow/refine/__main__.py
CHANGED
|
@@ -5,12 +5,11 @@ import sys
|
|
|
5
5
|
import fastworkflow
|
|
6
6
|
from fastworkflow.utils.logging import logger
|
|
7
7
|
from fastworkflow.build.genai_postprocessor import run_genai_postprocessor
|
|
8
|
-
from fastworkflow.utils.command_dependency_graph import generate_dependency_graph
|
|
9
8
|
|
|
10
9
|
|
|
11
10
|
def parse_args():
|
|
12
11
|
parser = argparse.ArgumentParser(
|
|
13
|
-
description="Refine a FastWorkflow by enhancing command metadata
|
|
12
|
+
description="Refine a FastWorkflow by enhancing command metadata."
|
|
14
13
|
)
|
|
15
14
|
parser.add_argument('--workflow-folderpath', '-w', required=True, help='Path to the workflow folder to refine')
|
|
16
15
|
return parser.parse_args()
|
|
@@ -26,24 +25,6 @@ def _validate_workflow_folder(workflow_folderpath: str) -> None:
|
|
|
26
25
|
sys.exit(1)
|
|
27
26
|
|
|
28
27
|
|
|
29
|
-
def _prompt_for_action(non_interactive_choice: int | None = None) -> int:
|
|
30
|
-
"""Prompt user to choose refine action. Returns 1, 2, or 3. Default is 1."""
|
|
31
|
-
# If a non-interactive choice is provided (e.g., tests), validate and use it
|
|
32
|
-
if non_interactive_choice in {1, 2, 3}:
|
|
33
|
-
return int(non_interactive_choice)
|
|
34
|
-
print("Select refine action:")
|
|
35
|
-
print(" 1) Generate dependency graph (default)")
|
|
36
|
-
print(" 2) Refine command metadata")
|
|
37
|
-
print(" 3) Do both")
|
|
38
|
-
while True:
|
|
39
|
-
choice = input("Enter choice [1/2/3]: ").strip()
|
|
40
|
-
if choice == "":
|
|
41
|
-
return 1
|
|
42
|
-
if choice in {"1", "2", "3"}:
|
|
43
|
-
return int(choice)
|
|
44
|
-
print("Invalid choice. Please enter 1, 2, or 3.")
|
|
45
|
-
|
|
46
|
-
|
|
47
28
|
def refine_main(args):
|
|
48
29
|
"""Entry point for the CLI refine command (invoked from fastworkflow.cli)."""
|
|
49
30
|
try:
|
|
@@ -53,29 +34,13 @@ def refine_main(args):
|
|
|
53
34
|
workflow_folderpath = args.workflow_folderpath
|
|
54
35
|
_validate_workflow_folder(workflow_folderpath)
|
|
55
36
|
|
|
56
|
-
# Ask user which actions to run (support non-interactive via env var for tests/automation)
|
|
57
|
-
env_choice = os.environ.get("FASTWORKFLOW_REFINE_CHOICE")
|
|
58
|
-
try:
|
|
59
|
-
env_choice_val = int(env_choice) if env_choice is not None else None
|
|
60
|
-
except ValueError:
|
|
61
|
-
env_choice_val = None
|
|
62
|
-
action = _prompt_for_action(non_interactive_choice=env_choice_val)
|
|
63
|
-
|
|
64
37
|
# Prepare args-like object for post-processor when needed
|
|
65
38
|
class _Dummy:
|
|
66
39
|
pass
|
|
67
40
|
_dummy = _Dummy()
|
|
68
41
|
_dummy.workflow_folderpath = workflow_folderpath
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if action in (2, 3):
|
|
72
|
-
logger.info("Running GenAI metadata refinement with LibCST...")
|
|
73
|
-
run_genai_postprocessor(_dummy, classes={}, functions={})
|
|
74
|
-
|
|
75
|
-
if action in (1, 3):
|
|
76
|
-
logger.info("Generating parameter dependency graph...")
|
|
77
|
-
graph_path = generate_dependency_graph(workflow_folderpath)
|
|
78
|
-
print(f"Generated parameter dependency graph at {graph_path}")
|
|
42
|
+
logger.info("Running GenAI metadata refinement with LibCST...")
|
|
43
|
+
run_genai_postprocessor(_dummy, classes={}, functions={})
|
|
79
44
|
|
|
80
45
|
return 0
|
|
81
46
|
except Exception as e:
|
fastworkflow/train/__main__.py
CHANGED
|
@@ -3,6 +3,7 @@ import os
|
|
|
3
3
|
import json
|
|
4
4
|
import shutil
|
|
5
5
|
from dotenv import dotenv_values
|
|
6
|
+
import importlib.util
|
|
6
7
|
|
|
7
8
|
from colorama import Fore, Style
|
|
8
9
|
|
|
@@ -16,6 +17,21 @@ from fastworkflow.command_directory import CommandDirectory, get_cached_command_
|
|
|
16
17
|
from fastworkflow.command_routing import RoutingDefinition, RoutingRegistry
|
|
17
18
|
from fastworkflow.command_context_model import CommandContextModel
|
|
18
19
|
|
|
20
|
+
# Cache the datasets availability check result
|
|
21
|
+
_DATASETS_AVAILABLE = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _datasets_available() -> bool:
|
|
25
|
+
"""Check if the datasets package is available in the environment.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
bool: True if datasets can be imported, False otherwise.
|
|
29
|
+
"""
|
|
30
|
+
global _DATASETS_AVAILABLE
|
|
31
|
+
if _DATASETS_AVAILABLE is None:
|
|
32
|
+
_DATASETS_AVAILABLE = importlib.util.find_spec("datasets") is not None
|
|
33
|
+
return _DATASETS_AVAILABLE
|
|
34
|
+
|
|
19
35
|
|
|
20
36
|
def train_workflow(workflow_path: str):
|
|
21
37
|
# Ensure context model is parsed so downstream helpers have contexts
|
|
@@ -44,6 +60,16 @@ def train_workflow(workflow_path: str):
|
|
|
44
60
|
logger.info(f"No _commands directory found at {workflow_path}, skipping training")
|
|
45
61
|
return
|
|
46
62
|
|
|
63
|
+
# Check if datasets package is available for training
|
|
64
|
+
# Command directory and routing artifacts are generated above regardless
|
|
65
|
+
if not _datasets_available():
|
|
66
|
+
logger.warning(
|
|
67
|
+
f"datasets package not found in environment. Skipping intent detection training "
|
|
68
|
+
f"and DSPy few-shot parameter extraction for workflow: {workflow_path}. "
|
|
69
|
+
f"Other artifacts such as command_directory.json and routing_definition.json have been generated successfully."
|
|
70
|
+
)
|
|
71
|
+
return
|
|
72
|
+
|
|
47
73
|
# create a workflow and train the main workflow
|
|
48
74
|
workflow = fastworkflow.Workflow.create(
|
|
49
75
|
workflow_path,
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
from typing import List
|
|
2
|
-
from datasets import load_dataset
|
|
3
2
|
import random
|
|
4
3
|
import fastworkflow
|
|
5
4
|
import litellm
|
|
6
5
|
|
|
6
|
+
# Conditional import of datasets - only required at runtime during training
|
|
7
|
+
try:
|
|
8
|
+
from datasets import load_dataset
|
|
9
|
+
_DATASETS_AVAILABLE = True
|
|
10
|
+
except ImportError:
|
|
11
|
+
_DATASETS_AVAILABLE = False
|
|
12
|
+
load_dataset = None
|
|
13
|
+
|
|
7
14
|
NUMOF_PERSONAS=fastworkflow.get_env_var('SYNTHETIC_UTTERANCE_GEN_NUMOF_PERSONAS', int)
|
|
8
15
|
UTTERANCES_PER_PERSONA=fastworkflow.get_env_var('SYNTHETIC_UTTERANCE_GEN_UTTERANCES_PER_PERSONA', int)
|
|
9
16
|
PERSONAS_PER_BATCH=fastworkflow.get_env_var('SYNTHETIC_UTTERANCE_GEN_PERSONAS_PER_BATCH', int)
|
|
@@ -15,6 +22,15 @@ def generate_diverse_utterances(
|
|
|
15
22
|
utterances_per_persona: int = UTTERANCES_PER_PERSONA,
|
|
16
23
|
personas_per_batch: int = PERSONAS_PER_BATCH
|
|
17
24
|
) -> list[str]:
|
|
25
|
+
# If datasets is not available, return only the seed utterances
|
|
26
|
+
if not _DATASETS_AVAILABLE:
|
|
27
|
+
from fastworkflow.utils.logging import logger
|
|
28
|
+
logger.warning(
|
|
29
|
+
f"datasets package not available. Skipping synthetic utterance generation "
|
|
30
|
+
f"for command '{command_name}'. Using only seed utterances."
|
|
31
|
+
)
|
|
32
|
+
return [command_name] + seed_utterances
|
|
33
|
+
|
|
18
34
|
# Initialize LiteLLM with API key
|
|
19
35
|
api_key = fastworkflow.get_env_var("LITELLM_API_KEY_SYNDATA_GEN")
|
|
20
36
|
model=fastworkflow.get_env_var("LLM_SYNDATA_GEN")
|
|
@@ -73,13 +73,20 @@ def get_module(module_path: str, search_root: Optional[str] = None) -> Any:
|
|
|
73
73
|
|
|
74
74
|
fw_pkg_root = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
|
|
75
75
|
|
|
76
|
-
if
|
|
76
|
+
if abs_module_path.startswith(project_root):
|
|
77
|
+
relative_base = project_root
|
|
78
|
+
elif abs_module_path.startswith(fw_pkg_root):
|
|
79
|
+
relative_base = fw_pkg_root
|
|
80
|
+
else:
|
|
77
81
|
raise ImportError(
|
|
78
82
|
f"Module {abs_module_path} is outside of permitted roots: {project_root} or {fw_pkg_root}")
|
|
79
83
|
|
|
80
84
|
# Build import path relative to project root
|
|
81
|
-
relative_path = os.path.relpath(abs_module_path,
|
|
85
|
+
relative_path = os.path.relpath(abs_module_path, relative_base)
|
|
82
86
|
module_pythonic_path = relative_path.replace(os.sep, ".").rsplit(".py", 1)[0]
|
|
87
|
+
if relative_base == fw_pkg_root:
|
|
88
|
+
pkg_name = Path(fw_pkg_root).name
|
|
89
|
+
module_pythonic_path = f"{pkg_name}.{module_pythonic_path}"
|
|
83
90
|
|
|
84
91
|
# Use spec_from_file_location for dynamic loading from file paths
|
|
85
92
|
spec = importlib.util.spec_from_file_location(module_pythonic_path, abs_module_path)
|
fastworkflow/utils/signatures.py
CHANGED
|
@@ -19,7 +19,6 @@ from fastworkflow.utils.logging import logger
|
|
|
19
19
|
from fastworkflow.model_pipeline_training import get_route_layer_filepath_model
|
|
20
20
|
from fastworkflow.utils.fuzzy_match import find_best_matches
|
|
21
21
|
from fastworkflow.command_directory import CommandDirectory
|
|
22
|
-
from fastworkflow.utils.command_dependency_graph import get_dependency_suggestions
|
|
23
22
|
|
|
24
23
|
MISSING_INFORMATION_ERRMSG = None
|
|
25
24
|
INVALID_INFORMATION_ERRMSG = None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: fastworkflow
|
|
3
|
-
Version: 2.17.
|
|
3
|
+
Version: 2.17.20
|
|
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
|
|
@@ -13,7 +13,6 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
14
|
Provides-Extra: fastapi
|
|
15
15
|
Requires-Dist: colorama (>=0.4.6,<0.5.0)
|
|
16
|
-
Requires-Dist: datasets (>=4.0.0,<5.0.0)
|
|
17
16
|
Requires-Dist: dspy (>=3.0.1,<4.0.0)
|
|
18
17
|
Requires-Dist: fastapi (>=0.115.5,<0.116.0) ; extra == "fastapi"
|
|
19
18
|
Requires-Dist: fastapi-mcp (>=0.4.0,<0.5.0) ; extra == "fastapi"
|
|
@@ -27,7 +26,6 @@ Requires-Dist: python-dotenv (>=1.0.1,<2.0.0)
|
|
|
27
26
|
Requires-Dist: python-jose[cryptography] (>=3.3.0,<4.0.0) ; extra == "fastapi"
|
|
28
27
|
Requires-Dist: python-levenshtein (>=0.27.1,<0.28.0)
|
|
29
28
|
Requires-Dist: scikit-learn (>=1.6.1,<2.0.0)
|
|
30
|
-
Requires-Dist: sentence-transformers (>=3.4.1,<4.0.0)
|
|
31
29
|
Requires-Dist: speedict (>=0.3.12,<0.4.0)
|
|
32
30
|
Requires-Dist: torch (>=2.7.1,<3.0.0)
|
|
33
31
|
Requires-Dist: transformers (>=4.48.2,<5.0.0)
|
|
@@ -168,6 +166,7 @@ uv pip install fastworkflow
|
|
|
168
166
|
- `fastWorkflow` currently works on Linux and MacOS only. On windows, use WSL.
|
|
169
167
|
- `fastWorkflow` installs PyTorch as a dependency. If you don't already have PyTorch installed, this could take a few minutes depending on your internet speed.
|
|
170
168
|
- `fastWorkflow` requires Python 3.11+ or higher.
|
|
169
|
+
- Training (`fastworkflow train`) also expects the optional Hugging Face `datasets` package. Install it by including the dev group when using Poetry.
|
|
171
170
|
|
|
172
171
|
---
|
|
173
172
|
|
|
@@ -211,6 +210,10 @@ You can get a free API key from [Mistral AI](https://mistral.ai) for the mistral
|
|
|
211
210
|
|
|
212
211
|
### Step 3: Train the Example
|
|
213
212
|
|
|
213
|
+
> [!note]
|
|
214
|
+
> The training CLI depends on the optional Hugging Face `datasets` package.
|
|
215
|
+
> Install it explicitly (`pip install datasets`) or, if you're working from this repo, run `poetry install --with dev` before training.
|
|
216
|
+
|
|
214
217
|
Train the intent-detection models for the workflow:
|
|
215
218
|
|
|
216
219
|
```sh
|
|
@@ -36,7 +36,7 @@ fastworkflow/build/pydantic_model_generator.py,sha256=oNyoANyUWBpHG-fE3tGL911RNv
|
|
|
36
36
|
fastworkflow/build/utterance_generator.py,sha256=UrtkF0wyAZ1hiFitHX0g8w7Wh-D0leLCrP1aUACSfHo,299
|
|
37
37
|
fastworkflow/cache_matching.py,sha256=OoB--1tO6-O4BKCuCrUbB0CkUr76J62K4VAf6MShi-w,7984
|
|
38
38
|
fastworkflow/chat_session.py,sha256=dvRH81Wmj0YCleZaCeWfx9zyqucYCOMJcldhvSLbFeY,35448
|
|
39
|
-
fastworkflow/cli.py,sha256=
|
|
39
|
+
fastworkflow/cli.py,sha256=Jz8pbs--pgaGU_7H8hjt_ayHbGvvB2HxmJaXamTE9OY,23507
|
|
40
40
|
fastworkflow/command_context_model.py,sha256=bQadDB_IH2lc0br46IT07Iej_j2KrAMderiVKqU7gno,15914
|
|
41
41
|
fastworkflow/command_directory.py,sha256=aJ6UQCwevfF11KbcQB2Qz6mQ7Kj91pZtvHmQY6JFnao,29030
|
|
42
42
|
fastworkflow/command_executor.py,sha256=WTSrukv6UDQfWUDSNleIQ1TxwDnAQIKIimh4sQVwnig,8457
|
|
@@ -143,7 +143,7 @@ fastworkflow/examples/simple_workflow_template/startup_action.json,sha256=gj0-B4
|
|
|
143
143
|
fastworkflow/intent_clarification_agent.py,sha256=VYgpfx7EE0oToewwSaiCdz0VYSFq4Ql0UEsvyXUQhwM,5051
|
|
144
144
|
fastworkflow/mcp_server.py,sha256=NxbLSKf2MA4lAHVcm6ZfiVuOjVO6IeV5Iw17wImFbxQ,8867
|
|
145
145
|
fastworkflow/model_pipeline_training.py,sha256=P_9wrYSfJVSYCTu8VEPkgXJ16eH58LLCK4rCRbRFAVg,46740
|
|
146
|
-
fastworkflow/refine/__main__.py,sha256=
|
|
146
|
+
fastworkflow/refine/__main__.py,sha256=7r4nVft1inkYQSDrrWD1poS5F8yWuatf98pCp6Lqo7Y,1842
|
|
147
147
|
fastworkflow/run/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
148
148
|
fastworkflow/run/__main__.py,sha256=WMrlkwUt0VTAp_DQwp3LAsJqZlFW63mmVXckuWdMiSo,12077
|
|
149
149
|
fastworkflow/run_fastapi_mcp/README.md,sha256=dAmG2KF-9mqSjyIPSA9vhUit-DjsDH6WJUDCkQ3C1is,11943
|
|
@@ -155,12 +155,11 @@ fastworkflow/run_fastapi_mcp/mcp_specific.py,sha256=RdOPcPn68KlxNSM9Vb2yeYEDNGoN
|
|
|
155
155
|
fastworkflow/run_fastapi_mcp/redoc_2_standalone_html.py,sha256=oYWn30O-xKX6pVjunCeLupyOM2DbeZ3QgFj-F2LalOE,1191
|
|
156
156
|
fastworkflow/run_fastapi_mcp/utils.py,sha256=SX6meWba0T-iYn7YmEajbwJrijfVVUuYGv4usDXzA2c,19589
|
|
157
157
|
fastworkflow/train/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
158
|
-
fastworkflow/train/__main__.py,sha256=
|
|
159
|
-
fastworkflow/train/generate_synthetic.py,sha256=
|
|
158
|
+
fastworkflow/train/__main__.py,sha256=m4v9uczmZ58EfNlJKc-cewMjPeltLL7tNRKotYtig3o,9532
|
|
159
|
+
fastworkflow/train/generate_synthetic.py,sha256=ingoGxpwlaHGM9WHeK1xULEZntr5HBmQohyLtpqVTD0,5917
|
|
160
160
|
fastworkflow/user_message_queues.py,sha256=svbuFxQ16q6Tz6urPWfD4IEsOTMxtS1Kc1PP8EE8AWg,1422
|
|
161
161
|
fastworkflow/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
162
162
|
fastworkflow/utils/chat_adapter.py,sha256=-U5JFiPynDhSYXJ75wdY0EA-hH8QPaq1bzA6ju4ZnVc,4090
|
|
163
|
-
fastworkflow/utils/command_dependency_graph.py,sha256=7YmAnVXcaLsPVeC3SvM4pGdUsCUxWM4H2qXrtGwQpAI,13512
|
|
164
163
|
fastworkflow/utils/context_utils.py,sha256=mjYVzNJCmimNMmBdOKfzFeDSws_oAADAwcfz_N6sR7M,749
|
|
165
164
|
fastworkflow/utils/dspy_cache_utils.py,sha256=OP2IsWPMGCdhjC-4iRqggWgTEfvPxFN_78tV1_C6uHY,3725
|
|
166
165
|
fastworkflow/utils/dspy_logger.py,sha256=NS40fYl-J-vps82BUh9D8kqv5dP3_qAY78HZWyZemEA,6571
|
|
@@ -171,15 +170,15 @@ fastworkflow/utils/generate_param_examples.py,sha256=K0x1Zwe82xqhKA15AYTodWg7mqu
|
|
|
171
170
|
fastworkflow/utils/logging.py,sha256=2SA-04fg7Lx_vGf980tfCOGDQxBvU9X6Vbhv47rbdaw,4110
|
|
172
171
|
fastworkflow/utils/parameterize_func_decorator.py,sha256=V6YJnishWRCdwiBQW6P17hmGGrga0Empk-AN5Gm7iMk,633
|
|
173
172
|
fastworkflow/utils/pydantic_model_2_dspy_signature_class.py,sha256=w1pvl8rJq48ulFwaAtBgfXYn_SBIDBgq1aLMUg1zJn8,12875
|
|
174
|
-
fastworkflow/utils/python_utils.py,sha256=
|
|
173
|
+
fastworkflow/utils/python_utils.py,sha256=KMxktfIVOre7qkLhd80Ig39g313EMx_I_oHSa6sC5wI,8512
|
|
175
174
|
fastworkflow/utils/react.py,sha256=FGDnzIPKSTwXOCrzUVluFtkZ06lVjgMdB-YQ8jhggZU,13065
|
|
176
|
-
fastworkflow/utils/signatures.py,sha256=
|
|
175
|
+
fastworkflow/utils/signatures.py,sha256=ddcwCLNF_5dpItvcHdkZ0WBMse7CaqYpAyg6WwoJZPo,33310
|
|
177
176
|
fastworkflow/utils/startup_progress.py,sha256=9icSdnpFAxzIq0sUliGpNaH0Efvrt5lDtGfURV5BD98,3539
|
|
178
177
|
fastworkflow/workflow.py,sha256=37gn7e3ct-gdGw43zS6Ab_ADoJJBO4eJW2PywfUpjEg,18825
|
|
179
178
|
fastworkflow/workflow_agent.py,sha256=LRPdl-3lDRPx8pQtK202JWGYMYBNz5Mruy630fCBCk0,18725
|
|
180
179
|
fastworkflow/workflow_inheritance_model.py,sha256=Pp-qSrQISgPfPjJVUfW84pc7HLmL2evuq0UVIYR51K0,7974
|
|
181
|
-
fastworkflow-2.17.
|
|
182
|
-
fastworkflow-2.17.
|
|
183
|
-
fastworkflow-2.17.
|
|
184
|
-
fastworkflow-2.17.
|
|
185
|
-
fastworkflow-2.17.
|
|
180
|
+
fastworkflow-2.17.20.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
181
|
+
fastworkflow-2.17.20.dist-info/METADATA,sha256=GRA2Txe-gPQQ35HCtWh2NiqLg2f4O5V0apIfEUjZXrY,30915
|
|
182
|
+
fastworkflow-2.17.20.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
183
|
+
fastworkflow-2.17.20.dist-info/entry_points.txt,sha256=m8HqoPzCyaZLAx-V5X8MJgw3Lx3GiPDlxNEZ7K-Gb-U,54
|
|
184
|
+
fastworkflow-2.17.20.dist-info/RECORD,,
|
|
@@ -1,364 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import os
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from typing import Dict, List, Any
|
|
5
|
-
|
|
6
|
-
from sentence_transformers import SentenceTransformer, util as st_util
|
|
7
|
-
|
|
8
|
-
# import dspy # type: ignore
|
|
9
|
-
|
|
10
|
-
# import fastworkflow # For env configuration when using DSPy
|
|
11
|
-
from fastworkflow.command_directory import CommandDirectory
|
|
12
|
-
from fastworkflow.command_routing import RoutingDefinition
|
|
13
|
-
from fastworkflow.utils import python_utils
|
|
14
|
-
from fastworkflow.command_metadata_api import CommandMetadataAPI
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@dataclass
|
|
18
|
-
class ParamMeta:
|
|
19
|
-
name: str
|
|
20
|
-
type_str: str
|
|
21
|
-
description: str
|
|
22
|
-
examples: List[str]
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@dataclass
|
|
26
|
-
class CommandParams:
|
|
27
|
-
inputs: List[ParamMeta]
|
|
28
|
-
outputs: List[ParamMeta]
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
# def _serialize_type_str(t: Any) -> str:
|
|
32
|
-
# try:
|
|
33
|
-
# # Basic serialization for common typing and classes
|
|
34
|
-
# return t.__name__ if hasattr(t, "__name__") else str(t)
|
|
35
|
-
# except Exception:
|
|
36
|
-
# return str(t)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def _collect_command_params(workflow_path: str) -> Dict[str, CommandParams]:
|
|
40
|
-
params_map = CommandMetadataAPI.get_params_for_all_commands(workflow_path)
|
|
41
|
-
|
|
42
|
-
results: Dict[str, CommandParams] = {}
|
|
43
|
-
for qualified_name, io in params_map.items():
|
|
44
|
-
inputs = [
|
|
45
|
-
ParamMeta(
|
|
46
|
-
name=p.get("name", ""),
|
|
47
|
-
type_str=str(p.get("type_str", "")),
|
|
48
|
-
description=str(p.get("description", "")),
|
|
49
|
-
examples=list(p.get("examples", []) or []),
|
|
50
|
-
)
|
|
51
|
-
for p in io.get("inputs", [])
|
|
52
|
-
]
|
|
53
|
-
outputs = [
|
|
54
|
-
ParamMeta(
|
|
55
|
-
name=p.get("name", ""),
|
|
56
|
-
type_str=str(p.get("type_str", "")),
|
|
57
|
-
description=str(p.get("description", "")),
|
|
58
|
-
examples=list(p.get("examples", []) or []),
|
|
59
|
-
)
|
|
60
|
-
for p in io.get("outputs", [])
|
|
61
|
-
]
|
|
62
|
-
# Include all discovered commands; context overlap is handled later
|
|
63
|
-
results[qualified_name] = CommandParams(inputs=inputs, outputs=outputs)
|
|
64
|
-
|
|
65
|
-
return results
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def _exact_match(out_param: ParamMeta, in_param: ParamMeta) -> bool:
|
|
69
|
-
return out_param.name.lower() == in_param.name.lower() and out_param.type_str == in_param.type_str
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def _semantic_match(out_param: ParamMeta, in_param: ParamMeta, threshold: float = 0.85) -> bool:
|
|
73
|
-
# Sentence Transformers cosine similarity between parameter texts
|
|
74
|
-
def to_text(p: ParamMeta) -> str:
|
|
75
|
-
parts = [
|
|
76
|
-
f"name: {p.name}",
|
|
77
|
-
f"type: {p.type_str}",
|
|
78
|
-
f"description: {p.description}",
|
|
79
|
-
f"examples: {'|'.join(map(str, p.examples))}",
|
|
80
|
-
]
|
|
81
|
-
return " | ".join(parts).lower()
|
|
82
|
-
|
|
83
|
-
out_param_text = to_text(out_param)
|
|
84
|
-
in_param_text = to_text(in_param)
|
|
85
|
-
if not out_param_text or not in_param_text:
|
|
86
|
-
return False
|
|
87
|
-
|
|
88
|
-
# Lazy-load model and cache embeddings to avoid repeated work
|
|
89
|
-
global _st_model # type: ignore
|
|
90
|
-
global _embedding_cache # type: ignore
|
|
91
|
-
if '_st_model' not in globals():
|
|
92
|
-
_st_model = None # type: ignore
|
|
93
|
-
if '_embedding_cache' not in globals():
|
|
94
|
-
_embedding_cache = {} # type: ignore
|
|
95
|
-
|
|
96
|
-
if _st_model is None:
|
|
97
|
-
_st_model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2") # type: ignore
|
|
98
|
-
|
|
99
|
-
if out_param_text not in _embedding_cache:
|
|
100
|
-
_embedding_cache[out_param_text] = _st_model.encode(
|
|
101
|
-
out_param_text, convert_to_tensor=True, normalize_embeddings=True
|
|
102
|
-
)
|
|
103
|
-
if in_param_text not in _embedding_cache:
|
|
104
|
-
_embedding_cache[in_param_text] = _st_model.encode(
|
|
105
|
-
in_param_text, convert_to_tensor=True, normalize_embeddings=True
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
emb_out = _embedding_cache[out_param_text]
|
|
109
|
-
emb_in = _embedding_cache[in_param_text]
|
|
110
|
-
sim = st_util.cos_sim(emb_out, emb_in) # 1x1 tensor
|
|
111
|
-
return float(sim.item()) >= threshold
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
# ----------------------------------------------------------------------------
|
|
115
|
-
# LLM-based matching using DSPy
|
|
116
|
-
# ----------------------------------------------------------------------------
|
|
117
|
-
|
|
118
|
-
# Lazy singletons
|
|
119
|
-
# _llm_initialized: bool = False # type: ignore
|
|
120
|
-
# _llm_module: Optional["CommandDependencyModule"] = None # type: ignore
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
# def _initialize_dspy_llm_if_needed() -> None:
|
|
124
|
-
# """Initialize DSPy LM once using FastWorkflow environment.
|
|
125
|
-
|
|
126
|
-
# Controlled by env vars:
|
|
127
|
-
# - LLM_COMMAND_METADATA_GEN (model id for LiteLLM via DSPy)
|
|
128
|
-
# - LITELLM_API_KEY_COMMANDMETADATA_GEN (API key)
|
|
129
|
-
# """
|
|
130
|
-
# global _llm_initialized, _llm_module
|
|
131
|
-
# if _llm_initialized:
|
|
132
|
-
# return
|
|
133
|
-
|
|
134
|
-
# model = fastworkflow.get_env_var("LLM_COMMAND_METADATA_GEN")
|
|
135
|
-
# api_key = fastworkflow.get_env_var("LITELLM_API_KEY_COMMANDMETADATA_GEN")
|
|
136
|
-
# lm = dspy.LM(model=model, api_key=api_key, max_tokens=1000)
|
|
137
|
-
|
|
138
|
-
# # Define signature and module only if dspy is available
|
|
139
|
-
# class CommandDependencySignature(dspy.Signature): # type: ignore
|
|
140
|
-
# """Analyze if two commands have a dependency relationship.
|
|
141
|
-
|
|
142
|
-
# There is a dependency relationship if and only if the outputs from one command can be used directly as inputs of the other.
|
|
143
|
-
# Tip for figuring out dependency direction: Commands with hard-to-remember inputs (such as id) typically depend on commands with easy to remember inputs (such as name, email).
|
|
144
|
-
# """
|
|
145
|
-
|
|
146
|
-
# cmd_x_name: str = dspy.InputField(desc="Name of command X")
|
|
147
|
-
# cmd_x_inputs: str = dspy.InputField(desc="Input parameters of command X (name:type)")
|
|
148
|
-
# cmd_x_outputs: str = dspy.InputField(desc="Output parameters of command X (name:type)")
|
|
149
|
-
|
|
150
|
-
# cmd_y_name: str = dspy.InputField(desc="Name of command Y")
|
|
151
|
-
# cmd_y_inputs: str = dspy.InputField(desc="Input parameters of command Y (name:type)")
|
|
152
|
-
# cmd_y_outputs: str = dspy.InputField(desc="Output parameters of command Y (name:type)")
|
|
153
|
-
|
|
154
|
-
# has_dependency: bool = dspy.OutputField(
|
|
155
|
-
# desc="True if there's a dependency between the commands"
|
|
156
|
-
# )
|
|
157
|
-
# direction: str = dspy.OutputField(
|
|
158
|
-
# desc="Direction: 'x_depends_on_y', 'y_depends_on_x', or 'none'"
|
|
159
|
-
# )
|
|
160
|
-
|
|
161
|
-
# class CommandDependencyModule(dspy.Module): # type: ignore
|
|
162
|
-
# def __init__(self):
|
|
163
|
-
# super().__init__()
|
|
164
|
-
# self.generate = dspy.ChainOfThought(CommandDependencySignature)
|
|
165
|
-
|
|
166
|
-
# def forward(
|
|
167
|
-
# self,
|
|
168
|
-
# cmd_x_name: str,
|
|
169
|
-
# cmd_x_inputs: str,
|
|
170
|
-
# cmd_x_outputs: str,
|
|
171
|
-
# cmd_y_name: str,
|
|
172
|
-
# cmd_y_inputs: str,
|
|
173
|
-
# cmd_y_outputs: str,
|
|
174
|
-
# ) -> tuple[bool, str]:
|
|
175
|
-
# with dspy.context(lm=lm):
|
|
176
|
-
# prediction = self.generate(
|
|
177
|
-
# cmd_x_name=cmd_x_name,
|
|
178
|
-
# cmd_x_inputs=cmd_x_inputs,
|
|
179
|
-
# cmd_x_outputs=cmd_x_outputs,
|
|
180
|
-
# cmd_y_name=cmd_y_name,
|
|
181
|
-
# cmd_y_inputs=cmd_y_inputs,
|
|
182
|
-
# cmd_y_outputs=cmd_y_outputs,
|
|
183
|
-
# )
|
|
184
|
-
# return prediction.has_dependency, prediction.direction
|
|
185
|
-
|
|
186
|
-
# _llm_module = CommandDependencyModule()
|
|
187
|
-
# _llm_initialized = True
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
# def _llm_command_dependency(
|
|
191
|
-
# cmd_x_name: str,
|
|
192
|
-
# cmd_x_params: CommandParams,
|
|
193
|
-
# cmd_y_name: str,
|
|
194
|
-
# cmd_y_params: CommandParams
|
|
195
|
-
# ) -> Optional[str]:
|
|
196
|
-
# """Check if two commands have a dependency using LLM.
|
|
197
|
-
|
|
198
|
-
# Returns:
|
|
199
|
-
# - "x_to_y" if Y depends on X (X's outputs feed Y's inputs)
|
|
200
|
-
# - "y_to_x" if X depends on Y (Y's outputs feed X's inputs)
|
|
201
|
-
# - None if no dependency
|
|
202
|
-
# """
|
|
203
|
-
# _initialize_dspy_llm_if_needed()
|
|
204
|
-
|
|
205
|
-
# # Format parameters for LLM
|
|
206
|
-
# x_inputs = ", ".join([f"{p.name}:{p.type_str}" for p in cmd_x_params.inputs])
|
|
207
|
-
# x_outputs = ", ".join([f"{p.name}:{p.type_str}" for p in cmd_x_params.outputs])
|
|
208
|
-
# y_inputs = ", ".join([f"{p.name}:{p.type_str}" for p in cmd_y_params.inputs])
|
|
209
|
-
# y_outputs = ", ".join([f"{p.name}:{p.type_str}" for p in cmd_y_params.outputs])
|
|
210
|
-
|
|
211
|
-
# has_dep, direction = _llm_module(
|
|
212
|
-
# cmd_x_name=cmd_x_name,
|
|
213
|
-
# cmd_x_inputs=x_inputs or "none",
|
|
214
|
-
# cmd_x_outputs=x_outputs or "none",
|
|
215
|
-
# cmd_y_name=cmd_y_name,
|
|
216
|
-
# cmd_y_inputs=y_inputs or "none",
|
|
217
|
-
# cmd_y_outputs=y_outputs or "none",
|
|
218
|
-
# )
|
|
219
|
-
|
|
220
|
-
# if not has_dep or direction == "none":
|
|
221
|
-
# return None
|
|
222
|
-
# if direction == "x_depends_on_y":
|
|
223
|
-
# return "y_to_x" # Y's outputs -> X's inputs
|
|
224
|
-
# return "x_to_y" if direction == "y_depends_on_x" else None
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
def _contexts_overlap(routing: RoutingDefinition, cmd_x: str, cmd_y: str) -> bool:
|
|
228
|
-
cx = routing.get_contexts_for_command(cmd_x)
|
|
229
|
-
cy = routing.get_contexts_for_command(cmd_y)
|
|
230
|
-
return False if not cx or not cy else bool(cx & cy)
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
def _check_param_dependencies(
|
|
234
|
-
outputs: List[ParamMeta],
|
|
235
|
-
inputs: List[ParamMeta],
|
|
236
|
-
semantic_threshold: float,
|
|
237
|
-
exact_only: bool
|
|
238
|
-
) -> bool:
|
|
239
|
-
"""Check if any output parameter can satisfy any input parameter."""
|
|
240
|
-
if not outputs or not inputs:
|
|
241
|
-
return False
|
|
242
|
-
|
|
243
|
-
for out_param in outputs:
|
|
244
|
-
for in_param in inputs:
|
|
245
|
-
# Check exact match
|
|
246
|
-
if _exact_match(out_param, in_param):
|
|
247
|
-
return True
|
|
248
|
-
# Check semantic match if not in exact_only mode
|
|
249
|
-
if not exact_only and _semantic_match(out_param, in_param, semantic_threshold):
|
|
250
|
-
return True
|
|
251
|
-
return False
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
def generate_dependency_graph(workflow_path: str) -> str:
|
|
255
|
-
"""
|
|
256
|
-
Build the parameter dependency graph and persist as JSON in command_dependency_graph.json.
|
|
257
|
-
|
|
258
|
-
Returns the path to the generated JSON file.
|
|
259
|
-
"""
|
|
260
|
-
# Use default values
|
|
261
|
-
semantic_threshold = 0.85
|
|
262
|
-
exact_only = False
|
|
263
|
-
params_by_command = _collect_command_params(workflow_path)
|
|
264
|
-
routing = RoutingDefinition.build(workflow_path)
|
|
265
|
-
|
|
266
|
-
# Exclude core commands and wildcard
|
|
267
|
-
excluded_commands = {
|
|
268
|
-
"IntentDetection/go_up",
|
|
269
|
-
"IntentDetection/reset_context",
|
|
270
|
-
"IntentDetection/what_can_i_do",
|
|
271
|
-
"IntentDetection/what_is_current_context",
|
|
272
|
-
"wildcard"
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
# Filter out excluded commands
|
|
276
|
-
filtered_params = {k: v for k, v in params_by_command.items() if k not in excluded_commands}
|
|
277
|
-
|
|
278
|
-
nodes = sorted(list(filtered_params.keys()))
|
|
279
|
-
edges: List[Dict[str, Any]] = []
|
|
280
|
-
|
|
281
|
-
# Process pairs only once, checking both directions efficiently
|
|
282
|
-
for i, cmd_x in enumerate(nodes):
|
|
283
|
-
for cmd_y in nodes[i+1:]: # Only check each pair once
|
|
284
|
-
if not _contexts_overlap(routing, cmd_x, cmd_y):
|
|
285
|
-
continue
|
|
286
|
-
|
|
287
|
-
# Print on-going progress status since this is a long-running operation
|
|
288
|
-
print(f"Checking {cmd_x} <-> {cmd_y}")
|
|
289
|
-
|
|
290
|
-
x_params = filtered_params[cmd_x]
|
|
291
|
-
y_params = filtered_params[cmd_y]
|
|
292
|
-
|
|
293
|
-
# Check both directions efficiently using helper function
|
|
294
|
-
x_to_y_match = _check_param_dependencies(
|
|
295
|
-
x_params.outputs, y_params.inputs, semantic_threshold, exact_only
|
|
296
|
-
)
|
|
297
|
-
y_to_x_match = _check_param_dependencies(
|
|
298
|
-
y_params.outputs, x_params.inputs, semantic_threshold, exact_only
|
|
299
|
-
)
|
|
300
|
-
|
|
301
|
-
# Add edges for matches found
|
|
302
|
-
if x_to_y_match:
|
|
303
|
-
edges.append({"from": cmd_y, "to": cmd_x})
|
|
304
|
-
if y_to_x_match:
|
|
305
|
-
edges.append({"from": cmd_x, "to": cmd_y})
|
|
306
|
-
|
|
307
|
-
# If no exact/semantic match found, try LLM (which also checks both directions)
|
|
308
|
-
# if not x_to_y_match and not y_to_x_match and not exact_only:
|
|
309
|
-
# llm_direction = _llm_command_dependency(cmd_x, x_params, cmd_y, y_params)
|
|
310
|
-
# if llm_direction == "x_to_y":
|
|
311
|
-
# edges.append({"from": cmd_y, "to": cmd_x})
|
|
312
|
-
# elif llm_direction == "y_to_x":
|
|
313
|
-
# edges.append({"from": cmd_x, "to": cmd_y})
|
|
314
|
-
|
|
315
|
-
out_path = os.path.join(workflow_path, "command_dependency_graph.json")
|
|
316
|
-
with open(out_path, "w", encoding="utf-8") as f:
|
|
317
|
-
json.dump({"nodes": nodes, "edges": edges}, f, indent=2)
|
|
318
|
-
|
|
319
|
-
return out_path
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
def _load_graph(graph_path: str) -> Dict[str, Any]:
|
|
323
|
-
try:
|
|
324
|
-
with open(graph_path, "r", encoding="utf-8") as f:
|
|
325
|
-
return json.load(f)
|
|
326
|
-
except Exception:
|
|
327
|
-
return {"nodes": [], "edges": []}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
def get_dependency_suggestions(
|
|
331
|
-
graph_path: str,
|
|
332
|
-
y_qualified_name: str,
|
|
333
|
-
missing_input_param: str,
|
|
334
|
-
max_depth: int = 3,
|
|
335
|
-
) -> List[Dict[str, Any]]:
|
|
336
|
-
"""
|
|
337
|
-
Recursively resolves dependencies, returning a list of plans for resolving the missing param.
|
|
338
|
-
Note: The simplified graph no longer tracks which specific params match, so this function
|
|
339
|
-
returns all possible dependency paths without filtering by parameter.
|
|
340
|
-
"""
|
|
341
|
-
graph = _load_graph(graph_path)
|
|
342
|
-
edges = graph.get("edges", [])
|
|
343
|
-
|
|
344
|
-
# Build adjacency: from -> list of to nodes
|
|
345
|
-
adj: Dict[str, List[str]] = {}
|
|
346
|
-
for e in edges:
|
|
347
|
-
adj.setdefault(e["from"], []).append(e["to"])
|
|
348
|
-
|
|
349
|
-
def recurse(node: str, depth: int) -> List[Dict[str, Any]]:
|
|
350
|
-
if depth > max_depth:
|
|
351
|
-
return []
|
|
352
|
-
plans: List[Dict[str, Any]] = []
|
|
353
|
-
for neighbor in adj.get(node, []):
|
|
354
|
-
sub_plans = recurse(neighbor, depth + 1)
|
|
355
|
-
plans.append({
|
|
356
|
-
"command": neighbor,
|
|
357
|
-
"sub_plans": sub_plans,
|
|
358
|
-
})
|
|
359
|
-
return plans
|
|
360
|
-
|
|
361
|
-
dependency_plans = recurse(y_qualified_name, 0)
|
|
362
|
-
# Prefer shallower trees
|
|
363
|
-
dependency_plans.sort(key=lambda p: len(p.get("sub_plans", [])))
|
|
364
|
-
return dependency_plans
|
|
File without changes
|
|
File without changes
|
|
File without changes
|