lfx-nightly 0.1.12.dev1__py3-none-any.whl → 0.1.12.dev2__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.

Potentially problematic release.


This version of lfx-nightly might be problematic. Click here for more details.

@@ -0,0 +1,377 @@
1
+ from typing import Any
2
+
3
+ from langflow.custom import Component
4
+ from langflow.io import BoolInput, HandleInput, MessageInput, MessageTextInput, MultilineInput, Output, TableInput
5
+ from langflow.schema.message import Message
6
+
7
+
8
+ class SmartRouterComponent(Component):
9
+ display_name = "Smart Router"
10
+ description = "Routes an input message using LLM-based categorization."
11
+ icon = "equal"
12
+ name = "SmartRouter"
13
+
14
+ def __init__(self, **kwargs):
15
+ super().__init__(**kwargs)
16
+ self._matched_category = None
17
+
18
+ inputs = [
19
+ HandleInput(
20
+ name="llm",
21
+ display_name="Language Model",
22
+ info="LLM to use for categorization.",
23
+ input_types=["LanguageModel"],
24
+ required=True,
25
+ ),
26
+ MessageTextInput(
27
+ name="input_text",
28
+ display_name="Input",
29
+ info="The primary text input for the operation.",
30
+ required=True,
31
+ ),
32
+ TableInput(
33
+ name="routes",
34
+ display_name="Routes",
35
+ info=(
36
+ "Define the categories for routing. Each row should have a route/category name "
37
+ "and optionally a custom output value."
38
+ ),
39
+ table_schema=[
40
+ {
41
+ "name": "route_category",
42
+ "display_name": "Route/Category",
43
+ "type": "str",
44
+ "description": "Name for the route/category (used for both output name and category matching)",
45
+ },
46
+ {
47
+ "name": "output_value",
48
+ "display_name": "Output Value",
49
+ "type": "str",
50
+ "description": "Custom message for this category (overrides default output message if filled)",
51
+ "default": "",
52
+ },
53
+ ],
54
+ value=[
55
+ {"route_category": "Positive", "output_value": ""},
56
+ {"route_category": "Negative", "output_value": ""},
57
+ ],
58
+ real_time_refresh=True,
59
+ required=True,
60
+ ),
61
+ MessageInput(
62
+ name="message",
63
+ display_name="Override Output",
64
+ info=(
65
+ "Optional override message that will replace both the Input and Output Value "
66
+ "for all routes when filled."
67
+ ),
68
+ required=False,
69
+ advanced=True,
70
+ ),
71
+ BoolInput(
72
+ name="enable_else_output",
73
+ display_name="Include Else Output",
74
+ info="Include an Else output for cases that don't match any route.",
75
+ value=False,
76
+ advanced=True,
77
+ ),
78
+ MultilineInput(
79
+ name="custom_prompt",
80
+ display_name="Additional Instructions",
81
+ info=(
82
+ "Additional instructions for LLM-based categorization. "
83
+ "These will be added to the base prompt. "
84
+ "Use {input_text} for the input text and {routes} for the available categories."
85
+ ),
86
+ advanced=True,
87
+ ),
88
+ ]
89
+
90
+ outputs: list[Output] = []
91
+
92
+ def update_outputs(self, frontend_node: dict, field_name: str, field_value: Any) -> dict:
93
+ """Create a dynamic output for each category in the categories table."""
94
+ if field_name in {"routes", "enable_else_output"}:
95
+ frontend_node["outputs"] = []
96
+
97
+ # Get the routes data - either from field_value (if routes field) or from component state
98
+ routes_data = field_value if field_name == "routes" else getattr(self, "routes", [])
99
+
100
+ # Add a dynamic output for each category - all using the same method
101
+ for i, row in enumerate(routes_data):
102
+ route_category = row.get("route_category", f"Category {i + 1}")
103
+ frontend_node["outputs"].append(
104
+ Output(
105
+ display_name=route_category,
106
+ name=f"category_{i + 1}_result",
107
+ method="process_case",
108
+ group_outputs=True,
109
+ )
110
+ )
111
+ # Add default output only if enabled
112
+ if field_name == "enable_else_output":
113
+ enable_else = field_value
114
+ else:
115
+ enable_else = getattr(self, "enable_else_output", False)
116
+
117
+ if enable_else:
118
+ frontend_node["outputs"].append(
119
+ Output(display_name="Else", name="default_result", method="default_response", group_outputs=True)
120
+ )
121
+ return frontend_node
122
+
123
+ def process_case(self) -> Message:
124
+ """Process all categories using LLM categorization and return message for matching category."""
125
+ # Clear any previous match state
126
+ self._matched_category = None
127
+
128
+ categories = getattr(self, "routes", [])
129
+ input_text = getattr(self, "input_text", "")
130
+
131
+ # Find the matching category using LLM-based categorization
132
+ matched_category = None
133
+ llm = getattr(self, "llm", None)
134
+
135
+ if llm and categories:
136
+ # Create prompt for categorization
137
+ category_values = [
138
+ category.get("route_category", f"Category {i + 1}") for i, category in enumerate(categories)
139
+ ]
140
+ categories_text = ", ".join([f'"{cat}"' for cat in category_values if cat])
141
+
142
+ # Create base prompt
143
+ base_prompt = (
144
+ f"You are a text classifier. Given the following text and categories, "
145
+ f"determine which category best matches the text.\n\n"
146
+ f'Text to classify: "{input_text}"\n\n'
147
+ f"Available categories: {categories_text}\n\n"
148
+ f"Respond with ONLY the exact category name that best matches the text. "
149
+ f'If none match well, respond with "NONE".\n\n'
150
+ f"Category:"
151
+ )
152
+
153
+ # Use custom prompt as additional instructions if provided
154
+ custom_prompt = getattr(self, "custom_prompt", "")
155
+ if custom_prompt and custom_prompt.strip():
156
+ self.status = "Using custom prompt as additional instructions"
157
+ # Format custom prompt with variables
158
+ formatted_custom = custom_prompt.format(input_text=input_text, routes=categories_text)
159
+ # Combine base prompt with custom instructions
160
+ prompt = f"{base_prompt}\n\nAdditional Instructions:\n{formatted_custom}"
161
+ else:
162
+ self.status = "Using default prompt for LLM categorization"
163
+ prompt = base_prompt
164
+
165
+ # Log the final prompt being sent to LLM
166
+ self.status = f"Prompt sent to LLM:\n{prompt}"
167
+
168
+ try:
169
+ # Use the LLM to categorize
170
+ if hasattr(llm, "invoke"):
171
+ response = llm.invoke(prompt)
172
+ if hasattr(response, "content"):
173
+ categorization = response.content.strip().strip('"')
174
+ else:
175
+ categorization = str(response).strip().strip('"')
176
+ else:
177
+ categorization = str(llm(prompt)).strip().strip('"')
178
+
179
+ # Log the categorization process
180
+ self.status = f"LLM response: '{categorization}'"
181
+
182
+ # Find matching category based on LLM response
183
+ for i, category in enumerate(categories):
184
+ route_category = category.get("route_category", "")
185
+
186
+ # Log each comparison attempt
187
+ self.status = (
188
+ f"Comparing '{categorization}' with category {i + 1}: route_category='{route_category}'"
189
+ )
190
+
191
+ if categorization.lower() == route_category.lower():
192
+ matched_category = i
193
+ self.status = f"MATCH FOUND! Category {i + 1} matched with '{categorization}'"
194
+ break
195
+
196
+ if matched_category is None:
197
+ self.status = (
198
+ f"No match found for '{categorization}'. Available categories: "
199
+ f"{[category.get('route_category', '') for category in categories]}"
200
+ )
201
+
202
+ except RuntimeError as e:
203
+ self.status = f"Error in LLM categorization: {e!s}"
204
+ else:
205
+ self.status = "No LLM provided for categorization"
206
+
207
+ if matched_category is not None:
208
+ # Store the matched category for other outputs to check
209
+ self._matched_category = matched_category
210
+
211
+ # Stop all category outputs except the matched one
212
+ for i in range(len(categories)):
213
+ if i != matched_category:
214
+ self.stop(f"category_{i + 1}_result")
215
+
216
+ # Also stop the default output (if it exists)
217
+ enable_else = getattr(self, "enable_else_output", False)
218
+ if enable_else:
219
+ self.stop("default_result")
220
+
221
+ route_category = categories[matched_category].get("route_category", f"Category {matched_category + 1}")
222
+ self.status = f"Categorized as {route_category}"
223
+
224
+ # Check if there's an override output (takes precedence over everything)
225
+ override_output = getattr(self, "message", None)
226
+ if (
227
+ override_output
228
+ and hasattr(override_output, "text")
229
+ and override_output.text
230
+ and str(override_output.text).strip()
231
+ ):
232
+ return Message(text=str(override_output.text))
233
+ if override_output and isinstance(override_output, str) and override_output.strip():
234
+ return Message(text=str(override_output))
235
+
236
+ # Check if there's a custom output value for this category
237
+ custom_output = categories[matched_category].get("output_value", "")
238
+ # Treat None, empty string, or whitespace as blank
239
+ if custom_output and str(custom_output).strip() and str(custom_output).strip().lower() != "none":
240
+ # Use custom output value
241
+ return Message(text=str(custom_output))
242
+ # Use input as default output
243
+ return Message(text=input_text)
244
+ # No match found, stop all category outputs
245
+ for i in range(len(categories)):
246
+ self.stop(f"category_{i + 1}_result")
247
+
248
+ # Check if else output is enabled
249
+ enable_else = getattr(self, "enable_else_output", False)
250
+ if enable_else:
251
+ # The default_response will handle the else case
252
+ self.stop("process_case")
253
+ return Message(text="")
254
+ # No else output, so no output at all
255
+ self.status = "No match found and Else output is disabled"
256
+ return Message(text="")
257
+
258
+ def default_response(self) -> Message:
259
+ """Handle the else case when no conditions match."""
260
+ # Check if else output is enabled
261
+ enable_else = getattr(self, "enable_else_output", False)
262
+ if not enable_else:
263
+ self.status = "Else output is disabled"
264
+ return Message(text="")
265
+
266
+ # Clear any previous match state if not already set
267
+ if not hasattr(self, "_matched_category"):
268
+ self._matched_category = None
269
+
270
+ categories = getattr(self, "routes", [])
271
+ input_text = getattr(self, "input_text", "")
272
+
273
+ # Check if a match was already found in process_case
274
+ if hasattr(self, "_matched_category") and self._matched_category is not None:
275
+ self.status = (
276
+ f"Match already found in process_case (Category {self._matched_category + 1}), "
277
+ "stopping default_response"
278
+ )
279
+ self.stop("default_result")
280
+ return Message(text="")
281
+
282
+ # Check if any category matches using LLM categorization
283
+ has_match = False
284
+ llm = getattr(self, "llm", None)
285
+
286
+ if llm and categories:
287
+ try:
288
+ # Create prompt for categorization
289
+ category_values = [
290
+ category.get("route_category", f"Category {i + 1}") for i, category in enumerate(categories)
291
+ ]
292
+ categories_text = ", ".join([f'"{cat}"' for cat in category_values if cat])
293
+
294
+ # Create base prompt
295
+ base_prompt = (
296
+ "You are a text classifier. Given the following text and categories, "
297
+ "determine which category best matches the text.\n\n"
298
+ f'Text to classify: "{input_text}"\n\n'
299
+ f"Available categories: {categories_text}\n\n"
300
+ "Respond with ONLY the exact category name that best matches the text. "
301
+ 'If none match well, respond with "NONE".\n\n'
302
+ "Category:"
303
+ )
304
+
305
+ # Use custom prompt as additional instructions if provided
306
+ custom_prompt = getattr(self, "custom_prompt", "")
307
+ if custom_prompt and custom_prompt.strip():
308
+ self.status = "Using custom prompt as additional instructions (default check)"
309
+ # Format custom prompt with variables
310
+ formatted_custom = custom_prompt.format(input_text=input_text, routes=categories_text)
311
+ # Combine base prompt with custom instructions
312
+ prompt = f"{base_prompt}\n\nAdditional Instructions:\n{formatted_custom}"
313
+ else:
314
+ self.status = "Using default prompt for LLM categorization (default check)"
315
+ prompt = base_prompt
316
+
317
+ # Log the final prompt being sent to LLM for default check
318
+ self.status = f"Default check - Prompt sent to LLM:\n{prompt}"
319
+
320
+ # Use the LLM to categorize
321
+ if hasattr(llm, "invoke"):
322
+ response = llm.invoke(prompt)
323
+ if hasattr(response, "content"):
324
+ categorization = response.content.strip().strip('"')
325
+ else:
326
+ categorization = str(response).strip().strip('"')
327
+ else:
328
+ categorization = str(llm(prompt)).strip().strip('"')
329
+
330
+ # Log the categorization process for default check
331
+ self.status = f"Default check - LLM response: '{categorization}'"
332
+
333
+ # Check if LLM response matches any category
334
+ for i, category in enumerate(categories):
335
+ route_category = category.get("route_category", "")
336
+
337
+ # Log each comparison attempt
338
+ self.status = (
339
+ f"Default check - Comparing '{categorization}' with category {i + 1}: "
340
+ f"route_category='{route_category}'"
341
+ )
342
+
343
+ if categorization.lower() == route_category.lower():
344
+ has_match = True
345
+ self.status = f"Default check - MATCH FOUND! Category {i + 1} matched with '{categorization}'"
346
+ break
347
+
348
+ if not has_match:
349
+ self.status = (
350
+ f"Default check - No match found for '{categorization}'. "
351
+ f"Available categories: "
352
+ f"{[category.get('route_category', '') for category in categories]}"
353
+ )
354
+
355
+ except RuntimeError:
356
+ pass # If there's an error, treat as no match
357
+
358
+ if has_match:
359
+ # A case matches, stop this output
360
+ self.stop("default_result")
361
+ return Message(text="")
362
+
363
+ # No case matches, check for override output first, then use input as default
364
+ override_output = getattr(self, "message", None)
365
+ if (
366
+ override_output
367
+ and hasattr(override_output, "text")
368
+ and override_output.text
369
+ and str(override_output.text).strip()
370
+ ):
371
+ self.status = "Routed to Else (no match) - using override output"
372
+ return Message(text=str(override_output.text))
373
+ if override_output and isinstance(override_output, str) and override_output.strip():
374
+ self.status = "Routed to Else (no match) - using override output"
375
+ return Message(text=str(override_output))
376
+ self.status = "Routed to Else (no match) - using input as default"
377
+ return Message(text=input_text)
@@ -1,9 +1,13 @@
1
1
  import ast
