lfx-nightly 0.2.0.dev26__py3-none-any.whl → 0.2.1.dev7__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.
Files changed (85) hide show
  1. lfx/_assets/component_index.json +1 -1
  2. lfx/base/agents/agent.py +9 -4
  3. lfx/base/agents/altk_base_agent.py +16 -3
  4. lfx/base/agents/altk_tool_wrappers.py +1 -1
  5. lfx/base/agents/utils.py +4 -0
  6. lfx/base/composio/composio_base.py +78 -41
  7. lfx/base/data/base_file.py +14 -4
  8. lfx/base/data/cloud_storage_utils.py +156 -0
  9. lfx/base/data/docling_utils.py +191 -65
  10. lfx/base/data/storage_utils.py +109 -0
  11. lfx/base/datastax/astradb_base.py +75 -64
  12. lfx/base/mcp/util.py +2 -2
  13. lfx/base/models/__init__.py +11 -1
  14. lfx/base/models/anthropic_constants.py +21 -12
  15. lfx/base/models/google_generative_ai_constants.py +33 -9
  16. lfx/base/models/model_metadata.py +6 -0
  17. lfx/base/models/ollama_constants.py +196 -30
  18. lfx/base/models/openai_constants.py +37 -10
  19. lfx/base/models/unified_models.py +1123 -0
  20. lfx/base/models/watsonx_constants.py +36 -0
  21. lfx/base/tools/component_tool.py +2 -9
  22. lfx/cli/commands.py +6 -1
  23. lfx/cli/run.py +65 -409
  24. lfx/cli/script_loader.py +13 -3
  25. lfx/components/__init__.py +0 -3
  26. lfx/components/composio/github_composio.py +1 -1
  27. lfx/components/cuga/cuga_agent.py +39 -27
  28. lfx/components/data_source/api_request.py +4 -2
  29. lfx/components/docling/__init__.py +45 -11
  30. lfx/components/docling/chunk_docling_document.py +3 -1
  31. lfx/components/docling/docling_inline.py +39 -49
  32. lfx/components/docling/export_docling_document.py +3 -1
  33. lfx/components/elastic/opensearch_multimodal.py +215 -57
  34. lfx/components/files_and_knowledge/file.py +439 -39
  35. lfx/components/files_and_knowledge/ingestion.py +8 -0
  36. lfx/components/files_and_knowledge/retrieval.py +10 -0
  37. lfx/components/files_and_knowledge/save_file.py +123 -53
  38. lfx/components/ibm/watsonx.py +7 -1
  39. lfx/components/input_output/chat_output.py +7 -1
  40. lfx/components/langchain_utilities/tool_calling.py +14 -6
  41. lfx/components/llm_operations/batch_run.py +80 -25
  42. lfx/components/llm_operations/lambda_filter.py +33 -6
  43. lfx/components/llm_operations/llm_conditional_router.py +39 -7
  44. lfx/components/llm_operations/structured_output.py +38 -12
  45. lfx/components/models/__init__.py +16 -74
  46. lfx/components/models_and_agents/agent.py +51 -201
  47. lfx/components/models_and_agents/embedding_model.py +185 -339
  48. lfx/components/models_and_agents/language_model.py +54 -318
  49. lfx/components/models_and_agents/mcp_component.py +58 -9
  50. lfx/components/ollama/ollama.py +9 -4
  51. lfx/components/ollama/ollama_embeddings.py +2 -1
  52. lfx/components/openai/openai_chat_model.py +1 -1
  53. lfx/components/processing/__init__.py +0 -3
  54. lfx/components/vllm/__init__.py +37 -0
  55. lfx/components/vllm/vllm.py +141 -0
  56. lfx/components/vllm/vllm_embeddings.py +110 -0
  57. lfx/custom/custom_component/custom_component.py +8 -6
  58. lfx/custom/directory_reader/directory_reader.py +5 -2
  59. lfx/graph/utils.py +64 -18
  60. lfx/inputs/__init__.py +2 -0
  61. lfx/inputs/input_mixin.py +54 -0
  62. lfx/inputs/inputs.py +115 -0
  63. lfx/interface/initialize/loading.py +42 -12
  64. lfx/io/__init__.py +2 -0
  65. lfx/run/__init__.py +5 -0
  66. lfx/run/base.py +494 -0
  67. lfx/schema/data.py +1 -1
  68. lfx/schema/image.py +28 -19
  69. lfx/schema/message.py +19 -3
  70. lfx/services/interfaces.py +5 -0
  71. lfx/services/manager.py +5 -4
  72. lfx/services/mcp_composer/service.py +45 -13
  73. lfx/services/settings/auth.py +18 -11
  74. lfx/services/settings/base.py +12 -24
  75. lfx/services/settings/constants.py +2 -0
  76. lfx/services/storage/local.py +37 -0
  77. lfx/services/storage/service.py +19 -0
  78. lfx/utils/constants.py +1 -0
  79. lfx/utils/image.py +29 -11
  80. lfx/utils/validate_cloud.py +14 -3
  81. {lfx_nightly-0.2.0.dev26.dist-info → lfx_nightly-0.2.1.dev7.dist-info}/METADATA +5 -2
  82. {lfx_nightly-0.2.0.dev26.dist-info → lfx_nightly-0.2.1.dev7.dist-info}/RECORD +84 -78
  83. lfx/components/processing/dataframe_to_toolset.py +0 -259
  84. {lfx_nightly-0.2.0.dev26.dist-info → lfx_nightly-0.2.1.dev7.dist-info}/WHEEL +0 -0
  85. {lfx_nightly-0.2.0.dev26.dist-info → lfx_nightly-0.2.1.dev7.dist-info}/entry_points.txt +0 -0
