lfx-nightly 0.2.0.dev41__py3-none-any.whl → 0.3.0.dev3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lfx/__main__.py +137 -6
- lfx/_assets/component_index.json +1 -1
- lfx/base/agents/agent.py +10 -6
- lfx/base/agents/altk_base_agent.py +5 -3
- lfx/base/agents/altk_tool_wrappers.py +1 -1
- lfx/base/agents/events.py +1 -1
- lfx/base/agents/utils.py +4 -0
- lfx/base/composio/composio_base.py +78 -41
- lfx/base/data/cloud_storage_utils.py +156 -0
- lfx/base/data/docling_utils.py +130 -55
- lfx/base/datastax/astradb_base.py +75 -64
- lfx/base/embeddings/embeddings_class.py +113 -0
- lfx/base/models/__init__.py +11 -1
- lfx/base/models/google_generative_ai_constants.py +33 -9
- lfx/base/models/model_metadata.py +6 -0
- lfx/base/models/ollama_constants.py +196 -30
- lfx/base/models/openai_constants.py +37 -10
- lfx/base/models/unified_models.py +1123 -0
- lfx/base/models/watsonx_constants.py +43 -4
- lfx/base/prompts/api_utils.py +40 -5
- lfx/base/tools/component_tool.py +2 -9
- lfx/cli/__init__.py +10 -2
- lfx/cli/commands.py +3 -0
- lfx/cli/run.py +65 -409
- lfx/cli/script_loader.py +18 -7
- lfx/cli/validation.py +6 -3
- lfx/components/__init__.py +0 -3
- lfx/components/composio/github_composio.py +1 -1
- lfx/components/cuga/cuga_agent.py +39 -27
- lfx/components/data_source/api_request.py +4 -2
- lfx/components/datastax/astradb_assistant_manager.py +4 -2
- lfx/components/docling/__init__.py +45 -11
- lfx/components/docling/docling_inline.py +39 -49
- lfx/components/docling/docling_remote.py +1 -0
- lfx/components/elastic/opensearch_multimodal.py +1733 -0
- lfx/components/files_and_knowledge/file.py +384 -36
- lfx/components/files_and_knowledge/ingestion.py +8 -0
- lfx/components/files_and_knowledge/retrieval.py +10 -0
- lfx/components/files_and_knowledge/save_file.py +91 -88
- lfx/components/langchain_utilities/ibm_granite_handler.py +211 -0
- lfx/components/langchain_utilities/tool_calling.py +37 -6
- lfx/components/llm_operations/batch_run.py +64 -18
- lfx/components/llm_operations/lambda_filter.py +213 -101
- lfx/components/llm_operations/llm_conditional_router.py +39 -7
- lfx/components/llm_operations/structured_output.py +38 -12
- lfx/components/models/__init__.py +16 -74
- lfx/components/models_and_agents/agent.py +51 -203
- lfx/components/models_and_agents/embedding_model.py +171 -255
- lfx/components/models_and_agents/language_model.py +54 -318
- lfx/components/models_and_agents/mcp_component.py +96 -10
- lfx/components/models_and_agents/prompt.py +105 -18
- lfx/components/ollama/ollama_embeddings.py +111 -29
- lfx/components/openai/openai_chat_model.py +1 -1
- lfx/components/processing/text_operations.py +580 -0
- lfx/components/vllm/__init__.py +37 -0
- lfx/components/vllm/vllm.py +141 -0
- lfx/components/vllm/vllm_embeddings.py +110 -0
- lfx/custom/custom_component/component.py +65 -10
- lfx/custom/custom_component/custom_component.py +8 -6
- lfx/events/observability/__init__.py +0 -0
- lfx/events/observability/lifecycle_events.py +111 -0
- lfx/field_typing/__init__.py +57 -58
- lfx/graph/graph/base.py +40 -1
- lfx/graph/utils.py +109 -30
- lfx/graph/vertex/base.py +75 -23
- lfx/graph/vertex/vertex_types.py +0 -5
- lfx/inputs/__init__.py +2 -0
- lfx/inputs/input_mixin.py +55 -0
- lfx/inputs/inputs.py +120 -0
- lfx/interface/components.py +24 -7
- lfx/interface/initialize/loading.py +42 -12
- lfx/io/__init__.py +2 -0
- lfx/run/__init__.py +5 -0
- lfx/run/base.py +464 -0
- lfx/schema/__init__.py +50 -0
- lfx/schema/data.py +1 -1
- lfx/schema/image.py +26 -7
- lfx/schema/message.py +104 -11
- lfx/schema/workflow.py +171 -0
- lfx/services/deps.py +12 -0
- lfx/services/interfaces.py +43 -1
- lfx/services/mcp_composer/service.py +7 -1
- lfx/services/schema.py +1 -0
- lfx/services/settings/auth.py +95 -4
- lfx/services/settings/base.py +11 -1
- lfx/services/settings/constants.py +2 -0
- lfx/services/settings/utils.py +82 -0
- lfx/services/storage/local.py +13 -8
- lfx/services/transaction/__init__.py +5 -0
- lfx/services/transaction/service.py +35 -0
- lfx/tests/unit/components/__init__.py +0 -0
- lfx/utils/constants.py +2 -0
- lfx/utils/mustache_security.py +79 -0
- lfx/utils/validate_cloud.py +81 -3
- {lfx_nightly-0.2.0.dev41.dist-info → lfx_nightly-0.3.0.dev3.dist-info}/METADATA +7 -2
- {lfx_nightly-0.2.0.dev41.dist-info → lfx_nightly-0.3.0.dev3.dist-info}/RECORD +98 -80
- {lfx_nightly-0.2.0.dev41.dist-info → lfx_nightly-0.3.0.dev3.dist-info}/WHEEL +0 -0
- {lfx_nightly-0.2.0.dev41.dist-info → lfx_nightly-0.3.0.dev3.dist-info}/entry_points.txt +0 -0
|
@@ -2,20 +2,46 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import re
|
|
5
|
-
from
|
|
6
|
-
|
|
5
|
+
from collections.abc import Callable # noqa: TC003 - required at runtime for dynamic exec()
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from lfx.base.models.unified_models import (
|
|
9
|
+
get_language_model_options,
|
|
10
|
+
get_llm,
|
|
11
|
+
update_model_options_in_build_config,
|
|
12
|
+
)
|
|
7
13
|
from lfx.custom.custom_component.component import Component
|
|
8
|
-
from lfx.io import DataInput,
|
|
14
|
+
from lfx.io import DataInput, IntInput, ModelInput, MultilineInput, Output, SecretStrInput
|
|
9
15
|
from lfx.schema.data import Data
|
|
10
16
|
from lfx.schema.dataframe import DataFrame
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
17
|
+
from lfx.schema.message import Message
|
|
18
|
+
from lfx.utils.constants import MESSAGE_SENDER_AI
|
|
19
|
+
|
|
20
|
+
TEXT_TRANSFORM_PROMPT = (
|
|
21
|
+
"Given this text, create a Python lambda function that transforms it "
|
|
22
|
+
"according to the instruction.\n"
|
|
23
|
+
"The lambda should take a string parameter and return the transformed string.\n\n"
|
|
24
|
+
"Text Preview:\n{text_preview}\n\n"
|
|
25
|
+
"Instruction: {instruction}\n\n"
|
|
26
|
+
"Return ONLY the lambda function and nothing else. No need for ```python or whatever.\n"
|
|
27
|
+
"Just a string starting with lambda.\n"
|
|
28
|
+
"Example: lambda text: text.upper()"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
DATA_TRANSFORM_PROMPT = (
|
|
32
|
+
"Given this data structure and examples, create a Python lambda function "
|
|
33
|
+
"that implements the following instruction:\n\n"
|
|
34
|
+
"Data Structure:\n{dump_structure}\n\n"
|
|
35
|
+
"Example Items:\n{data_sample}\n\n"
|
|
36
|
+
"Instruction: {instruction}\n\n"
|
|
37
|
+
"Return ONLY the lambda function and nothing else. No need for ```python or whatever.\n"
|
|
38
|
+
"Just a string starting with lambda."
|
|
39
|
+
)
|
|
14
40
|
|
|
15
41
|
|
|
16
42
|
class LambdaFilterComponent(Component):
|
|
17
43
|
display_name = "Smart Transform"
|
|
18
|
-
description = "Uses an LLM to generate a function for filtering or transforming structured data."
|
|
44
|
+
description = "Uses an LLM to generate a function for filtering or transforming structured data and messages."
|
|
19
45
|
documentation: str = "https://docs.langflow.org/smart-transform"
|
|
20
46
|
icon = "square-function"
|
|
21
47
|
name = "Smart Transform"
|
|
@@ -24,26 +50,34 @@ class LambdaFilterComponent(Component):
|
|
|
24
50
|
DataInput(
|
|
25
51
|
name="data",
|
|
26
52
|
display_name="Data",
|
|
27
|
-
info="The structured data to filter or transform using a lambda function.",
|
|
28
|
-
input_types=["Data", "DataFrame"],
|
|
53
|
+
info="The structured data or text messages to filter or transform using a lambda function.",
|
|
54
|
+
input_types=["Data", "DataFrame", "Message"],
|
|
29
55
|
is_list=True,
|
|
30
56
|
required=True,
|
|
31
57
|
),
|
|
32
|
-
|
|
33
|
-
name="
|
|
58
|
+
ModelInput(
|
|
59
|
+
name="model",
|
|
34
60
|
display_name="Language Model",
|
|
35
|
-
info="
|
|
36
|
-
|
|
61
|
+
info="Select your model provider",
|
|
62
|
+
real_time_refresh=True,
|
|
37
63
|
required=True,
|
|
38
64
|
),
|
|
65
|
+
SecretStrInput(
|
|
66
|
+
name="api_key",
|
|
67
|
+
display_name="API Key",
|
|
68
|
+
info="Model Provider API key",
|
|
69
|
+
real_time_refresh=True,
|
|
70
|
+
advanced=True,
|
|
71
|
+
),
|
|
39
72
|
MultilineInput(
|
|
40
73
|
name="filter_instruction",
|
|
41
74
|
display_name="Instructions",
|
|
42
75
|
info=(
|
|
43
76
|
"Natural language instructions for how to filter or transform the data using a lambda function. "
|
|
44
|
-
"
|
|
77
|
+
"Examples: 'Filter the data to only include items where status is active', "
|
|
78
|
+
"'Convert the text to uppercase', 'Keep only first 100 characters'"
|
|
45
79
|
),
|
|
46
|
-
value="
|
|
80
|
+
value="Transform the data to...",
|
|
47
81
|
required=True,
|
|
48
82
|
),
|
|
49
83
|
IntInput(
|
|
@@ -73,8 +107,24 @@ class LambdaFilterComponent(Component):
|
|
|
73
107
|
name="dataframe_output",
|
|
74
108
|
method="process_as_dataframe",
|
|
75
109
|
),
|
|
110
|
+
Output(
|
|
111
|
+
display_name="Output",
|
|
112
|
+
name="message_output",
|
|
113
|
+
method="process_as_message",
|
|
114
|
+
),
|
|
76
115
|
]
|
|
77
116
|
|
|
117
|
+
def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):
|
|
118
|
+
"""Dynamically update build config with user-filtered model options."""
|
|
119
|
+
return update_model_options_in_build_config(
|
|
120
|
+
component=self,
|
|
121
|
+
build_config=build_config,
|
|
122
|
+
cache_key_prefix="language_model_options",
|
|
123
|
+
get_options_func=get_language_model_options,
|
|
124
|
+
field_name=field_name,
|
|
125
|
+
field_value=field_value,
|
|
126
|
+
)
|
|
127
|
+
|
|
78
128
|
def get_data_structure(self, data):
|
|
79
129
|
"""Extract the structure of data, replacing values with their types."""
|
|
80
130
|
if isinstance(data, list):
|
|
@@ -92,127 +142,189 @@ class LambdaFilterComponent(Component):
|
|
|
92
142
|
# Return False if the lambda function does not start with 'lambda' or does not contain a colon
|
|
93
143
|
return lambda_text.strip().startswith("lambda") and ":" in lambda_text
|
|
94
144
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if isinstance(self.data,
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
145
|
+
def _get_input_type_name(self) -> str:
|
|
146
|
+
"""Detect and return the input type name for error messages."""
|
|
147
|
+
if isinstance(self.data, Message):
|
|
148
|
+
return "Message"
|
|
149
|
+
if isinstance(self.data, DataFrame):
|
|
150
|
+
return "DataFrame"
|
|
151
|
+
if isinstance(self.data, Data):
|
|
152
|
+
return "Data"
|
|
153
|
+
if isinstance(self.data, list) and len(self.data) > 0:
|
|
154
|
+
first = self.data[0]
|
|
155
|
+
if isinstance(first, Message):
|
|
156
|
+
return "Message"
|
|
157
|
+
if isinstance(first, DataFrame):
|
|
158
|
+
return "DataFrame"
|
|
159
|
+
if isinstance(first, Data):
|
|
160
|
+
return "Data"
|
|
161
|
+
return "unknown"
|
|
162
|
+
|
|
163
|
+
def _extract_message_text(self) -> str:
|
|
164
|
+
"""Extract text content from Message input(s)."""
|
|
165
|
+
if isinstance(self.data, Message):
|
|
166
|
+
return self.data.text or ""
|
|
167
|
+
|
|
168
|
+
texts = [msg.text or "" for msg in self.data if isinstance(msg, Message)]
|
|
169
|
+
return "\n\n".join(texts) if len(texts) > 1 else (texts[0] if texts else "")
|
|
170
|
+
|
|
171
|
+
def _extract_structured_data(self) -> dict | list:
|
|
172
|
+
"""Extract structured data from Data or DataFrame input(s)."""
|
|
173
|
+
if isinstance(self.data, DataFrame):
|
|
174
|
+
return self.data.to_dict(orient="records")
|
|
175
|
+
|
|
176
|
+
if hasattr(self.data, "data"):
|
|
177
|
+
return self.data.data
|
|
178
|
+
|
|
179
|
+
if not isinstance(self.data, list):
|
|
180
|
+
return self.data
|
|
181
|
+
|
|
182
|
+
combined_data: list[dict] = []
|
|
183
|
+
for item in self.data:
|
|
184
|
+
if isinstance(item, DataFrame):
|
|
185
|
+
combined_data.extend(item.to_dict(orient="records"))
|
|
186
|
+
elif hasattr(item, "data"):
|
|
187
|
+
if isinstance(item.data, dict):
|
|
188
|
+
combined_data.append(item.data)
|
|
189
|
+
elif isinstance(item.data, list):
|
|
190
|
+
combined_data.extend(item.data)
|
|
191
|
+
|
|
192
|
+
if len(combined_data) == 1 and isinstance(combined_data[0], dict):
|
|
193
|
+
return combined_data[0]
|
|
194
|
+
if len(combined_data) == 0:
|
|
195
|
+
return {}
|
|
196
|
+
return combined_data
|
|
197
|
+
|
|
198
|
+
def _is_message_input(self) -> bool:
|
|
199
|
+
"""Check if input is Message type."""
|
|
200
|
+
if isinstance(self.data, Message):
|
|
201
|
+
return True
|
|
202
|
+
return isinstance(self.data, list) and len(self.data) > 0 and isinstance(self.data[0], Message)
|
|
203
|
+
|
|
204
|
+
def _build_text_prompt(self, text: str) -> str:
|
|
205
|
+
"""Build prompt for text/Message transformation."""
|
|
206
|
+
text_length = len(text)
|
|
207
|
+
if text_length > self.max_size:
|
|
208
|
+
text_preview = (
|
|
209
|
+
f"Text length: {text_length} characters\n\n"
|
|
210
|
+
f"First {self.sample_size} characters:\n{text[: self.sample_size]}\n\n"
|
|
211
|
+
f"Last {self.sample_size} characters:\n{text[-self.sample_size :]}"
|
|
212
|
+
)
|
|
126
213
|
else:
|
|
127
|
-
|
|
214
|
+
text_preview = text
|
|
128
215
|
|
|
129
|
-
|
|
130
|
-
self.log(str(data))
|
|
216
|
+
return TEXT_TRANSFORM_PROMPT.format(text_preview=text_preview, instruction=self.filter_instruction)
|
|
131
217
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
# Get data structure and samples
|
|
137
|
-
data_structure = self.get_data_structure(data)
|
|
138
|
-
dump_structure = json.dumps(data_structure)
|
|
139
|
-
self.log(dump_structure)
|
|
218
|
+
def _build_data_prompt(self, data: dict | list) -> str:
|
|
219
|
+
"""Build prompt for structured data transformation."""
|
|
220
|
+
dump = json.dumps(data)
|
|
221
|
+
dump_structure = json.dumps(self.get_data_structure(data))
|
|
140
222
|
|
|
141
|
-
# For large datasets, sample from head and tail
|
|
142
223
|
if len(dump) > self.max_size:
|
|
143
224
|
data_sample = (
|
|
144
|
-
f"Data is too long to display
|
|
145
|
-
f"
|
|
225
|
+
f"Data is too long to display...\n\nFirst lines (head): {dump[: self.sample_size]}\n\n"
|
|
226
|
+
f"Last lines (tail): {dump[-self.sample_size :]}"
|
|
146
227
|
)
|
|
147
228
|
else:
|
|
148
229
|
data_sample = dump
|
|
149
230
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
implements the following instruction:
|
|
154
|
-
|
|
155
|
-
Data Structure:
|
|
156
|
-
{dump_structure}
|
|
157
|
-
|
|
158
|
-
Example Items:
|
|
159
|
-
{data_sample}
|
|
160
|
-
|
|
161
|
-
Instruction: {instruction}
|
|
162
|
-
|
|
163
|
-
Return ONLY the lambda function and nothing else. No need for ```python or whatever.
|
|
164
|
-
Just a string starting with lambda.
|
|
165
|
-
"""
|
|
166
|
-
|
|
167
|
-
response = await llm.ainvoke(prompt)
|
|
168
|
-
response_text = response.content if hasattr(response, "content") else str(response)
|
|
169
|
-
self.log(response_text)
|
|
231
|
+
return DATA_TRANSFORM_PROMPT.format(
|
|
232
|
+
dump_structure=dump_structure, data_sample=data_sample, instruction=self.filter_instruction
|
|
233
|
+
)
|
|
170
234
|
|
|
171
|
-
|
|
235
|
+
def _parse_lambda_from_response(self, response_text: str) -> Callable[[Any], Any]:
|
|
236
|
+
"""Extract and validate lambda function from LLM response."""
|
|
172
237
|
lambda_match = re.search(r"lambda\s+\w+\s*:.*?(?=\n|$)", response_text)
|
|
173
238
|
if not lambda_match:
|
|
174
239
|
msg = f"Could not find lambda in response: {response_text}"
|
|
175
240
|
raise ValueError(msg)
|
|
176
241
|
|
|
177
242
|
lambda_text = lambda_match.group().strip()
|
|
178
|
-
self.log(lambda_text)
|
|
243
|
+
self.log(f"Generated lambda: {lambda_text}")
|
|
179
244
|
|
|
180
|
-
# Validation is commented out as requested
|
|
181
245
|
if not self._validate_lambda(lambda_text):
|
|
182
246
|
msg = f"Invalid lambda format: {lambda_text}"
|
|
183
247
|
raise ValueError(msg)
|
|
184
248
|
|
|
185
|
-
#
|
|
186
|
-
fn: Callable[[Any], Any] = eval(lambda_text) # noqa: S307
|
|
249
|
+
return eval(lambda_text) # noqa: S307
|
|
187
250
|
|
|
188
|
-
|
|
189
|
-
|
|
251
|
+
async def _execute_lambda(self) -> Any:
|
|
252
|
+
"""Generate and execute a lambda function based on input type."""
|
|
253
|
+
if self._is_message_input():
|
|
254
|
+
data: Any = self._extract_message_text()
|
|
255
|
+
prompt = self._build_text_prompt(data)
|
|
256
|
+
else:
|
|
257
|
+
data = self._extract_structured_data()
|
|
258
|
+
prompt = self._build_data_prompt(data)
|
|
190
259
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
260
|
+
llm = get_llm(model=self.model, user_id=self.user_id, api_key=self.api_key)
|
|
261
|
+
response = await llm.ainvoke(prompt)
|
|
262
|
+
response_text = response.content if hasattr(response, "content") else str(response)
|
|
263
|
+
|
|
264
|
+
fn = self._parse_lambda_from_response(response_text)
|
|
265
|
+
return fn(data)
|
|
194
266
|
|
|
195
|
-
|
|
267
|
+
def _handle_process_error(self, error: Exception, output_type: str) -> None:
|
|
268
|
+
"""Handle errors from process methods with context-aware messages."""
|
|
269
|
+
input_type = self._get_input_type_name()
|
|
270
|
+
error_msg = (
|
|
271
|
+
f"Failed to convert result to {output_type} output. "
|
|
272
|
+
f"Error: {error}. "
|
|
273
|
+
f"Input type was {input_type}. "
|
|
274
|
+
f"Try using the same output type as the input."
|
|
275
|
+
)
|
|
276
|
+
raise ValueError(error_msg) from error
|
|
277
|
+
|
|
278
|
+
def _convert_result_to_data(self, result: Any) -> Data:
|
|
279
|
+
"""Convert lambda result to Data object."""
|
|
196
280
|
if isinstance(result, dict):
|
|
197
281
|
return Data(data=result)
|
|
198
282
|
if isinstance(result, list):
|
|
199
283
|
return Data(data={"_results": result})
|
|
200
|
-
# For other types, convert to string
|
|
201
284
|
return Data(data={"text": str(result)})
|
|
202
285
|
|
|
203
|
-
|
|
204
|
-
"""
|
|
205
|
-
result = await self._execute_lambda()
|
|
206
|
-
|
|
207
|
-
# Convert result to DataFrame based on type
|
|
286
|
+
def _convert_result_to_dataframe(self, result: Any) -> DataFrame:
|
|
287
|
+
"""Convert lambda result to DataFrame object."""
|
|
208
288
|
if isinstance(result, list):
|
|
209
|
-
# Check if it's a list of dicts
|
|
210
289
|
if all(isinstance(item, dict) for item in result):
|
|
211
290
|
return DataFrame(result)
|
|
212
|
-
# List of non-dicts: wrap each value
|
|
213
291
|
return DataFrame([{"value": item} for item in result])
|
|
214
292
|
if isinstance(result, dict):
|
|
215
|
-
# Single dict becomes single-row DataFrame
|
|
216
293
|
return DataFrame([result])
|
|
217
|
-
# Other types: convert to string and wrap
|
|
218
294
|
return DataFrame([{"value": str(result)}])
|
|
295
|
+
|
|
296
|
+
def _convert_result_to_message(self, result: Any) -> Message:
|
|
297
|
+
"""Convert lambda result to Message object."""
|
|
298
|
+
if isinstance(result, str):
|
|
299
|
+
return Message(text=result, sender=MESSAGE_SENDER_AI)
|
|
300
|
+
if isinstance(result, list):
|
|
301
|
+
text = "\n".join(str(item) for item in result)
|
|
302
|
+
return Message(text=text, sender=MESSAGE_SENDER_AI)
|
|
303
|
+
if isinstance(result, dict):
|
|
304
|
+
text = json.dumps(result, indent=2)
|
|
305
|
+
return Message(text=text, sender=MESSAGE_SENDER_AI)
|
|
306
|
+
return Message(text=str(result), sender=MESSAGE_SENDER_AI)
|
|
307
|
+
|
|
308
|
+
async def process_as_data(self) -> Data:
|
|
309
|
+
"""Process the data and return as a Data object."""
|
|
310
|
+
try:
|
|
311
|
+
result = await self._execute_lambda()
|
|
312
|
+
return self._convert_result_to_data(result)
|
|
313
|
+
except Exception as e: # noqa: BLE001 - dynamic lambda can raise any exception
|
|
314
|
+
self._handle_process_error(e, "Data")
|
|
315
|
+
|
|
316
|
+
async def process_as_dataframe(self) -> DataFrame:
|
|
317
|
+
"""Process the data and return as a DataFrame."""
|
|
318
|
+
try:
|
|
319
|
+
result = await self._execute_lambda()
|
|
320
|
+
return self._convert_result_to_dataframe(result)
|
|
321
|
+
except Exception as e: # noqa: BLE001 - dynamic lambda can raise any exception
|
|
322
|
+
self._handle_process_error(e, "DataFrame")
|
|
323
|
+
|
|
324
|
+
async def process_as_message(self) -> Message:
|
|
325
|
+
"""Process the data and return as a Message."""
|
|
326
|
+
try:
|
|
327
|
+
result = await self._execute_lambda()
|
|
328
|
+
return self._convert_result_to_message(result)
|
|
329
|
+
except Exception as e: # noqa: BLE001 - dynamic lambda can raise any exception
|
|
330
|
+
self._handle_process_error(e, "Message")
|
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
from typing import Any
|
|
2
2
|
|
|
3
|
+
from lfx.base.models.unified_models import (
|
|
4
|
+
get_language_model_options,
|
|
5
|
+
get_llm,
|
|
6
|
+
update_model_options_in_build_config,
|
|
7
|
+
)
|
|
3
8
|
from lfx.custom import Component
|
|
4
|
-
from lfx.io import
|
|
9
|
+
from lfx.io import (
|
|
10
|
+
BoolInput,
|
|
11
|
+
MessageInput,
|
|
12
|
+
MessageTextInput,
|
|
13
|
+
ModelInput,
|
|
14
|
+
MultilineInput,
|
|
15
|
+
Output,
|
|
16
|
+
SecretStrInput,
|
|
17
|
+
TableInput,
|
|
18
|
+
)
|
|
5
19
|
from lfx.schema.message import Message
|
|
6
20
|
from lfx.schema.table import EditMode
|
|
7
21
|
|
|
@@ -17,13 +31,20 @@ class SmartRouterComponent(Component):
|
|
|
17
31
|
self._matched_category = None
|
|
18
32
|
|
|
19
33
|
inputs = [
|
|
20
|
-
|
|
21
|
-
name="
|
|
34
|
+
ModelInput(
|
|
35
|
+
name="model",
|
|
22
36
|
display_name="Language Model",
|
|
23
|
-
info="
|
|
24
|
-
|
|
37
|
+
info="Select your model provider",
|
|
38
|
+
real_time_refresh=True,
|
|
25
39
|
required=True,
|
|
26
40
|
),
|
|
41
|
+
SecretStrInput(
|
|
42
|
+
name="api_key",
|
|
43
|
+
display_name="API Key",
|
|
44
|
+
info="Model Provider API key",
|
|
45
|
+
real_time_refresh=True,
|
|
46
|
+
advanced=True,
|
|
47
|
+
),
|
|
27
48
|
MessageTextInput(
|
|
28
49
|
name="input_text",
|
|
29
50
|
display_name="Input",
|
|
@@ -111,6 +132,17 @@ class SmartRouterComponent(Component):
|
|
|
111
132
|
|
|
112
133
|
outputs: list[Output] = []
|
|
113
134
|
|
|
135
|
+
def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):
|
|
136
|
+
"""Dynamically update build config with user-filtered model options."""
|
|
137
|
+
return update_model_options_in_build_config(
|
|
138
|
+
component=self,
|
|
139
|
+
build_config=build_config,
|
|
140
|
+
cache_key_prefix="language_model_options",
|
|
141
|
+
get_options_func=get_language_model_options,
|
|
142
|
+
field_name=field_name,
|
|
143
|
+
field_value=field_value,
|
|
144
|
+
)
|
|
145
|
+
|
|
114
146
|
def update_outputs(self, frontend_node: dict, field_name: str, field_value: Any) -> dict:
|
|
115
147
|
"""Create a dynamic output for each category in the categories table."""
|
|
116
148
|
if field_name in {"routes", "enable_else_output"}:
|
|
@@ -153,7 +185,7 @@ class SmartRouterComponent(Component):
|
|
|
153
185
|
|
|
154
186
|
# Find the matching category using LLM-based categorization
|
|
155
187
|
matched_category = None
|
|
156
|
-
llm =
|
|
188
|
+
llm = get_llm(model=self.model, user_id=self.user_id, api_key=self.api_key)
|
|
157
189
|
|
|
158
190
|
if llm and categories:
|
|
159
191
|
# Create prompt for categorization
|
|
@@ -315,7 +347,7 @@ class SmartRouterComponent(Component):
|
|
|
315
347
|
|
|
316
348
|
# Check if any category matches using LLM categorization
|
|
317
349
|
has_match = False
|
|
318
|
-
llm =
|
|
350
|
+
llm = get_llm(model=self.model, user_id=self.user_id, api_key=self.api_key)
|
|
319
351
|
|
|
320
352
|
if llm and categories:
|
|
321
353
|
try:
|
|
@@ -2,13 +2,19 @@ from pydantic import BaseModel, Field, create_model
|
|
|
2
2
|
from trustcall import create_extractor
|
|
3
3
|
|
|
4
4
|
from lfx.base.models.chat_result import get_chat_result
|
|
5
|
+
from lfx.base.models.unified_models import (
|
|
6
|
+
get_language_model_options,
|
|
7
|
+
get_llm,
|
|
8
|
+
update_model_options_in_build_config,
|
|
9
|
+
)
|
|
5
10
|
from lfx.custom.custom_component.component import Component
|
|
6
11
|
from lfx.helpers.base_model import build_model_from_schema
|
|
7
12
|
from lfx.io import (
|
|
8
|
-
HandleInput,
|
|
9
13
|
MessageTextInput,
|
|
14
|
+
ModelInput,
|
|
10
15
|
MultilineInput,
|
|
11
16
|
Output,
|
|
17
|
+
SecretStrInput,
|
|
12
18
|
TableInput,
|
|
13
19
|
)
|
|
14
20
|
from lfx.log.logger import logger
|
|
@@ -25,13 +31,20 @@ class StructuredOutputComponent(Component):
|
|
|
25
31
|
icon = "braces"
|
|
26
32
|
|
|
27
33
|
inputs = [
|
|
28
|
-
|
|
29
|
-
name="
|
|
34
|
+
ModelInput(
|
|
35
|
+
name="model",
|
|
30
36
|
display_name="Language Model",
|
|
31
|
-
info="
|
|
32
|
-
|
|
37
|
+
info="Select your model provider",
|
|
38
|
+
real_time_refresh=True,
|
|
33
39
|
required=True,
|
|
34
40
|
),
|
|
41
|
+
SecretStrInput(
|
|
42
|
+
name="api_key",
|
|
43
|
+
display_name="API Key",
|
|
44
|
+
info="Model Provider API key",
|
|
45
|
+
real_time_refresh=True,
|
|
46
|
+
advanced=True,
|
|
47
|
+
),
|
|
35
48
|
MultilineInput(
|
|
36
49
|
name="input_value",
|
|
37
50
|
display_name="Input Message",
|
|
@@ -126,10 +139,23 @@ class StructuredOutputComponent(Component):
|
|
|
126
139
|
),
|
|
127
140
|
]
|
|
128
141
|
|
|
142
|
+
def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):
|
|
143
|
+
"""Dynamically update build config with user-filtered model options."""
|
|
144
|
+
return update_model_options_in_build_config(
|
|
145
|
+
component=self,
|
|
146
|
+
build_config=build_config,
|
|
147
|
+
cache_key_prefix="language_model_options",
|
|
148
|
+
get_options_func=get_language_model_options,
|
|
149
|
+
field_name=field_name,
|
|
150
|
+
field_value=field_value,
|
|
151
|
+
)
|
|
152
|
+
|
|
129
153
|
def build_structured_output_base(self):
|
|
130
154
|
schema_name = self.schema_name or "OutputModel"
|
|
131
155
|
|
|
132
|
-
|
|
156
|
+
llm = get_llm(model=self.model, user_id=self.user_id, api_key=self.api_key)
|
|
157
|
+
|
|
158
|
+
if not hasattr(llm, "with_structured_output"):
|
|
133
159
|
msg = "Language model does not support structured output."
|
|
134
160
|
raise TypeError(msg)
|
|
135
161
|
if not self.output_schema:
|
|
@@ -155,9 +181,9 @@ class StructuredOutputComponent(Component):
|
|
|
155
181
|
"callbacks": self.get_langchain_callbacks(),
|
|
156
182
|
}
|
|
157
183
|
# Generate structured output using Trustcall first, then fallback to Langchain if it fails
|
|
158
|
-
result = self._extract_output_with_trustcall(output_model, config_dict)
|
|
184
|
+
result = self._extract_output_with_trustcall(llm, output_model, config_dict)
|
|
159
185
|
if result is None:
|
|
160
|
-
result = self._extract_output_with_langchain(output_model, config_dict)
|
|
186
|
+
result = self._extract_output_with_langchain(llm, output_model, config_dict)
|
|
161
187
|
|
|
162
188
|
# OPTIMIZATION NOTE: Simplified processing based on trustcall response structure
|
|
163
189
|
# Handle non-dict responses (shouldn't happen with trustcall, but defensive)
|
|
@@ -204,9 +230,9 @@ class StructuredOutputComponent(Component):
|
|
|
204
230
|
return DataFrame(output)
|
|
205
231
|
return DataFrame()
|
|
206
232
|
|
|
207
|
-
def _extract_output_with_trustcall(self, schema: BaseModel, config_dict: dict) -> list[BaseModel] | None:
|
|
233
|
+
def _extract_output_with_trustcall(self, llm, schema: BaseModel, config_dict: dict) -> list[BaseModel] | None:
|
|
208
234
|
try:
|
|
209
|
-
llm_with_structured_output = create_extractor(
|
|
235
|
+
llm_with_structured_output = create_extractor(llm, tools=[schema], tool_choice=schema.__name__)
|
|
210
236
|
result = get_chat_result(
|
|
211
237
|
runnable=llm_with_structured_output,
|
|
212
238
|
system_message=self.system_prompt,
|
|
@@ -222,9 +248,9 @@ class StructuredOutputComponent(Component):
|
|
|
222
248
|
return None
|
|
223
249
|
return result or None # langchain fallback is used if error occurs or the result is empty
|
|
224
250
|
|
|
225
|
-
def _extract_output_with_langchain(self, schema: BaseModel, config_dict: dict) -> list[BaseModel] | None:
|
|
251
|
+
def _extract_output_with_langchain(self, llm, schema: BaseModel, config_dict: dict) -> list[BaseModel] | None:
|
|
226
252
|
try:
|
|
227
|
-
llm_with_structured_output =
|
|
253
|
+
llm_with_structured_output = llm.with_structured_output(schema)
|
|
228
254
|
result = get_chat_result(
|
|
229
255
|
runnable=llm_with_structured_output,
|
|
230
256
|
system_message=self.system_prompt,
|