2
+ import json
2
3
  from typing import TYPE_CHECKING, Any
3
4
 
5
+ import jq
6
+ from json_repair import repair_json
7
+
4
8
  from lfx.custom import Component
5
9
  from lfx.inputs import DictInput, DropdownInput, MessageTextInput, SortableListInput
6
- from lfx.io import DataInput, Output
10
+ from lfx.io import DataInput, MultilineInput, Output
7
11
  from lfx.log.logger import logger
8
12
  from lfx.schema import Data
9
13
  from lfx.schema.dotdict import dotdict
@@ -20,6 +24,8 @@ ACTION_CONFIG = {
20
24
  "Append or Update": {"is_list": False, "log_msg": "setting Append or Update fields"},
21
25
  "Remove Keys": {"is_list": False, "log_msg": "setting remove keys fields"},
22
26
  "Rename Keys": {"is_list": False, "log_msg": "setting rename keys fields"},
27
+ "Path Selection": {"is_list": False, "log_msg": "setting mapped key extractor fields"},
28
+ "JQ Expression": {"is_list": False, "log_msg": "setting parse json fields"},
23
29
  }
24
30
  OPERATORS = {
25
31
  "equals": lambda a, b: str(a) == str(b),
@@ -33,7 +39,6 @@ OPERATORS = {
33
39
  class DataOperationsComponent(Component):
34
40
  display_name = "Data Operations"
35
41
  description = "Perform various operations on a Data object."
36
- documentation: str = "https://docs.langflow.org/components-processing#data-operations"
37
42
  icon = "file-json"
38
43
  name = "DataOperations"
39
44
  default_keys = ["operations", "data"]
@@ -59,6 +64,9 @@ class DataOperationsComponent(Component):
59
64
  "data filtering",
60
65
  "data selection",
61
66
  "data combination",
67
+ "Parse JSON",
68
+ "JSON Query",
69
+ "JQ Query",
62
70
  ],
63
71
  }
