lfx-nightly 0.1.12.dev0__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.
- lfx/base/data/docling_utils.py +59 -1
- lfx/components/docling/docling_inline.py +36 -43
- lfx/components/logic/llm_conditional_router.py +377 -0
- lfx/components/processing/data_operations.py +164 -74
- {lfx_nightly-0.1.12.dev0.dist-info → lfx_nightly-0.1.12.dev2.dist-info}/METADATA +1 -1
- {lfx_nightly-0.1.12.dev0.dist-info → lfx_nightly-0.1.12.dev2.dist-info}/RECORD +8 -7
- {lfx_nightly-0.1.12.dev0.dist-info → lfx_nightly-0.1.12.dev2.dist-info}/WHEEL +0 -0
- {lfx_nightly-0.1.12.dev0.dist-info → lfx_nightly-0.1.12.dev2.dist-info}/entry_points.txt +0 -0
lfx/base/data/docling_utils.py
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
|
+
import importlib
|
|
1
2
|
import signal
|
|
2
3
|
import sys
|
|
3
4
|
import traceback
|
|
4
5
|
from contextlib import suppress
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
5
7
|
|
|
6
8
|
from docling_core.types.doc import DoclingDocument
|
|
9
|
+
from pydantic import BaseModel, SecretStr, TypeAdapter
|
|
7
10
|
|
|
8
11
|
from lfx.log.logger import logger
|
|
9
12
|
from lfx.schema.data import Data
|
|
10
13
|
from lfx.schema.dataframe import DataFrame
|
|
11
14
|
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from langchain_core.language_models.chat_models import BaseChatModel
|
|
17
|
+
|
|
12
18
|
|
|
13
19
|
def extract_docling_documents(data_inputs: Data | list[Data] | DataFrame, doc_key: str) -> list[DoclingDocument]:
|
|
14
20
|
documents: list[DoclingDocument] = []
|
|
@@ -57,7 +63,45 @@ def extract_docling_documents(data_inputs: Data | list[Data] | DataFrame, doc_ke
|
|
|
57
63
|
return documents
|
|
58
64
|
|
|
59
65
|
|
|
60
|
-
def
|
|
66
|
+
def _unwrap_secrets(obj):
|
|
67
|
+
if isinstance(obj, SecretStr):
|
|
68
|
+
return obj.get_secret_value()
|
|
69
|
+
if isinstance(obj, dict):
|
|
70
|
+
return {k: _unwrap_secrets(v) for k, v in obj.items()}
|
|
71
|
+
if isinstance(obj, list):
|
|
72
|
+
return [_unwrap_secrets(v) for v in obj]
|
|
73
|
+
return obj
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _dump_with_secrets(model: BaseModel):
|
|
77
|
+
return _unwrap_secrets(model.model_dump(mode="python", round_trip=True))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _serialize_pydantic_model(model: BaseModel):
|
|
81
|
+
return {
|
|
82
|
+
"__class_path__": f"{model.__class__.__module__}.{model.__class__.__name__}",
|
|
83
|
+
"config": _dump_with_secrets(model),
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _deserialize_pydantic_model(data: dict):
|
|
88
|
+
module_name, class_name = data["__class_path__"].rsplit(".", 1)
|
|
89
|
+
module = importlib.import_module(module_name)
|
|
90
|
+
cls = getattr(module, class_name)
|
|
91
|
+
adapter = TypeAdapter(cls)
|
|
92
|
+
return adapter.validate_python(data["config"])
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def docling_worker(
|
|
96
|
+
*,
|
|
97
|
+
file_paths: list[str],
|
|
98
|
+
queue,
|
|
99
|
+
pipeline: str,
|
|
100
|
+
ocr_engine: str,
|
|
101
|
+
do_picture_classification: bool,
|
|
102
|
+
pic_desc_config: dict | None,
|
|
103
|
+
pic_desc_prompt: str,
|
|
104
|
+
):
|
|
61
105
|
"""Worker function for processing files with Docling in a separate process."""
|
|
62
106
|
# Signal handling for graceful shutdown
|
|
63
107
|
shutdown_requested = False
|
|
@@ -106,6 +150,7 @@ def docling_worker(file_paths: list[str], queue, pipeline: str, ocr_engine: str)
|
|
|
106
150
|
from docling.document_converter import DocumentConverter, FormatOption, PdfFormatOption
|
|
107
151
|
from docling.models.factories import get_ocr_factory
|
|
108
152
|
from docling.pipeline.vlm_pipeline import VlmPipeline
|
|
153
|
+
from langchain_docling.picture_description import PictureDescriptionLangChainOptions
|
|
109
154
|
|
|
110
155
|
# Check for shutdown after imports
|
|
111
156
|
check_shutdown()
|
|
@@ -143,6 +188,19 @@ def docling_worker(file_paths: list[str], queue, pipeline: str, ocr_engine: str)
|
|
|
143
188
|
kind=ocr_engine,
|
|
144
189
|
)
|
|
145
190
|
pipeline_options.ocr_options = ocr_options
|
|
191
|
+
|
|
192
|
+
pipeline_options.do_picture_classification = do_picture_classification
|
|
193
|
+
|
|
194
|
+
if pic_desc_config:
|
|
195
|
+
pic_desc_llm: BaseChatModel = _deserialize_pydantic_model(pic_desc_config)
|
|
196
|
+
|
|
197
|
+
logger.info("Docling enabling the picture description stage.")
|
|
198
|
+
pipeline_options.do_picture_description = True
|
|
199
|
+
pipeline_options.allow_external_plugins = True
|
|
200
|
+
pipeline_options.picture_description_options = PictureDescriptionLangChainOptions(
|
|
201
|
+
llm=pic_desc_llm,
|
|
202
|
+
prompt=pic_desc_prompt,
|
|
203
|
+
)
|
|
146
204
|
return pipeline_options
|
|
147
205
|
|
|
148
206
|
# Configure the VLM pipeline
|
|
@@ -3,8 +3,8 @@ from multiprocessing import Queue, get_context
|
|
|
3
3
|
from queue import Empty
|
|
4
4
|
|
|
5
5
|
from lfx.base.data import BaseFileComponent
|
|
6
|
-
from lfx.base.data.docling_utils import docling_worker
|
|
7
|
-
from lfx.inputs import DropdownInput
|
|
6
|
+
from lfx.base.data.docling_utils import _serialize_pydantic_model, docling_worker
|
|
7
|
+
from lfx.inputs import BoolInput, DropdownInput, HandleInput, StrInput
|
|
8
8
|
from lfx.schema import Data
|
|
9
9
|
|
|
10
10
|
|
|
@@ -67,6 +67,26 @@ class DoclingInlineComponent(BaseFileComponent):
|
|
|
67
67
|
real_time_refresh=False,
|
|
68
68
|
value="None",
|
|
69
69
|
),
|
|
70
|
+
BoolInput(
|
|
71
|
+
name="do_picture_classification",
|
|
72
|
+
display_name="Picture classification",
|
|
73
|
+
info="If enabled, the Docling pipeline will classify the pictures type.",
|
|
74
|
+
value=False,
|
|
75
|
+
),
|
|
76
|
+
HandleInput(
|
|
77
|
+
name="pic_desc_llm",
|
|
78
|
+
display_name="Picture description LLM",
|
|
79
|
+
info="If connected, the model to use for running the picture description task.",
|
|
80
|
+
input_types=["LanguageModel"],
|
|
81
|
+
required=False,
|
|
82
|
+
),
|
|
83
|
+
StrInput(
|
|
84
|
+
name="pic_desc_prompt",
|
|
85
|
+
display_name="Picture description prompt",
|
|
86
|
+
value="Describe the image in three sentences. Be concise and accurate.",
|
|
87
|
+
info="The user prompt to use when invoking the model.",
|
|
88
|
+
advanced=True,
|
|
89
|
+
),
|
|
70
90
|
# TODO: expose more Docling options
|
|
71
91
|
]
|
|
72
92
|
|
|
@@ -131,11 +151,7 @@ class DoclingInlineComponent(BaseFileComponent):
|
|
|
131
151
|
|
|
132
152
|
def process_files(self, file_list: list[BaseFileComponent.BaseFile]) -> list[BaseFileComponent.BaseFile]:
|
|
133
153
|
try:
|
|
134
|
-
from docling.
|
|
135
|
-
from docling.datamodel.pipeline_options import OcrOptions, PdfPipelineOptions, VlmPipelineOptions
|
|
136
|
-
from docling.document_converter import DocumentConverter, FormatOption, PdfFormatOption
|
|
137
|
-
from docling.models.factories import get_ocr_factory
|
|
138
|
-
from docling.pipeline.vlm_pipeline import VlmPipeline
|
|
154
|
+
from docling.document_converter import DocumentConverter # noqa: F401
|
|
139
155
|
except ImportError as e:
|
|
140
156
|
msg = (
|
|
141
157
|
"Docling is an optional dependency. Install with `uv pip install 'langflow[docling]'` or refer to the "
|
|
@@ -143,52 +159,29 @@ class DoclingInlineComponent(BaseFileComponent):
|
|
|
143
159
|
)
|
|
144
160
|
raise ImportError(msg) from e
|
|
145
161
|
|
|
146
|
-
# Configure the standard PDF pipeline
|
|
147
|
-
def _get_standard_opts() -> PdfPipelineOptions:
|
|
148
|
-
pipeline_options = PdfPipelineOptions()
|
|
149
|
-
pipeline_options.do_ocr = self.ocr_engine != "None"
|
|
150
|
-
if pipeline_options.do_ocr:
|
|
151
|
-
ocr_factory = get_ocr_factory(
|
|
152
|
-
allow_external_plugins=False,
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
ocr_options: OcrOptions = ocr_factory.create_options(
|
|
156
|
-
kind=self.ocr_engine,
|
|
157
|
-
)
|
|
158
|
-
pipeline_options.ocr_options = ocr_options
|
|
159
|
-
return pipeline_options
|
|
160
|
-
|
|
161
|
-
# Configure the VLM pipeline
|
|
162
|
-
def _get_vlm_opts() -> VlmPipelineOptions:
|
|
163
|
-
return VlmPipelineOptions()
|
|
164
|
-
|
|
165
|
-
# Configure the main format options and create the DocumentConverter()
|
|
166
|
-
def _get_converter() -> DocumentConverter:
|
|
167
|
-
if self.pipeline == "standard":
|
|
168
|
-
pdf_format_option = PdfFormatOption(
|
|
169
|
-
pipeline_options=_get_standard_opts(),
|
|
170
|
-
)
|
|
171
|
-
elif self.pipeline == "vlm":
|
|
172
|
-
pdf_format_option = PdfFormatOption(pipeline_cls=VlmPipeline, pipeline_options=_get_vlm_opts())
|
|
173
|
-
|
|
174
|
-
format_options: dict[InputFormat, FormatOption] = {
|
|
175
|
-
InputFormat.PDF: pdf_format_option,
|
|
176
|
-
InputFormat.IMAGE: pdf_format_option,
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return DocumentConverter(format_options=format_options)
|
|
180
|
-
|
|
181
162
|
file_paths = [file.path for file in file_list if file.path]
|
|
182
163
|
|
|
183
164
|
if not file_paths:
|
|
184
165
|
self.log("No files to process.")
|
|
185
166
|
return file_list
|
|
186
167
|
|
|
168
|
+
pic_desc_config: dict | None = None
|
|
169
|
+
if self.pic_desc_llm is not None:
|
|
170
|
+
pic_desc_config = _serialize_pydantic_model(self.pic_desc_llm)
|
|
171
|
+
|
|
187
172
|
ctx = get_context("spawn")
|
|
188
173
|
queue: Queue = ctx.Queue()
|
|
189
174
|
proc = ctx.Process(
|
|
190
175
|
target=docling_worker,
|
|
191
|
-
|
|
176
|
+
kwargs={
|
|
177
|
+
"file_paths": file_paths,
|
|
178
|
+
"queue": queue,
|
|
179
|
+
"pipeline": self.pipeline,
|
|
180
|
+
"ocr_engine": self.ocr_engine,
|
|
181
|
+
"do_picture_classification": self.do_picture_classification,
|
|
182
|
+
"pic_desc_config": pic_desc_config,
|
|
183
|
+
"pic_desc_prompt": self.pic_desc_prompt,
|
|
184
|
+
},
|
|
192
185
|
)
|
|
193
186
|
|
|
194
187
|
result = None
|
|
@@ -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=
|
|
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
|
|
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
|
-
|
|
213
|
-
|
|
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
|
-
|
|
227
|
-
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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 {
|
|
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.
|
|
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
|
|
@@ -29,7 +29,7 @@ lfx/base/curl/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
29
29
|
lfx/base/curl/parse.py,sha256=Yw6mMbGg7e-ffrBItEUJeTiljneCXlNyt5afzEP9eUI,6094
|
|
30
30
|
lfx/base/data/__init__.py,sha256=lQsYYMyAg_jA9ZF7oc-LNZsRE2uMGT6g16WzsUByHqs,81
|
|
31
31
|
lfx/base/data/base_file.py,sha256=XFj3u9OGHcRbWfzslzvvxn-qpaCeX0uUQ0fStUCo65I,25495
|
|
32
|
-
lfx/base/data/docling_utils.py,sha256=
|
|
32
|
+
lfx/base/data/docling_utils.py,sha256=i0KpNNLgPJ0D226Tm5j_oaCv09w9IspBU2OwTDCfnBc,11625
|
|
33
33
|
lfx/base/data/utils.py,sha256=eZJgkOvQ3MaURDfgkH2MiZZOBF5_D0nSlmDY6LgLRik,5960
|
|
34
34
|
lfx/base/document_transformers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
35
35
|
lfx/base/document_transformers/model.py,sha256=etVEmyakiEgflB-fayClPnFRhaEdXfdUu4cqpgtk8ek,1317
|
|
@@ -242,7 +242,7 @@ lfx/components/deepseek/__init__.py,sha256=gmyOcLeNEcnwSeowow0N0UhBDlSuZ_8x-DMUj
|
|
|
242
242
|
lfx/components/deepseek/deepseek.py,sha256=yNrHoljXOMScKng-oSB-ceWhVZeuh11lmrAY7WiB2H0,4702
|
|
243
243
|
lfx/components/docling/__init__.py,sha256=O4utz9GHFpTVe_Wy0PR80yA1irJQRnAFQWkoLCVj888,1424
|
|
244
244
|
lfx/components/docling/chunk_docling_document.py,sha256=OX-jj4nX3UZgopViMAGAnFgtLql0sgs6cVmU8p9QbqA,7600
|
|
245
|
-
lfx/components/docling/docling_inline.py,sha256
|
|
245
|
+
lfx/components/docling/docling_inline.py,sha256=-m8hTANtdUDUjsJtJTB1sl6MJMhXG8zMeBMwbn0w9Ig,7871
|
|
246
246
|
lfx/components/docling/docling_remote.py,sha256=kwMS_-QMiM_JmPqvtHf4gDS73d2hZrIbtAPsN8bZxGE,6769
|
|
247
247
|
lfx/components/docling/export_docling_document.py,sha256=TeFt3TesCxSqW57nv-30gf2dX8qMDUHLRhwU-1ciq08,4681
|
|
248
248
|
lfx/components/documentloaders/__init__.py,sha256=LNl2hG2InevQCUREFKhF9ylaTf_kwPsdjiDbx2ElX3M,69
|
|
@@ -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=
|
|
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.
|
|
697
|
-
lfx_nightly-0.1.12.
|
|
698
|
-
lfx_nightly-0.1.12.
|
|
699
|
-
lfx_nightly-0.1.12.
|
|
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,,
|
|
File without changes
|
|
File without changes
|