lfx-nightly 0.2.0.dev41__py3-none-any.whl → 0.3.0.dev3__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.
- lfx/__main__.py +137 -6
- lfx/_assets/component_index.json +1 -1
- lfx/base/agents/agent.py +10 -6
- lfx/base/agents/altk_base_agent.py +5 -3
- lfx/base/agents/altk_tool_wrappers.py +1 -1
- lfx/base/agents/events.py +1 -1
- lfx/base/agents/utils.py +4 -0
- lfx/base/composio/composio_base.py +78 -41
- lfx/base/data/cloud_storage_utils.py +156 -0
- lfx/base/data/docling_utils.py +130 -55
- lfx/base/datastax/astradb_base.py +75 -64
- lfx/base/embeddings/embeddings_class.py +113 -0
- lfx/base/models/__init__.py +11 -1
- lfx/base/models/google_generative_ai_constants.py +33 -9
- lfx/base/models/model_metadata.py +6 -0
- lfx/base/models/ollama_constants.py +196 -30
- lfx/base/models/openai_constants.py +37 -10
- lfx/base/models/unified_models.py +1123 -0
- lfx/base/models/watsonx_constants.py +43 -4
- lfx/base/prompts/api_utils.py +40 -5
- lfx/base/tools/component_tool.py +2 -9
- lfx/cli/__init__.py +10 -2
- lfx/cli/commands.py +3 -0
- lfx/cli/run.py +65 -409
- lfx/cli/script_loader.py +18 -7
- lfx/cli/validation.py +6 -3
- lfx/components/__init__.py +0 -3
- lfx/components/composio/github_composio.py +1 -1
- lfx/components/cuga/cuga_agent.py +39 -27
- lfx/components/data_source/api_request.py +4 -2
- lfx/components/datastax/astradb_assistant_manager.py +4 -2
- lfx/components/docling/__init__.py +45 -11
- lfx/components/docling/docling_inline.py +39 -49
- lfx/components/docling/docling_remote.py +1 -0
- lfx/components/elastic/opensearch_multimodal.py +1733 -0
- lfx/components/files_and_knowledge/file.py +384 -36
- lfx/components/files_and_knowledge/ingestion.py +8 -0
- lfx/components/files_and_knowledge/retrieval.py +10 -0
- lfx/components/files_and_knowledge/save_file.py +91 -88
- lfx/components/langchain_utilities/ibm_granite_handler.py +211 -0
- lfx/components/langchain_utilities/tool_calling.py +37 -6
- lfx/components/llm_operations/batch_run.py +64 -18
- lfx/components/llm_operations/lambda_filter.py +213 -101
- lfx/components/llm_operations/llm_conditional_router.py +39 -7
- lfx/components/llm_operations/structured_output.py +38 -12
- lfx/components/models/__init__.py +16 -74
- lfx/components/models_and_agents/agent.py +51 -203
- lfx/components/models_and_agents/embedding_model.py +171 -255
- lfx/components/models_and_agents/language_model.py +54 -318
- lfx/components/models_and_agents/mcp_component.py +96 -10
- lfx/components/models_and_agents/prompt.py +105 -18
- lfx/components/ollama/ollama_embeddings.py +111 -29
- lfx/components/openai/openai_chat_model.py +1 -1
- lfx/components/processing/text_operations.py +580 -0
- lfx/components/vllm/__init__.py +37 -0
- lfx/components/vllm/vllm.py +141 -0
- lfx/components/vllm/vllm_embeddings.py +110 -0
- lfx/custom/custom_component/component.py +65 -10
- lfx/custom/custom_component/custom_component.py +8 -6
- lfx/events/observability/__init__.py +0 -0
- lfx/events/observability/lifecycle_events.py +111 -0
- lfx/field_typing/__init__.py +57 -58
- lfx/graph/graph/base.py +40 -1
- lfx/graph/utils.py +109 -30
- lfx/graph/vertex/base.py +75 -23
- lfx/graph/vertex/vertex_types.py +0 -5
- lfx/inputs/__init__.py +2 -0
- lfx/inputs/input_mixin.py +55 -0
- lfx/inputs/inputs.py +120 -0
- lfx/interface/components.py +24 -7
- lfx/interface/initialize/loading.py +42 -12
- lfx/io/__init__.py +2 -0
- lfx/run/__init__.py +5 -0
- lfx/run/base.py +464 -0
- lfx/schema/__init__.py +50 -0
- lfx/schema/data.py +1 -1
- lfx/schema/image.py +26 -7
- lfx/schema/message.py +104 -11
- lfx/schema/workflow.py +171 -0
- lfx/services/deps.py +12 -0
- lfx/services/interfaces.py +43 -1
- lfx/services/mcp_composer/service.py +7 -1
- lfx/services/schema.py +1 -0
- lfx/services/settings/auth.py +95 -4
- lfx/services/settings/base.py +11 -1
- lfx/services/settings/constants.py +2 -0
- lfx/services/settings/utils.py +82 -0
- lfx/services/storage/local.py +13 -8
- lfx/services/transaction/__init__.py +5 -0
- lfx/services/transaction/service.py +35 -0
- lfx/tests/unit/components/__init__.py +0 -0
- lfx/utils/constants.py +2 -0
- lfx/utils/mustache_security.py +79 -0
- lfx/utils/validate_cloud.py +81 -3
- {lfx_nightly-0.2.0.dev41.dist-info → lfx_nightly-0.3.0.dev3.dist-info}/METADATA +7 -2
- {lfx_nightly-0.2.0.dev41.dist-info → lfx_nightly-0.3.0.dev3.dist-info}/RECORD +98 -80
- {lfx_nightly-0.2.0.dev41.dist-info → lfx_nightly-0.3.0.dev3.dist-info}/WHEEL +0 -0
- {lfx_nightly-0.2.0.dev41.dist-info → lfx_nightly-0.3.0.dev3.dist-info}/entry_points.txt +0 -0
lfx/cli/run.py
CHANGED
|
@@ -1,47 +1,50 @@
|
|
|
1
|
+
"""CLI wrapper for the run command."""
|
|
2
|
+
|
|
1
3
|
import json
|
|
2
|
-
import re
|
|
3
|
-
import sys
|
|
4
|
-
import tempfile
|
|
5
4
|
from functools import partial
|
|
6
|
-
from io import StringIO
|
|
7
5
|
from pathlib import Path
|
|
8
6
|
|
|
9
7
|
import typer
|
|
10
8
|
from asyncer import syncify
|
|
11
9
|
|
|
12
|
-
from lfx.
|
|
13
|
-
extract_structured_result,
|
|
14
|
-
extract_text_from_result,
|
|
15
|
-
find_graph_variable,
|
|
16
|
-
load_graph_from_script,
|
|
17
|
-
)
|
|
18
|
-
from lfx.cli.validation import validate_global_variables_for_env
|
|
19
|
-
from lfx.log.logger import logger
|
|
20
|
-
from lfx.schema.schema import InputValueRequest
|
|
10
|
+
from lfx.run.base import RunError, run_flow
|
|
21
11
|
|
|
22
12
|
# Verbosity level constants
|
|
23
13
|
VERBOSITY_DETAILED = 2
|
|
24
14
|
VERBOSITY_FULL = 3
|
|
25
15
|
|
|
26
16
|
|
|
27
|
-
def
|
|
28
|
-
"""
|
|
29
|
-
if verbose:
|
|
30
|
-
typer.echo(f"{error_message}", file=sys.stderr)
|
|
31
|
-
|
|
32
|
-
error_response = {
|
|
33
|
-
"success": False,
|
|
34
|
-
"type": "error",
|
|
35
|
-
}
|
|
17
|
+
def _check_langchain_version_compatibility(error_message: str) -> str | None:
|
|
18
|
+
"""Check if error is due to langchain-core version incompatibility.
|
|
36
19
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
20
|
+
Returns a helpful error message if incompatibility is detected, None otherwise.
|
|
21
|
+
"""
|
|
22
|
+
# Check for the specific error that occurs with langchain-core 1.x
|
|
23
|
+
# The langchain_core.memory module was removed in langchain-core 1.x
|
|
24
|
+
if "langchain_core.memory" in error_message or "No module named 'langchain_core.memory'" in error_message:
|
|
25
|
+
try:
|
|
26
|
+
import langchain_core
|
|
27
|
+
|
|
28
|
+
version = getattr(langchain_core, "__version__", "unknown")
|
|
29
|
+
except ImportError:
|
|
30
|
+
version = "unknown"
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
f"ERROR: Incompatible langchain-core version (v{version}).\n\n"
|
|
34
|
+
"The 'langchain_core.memory' module was removed in langchain-core 1.x.\n"
|
|
35
|
+
"lfx requires langchain-core < 1.0.0.\n\n"
|
|
36
|
+
"This usually happens when langchain-openai >= 1.0.0 is installed,\n"
|
|
37
|
+
"which pulls in langchain-core >= 1.0.0.\n\n"
|
|
38
|
+
"FIX: Reinstall with compatible versions:\n\n"
|
|
39
|
+
" uv pip install 'langchain-core>=0.3.0,<1.0.0' \\\n"
|
|
40
|
+
" 'langchain-openai>=0.3.0,<1.0.0' \\\n"
|
|
41
|
+
" 'langchain-community>=0.3.0,<1.0.0'\n\n"
|
|
42
|
+
"Or with pip:\n\n"
|
|
43
|
+
" pip install 'langchain-core>=0.3.0,<1.0.0' \\\n"
|
|
44
|
+
" 'langchain-openai>=0.3.0,<1.0.0' \\\n"
|
|
45
|
+
" 'langchain-community>=0.3.0,<1.0.0'"
|
|
46
|
+
)
|
|
47
|
+
return None
|
|
45
48
|
|
|
46
49
|
|
|
47
50
|
@partial(syncify, raise_sync_error=False)
|
|
@@ -119,388 +122,41 @@ async def run(
|
|
|
119
122
|
check_variables: Check global variables for environment compatibility
|
|
120
123
|
timing: Include detailed timing information in output
|
|
121
124
|
"""
|
|
122
|
-
#
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
# Configure logger based on verbosity level
|
|
126
|
-
from lfx.log.logger import configure
|
|
127
|
-
|
|
128
|
-
if verbose_full:
|
|
129
|
-
configure(log_level="DEBUG", output_file=sys.stderr) # Show everything including component debug logs
|
|
130
|
-
verbosity = 3
|
|
131
|
-
elif verbose_detailed:
|
|
132
|
-
configure(log_level="DEBUG", output_file=sys.stderr) # Show debug and above
|
|
133
|
-
verbosity = 2
|
|
134
|
-
elif verbose:
|
|
135
|
-
configure(log_level="INFO", output_file=sys.stderr) # Show info and above including our CLI info messages
|
|
136
|
-
verbosity = 1
|
|
137
|
-
else:
|
|
138
|
-
configure(log_level="CRITICAL", output_file=sys.stderr) # Only critical errors
|
|
139
|
-
verbosity = 0
|
|
140
|
-
|
|
141
|
-
start_time = time.time() if timing else None
|
|
142
|
-
|
|
143
|
-
# Use either positional input_value or --input-value option
|
|
144
|
-
final_input_value = input_value or input_value_option
|
|
145
|
-
|
|
146
|
-
# Validate input sources - exactly one must be provided
|
|
147
|
-
input_sources = [script_path is not None, flow_json is not None, bool(stdin)]
|
|
148
|
-
if sum(input_sources) != 1:
|
|
149
|
-
if sum(input_sources) == 0:
|
|
150
|
-
error_msg = "No input source provided. Must provide either script_path, --flow-json, or --stdin"
|
|
151
|
-
else:
|
|
152
|
-
error_msg = (
|
|
153
|
-
"Multiple input sources provided. Cannot use script_path, --flow-json, and "
|
|
154
|
-
"--stdin together. Choose exactly one."
|
|
155
|
-
)
|
|
156
|
-
output_error(error_msg, verbose=verbose)
|
|
157
|
-
raise typer.Exit(1)
|
|
158
|
-
|
|
159
|
-
temp_file_to_cleanup = None
|
|
160
|
-
|
|
161
|
-
if flow_json is not None:
|
|
162
|
-
if verbosity > 0:
|
|
163
|
-
typer.echo("Processing inline JSON content...", file=sys.stderr)
|
|
164
|
-
try:
|
|
165
|
-
json_data = json.loads(flow_json)
|
|
166
|
-
if verbosity > 0:
|
|
167
|
-
typer.echo("JSON content is valid", file=sys.stderr)
|
|
168
|
-
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as temp_file:
|
|
169
|
-
json.dump(json_data, temp_file, indent=2)
|
|
170
|
-
temp_file_to_cleanup = temp_file.name
|
|
171
|
-
script_path = Path(temp_file_to_cleanup)
|
|
172
|
-
if verbosity > 0:
|
|
173
|
-
typer.echo(f"Created temporary file: {script_path}", file=sys.stderr)
|
|
174
|
-
except json.JSONDecodeError as e:
|
|
175
|
-
output_error(f"Invalid JSON content: {e}", verbose=verbose)
|
|
176
|
-
raise typer.Exit(1) from e
|
|
177
|
-
except Exception as e:
|
|
178
|
-
output_error(f"Error processing JSON content: {e}", verbose=verbose)
|
|
179
|
-
raise typer.Exit(1) from e
|
|
180
|
-
elif stdin:
|
|
181
|
-
if verbosity > 0:
|
|
182
|
-
typer.echo("Reading JSON content from stdin...", file=sys.stderr)
|
|
183
|
-
try:
|
|
184
|
-
stdin_content = sys.stdin.read().strip()
|
|
185
|
-
if not stdin_content:
|
|
186
|
-
output_error("No content received from stdin", verbose=verbose)
|
|
187
|
-
raise typer.Exit(1)
|
|
188
|
-
json_data = json.loads(stdin_content)
|
|
189
|
-
if verbosity > 0:
|
|
190
|
-
typer.echo("JSON content from stdin is valid", file=sys.stderr)
|
|
191
|
-
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as temp_file:
|
|
192
|
-
json.dump(json_data, temp_file, indent=2)
|
|
193
|
-
temp_file_to_cleanup = temp_file.name
|
|
194
|
-
script_path = Path(temp_file_to_cleanup)
|
|
195
|
-
if verbosity > 0:
|
|
196
|
-
typer.echo(f"Created temporary file from stdin: {script_path}", file=sys.stderr)
|
|
197
|
-
except json.JSONDecodeError as e:
|
|
198
|
-
output_error(f"Invalid JSON content from stdin: {e}", verbose=verbose)
|
|
199
|
-
raise typer.Exit(1) from e
|
|
200
|
-
except Exception as e:
|
|
201
|
-
output_error(f"Error reading from stdin: {e}", verbose=verbose)
|
|
202
|
-
raise typer.Exit(1) from e
|
|
203
|
-
|
|
204
|
-
try:
|
|
205
|
-
if not script_path or not script_path.exists():
|
|
206
|
-
error_msg = f"File '{script_path}' does not exist."
|
|
207
|
-
raise ValueError(error_msg)
|
|
208
|
-
if not script_path.is_file():
|
|
209
|
-
error_msg = f"'{script_path}' is not a file."
|
|
210
|
-
raise ValueError(error_msg)
|
|
211
|
-
file_extension = script_path.suffix.lower()
|
|
212
|
-
if file_extension not in [".py", ".json"]:
|
|
213
|
-
error_msg = f"'{script_path}' must be a .py or .json file."
|
|
214
|
-
raise ValueError(error_msg)
|
|
215
|
-
file_type = "Python script" if file_extension == ".py" else "JSON flow"
|
|
216
|
-
if verbosity > 0:
|
|
217
|
-
typer.echo(f"Analyzing {file_type}: {script_path}", file=sys.stderr)
|
|
218
|
-
if file_extension == ".py":
|
|
219
|
-
graph_info = find_graph_variable(script_path)
|
|
220
|
-
if not graph_info:
|
|
221
|
-
error_msg = (
|
|
222
|
-
"No 'graph' variable found in the script. Expected to find an assignment like: graph = Graph(...)"
|
|
223
|
-
)
|
|
224
|
-
raise ValueError(error_msg)
|
|
225
|
-
if verbosity > 0:
|
|
226
|
-
typer.echo(f"Found 'graph' variable at line {graph_info['line_number']}", file=sys.stderr)
|
|
227
|
-
typer.echo(f"Type: {graph_info['type']}", file=sys.stderr)
|
|
228
|
-
typer.echo(f"Source: {graph_info['source_line']}", file=sys.stderr)
|
|
229
|
-
typer.echo("Loading and executing script...", file=sys.stderr)
|
|
230
|
-
graph = await load_graph_from_script(script_path)
|
|
231
|
-
elif file_extension == ".json":
|
|
232
|
-
if verbosity > 0:
|
|
233
|
-
typer.echo("Valid JSON flow file detected", file=sys.stderr)
|
|
234
|
-
typer.echo("Loading and executing JSON flow", file=sys.stderr)
|
|
235
|
-
from lfx.load import aload_flow_from_json
|
|
236
|
-
|
|
237
|
-
graph = await aload_flow_from_json(script_path, disable_logs=not verbose)
|
|
238
|
-
except Exception as e:
|
|
239
|
-
error_type = type(e).__name__
|
|
240
|
-
logger.error(f"Graph loading failed with {error_type}")
|
|
241
|
-
|
|
242
|
-
if verbosity > 0:
|
|
243
|
-
# Try to identify common error patterns
|
|
244
|
-
if "ModuleNotFoundError" in str(e) or "No module named" in str(e):
|
|
245
|
-
logger.info("This appears to be a missing dependency issue")
|
|
246
|
-
if "langchain" in str(e).lower():
|
|
247
|
-
match = re.search(r"langchain_(.*)", str(e).lower())
|
|
248
|
-
if match:
|
|
249
|
-
module_name = match.group(1)
|
|
250
|
-
logger.info(
|
|
251
|
-
f"Missing LangChain dependency detected. Try: pip install langchain-{module_name}",
|
|
252
|
-
)
|
|
253
|
-
elif "ImportError" in str(e):
|
|
254
|
-
logger.info("This appears to be an import issue - check component dependencies")
|
|
255
|
-
elif "AttributeError" in str(e):
|
|
256
|
-
logger.info("This appears to be a component configuration issue")
|
|
257
|
-
|
|
258
|
-
# Show full traceback in debug mode
|
|
259
|
-
logger.exception("Failed to load graph.")
|
|
260
|
-
|
|
261
|
-
output_error(f"Failed to load graph. {e}", verbose=verbose, exception=e)
|
|
262
|
-
if temp_file_to_cleanup:
|
|
263
|
-
try:
|
|
264
|
-
Path(temp_file_to_cleanup).unlink()
|
|
265
|
-
logger.info(f"Cleaned up temporary file: {temp_file_to_cleanup}")
|
|
266
|
-
except OSError:
|
|
267
|
-
pass
|
|
268
|
-
raise typer.Exit(1) from e
|
|
269
|
-
|
|
270
|
-
inputs = InputValueRequest(input_value=final_input_value) if final_input_value else None
|
|
125
|
+
# Determine verbosity for output formatting
|
|
126
|
+
verbosity = 3 if verbose_full else (2 if verbose_detailed else (1 if verbose else 0))
|
|
271
127
|
|
|
272
|
-
# Mark end of loading phase if timing
|
|
273
|
-
load_end_time = time.time() if timing else None
|
|
274
|
-
|
|
275
|
-
if verbosity > 0:
|
|
276
|
-
typer.echo("Preparing graph for execution...", file=sys.stderr)
|
|
277
128
|
try:
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
#
|
|
294
|
-
if
|
|
295
|
-
|
|
296
|
-
validation_errors = validate_global_variables_for_env(graph)
|
|
297
|
-
if validation_errors:
|
|
298
|
-
error_details = "Global variable validation failed: " + "; ".join(validation_errors)
|
|
299
|
-
logger.info(f"Variable validation failed: {len(validation_errors)} errors")
|
|
300
|
-
for error in validation_errors:
|
|
301
|
-
logger.debug(f"Validation error: {error}")
|
|
302
|
-
output_error(error_details, verbose=verbose)
|
|
303
|
-
if temp_file_to_cleanup:
|
|
304
|
-
try:
|
|
305
|
-
Path(temp_file_to_cleanup).unlink()
|
|
306
|
-
logger.info(f"Cleaned up temporary file: {temp_file_to_cleanup}")
|
|
307
|
-
except OSError:
|
|
308
|
-
pass
|
|
309
|
-
if validation_errors:
|
|
310
|
-
raise typer.Exit(1)
|
|
311
|
-
logger.info("Global variable validation passed")
|
|
129
|
+
result = await run_flow(
|
|
130
|
+
script_path=script_path,
|
|
131
|
+
input_value=input_value,
|
|
132
|
+
input_value_option=input_value_option,
|
|
133
|
+
output_format=output_format,
|
|
134
|
+
flow_json=flow_json,
|
|
135
|
+
stdin=bool(stdin),
|
|
136
|
+
check_variables=check_variables,
|
|
137
|
+
verbose=verbose,
|
|
138
|
+
verbose_detailed=verbose_detailed,
|
|
139
|
+
verbose_full=verbose_full,
|
|
140
|
+
timing=timing,
|
|
141
|
+
global_variables=None,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Output based on format
|
|
145
|
+
if output_format in {"text", "message", "result"}:
|
|
146
|
+
typer.echo(result.get("output", ""))
|
|
312
147
|
else:
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
error_type = type(e).__name__
|
|
316
|
-
logger.info(f"Graph preparation failed with {error_type}")
|
|
317
|
-
|
|
318
|
-
if verbosity > 0:
|
|
319
|
-
logger.debug(f"Preparation error: {e!s}")
|
|
320
|
-
logger.exception("Failed to prepare graph - full traceback:")
|
|
148
|
+
indent = 2 if verbosity > 0 else None
|
|
149
|
+
typer.echo(json.dumps(result, indent=indent))
|
|
321
150
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
logger.info("Executing graph...")
|
|
332
|
-
execution_start_time = time.time() if timing else None
|
|
333
|
-
if verbose:
|
|
334
|
-
logger.debug("Setting up execution environment")
|
|
335
|
-
if inputs:
|
|
336
|
-
logger.debug(f"Input provided: {inputs.input_value}")
|
|
151
|
+
except RunError as e:
|
|
152
|
+
error_response = {
|
|
153
|
+
"success": False,
|
|
154
|
+
"type": "error",
|
|
155
|
+
}
|
|
156
|
+
if e.original_exception:
|
|
157
|
+
error_response["exception_type"] = type(e.original_exception).__name__
|
|
158
|
+
error_response["exception_message"] = str(e.original_exception)
|
|
337
159
|
else:
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
captured_stdout = StringIO()
|
|
341
|
-
captured_stderr = StringIO()
|
|
342
|
-
original_stdout = sys.stdout
|
|
343
|
-
original_stderr = sys.stderr
|
|
344
|
-
|
|
345
|
-
# Track component timing if requested
|
|
346
|
-
component_timings = [] if timing else None
|
|
347
|
-
execution_step_start = execution_start_time if timing else None
|
|
348
|
-
|
|
349
|
-
try:
|
|
350
|
-
sys.stdout = captured_stdout
|
|
351
|
-
# Don't capture stderr at high verbosity levels to avoid duplication with direct logging
|
|
352
|
-
if verbosity < VERBOSITY_FULL:
|
|
353
|
-
sys.stderr = captured_stderr
|
|
354
|
-
results = []
|
|
355
|
-
|
|
356
|
-
logger.info("Starting graph execution...", level="DEBUG")
|
|
357
|
-
result_count = 0
|
|
358
|
-
|
|
359
|
-
async for result in graph.async_start(inputs):
|
|
360
|
-
result_count += 1
|
|
361
|
-
if verbosity > 0:
|
|
362
|
-
logger.debug(f"Processing result #{result_count}")
|
|
363
|
-
if hasattr(result, "vertex") and hasattr(result.vertex, "display_name"):
|
|
364
|
-
logger.debug(f"Component: {result.vertex.display_name}")
|
|
365
|
-
if timing:
|
|
366
|
-
step_end_time = time.time()
|
|
367
|
-
step_duration = step_end_time - execution_step_start
|
|
368
|
-
|
|
369
|
-
# Extract component information
|
|
370
|
-
if hasattr(result, "vertex"):
|
|
371
|
-
component_name = getattr(result.vertex, "display_name", "Unknown")
|
|
372
|
-
component_id = getattr(result.vertex, "id", "Unknown")
|
|
373
|
-
component_timings.append(
|
|
374
|
-
{
|
|
375
|
-
"component": component_name,
|
|
376
|
-
"component_id": component_id,
|
|
377
|
-
"duration": step_duration,
|
|
378
|
-
"cumulative_time": step_end_time - execution_start_time,
|
|
379
|
-
}
|
|
380
|
-
)
|
|
381
|
-
|
|
382
|
-
execution_step_start = step_end_time
|
|
383
|
-
|
|
384
|
-
results.append(result)
|
|
385
|
-
|
|
386
|
-
logger.info(f"Graph execution completed. Processed {result_count} results")
|
|
387
|
-
|
|
388
|
-
except Exception as e:
|
|
389
|
-
error_type = type(e).__name__
|
|
390
|
-
logger.info(f"Graph execution failed with {error_type}")
|
|
391
|
-
|
|
392
|
-
if verbosity >= VERBOSITY_DETAILED: # Only show details at -vv and above
|
|
393
|
-
logger.debug(f"Failed after processing {result_count} results")
|
|
394
|
-
|
|
395
|
-
# Only show component output at maximum verbosity (-vvv)
|
|
396
|
-
if verbosity >= VERBOSITY_FULL:
|
|
397
|
-
# Capture any output that was generated before the error
|
|
398
|
-
# Only show captured stdout since stderr logging is already shown directly in verbose mode
|
|
399
|
-
captured_content = captured_stdout.getvalue()
|
|
400
|
-
if captured_content.strip():
|
|
401
|
-
# Check if captured content contains the same error that will be displayed at the end
|
|
402
|
-
error_text = str(e)
|
|
403
|
-
captured_lines = captured_content.strip().split("\n")
|
|
404
|
-
|
|
405
|
-
# Filter out lines that are duplicates of the final error message
|
|
406
|
-
unique_lines = [
|
|
407
|
-
line
|
|
408
|
-
for line in captured_lines
|
|
409
|
-
if not any(
|
|
410
|
-
error_part.strip() in line for error_part in error_text.split("\n") if error_part.strip()
|
|
411
|
-
)
|
|
412
|
-
]
|
|
413
|
-
|
|
414
|
-
if unique_lines:
|
|
415
|
-
logger.info("Component output before error:", level="DEBUG")
|
|
416
|
-
for line in unique_lines:
|
|
417
|
-
# Log each line directly using the logger to avoid nested formatting
|
|
418
|
-
if verbosity > 0:
|
|
419
|
-
# Remove any existing timestamp prefix to avoid duplication
|
|
420
|
-
clean_line = line
|
|
421
|
-
if "] " in line and line.startswith("2025-"):
|
|
422
|
-
# Extract just the log message after the timestamp and level
|
|
423
|
-
parts = line.split("] ", 1)
|
|
424
|
-
if len(parts) > 1:
|
|
425
|
-
clean_line = parts[1]
|
|
426
|
-
logger.debug(clean_line)
|
|
427
|
-
|
|
428
|
-
# Provide context about common execution errors
|
|
429
|
-
if "list can't be used in 'await' expression" in str(e):
|
|
430
|
-
logger.info("This appears to be an async/await mismatch in a component")
|
|
431
|
-
logger.info("Check that async methods are properly awaited")
|
|
432
|
-
elif "AttributeError" in error_type and "NoneType" in str(e):
|
|
433
|
-
logger.info("This appears to be a null reference error")
|
|
434
|
-
logger.info("A component may be receiving unexpected None values")
|
|
435
|
-
elif "ConnectionError" in str(e) or "TimeoutError" in str(e):
|
|
436
|
-
logger.info("This appears to be a network connectivity issue")
|
|
437
|
-
logger.info("Check API keys and network connectivity")
|
|
438
|
-
|
|
439
|
-
logger.exception("Failed to execute graph - full traceback:")
|
|
440
|
-
|
|
441
|
-
if temp_file_to_cleanup:
|
|
442
|
-
try:
|
|
443
|
-
Path(temp_file_to_cleanup).unlink()
|
|
444
|
-
logger.info(f"Cleaned up temporary file: {temp_file_to_cleanup}")
|
|
445
|
-
except OSError:
|
|
446
|
-
pass
|
|
447
|
-
sys.stdout = original_stdout
|
|
448
|
-
sys.stderr = original_stderr
|
|
449
|
-
output_error(f"Failed to execute graph: {e}", verbose=verbosity > 0, exception=e)
|
|
160
|
+
error_response["exception_message"] = str(e)
|
|
161
|
+
typer.echo(json.dumps(error_response))
|
|
450
162
|
raise typer.Exit(1) from e
|
|
451
|
-
finally:
|
|
452
|
-
sys.stdout = original_stdout
|
|
453
|
-
sys.stderr = original_stderr
|
|
454
|
-
if temp_file_to_cleanup:
|
|
455
|
-
try:
|
|
456
|
-
Path(temp_file_to_cleanup).unlink()
|
|
457
|
-
logger.info(f"Cleaned up temporary file: {temp_file_to_cleanup}")
|
|
458
|
-
except OSError:
|
|
459
|
-
pass
|
|
460
|
-
|
|
461
|
-
execution_end_time = time.time() if timing else None
|
|
462
|
-
|
|
463
|
-
captured_logs = captured_stdout.getvalue() + captured_stderr.getvalue()
|
|
464
|
-
|
|
465
|
-
# Create timing metadata if requested
|
|
466
|
-
timing_metadata = None
|
|
467
|
-
if timing:
|
|
468
|
-
load_duration = load_end_time - start_time
|
|
469
|
-
execution_duration = execution_end_time - execution_start_time
|
|
470
|
-
total_duration = execution_end_time - start_time
|
|
471
|
-
|
|
472
|
-
timing_metadata = {
|
|
473
|
-
"load_time": round(load_duration, 3),
|
|
474
|
-
"execution_time": round(execution_duration, 3),
|
|
475
|
-
"total_time": round(total_duration, 3),
|
|
476
|
-
"component_timings": [
|
|
477
|
-
{
|
|
478
|
-
"component": ct["component"],
|
|
479
|
-
"component_id": ct["component_id"],
|
|
480
|
-
"duration": round(ct["duration"], 3),
|
|
481
|
-
"cumulative_time": round(ct["cumulative_time"], 3),
|
|
482
|
-
}
|
|
483
|
-
for ct in component_timings
|
|
484
|
-
],
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
if output_format == "json":
|
|
488
|
-
result_data = extract_structured_result(results)
|
|
489
|
-
result_data["logs"] = captured_logs
|
|
490
|
-
if timing_metadata:
|
|
491
|
-
result_data["timing"] = timing_metadata
|
|
492
|
-
indent = 2 if verbosity > 0 else None
|
|
493
|
-
typer.echo(json.dumps(result_data, indent=indent))
|
|
494
|
-
elif output_format in {"text", "message"}:
|
|
495
|
-
result_data = extract_structured_result(results)
|
|
496
|
-
output_text = result_data.get("result", result_data.get("text", ""))
|
|
497
|
-
typer.echo(str(output_text))
|
|
498
|
-
elif output_format == "result":
|
|
499
|
-
typer.echo(extract_text_from_result(results))
|
|
500
|
-
else:
|
|
501
|
-
result_data = extract_structured_result(results)
|
|
502
|
-
result_data["logs"] = captured_logs
|
|
503
|
-
if timing_metadata:
|
|
504
|
-
result_data["timing"] = timing_metadata
|
|
505
|
-
indent = 2 if verbosity > 0 else None
|
|
506
|
-
typer.echo(json.dumps(result_data, indent=indent))
|
lfx/cli/script_loader.py
CHANGED
|
@@ -15,9 +15,8 @@ from typing import TYPE_CHECKING, Any
|
|
|
15
15
|
|
|
16
16
|
import typer
|
|
17
17
|
|
|
18
|
-
from lfx.graph import Graph
|
|
19
|
-
|
|
20
18
|
if TYPE_CHECKING:
|
|
19
|
+
from lfx.graph import Graph
|
|
21
20
|
from lfx.schema.message import Message
|
|
22
21
|
|
|
23
22
|
|
|
@@ -36,21 +35,33 @@ def temporary_sys_path(path: str):
|
|
|
36
35
|
|
|
37
36
|
def _load_module_from_script(script_path: Path) -> Any:
|
|
38
37
|
"""Load a Python module from a script file."""
|
|
39
|
-
|
|
38
|
+
# Use the script name as the module name to allow inspect to find it
|
|
39
|
+
module_name = script_path.stem
|
|
40
|
+
spec = importlib.util.spec_from_file_location(module_name, script_path)
|
|
40
41
|
if spec is None or spec.loader is None:
|
|
41
42
|
msg = f"Could not create module spec for '{script_path}'"
|
|
42
43
|
raise ImportError(msg)
|
|
43
44
|
|
|
44
45
|
module = importlib.util.module_from_spec(spec)
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
# Register in sys.modules so inspect.getmodule works
|
|
48
|
+
sys.modules[module_name] = module
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
with temporary_sys_path(str(script_path.parent)):
|
|
52
|
+
spec.loader.exec_module(module)
|
|
53
|
+
except Exception:
|
|
54
|
+
if module_name in sys.modules:
|
|
55
|
+
del sys.modules[module_name]
|
|
56
|
+
raise
|
|
48
57
|
|
|
49
58
|
return module
|
|
50
59
|
|
|
51
60
|
|
|
52
|
-
def _validate_graph_instance(graph_obj: Any) -> Graph:
|
|
61
|
+
def _validate_graph_instance(graph_obj: Any) -> "Graph":
|
|
53
62
|
"""Extract information from a graph object."""
|
|
63
|
+
from lfx.graph import Graph
|
|
64
|
+
|
|
54
65
|
if not isinstance(graph_obj, Graph):
|
|
55
66
|
msg = f"Graph object is not a LFX Graph instance: {type(graph_obj)}"
|
|
56
67
|
raise TypeError(msg)
|
|
@@ -72,7 +83,7 @@ def _validate_graph_instance(graph_obj: Any) -> Graph:
|
|
|
72
83
|
return graph_obj
|
|
73
84
|
|
|
74
85
|
|
|
75
|
-
async def load_graph_from_script(script_path: Path) -> Graph:
|
|
86
|
+
async def load_graph_from_script(script_path: Path) -> "Graph":
|
|
76
87
|
"""Load and execute a Python script to extract the 'graph' variable or call 'get_graph' function.
|
|
77
88
|
|
|
78
89
|
Args:
|
lfx/cli/validation.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"""Validation utilities for CLI commands."""
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
from lfx.
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from lfx.graph.graph.base import Graph
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
def is_valid_env_var_name(name: str) -> bool:
|
|
@@ -26,7 +27,7 @@ def is_valid_env_var_name(name: str) -> bool:
|
|
|
26
27
|
return bool(re.match(pattern, name))
|
|
27
28
|
|
|
28
29
|
|
|
29
|
-
def validate_global_variables_for_env(graph: Graph) -> list[str]:
|
|
30
|
+
def validate_global_variables_for_env(graph: "Graph") -> list[str]:
|
|
30
31
|
"""Validate that all global variables with load_from_db=True can be used as environment variables.
|
|
31
32
|
|
|
32
33
|
When the database is not available (noop mode), global variables with load_from_db=True
|
|
@@ -39,6 +40,8 @@ def validate_global_variables_for_env(graph: Graph) -> list[str]:
|
|
|
39
40
|
Returns:
|
|
40
41
|
list[str]: List of error messages for invalid variable names
|
|
41
42
|
"""
|
|
43
|
+
from lfx.services.deps import get_settings_service
|
|
44
|
+
|
|
42
45
|
errors = []
|
|
43
46
|
settings_service = get_settings_service()
|
|
44
47
|
|
lfx/components/__init__.py
CHANGED
|
@@ -64,7 +64,6 @@ if TYPE_CHECKING:
|
|
|
64
64
|
mem0,
|
|
65
65
|
milvus,
|
|
66
66
|
mistral,
|
|
67
|
-
models,
|
|
68
67
|
models_and_agents,
|
|
69
68
|
mongodb,
|
|
70
69
|
needle,
|
|
@@ -168,7 +167,6 @@ _dynamic_imports = {
|
|
|
168
167
|
"mem0": "__module__",
|
|
169
168
|
"milvus": "__module__",
|
|
170
169
|
"mistral": "__module__",
|
|
171
|
-
"models": "__module__",
|
|
172
170
|
"models_and_agents": "__module__",
|
|
173
171
|
"mongodb": "__module__",
|
|
174
172
|
"needle": "__module__",
|
|
@@ -300,7 +298,6 @@ __all__ = [
|
|
|
300
298
|
"mem0",
|
|
301
299
|
"milvus",
|
|
302
300
|
"mistral",
|
|
303
|
-
"models",
|
|
304
301
|
"models_and_agents",
|
|
305
302
|
"mongodb",
|
|
306
303
|
"needle",
|
|
@@ -3,7 +3,7 @@ from lfx.base.composio.composio_base import ComposioBaseComponent
|
|
|
3
3
|
|
|
4
4
|
class ComposioGitHubAPIComponent(ComposioBaseComponent):
|
|
5
5
|
display_name: str = "GitHub"
|
|
6
|
-
icon = "
|
|
6
|
+
icon = "GithubComposio"
|
|
7
7
|
documentation: str = "https://docs.composio.dev"
|
|
8
8
|
app_name = "github"
|
|
9
9
|
|