fastworkflow 2.17.18__py3-none-any.whl → 2.17.19__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 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: enhance metadata and build dependency graph.",
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):
@@ -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 and generating dependency graph."
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
- # Execute selected actions
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:
@@ -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 not (abs_module_path.startswith(project_root) or abs_module_path.startswith(fw_pkg_root)):
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, project_root)
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)
@@ -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.18
3
+ Version: 2.17.19
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=n-PFDC0EOkq1zIK1Ip44at-VcaP9cW9aSxZ2IVj3VoE,23796
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=bDLpPNMcdp8U4EFnMdjxx1sPDQCZuEJoBURr2KebTng,3398
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
@@ -160,7 +160,6 @@ fastworkflow/train/generate_synthetic.py,sha256=sTDk-E5ewkS4o-0LJeofiEv4uXGpqdGc
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=D0JBdzkwKoyK7XvZcnIxOgsS8CRGdvuW-zcO45_pfOA,8252
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=uv1HxkVK8yzu6xt0ci8RUSgQW0Njaz22YuJVO_aNUEM,33393
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.18.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
182
- fastworkflow-2.17.18.dist-info/METADATA,sha256=xezHaSsRqYq4H9QilU8QxildJOECkMUE6tQxgVFz8CM,30635
183
- fastworkflow-2.17.18.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
184
- fastworkflow-2.17.18.dist-info/entry_points.txt,sha256=m8HqoPzCyaZLAx-V5X8MJgw3Lx3GiPDlxNEZ7K-Gb-U,54
185
- fastworkflow-2.17.18.dist-info/RECORD,,
180
+ fastworkflow-2.17.19.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
181
+ fastworkflow-2.17.19.dist-info/METADATA,sha256=98M30tfZ6P5KinxaJlRtX4aP4EwoThNzt3wEbvgRIuQ,30915
182
+ fastworkflow-2.17.19.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
183
+ fastworkflow-2.17.19.dist-info/entry_points.txt,sha256=m8HqoPzCyaZLAx-V5X8MJgw3Lx3GiPDlxNEZ7K-Gb-U,54
184
+ fastworkflow-2.17.19.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