@@ -1,29 +1,65 @@
1
1
  from .model_metadata import create_model_metadata
2
2
 
3
+ WATSONX_DEFAULT_LLM_MODELS = [
4
+ create_model_metadata(
5
+ provider="IBM Watsonx",
6
+ name="ibm/granite-3-2b-instruct",
7
+ icon="WatsonxAI",
8
+ model_type="llm",
9
+ default=True,
10
+ ),
11
+ create_model_metadata(
12
+ provider="IBM Watsonx",
13
+ name="ibm/granite-3-8b-instruct",
14
+ icon="WatsonxAI",
15
+ model_type="llm",
16
+ default=True,
17
+ ),
18
+ create_model_metadata(
19
+ provider="IBM Watsonx",
20
+ name="ibm/granite-13b-instruct-v2",
21
+ icon="WatsonxAI",
22
+ model_type="llm",
23
+ default=True,
24
+ ),
25
+ ]
26
+
3
27
  WATSONX_DEFAULT_EMBEDDING_MODELS = [
4
28
  create_model_metadata(
5
29
  provider="IBM Watsonx",
6
30
  name="sentence-transformers/all-minilm-l12-v2",
7
31
  icon="WatsonxAI",
32
+ model_type="embeddings",
33
+ default=True,
8
34
  ),
9
35
  create_model_metadata(
10
36
  provider="IBM Watsonx",
11
37
  name="ibm/slate-125m-english-rtrvr-v2",
12
38
  icon="WatsonxAI",
39
+ model_type="embeddings",
40
+ default=True,
13
41
  ),
14
42
  create_model_metadata(
15
43
  provider="IBM Watsonx",
16
44
  name="ibm/slate-30m-english-rtrvr-v2",
17
45
  icon="WatsonxAI",
46
+ model_type="embeddings",
47
+ default=True,
18
48
  ),
19
49
  create_model_metadata(
20
50
  provider="IBM Watsonx",
21
51
  name="intfloat/multilingual-e5-large",
22
52
  icon="WatsonxAI",
53
+ model_type="embeddings",
54
+ default=True,
23
55
  ),
24
56
  ]
25
57
 
26
58
 
59
+ WATSONX_EMBEDDING_MODELS_DETAILED = WATSONX_DEFAULT_EMBEDDING_MODELS
60
+ # Combined list for all watsonx models
61
+ WATSONX_MODELS_DETAILED = WATSONX_DEFAULT_LLM_MODELS + WATSONX_DEFAULT_EMBEDDING_MODELS
62
+
27
63
  WATSONX_EMBEDDING_MODEL_NAMES = [metadata["name"] for metadata in WATSONX_DEFAULT_EMBEDDING_MODELS]
28
64
 