64
72
  actions_data = {
@@ -69,8 +77,47 @@ class DataOperationsComponent(Component):
69
77
  "Append or Update": ["append_update_data", "operations"],
70
78
  "Remove Keys": ["remove_keys_input", "operations"],
71
79
  "Rename Keys": ["rename_keys_input", "operations"],
80
+ "Path Selection": ["mapped_json_display", "selected_key", "operations"],
81
+ "JQ Expression": ["query", "operations"],
72
82
  }
73
83
 
84
+ @staticmethod
85
+ def extract_all_paths(obj, path=""):
86
+ paths = []
87
+ if isinstance(obj, dict):
88
+ for k, v in obj.items():
89
+ new_path = f"{path}.{k}" if path else f".{k}"
90
+ paths.append(new_path)
91
+ paths.extend(DataOperationsComponent.extract_all_paths(v, new_path))
92
+ elif isinstance(obj, list) and obj:
93
+ new_path = f"{path}[0]"
94
+ paths.append(new_path)
95
+ paths.extend(DataOperationsComponent.extract_all_paths(obj[0], new_path))
96
+ return paths
97
+
98
+ @staticmethod
99
+ def remove_keys_recursive(obj, keys_to_remove):
100
+ if isinstance(obj, dict):
101
+ return {
102
+ k: DataOperationsComponent.remove_keys_recursive(v, keys_to_remove)
103
+ for k, v in obj.items()
104
+ if k not in keys_to_remove
105
+ }
106
+ if isinstance(obj, list):
107
+ return [DataOperationsComponent.remove_keys_recursive(item, keys_to_remove) for item in obj]
108
+ return obj
109
+
110
+ @staticmethod
111
+ def rename_keys_recursive(obj, rename_map):
112
+ if isinstance(obj, dict):
113
+ return {
114
+ rename_map.get(k, k): DataOperationsComponent.rename_keys_recursive(v, rename_map)
115
+ for k, v in obj.items()
116
+ }
117
+ if isinstance(obj, list):
118
+ return [DataOperationsComponent.rename_keys_recursive(item, rename_map) for item in obj]
119
+ return obj
120
+
74
121
  inputs = [
75
122
  DataInput(name="data", display_name="Data", info="Data object to filter.", required=True, is_list=True),
76
123
  SortableListInput(
@@ -86,6 +133,8 @@ class DataOperationsComponent(Component):
86
133
  {"name": "Append or Update", "icon": "circle-plus"},
87
134
  {"name": "Remove Keys", "icon": "eraser"},
88
135
  {"name": "Rename Keys", "icon": "pencil-line"},
136
+ {"name": "Path Selection", "icon": "mouse-pointer"},
137
+ {"name": "JQ Expression", "icon": "terminal"},
89
138
  ],
90
139
  real_time_refresh=True,
91
140
  limit=1,
@@ -94,7 +143,7 @@ class DataOperationsComponent(Component):
94
143
  MessageTextInput(
95
144
  name="select_keys_input",
96
145
  display_name="Select Keys",
97
- info="List of keys to select from the data.",
146
+ info="List of keys to select from the data. Only top-level keys can be selected.",
98
147
  show=False,
99
148
  is_list=True,
100
149
  ),
@@ -102,7 +151,10 @@ class DataOperationsComponent(Component):
102
151
  MessageTextInput(
103
152
  name="filter_key",
104
153
  display_name="Filter Key",
105
- info="Key to filter by.",
154
+ info=(
155
+ "Name of the key containing the list to filter. "
156
+ "It must be a top-level key in the JSON and its value must be a list."
157
+ ),
106
158
  is_list=True,
107
159
  show=False,
108
160
  ),
@@ -126,7 +178,7 @@ class DataOperationsComponent(Component):
126
178
  DictInput(
127
179
  name="append_update_data",
128
180
  display_name="Append or Update",
129
- info="Data to Append or Updatethe existing data with.",
181
+ info="Data to append or update the existing data with. Only top-level keys are checked.",
130
182
  show=False,
131
183
  value={"key": "value"},
132
184
  is_list=True,
@@ -148,6 +200,26 @@ class DataOperationsComponent(Component):
148
200
  is_list=True,
149
201
  value={"old_key": "new_key"},
150
202
  ),
203
+ MultilineInput(
204
+ name="mapped_json_display",
205
+ display_name="JSON to Map",
206
+ info="Paste or preview your JSON here to explore its structure and select a path for extraction.",
207
+ required=False,
208
+ refresh_button=True,
209
+ real_time_refresh=True,
210
+ placeholder="Add a JSON example.",
211
+ show=False,
212
+ ),
213
+ DropdownInput(
214
+ name="selected_key", display_name="Select Path", options=[], required=False, dynamic=True, show=False
215
+ ),
216
+ MessageTextInput(
217
+ name="query",
218
+ display_name="JQ Expression",
219
+ info="JSON Query to filter the data. Used by Parse JSON operation.",
220
+ placeholder="e.g., .properties.id",
221
+ show=False,
222
+ ),
151
223
  ]
