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.
Files changed (98) hide show
  1. lfx/__main__.py +137 -6
  2. lfx/_assets/component_index.json +1 -1
  3. lfx/base/agents/agent.py +10 -6
  4. lfx/base/agents/altk_base_agent.py +5 -3
  5. lfx/base/agents/altk_tool_wrappers.py +1 -1
  6. lfx/base/agents/events.py +1 -1
  7. lfx/base/agents/utils.py +4 -0
  8. lfx/base/composio/composio_base.py +78 -41
  9. lfx/base/data/cloud_storage_utils.py +156 -0
  10. lfx/base/data/docling_utils.py +130 -55
  11. lfx/base/datastax/astradb_base.py +75 -64
  12. lfx/base/embeddings/embeddings_class.py +113 -0
  13. lfx/base/models/__init__.py +11 -1
  14. lfx/base/models/google_generative_ai_constants.py +33 -9
  15. lfx/base/models/model_metadata.py +6 -0
  16. lfx/base/models/ollama_constants.py +196 -30
  17. lfx/base/models/openai_constants.py +37 -10
  18. lfx/base/models/unified_models.py +1123 -0
  19. lfx/base/models/watsonx_constants.py +43 -4
  20. lfx/base/prompts/api_utils.py +40 -5
  21. lfx/base/tools/component_tool.py +2 -9
  22. lfx/cli/__init__.py +10 -2
  23. lfx/cli/commands.py +3 -0
  24. lfx/cli/run.py +65 -409
  25. lfx/cli/script_loader.py +18 -7
  26. lfx/cli/validation.py +6 -3
  27. lfx/components/__init__.py +0 -3
  28. lfx/components/composio/github_composio.py +1 -1
  29. lfx/components/cuga/cuga_agent.py +39 -27
  30. lfx/components/data_source/api_request.py +4 -2
  31. lfx/components/datastax/astradb_assistant_manager.py +4 -2
  32. lfx/components/docling/__init__.py +45 -11
  33. lfx/components/docling/docling_inline.py +39 -49
  34. lfx/components/docling/docling_remote.py +1 -0
  35. lfx/components/elastic/opensearch_multimodal.py +1733 -0
  36. lfx/components/files_and_knowledge/file.py +384 -36
  37. lfx/components/files_and_knowledge/ingestion.py +8 -0
  38. lfx/components/files_and_knowledge/retrieval.py +10 -0
  39. lfx/components/files_and_knowledge/save_file.py +91 -88
  40. lfx/components/langchain_utilities/ibm_granite_handler.py +211 -0
  41. lfx/components/langchain_utilities/tool_calling.py +37 -6
  42. lfx/components/llm_operations/batch_run.py +64 -18
  43. lfx/components/llm_operations/lambda_filter.py +213 -101
  44. lfx/components/llm_operations/llm_conditional_router.py +39 -7
  45. lfx/components/llm_operations/structured_output.py +38 -12
  46. lfx/components/models/__init__.py +16 -74
  47. lfx/components/models_and_agents/agent.py +51 -203
  48. lfx/components/models_and_agents/embedding_model.py +171 -255
  49. lfx/components/models_and_agents/language_model.py +54 -318
  50. lfx/components/models_and_agents/mcp_component.py +96 -10
  51. lfx/components/models_and_agents/prompt.py +105 -18
  52. lfx/components/ollama/ollama_embeddings.py +111 -29
  53. lfx/components/openai/openai_chat_model.py +1 -1
  54. lfx/components/processing/text_operations.py +580 -0
  55. lfx/components/vllm/__init__.py +37 -0
  56. lfx/components/vllm/vllm.py +141 -0
  57. lfx/components/vllm/vllm_embeddings.py +110 -0
  58. lfx/custom/custom_component/component.py +65 -10
  59. lfx/custom/custom_component/custom_component.py +8 -6
  60. lfx/events/observability/__init__.py +0 -0
  61. lfx/events/observability/lifecycle_events.py +111 -0
  62. lfx/field_typing/__init__.py +57 -58
  63. lfx/graph/graph/base.py +40 -1
  64. lfx/graph/utils.py +109 -30
  65. lfx/graph/vertex/base.py +75 -23
  66. lfx/graph/vertex/vertex_types.py +0 -5
  67. lfx/inputs/__init__.py +2 -0
  68. lfx/inputs/input_mixin.py +55 -0
  69. lfx/inputs/inputs.py +120 -0
  70. lfx/interface/components.py +24 -7
  71. lfx/interface/initialize/loading.py +42 -12
  72. lfx/io/__init__.py +2 -0
  73. lfx/run/__init__.py +5 -0
  74. lfx/run/base.py +464 -0
  75. lfx/schema/__init__.py +50 -0
  76. lfx/schema/data.py +1 -1
  77. lfx/schema/image.py +26 -7
  78. lfx/schema/message.py +104 -11
  79. lfx/schema/workflow.py +171 -0
  80. lfx/services/deps.py +12 -0
  81. lfx/services/interfaces.py +43 -1
  82. lfx/services/mcp_composer/service.py +7 -1
  83. lfx/services/schema.py +1 -0
  84. lfx/services/settings/auth.py +95 -4
  85. lfx/services/settings/base.py +11 -1
  86. lfx/services/settings/constants.py +2 -0
  87. lfx/services/settings/utils.py +82 -0
  88. lfx/services/storage/local.py +13 -8
  89. lfx/services/transaction/__init__.py +5 -0
  90. lfx/services/transaction/service.py +35 -0
  91. lfx/tests/unit/components/__init__.py +0 -0
  92. lfx/utils/constants.py +2 -0
  93. lfx/utils/mustache_security.py +79 -0
  94. lfx/utils/validate_cloud.py +81 -3
  95. {lfx_nightly-0.2.0.dev41.dist-info → lfx_nightly-0.3.0.dev3.dist-info}/METADATA +7 -2
  96. {lfx_nightly-0.2.0.dev41.dist-info → lfx_nightly-0.3.0.dev3.dist-info}/RECORD +98 -80
  97. {lfx_nightly-0.2.0.dev41.dist-info → lfx_nightly-0.3.0.dev3.dist-info}/WHEEL +0 -0
  98. {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 typing import TYPE_CHECKING, Any
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, HandleInput, IntInput, MultilineInput, Output
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
- if TYPE_CHECKING:
13
- from collections.abc import Callable
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
- HandleInput(
33
- name="llm",
58
+ ModelInput(
59
+ name="model",
34
60
  display_name="Language Model",
35
- info="Connect the 'Language Model' output from your LLM component here.",
36
- input_types=["LanguageModel"],
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
- "Example: Filter the data to only include items where the 'status' is 'active'."
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="Filter the data to...",
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
- async def _execute_lambda(self) -> Any:
96
- self.log(str(self.data))
97
-
98
- # Convert input to a unified format
99
- if isinstance(self.data, list):
100
- # Handle list of Data or DataFrame objects
101
- combined_data = []
102
- for item in self.data:
103
- if isinstance(item, DataFrame):
104
- # DataFrame to list of dicts
105
- combined_data.extend(item.to_dict(orient="records"))
106
- elif hasattr(item, "data"):
107
- # Data object
108
- if isinstance(item.data, dict):
109
- combined_data.append(item.data)
110
- elif isinstance(item.data, list):
111
- combined_data.extend(item.data)
112
-
113
- # If we have a single dict, unwrap it so lambdas can access it directly
114
- if len(combined_data) == 1 and isinstance(combined_data[0], dict):
115
- data = combined_data[0]
116
- elif len(combined_data) == 0:
117
- data = {}
118
- else:
119
- data = combined_data # type: ignore[assignment]
120
- elif isinstance(self.data, DataFrame):
121
- # Single DataFrame to list of dicts
122
- data = self.data.to_dict(orient="records")
123
- elif hasattr(self.data, "data"):
124
- # Single Data object
125
- data = self.data.data
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
- data = self.data
214
+ text_preview = text
128
215
 
129
- dump = json.dumps(data)
130
- self.log(str(data))
216
+ return TEXT_TRANSFORM_PROMPT.format(text_preview=text_preview, instruction=self.filter_instruction)
131
217
 
132
- llm = self.llm
133
- instruction = self.filter_instruction
134
- sample_size = self.sample_size
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... \n\n First lines (head): {dump[:sample_size]} \n\n"
145
- f" Last lines (tail): {dump[-sample_size:]})"
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
- self.log(data_sample)
151
-
152
- prompt = f"""Given this data structure and examples, create a Python lambda function that
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
- # Extract lambda using regex
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
- # Create and apply the function
186
- fn: Callable[[Any], Any] = eval(lambda_text) # noqa: S307
249
+ return eval(lambda_text) # noqa: S307
187
250
 
188
- # Apply the lambda function to the data
189
- return fn(data)
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
- async def process_as_data(self) -> Data:
192
- """Process the data and return as a Data object."""
193
- result = await self._execute_lambda()
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
- # Convert result to Data based on type
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
- async def process_as_dataframe(self) -> DataFrame:
204
- """Process the data and return as a DataFrame."""
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 BoolInput, HandleInput, MessageInput, MessageTextInput, MultilineInput, Output, TableInput
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
- HandleInput(
21
- name="llm",
34
+ ModelInput(
35
+ name="model",
22
36
  display_name="Language Model",
23
- info="LLM to use for categorization.",
24
- input_types=["LanguageModel"],
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 = getattr(self, "llm", None)
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 = getattr(self, "llm", None)
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
- HandleInput(
29
- name="llm",
34
+ ModelInput(
35
+ name="model",
30
36
  display_name="Language Model",
31
- info="The language model to use to generate the structured output.",
32
- input_types=["LanguageModel"],
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
- if not hasattr(self.llm, "with_structured_output"):
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(self.llm, tools=[schema], tool_choice=schema.__name__)
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 = self.llm.with_structured_output(schema)
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,