29
65
  IBM_WATSONX_URLS = [
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import re
5
- from typing import TYPE_CHECKING, Literal
5
+ from typing import TYPE_CHECKING
6
6
 
7
7
  import pandas as pd
8
8
  from langchain_core.tools import BaseTool, ToolException
@@ -22,7 +22,6 @@ if TYPE_CHECKING:
22
22
  from lfx.events.event_manager import EventManager
23
23
  from lfx.inputs.inputs import InputTypes
24
24
  from lfx.io import Output
25
- from lfx.schema.content_block import ContentBlock
26
25
  from lfx.schema.dotdict import dotdict
27
26
 
28
27
  TOOL_TYPES_SET = {"Tool", "BaseTool", "StructuredTool"}
@@ -42,15 +41,9 @@ def build_description(component: Component) -> str:
42
41
 
43
42
  async def send_message_noop(
44
43
  message: Message,
45
- text: str | None = None, # noqa: ARG001
46
- background_color: str | None = None, # noqa: ARG001
47
- text_color: str | None = None, # noqa: ARG001
48
- icon: str | None = None, # noqa: ARG001
49
- content_blocks: list[ContentBlock] | None = None, # noqa: ARG001
50
- format_type: Literal["default", "error", "warning", "info"] = "default", # noqa: ARG001
51
44
  id_: str | None = None, # noqa: ARG001
52
45
  *,
53
- allow_markdown: bool = True, # noqa: ARG001
46
+ skip_db_update: bool = False, # noqa: ARG001
54
47
  ) -> Message:
55
48
  """No-op implementation of send_message."""
56
49
  return message
lfx/cli/commands.py CHANGED
@@ -303,13 +303,18 @@ async def serve_command(
303
303
  console.print()
304
304
 
305
305
  # Start the server
306
+ # Use uvicorn.Server to properly handle async context
307
+ # uvicorn.run() uses asyncio.run() internally which fails when
308
+ # an event loop is already running (due to syncify decorator)
306
309
  try:
307
- uvicorn.run(
310
+ config = uvicorn.Config(
308
311
  serve_app,
309
312
  host=host,
310
313
  port=port,
311
314
  log_level=log_level,
312
315
  )
316
+ server = uvicorn.Server(config)
317
+ await server.serve()
313
318
  except KeyboardInterrupt:
314
319
  verbose_print("\n👋 Server stopped")
315
320
  raise typer.Exit(0) from None
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.cli.script_loader import (
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 output_error(error_message: str, *, verbose: bool, exception: Exception | None = None) -> None:
28
- """Output error in JSON format to stdout when not verbose, or to stderr when verbose."""
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
- # Add clean exception data if available
38
- if exception:
39
- error_response["exception_type"] = type(exception).__name__
40
- error_response["exception_message"] = str(exception)
41
- else:
42
- error_response["exception_message"] = error_message
43
-
44
- typer.echo(json.dumps(error_response))
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
- # Start timing if requested
123
- import time
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
- # Add detailed preparation steps
279
- if verbosity > 0:
280
- logger.debug(f"Graph contains {len(graph.vertices)} vertices")
281
- logger.debug(f"Graph contains {len(graph.edges)} edges")
282
-
283
- # Show component types being used
284
- component_types = set()
285
- for vertex in graph.vertices:
286
- if hasattr(vertex, "display_name"):
287
- component_types.add(vertex.display_name)
288
- logger.debug(f"Component types in graph: {', '.join(sorted(component_types))}")
289
-
290
- graph.prepare()
291
- logger.info("Graph preparation completed")
292
-
293
- # Validate global variables for environment compatibility
294
- if check_variables:
295
- logger.info("Validating global variables...")
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
- logger.info("Global variable validation skipped")
314
- except Exception as e:
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
- output_error(f"Failed to prepare graph: {e}", verbose=verbose, exception=e)
323
- if temp_file_to_cleanup:
324
- try:
325
- Path(temp_file_to_cleanup).unlink()
326
- logger.info(f"Cleaned up temporary file: {temp_file_to_cleanup}")
327
- except OSError:
328
- pass
329
- raise typer.Exit(1) from e
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
- logger.debug("No input provided")
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
@@ -36,15 +36,25 @@ def temporary_sys_path(path: str):
36
36
 
37
37
  def _load_module_from_script(script_path: Path) -> Any:
38
38
  """Load a Python module from a script file."""
39
- spec = importlib.util.spec_from_file_location("script_module", script_path)
39
+ # Use the script name as the module name to allow inspect to find it
40
+ module_name = script_path.stem
41
+ spec = importlib.util.spec_from_file_location(module_name, script_path)
40
42
  if spec is None or spec.loader is None:
41
43
  msg = f"Could not create module spec for '{script_path}'"
42
44
  raise ImportError(msg)
43
45
 
44
46
  module = importlib.util.module_from_spec(spec)
45
47
 
46
- with temporary_sys_path(str(script_path.parent)):
47
- spec.loader.exec_module(module)
48
+ # Register in sys.modules so inspect.getmodule works
49
+ sys.modules[module_name] = module
50
+
51
+ try:
52
+ with temporary_sys_path(str(script_path.parent)):
53
+ spec.loader.exec_module(module)
54
+ except Exception:
55
+ if module_name in sys.modules:
56
+ del sys.modules[module_name]
57
+ raise
48
58
 
49
59
  return module
50
60
 
@@ -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",