152
224
  outputs = [
153
225
  Output(display_name="Data", name="data_output", method="as_data"),
@@ -156,10 +228,39 @@ class DataOperationsComponent(Component):
156
228
  # Helper methods for data operations
157
229
  def get_data_dict(self) -> dict:
158
230
  """Extract data dictionary from Data object."""
159
- # TODO: rasie error if it s list of data objects
160
231
  data = self.data[0] if isinstance(self.data, list) and len(self.data) == 1 else self.data
161
232
  return data.model_dump()
162
233
 
234
+ def json_query(self) -> Data:
235
+ import json
236
+
237
+ import jq
238
+
239
+ if not self.query or not self.query.strip():
240
+ msg = "JSON Query is required and cannot be blank."
241
+ raise ValueError(msg)
242
+ raw_data = self.get_data_dict()
243
+ try:
244
+ input_str = json.dumps(raw_data)
245
+ repaired = repair_json(input_str)
246
+ data_json = json.loads(repaired)
247
+ jq_input = data_json["data"] if isinstance(data_json, dict) and "data" in data_json else data_json
248
+ results = jq.compile(self.query).input(jq_input).all()
249
+ if not results:
250
+ msg = "No result from JSON query."
251
+ raise ValueError(msg)
252
+ result = results[0] if len(results) == 1 else results
253
+ if result is None or result == "None":
254
+ msg = "JSON query returned null/None. Check if the path exists in your data."
255
+ raise ValueError(msg)
256
+ if isinstance(result, dict):
257
+ return Data(data=result)
258
+ return Data(data={"result": result})
259
+ except (ValueError, TypeError, KeyError, json.JSONDecodeError) as e:
260
+ logger.error(f"JSON Query failed: {e}")
261
+ msg = f"JSON Query error: {e}"
262
+ raise ValueError(msg) from e
263
+
163
264
  def get_normalized_data(self) -> dict:
164
265
  """Get normalized data dictionary, handling the 'data' key if present."""
165
266
  data_dict = self.get_data_dict()
@@ -204,34 +305,22 @@ class DataOperationsComponent(Component):
204
305
  return Data(data=filtered)
205
306
 
206
307
  def remove_keys(self) -> Data:
207
- """Remove specified keys from the data dictionary."""
308
+ """Remove specified keys from the data dictionary, recursively."""
208
309
  self.validate_single_data("Remove Keys")
209
310
  data_dict = self.get_normalized_data()
210
311
  remove_keys_input: list[str] = self.remove_keys_input
211
312
 
212
- for key in remove_keys_input:
213
- if key in data_dict:
214
- data_dict.pop(key)
215
- else:
216
- logger.warning(f"Key '{key}' not found in data. Skipping removal.")
217
-
218
- return Data(**data_dict)
313
+ filtered = DataOperationsComponent.remove_keys_recursive(data_dict, set(remove_keys_input))
314
+ return Data(data=filtered)
219
315
 
220
316
  def rename_keys(self) -> Data:
221
- """Rename keys in the data dictionary."""
317
+ """Rename keys in the data dictionary, recursively."""
222
318
  self.validate_single_data("Rename Keys")
223
319
  data_dict = self.get_normalized_data()
224
320
  rename_keys_input: dict[str, str] = self.rename_keys_input
225
321
 
226
- for old_key, new_key in rename_keys_input.items():
227
- if old_key in data_dict:
228
- data_dict[new_key] = data_dict[old_key]
229
- data_dict.pop(old_key)
230
- else:
231
- msg = f"Key '{old_key}' not found in data. Skipping rename."
232
- raise ValueError(msg)
233
-
234
- return Data(**data_dict)
322
+ renamed = DataOperationsComponent.rename_keys_recursive(data_dict, rename_keys_input)
323
+ return Data(data=renamed)
235
324
 
236
325
  def recursive_eval(self, data: Any) -> Any:
237
326
  """Recursively evaluate string values in a dictionary or list.
@@ -299,13 +388,6 @@ class DataOperationsComponent(Component):
299
388
 
300
389
  return Data(**combined_data)
301
390
 
302
- def compare_values(self, item_value: Any, filter_value: str, operator: str) -> bool:
303
- """Compare values based on the specified operator."""
304
- comparison_func = OPERATORS.get(operator)
305
- if comparison_func:
306
- return comparison_func(item_value, filter_value)
307
- return False
308
-
309
391
  def filter_data(self, input_data: list[dict[str, Any]], filter_key: str, filter_value: str, operator: str) -> list:
310
392
  """Filter list data based on key, value, and operator."""
311
393
  # Validate inputs
@@ -328,6 +410,12 @@ class DataOperationsComponent(Component):
328
410
 
329
411
  return filtered_data
330
412
 
413
+ def compare_values(self, item_value: Any, filter_value: str, operator: str) -> bool:
414
+ comparison_func = OPERATORS.get(operator)
415
+ if comparison_func:
416
+ return comparison_func(item_value, filter_value)
417
+ return False
418
+
331
419
  def multi_filter_data(self) -> Data:
332
420
  """Apply multiple filters to the data."""
333
421
  self.validate_single_data("Filter Values")
@@ -366,57 +454,59 @@ class DataOperationsComponent(Component):
366
454
 
367
455
  # Configuration and execution methods
368
456
  def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None) -> dotdict:
369
- """Update build configuration based on selected action."""
370
- if field_name != "operations":
371
- return build_config
372
-
373
- build_config["operations"]["value"] = field_value
374
- selected_actions = [action["name"] for action in field_value]
375
-
376
- # Handle single action case
377
- if len(selected_actions) == 1 and selected_actions[0] in ACTION_CONFIG:
378
- action = selected_actions[0]
379
- config = ACTION_CONFIG[action]
380
-
381
- build_config["data"]["is_list"] = config["is_list"]
382
- logger.info(config["log_msg"])
383
-
384
- return set_current_fields(
385
- build_config=build_config,
386
- action_fields=self.actions_data,
387
- selected_action=action,
388
- default_fields=self.default_keys,
389
- func=set_field_display,
390
- )
391
-
392
- # Handle no operations case
393
- if not selected_actions:
394
- logger.info("setting default fields")
395
- return set_current_fields(
396
- build_config=build_config,
397
- action_fields=self.actions_data,
398
- selected_action=None,
399
- default_fields=self.default_keys,
400
- func=set_field_display,
401
- )
457
+ if field_name == "operations":
458
+ build_config["operations"]["value"] = field_value
459
+ selected_actions = [action["name"] for action in field_value]
460
+ if len(selected_actions) == 1 and selected_actions[0] in ACTION_CONFIG:
461
+ action = selected_actions[0]
462
+ config = ACTION_CONFIG[action]
463
+ build_config["data"]["is_list"] = config["is_list"]
464
+ logger.info(config["log_msg"])
465
+ return set_current_fields(
466
+ build_config=build_config,
467
+ action_fields=self.actions_data,
468
+ selected_action=action,
469
+ default_fields=["operations", "data"],
470
+ func=set_field_display,
471
+ )
472
+
473
+ if field_name == "mapped_json_display":
474
+ try:
475
+ parsed_json = json.loads(field_value)
476
+ keys = DataOperationsComponent.extract_all_paths(parsed_json)
477
+ build_config["selected_key"]["options"] = keys
478
+ build_config["selected_key"]["show"] = True
479
+ except (json.JSONDecodeError, TypeError, ValueError) as e:
480
+ logger.error(f"Error parsing mapped JSON: {e}")
481
+ build_config["selected_key"]["show"] = False
402
482
 
403
483
  return build_config
404
484
 
485
+ def json_path(self) -> Data:
486
+ try:
487
+ if not self.data or not self.selected_key:
488
+ msg = "Missing input data or selected key."
489
+ raise ValueError(msg)
490
+ input_payload = self.data[0].data if isinstance(self.data, list) else self.data.data
491
+ compiled = jq.compile(self.selected_key)
492
+ result = compiled.input(input_payload).first()
493
+ if isinstance(result, dict):
494
+ return Data(data=result)
495
+ return Data(data={"result": result})
496
+ except (ValueError, TypeError, KeyError) as e:
497
+ self.status = f"Error: {e!s}"
498
+ self.log(self.status)
499
+ return Data(data={"error": str(e)})
500
+
405
501
  def as_data(self) -> Data:
406
- """Execute the selected action on the data."""
407
502
  if not hasattr(self, "operations") or not self.operations:
408
503
  return Data(data={})
409
504
 
410
505
  selected_actions = [action["name"] for action in self.operations]
411
506
  logger.info(f"selected_actions: {selected_actions}")
412
-
413
- # Only handle single action case for now
414
507
  if len(selected_actions) != 1:
415
508
  return Data(data={})
416
509
 
417
- action = selected_actions[0]
418
-
419
- # Explicitly type the action_map
420
510
  action_map: dict[str, Callable[[], Data]] = {
421
511
  "Select Keys": self.select_keys,
422
512
  "Literal Eval": self.evaluate_data,
@@ -425,14 +515,14 @@ class DataOperationsComponent(Component):
425
515
  "Append or Update": self.append_update,
426
516
  "Remove Keys": self.remove_keys,
427
517
  "Rename Keys": self.rename_keys,
518
+ "Path Selection": self.json_path,
519
+ "JQ Expression": self.json_query,
428
520
  }
429
-
430
- handler: Callable[[], Data] | None = action_map.get(action)
521
+ handler: Callable[[], Data] | None = action_map.get(selected_actions[0])
431
522
  if handler:
432
523
  try:
433
524
  return handler()
434
525
  except Exception as e:
435
- logger.error(f"Error executing {action}: {e!s}")
526
+ logger.error(f"Error executing {selected_actions[0]}: {e!s}")
436
527
  raise
437
-
438
528
  return Data(data={})
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lfx-nightly
3
- Version: 0.1.12.dev1
3
+ Version: 0.1.12.dev2
4
4
  Summary: Langflow Executor - A lightweight CLI tool for executing and serving Langflow AI flows
5
5
  Author-email: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>
6
6
  Requires-Python: <3.14,>=3.10
@@ -352,6 +352,7 @@ lfx/components/logic/conditional_router.py,sha256=xhNfHRsL_-81Jp51u7z59fjQuvUHlt
352
352
  lfx/components/logic/data_conditional_router.py,sha256=34QXJcZeL0vDDEhnyen1s-71yhO5FVhBTl2d5Am-OVI,5008
353
353
  lfx/components/logic/flow_tool.py,sha256=yxfUaTibZUAv6PZT-5zQX-KLS35iRjNPkLKKsVtyvh8,3966
354
354
  lfx/components/logic/listen.py,sha256=k_wRN3yW5xtG1CjTdGYhL5LxdgCZ0Bi9cbWP54FkyuY,935
355
+ lfx/components/logic/llm_conditional_router.py,sha256=x-qCphrRd16yh_n2IQxqoCWu4AMMlI1QNLKBA0r7Rz8,16613
355
356
  lfx/components/logic/loop.py,sha256=F9vGbfAH-zDQgnJpVy9yk4fdrSIXz1gomnAOYW71Gto,4682
356
357
  lfx/components/logic/notify.py,sha256=A9aLooUwudRUsf2BRdE7CmGibCCRuQeCadneart9BEg,3086
357
358
  lfx/components/logic/pass_message.py,sha256=cdgzDjz6qSe2ekuCBzScWK8MI9spc81854iB-oQ3YGs,1039
@@ -406,7 +407,7 @@ lfx/components/processing/batch_run.py,sha256=KZtEaQMuSEUsQ5qwiU-dJPMAqNE5LA83Ho
406
407
  lfx/components/processing/combine_text.py,sha256=Zwh0F0v8vaTzmNK0T2D1c5LaixUKVINRZE8ulPjumKg,1242
407
408
  lfx/components/processing/converter.py,sha256=leNULEhmnkmB5dGfOmvlqGfY50870cebjTBfFFHAnX4,5140
408
409
  lfx/components/processing/create_data.py,sha256=PdoGU7hmDnLAtBxTTZQH72_B3mOdl8GDGcGgzrzsEkg,4422
409
- lfx/components/processing/data_operations.py,sha256=pojOpMEoc6WQsTZwVVev5YCGlCkibAEY9gcWwlcVrz8,16977
410
+ lfx/components/processing/data_operations.py,sha256=9dloD4ZEvwlpQwpV2Tig6sGwWTOxWXb9gMX6RO_hiL0,21515
410
411
  lfx/components/processing/data_to_dataframe.py,sha256=5RT98DzwOHEzX0VHr1376sDiSw0GVpdLmF4zYT4XuVU,2323
411
412
  lfx/components/processing/dataframe_operations.py,sha256=tNaxm27vTkH_uVqqQ5k-c0HwVuvGAgNRzT0LCCbqmnI,11552
412
413
  lfx/components/processing/extract_key.py,sha256=wyX6uUzk9mlG3n_-CTIbNYEB9h9DDa4i_6ADRvUVOBU,1964
@@ -693,7 +694,7 @@ lfx/utils/schemas.py,sha256=NbOtVQBrn4d0BAu-0H_eCTZI2CXkKZlRY37XCSmuJwc,3865
693
694
  lfx/utils/util.py,sha256=xGR32XDRr_TtruhjnXfI7lEWmk-vgywHAy3kz5SBowc,15725
694
695
  lfx/utils/util_strings.py,sha256=nU_IcdphNaj6bAPbjeL-c1cInQPfTBit8mp5Y57lwQk,1686
695
696
  lfx/utils/version.py,sha256=cHpbO0OJD2JQAvVaTH_6ibYeFbHJV0QDHs_YXXZ-bT8,671
696
- lfx_nightly-0.1.12.dev1.dist-info/METADATA,sha256=eMZwEM_BySUNrUL6AE3XpjsO-k1I1zchBEvtSHaZF4M,8000
697
- lfx_nightly-0.1.12.dev1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
698
- lfx_nightly-0.1.12.dev1.dist-info/entry_points.txt,sha256=1724p3RHDQRT2CKx_QRzEIa7sFuSVO0Ux70YfXfoMT4,42
699
- lfx_nightly-0.1.12.dev1.dist-info/RECORD,,
697
+ lfx_nightly-0.1.12.dev2.dist-info/METADATA,sha256=E_Wuq_RZk_EafljGZMzaQkPtA3NpYNDjsYmE0iXp6PE,8000
698
+ lfx_nightly-0.1.12.dev2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
699
+ lfx_nightly-0.1.12.dev2.dist-info/entry_points.txt,sha256=1724p3RHDQRT2CKx_QRzEIa7sFuSVO0Ux70YfXfoMT4,42
700
+ lfx_nightly-0.1.12.dev2.dist-info/RECORD,,