lfx-nightly 0.1.12.dev38__py3-none-any.whl → 0.1.12.dev40__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/_assets/component_index.json +1 -0
- lfx/cli/run.py +7 -3
- lfx/components/agents/agent.py +1 -0
- lfx/components/arxiv/arxiv.py +8 -2
- lfx/components/composio/__init__.py +71 -17
- lfx/components/composio/agentql_composio.py +11 -0
- lfx/components/composio/agiled_composio.py +11 -0
- lfx/components/composio/bolna_composio.py +11 -0
- lfx/components/composio/brightdata_composio.py +11 -0
- lfx/components/composio/canvas_composio.py +11 -0
- lfx/components/composio/digicert_composio.py +11 -0
- lfx/components/composio/finage_composio.py +11 -0
- lfx/components/composio/fixer_composio.py +11 -0
- lfx/components/composio/flexisign_composio.py +11 -0
- lfx/components/composio/freshdesk_composio.py +11 -0
- lfx/components/composio/googleclassroom_composio.py +11 -0
- lfx/components/composio/instagram_composio.py +11 -0
- lfx/components/composio/jira_composio.py +11 -0
- lfx/components/composio/jotform_composio.py +11 -0
- lfx/components/composio/listennotes_composio.py +11 -0
- lfx/components/composio/missive_composio.py +11 -0
- lfx/components/composio/pandadoc_composio.py +11 -0
- lfx/components/composio/timelinesai_composio.py +11 -0
- lfx/components/helpers/current_date.py +1 -1
- lfx/components/lmstudio/lmstudiomodel.py +9 -5
- lfx/components/logic/__init__.py +3 -0
- lfx/components/logic/llm_conditional_router.py +65 -21
- lfx/components/nvidia/nvidia.py +3 -3
- lfx/components/processing/__init__.py +3 -0
- lfx/components/processing/dynamic_create_data.py +357 -0
- lfx/components/processing/lambda_filter.py +82 -18
- lfx/custom/validate.py +12 -3
- lfx/interface/components.py +336 -8
- lfx/services/settings/base.py +7 -0
- {lfx_nightly-0.1.12.dev38.dist-info → lfx_nightly-0.1.12.dev40.dist-info}/METADATA +1 -1
- {lfx_nightly-0.1.12.dev38.dist-info → lfx_nightly-0.1.12.dev40.dist-info}/RECORD +38 -18
- {lfx_nightly-0.1.12.dev38.dist-info → lfx_nightly-0.1.12.dev40.dist-info}/WHEEL +0 -0
- {lfx_nightly-0.1.12.dev38.dist-info → lfx_nightly-0.1.12.dev40.dist-info}/entry_points.txt +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from typing import Any
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
from
|
|
5
|
-
from
|
|
3
|
+
from lfx.custom import Component
|
|
4
|
+
from lfx.io import BoolInput, HandleInput, MessageInput, MessageTextInput, MultilineInput, Output, TableInput
|
|
5
|
+
from lfx.schema.message import Message
|
|
6
|
+
from lfx.schema.table import EditMode
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class SmartRouterComponent(Component):
|
|
@@ -39,21 +40,42 @@ class SmartRouterComponent(Component):
|
|
|
39
40
|
table_schema=[
|
|
40
41
|
{
|
|
41
42
|
"name": "route_category",
|
|
42
|
-
"display_name": "Route
|
|
43
|
+
"display_name": "Route Name",
|
|
43
44
|
"type": "str",
|
|
44
|
-
"description": "Name for the route
|
|
45
|
+
"description": "Name for the route (used for both output name and category matching)",
|
|
46
|
+
"edit_mode": EditMode.INLINE,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "route_description",
|
|
50
|
+
"display_name": "Route Description",
|
|
51
|
+
"type": "str",
|
|
52
|
+
"description": "Description of when this route should be used (helps LLM understand the category)",
|
|
53
|
+
"default": "",
|
|
54
|
+
"edit_mode": EditMode.POPOVER,
|
|
45
55
|
},
|
|
46
56
|
{
|
|
47
57
|
"name": "output_value",
|
|
48
|
-
"display_name": "
|
|
58
|
+
"display_name": "Route Message (Optional)",
|
|
49
59
|
"type": "str",
|
|
50
|
-
"description":
|
|
60
|
+
"description": (
|
|
61
|
+
"Optional message to send when this route is matched."
|
|
62
|
+
"Leave empty to pass through the original input text."
|
|
63
|
+
),
|
|
51
64
|
"default": "",
|
|
65
|
+
"edit_mode": EditMode.POPOVER,
|
|
52
66
|
},
|
|
53
67
|
],
|
|
54
68
|
value=[
|
|
55
|
-
{
|
|
56
|
-
|
|
69
|
+
{
|
|
70
|
+
"route_category": "Positive",
|
|
71
|
+
"route_description": "Positive feedback, satisfaction, or compliments",
|
|
72
|
+
"output_value": "",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"route_category": "Negative",
|
|
76
|
+
"route_description": "Complaints, issues, or dissatisfaction",
|
|
77
|
+
"output_value": "",
|
|
78
|
+
},
|
|
57
79
|
],
|
|
58
80
|
real_time_refresh=True,
|
|
59
81
|
required=True,
|
|
@@ -125,6 +147,7 @@ class SmartRouterComponent(Component):
|
|
|
125
147
|
# Clear any previous match state
|
|
126
148
|
self._matched_category = None
|
|
127
149
|
|
|
150
|
+
# Get categories and input text
|
|
128
151
|
categories = getattr(self, "routes", [])
|
|
129
152
|
input_text = getattr(self, "input_text", "")
|
|
130
153
|
|
|
@@ -134,17 +157,23 @@ class SmartRouterComponent(Component):
|
|
|
134
157
|
|
|
135
158
|
if llm and categories:
|
|
136
159
|
# Create prompt for categorization
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
160
|
+
category_info = []
|
|
161
|
+
for i, category in enumerate(categories):
|
|
162
|
+
cat_name = category.get("route_category", f"Category {i + 1}")
|
|
163
|
+
cat_desc = category.get("route_description", "")
|
|
164
|
+
if cat_desc and cat_desc.strip():
|
|
165
|
+
category_info.append(f'"{cat_name}": {cat_desc}')
|
|
166
|
+
else:
|
|
167
|
+
category_info.append(f'"{cat_name}"')
|
|
168
|
+
|
|
169
|
+
categories_text = "\n".join([f"- {info}" for info in category_info if info])
|
|
141
170
|
|
|
142
171
|
# Create base prompt
|
|
143
172
|
base_prompt = (
|
|
144
173
|
f"You are a text classifier. Given the following text and categories, "
|
|
145
174
|
f"determine which category best matches the text.\n\n"
|
|
146
175
|
f'Text to classify: "{input_text}"\n\n'
|
|
147
|
-
f"Available categories
|
|
176
|
+
f"Available categories:\n{categories_text}\n\n"
|
|
148
177
|
f"Respond with ONLY the exact category name that best matches the text. "
|
|
149
178
|
f'If none match well, respond with "NONE".\n\n'
|
|
150
179
|
f"Category:"
|
|
@@ -155,7 +184,11 @@ class SmartRouterComponent(Component):
|
|
|
155
184
|
if custom_prompt and custom_prompt.strip():
|
|
156
185
|
self.status = "Using custom prompt as additional instructions"
|
|
157
186
|
# Format custom prompt with variables
|
|
158
|
-
|
|
187
|
+
# For the routes variable, create a simpler format for custom prompt usage
|
|
188
|
+
simple_routes = ", ".join(
|
|
189
|
+
[f'"{cat.get("route_category", f"Category {i + 1}")}"' for i, cat in enumerate(categories)]
|
|
190
|
+
)
|
|
191
|
+
formatted_custom = custom_prompt.format(input_text=input_text, routes=simple_routes)
|
|
159
192
|
# Combine base prompt with custom instructions
|
|
160
193
|
prompt = f"{base_prompt}\n\nAdditional Instructions:\n{formatted_custom}"
|
|
161
194
|
else:
|
|
@@ -188,6 +221,7 @@ class SmartRouterComponent(Component):
|
|
|
188
221
|
f"Comparing '{categorization}' with category {i + 1}: route_category='{route_category}'"
|
|
189
222
|
)
|
|
190
223
|
|
|
224
|
+
# Case-insensitive match
|
|
191
225
|
if categorization.lower() == route_category.lower():
|
|
192
226
|
matched_category = i
|
|
193
227
|
self.status = f"MATCH FOUND! Category {i + 1} matched with '{categorization}'"
|
|
@@ -286,17 +320,23 @@ class SmartRouterComponent(Component):
|
|
|
286
320
|
if llm and categories:
|
|
287
321
|
try:
|
|
288
322
|
# Create prompt for categorization
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
323
|
+
category_info = []
|
|
324
|
+
for i, category in enumerate(categories):
|
|
325
|
+
cat_name = category.get("route_category", f"Category {i + 1}")
|
|
326
|
+
cat_desc = category.get("route_description", "")
|
|
327
|
+
if cat_desc and cat_desc.strip():
|
|
328
|
+
category_info.append(f'"{cat_name}": {cat_desc}')
|
|
329
|
+
else:
|
|
330
|
+
category_info.append(f'"{cat_name}"')
|
|
331
|
+
|
|
332
|
+
categories_text = "\n".join([f"- {info}" for info in category_info if info])
|
|
293
333
|
|
|
294
334
|
# Create base prompt
|
|
295
335
|
base_prompt = (
|
|
296
336
|
"You are a text classifier. Given the following text and categories, "
|
|
297
337
|
"determine which category best matches the text.\n\n"
|
|
298
338
|
f'Text to classify: "{input_text}"\n\n'
|
|
299
|
-
f"Available categories
|
|
339
|
+
f"Available categories:\n{categories_text}\n\n"
|
|
300
340
|
"Respond with ONLY the exact category name that best matches the text. "
|
|
301
341
|
'If none match well, respond with "NONE".\n\n'
|
|
302
342
|
"Category:"
|
|
@@ -307,7 +347,11 @@ class SmartRouterComponent(Component):
|
|
|
307
347
|
if custom_prompt and custom_prompt.strip():
|
|
308
348
|
self.status = "Using custom prompt as additional instructions (default check)"
|
|
309
349
|
# Format custom prompt with variables
|
|
310
|
-
|
|
350
|
+
# For the routes variable, create a simpler format for custom prompt usage
|
|
351
|
+
simple_routes = ", ".join(
|
|
352
|
+
[f'"{cat.get("route_category", f"Category {i + 1}")}"' for i, cat in enumerate(categories)]
|
|
353
|
+
)
|
|
354
|
+
formatted_custom = custom_prompt.format(input_text=input_text, routes=simple_routes)
|
|
311
355
|
# Combine base prompt with custom instructions
|
|
312
356
|
prompt = f"{base_prompt}\n\nAdditional Instructions:\n{formatted_custom}"
|
|
313
357
|
else:
|
lfx/components/nvidia/nvidia.py
CHANGED
|
@@ -42,7 +42,7 @@ class NVIDIAModelComponent(LCModelComponent):
|
|
|
42
42
|
info="The name of the NVIDIA model to use.",
|
|
43
43
|
advanced=False,
|
|
44
44
|
value=None,
|
|
45
|
-
options=
|
|
45
|
+
options=sorted(model.id for model in all_models),
|
|
46
46
|
combobox=True,
|
|
47
47
|
refresh_button=True,
|
|
48
48
|
),
|
|
@@ -102,8 +102,8 @@ class NVIDIAModelComponent(LCModelComponent):
|
|
|
102
102
|
model = ChatNVIDIA(base_url=self.base_url, api_key=self.api_key)
|
|
103
103
|
if tool_model_enabled:
|
|
104
104
|
tool_models = [m for m in model.get_available_models() if m.supports_tools]
|
|
105
|
-
return
|
|
106
|
-
return
|
|
105
|
+
return sorted(m.id for m in tool_models)
|
|
106
|
+
return sorted(m.id for m in model.available_models)
|
|
107
107
|
|
|
108
108
|
def update_build_config(self, build_config: dotdict, _field_value: Any, field_name: str | None = None):
|
|
109
109
|
if field_name in {"model_name", "tool_model_enabled", "base_url", "api_key"}:
|
|
@@ -16,6 +16,7 @@ if TYPE_CHECKING:
|
|
|
16
16
|
from lfx.components.processing.data_to_dataframe import DataToDataFrameComponent
|
|
17
17
|
from lfx.components.processing.dataframe_operations import DataFrameOperationsComponent
|
|
18
18
|
from lfx.components.processing.dataframe_to_toolset import DataFrameToToolsetComponent
|
|
19
|
+
from lfx.components.processing.dynamic_create_data import DynamicCreateDataComponent
|
|
19
20
|
from lfx.components.processing.extract_key import ExtractDataKeyComponent
|
|
20
21
|
from lfx.components.processing.filter_data import FilterDataComponent
|
|
21
22
|
from lfx.components.processing.filter_data_values import DataFilterComponent
|
|
@@ -46,6 +47,7 @@ _dynamic_imports = {
|
|
|
46
47
|
"DataToDataFrameComponent": "data_to_dataframe",
|
|
47
48
|
"DataFrameOperationsComponent": "dataframe_operations",
|
|
48
49
|
"DataFrameToToolsetComponent": "dataframe_to_toolset",
|
|
50
|
+
"DynamicCreateDataComponent": "dynamic_create_data",
|
|
49
51
|
"ExtractDataKeyComponent": "extract_key",
|
|
50
52
|
"FilterDataComponent": "filter_data",
|
|
51
53
|
"DataFilterComponent": "filter_data_values",
|
|
@@ -77,6 +79,7 @@ __all__ = [
|
|
|
77
79
|
"DataFrameToToolsetComponent",
|
|
78
80
|
"DataOperationsComponent",
|
|
79
81
|
"DataToDataFrameComponent",
|
|
82
|
+
"DynamicCreateDataComponent",
|
|
80
83
|
"ExtractDataKeyComponent",
|
|
81
84
|
"FilterDataComponent",
|
|
82
85
|
"JSONCleaner",
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from lfx.custom import Component
|
|
4
|
+
from lfx.io import (
|
|
5
|
+
BoolInput,
|
|
6
|
+
FloatInput,
|
|
7
|
+
HandleInput,
|
|
8
|
+
IntInput,
|
|
9
|
+
MultilineInput,
|
|
10
|
+
Output,
|
|
11
|
+
StrInput,
|
|
12
|
+
TableInput,
|
|
13
|
+
)
|
|
14
|
+
from lfx.schema.data import Data
|
|
15
|
+
from lfx.schema.message import Message
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DynamicCreateDataComponent(Component):
|
|
19
|
+
display_name: str = "Dynamic Create Data"
|
|
20
|
+
description: str = "Dynamically create a Data with a specified number of fields."
|
|
21
|
+
name: str = "DynamicCreateData"
|
|
22
|
+
MAX_FIELDS = 15 # Define a constant for maximum number of fields
|
|
23
|
+
icon = "ListFilter"
|
|
24
|
+
|
|
25
|
+
def __init__(self, **kwargs):
|
|
26
|
+
super().__init__(**kwargs)
|
|
27
|
+
|
|
28
|
+
inputs = [
|
|
29
|
+
TableInput(
|
|
30
|
+
name="form_fields",
|
|
31
|
+
display_name="Input Configuration",
|
|
32
|
+
info=(
|
|
33
|
+
"Define the dynamic form fields. Each row creates a new input field "
|
|
34
|
+
"that can connect to other components."
|
|
35
|
+
),
|
|
36
|
+
table_schema=[
|
|
37
|
+
{
|
|
38
|
+
"name": "field_name",
|
|
39
|
+
"display_name": "Field Name",
|
|
40
|
+
"type": "str",
|
|
41
|
+
"description": "Name for the field (used as both internal name and display label)",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"name": "field_type",
|
|
45
|
+
"display_name": "Field Type",
|
|
46
|
+
"type": "str",
|
|
47
|
+
"description": "Type of input field to create",
|
|
48
|
+
"options": ["Text", "Data", "Number", "Handle", "Boolean"],
|
|
49
|
+
"value": "Text",
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
value=[],
|
|
53
|
+
real_time_refresh=True,
|
|
54
|
+
),
|
|
55
|
+
BoolInput(
|
|
56
|
+
name="include_metadata",
|
|
57
|
+
display_name="Include Metadata",
|
|
58
|
+
info="Include form configuration metadata in the output.",
|
|
59
|
+
value=False,
|
|
60
|
+
advanced=True,
|
|
61
|
+
),
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
outputs = [
|
|
65
|
+
Output(display_name="Data", name="form_data", method="process_form"),
|
|
66
|
+
Output(display_name="Message", name="message", method="get_message"),
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
def update_build_config(self, build_config: dict, field_value: Any, field_name: str | None = None) -> dict:
|
|
70
|
+
"""Update build configuration to add dynamic inputs that can connect to other components."""
|
|
71
|
+
if field_name == "form_fields":
|
|
72
|
+
# Clear existing dynamic inputs from build config
|
|
73
|
+
keys_to_remove = [key for key in build_config if key.startswith("dynamic_")]
|
|
74
|
+
for key in keys_to_remove:
|
|
75
|
+
del build_config[key]
|
|
76
|
+
|
|
77
|
+
# Add dynamic inputs based on table configuration
|
|
78
|
+
# Safety check to ensure field_value is not None and is iterable
|
|
79
|
+
if field_value is None:
|
|
80
|
+
field_value = []
|
|
81
|
+
|
|
82
|
+
for i, field_config in enumerate(field_value):
|
|
83
|
+
# Safety check to ensure field_config is not None
|
|
84
|
+
if field_config is None:
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
field_name = field_config.get("field_name", f"field_{i}")
|
|
88
|
+
display_name = field_name # Use field_name as display_name
|
|
89
|
+
field_type_option = field_config.get("field_type", "Text")
|
|
90
|
+
default_value = "" # All fields have empty default value
|
|
91
|
+
required = False # All fields are optional by default
|
|
92
|
+
help_text = "" # All fields have empty help text
|
|
93
|
+
|
|
94
|
+
# Map field type options to actual field types and input types
|
|
95
|
+
field_type_mapping = {
|
|
96
|
+
"Text": {"field_type": "multiline", "input_types": ["Text", "Message"]},
|
|
97
|
+
"Data": {"field_type": "data", "input_types": ["Data"]},
|
|
98
|
+
"Number": {"field_type": "number", "input_types": ["Text", "Message"]},
|
|
99
|
+
"Handle": {"field_type": "handle", "input_types": ["Text", "Data", "Message"]},
|
|
100
|
+
"Boolean": {"field_type": "boolean", "input_types": None},
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
field_config_mapped = field_type_mapping.get(
|
|
104
|
+
field_type_option, {"field_type": "text", "input_types": []}
|
|
105
|
+
)
|
|
106
|
+
if not isinstance(field_config_mapped, dict):
|
|
107
|
+
field_config_mapped = {"field_type": "text", "input_types": []}
|
|
108
|
+
field_type = field_config_mapped["field_type"]
|
|
109
|
+
input_types_list = field_config_mapped["input_types"]
|
|
110
|
+
|
|
111
|
+
# Create the appropriate input type based on field_type
|
|
112
|
+
dynamic_input_name = f"dynamic_{field_name}"
|
|
113
|
+
|
|
114
|
+
if field_type == "text":
|
|
115
|
+
if input_types_list:
|
|
116
|
+
build_config[dynamic_input_name] = StrInput(
|
|
117
|
+
name=dynamic_input_name,
|
|
118
|
+
display_name=display_name,
|
|
119
|
+
info=f"{help_text} (Can connect to: {', '.join(input_types_list)})",
|
|
120
|
+
value=default_value,
|
|
121
|
+
required=required,
|
|
122
|
+
input_types=input_types_list,
|
|
123
|
+
)
|
|
124
|
+
else:
|
|
125
|
+
build_config[dynamic_input_name] = StrInput(
|
|
126
|
+
name=dynamic_input_name,
|
|
127
|
+
display_name=display_name,
|
|
128
|
+
info=help_text,
|
|
129
|
+
value=default_value,
|
|
130
|
+
required=required,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
elif field_type == "multiline":
|
|
134
|
+
if input_types_list:
|
|
135
|
+
build_config[dynamic_input_name] = MultilineInput(
|
|
136
|
+
name=dynamic_input_name,
|
|
137
|
+
display_name=display_name,
|
|
138
|
+
info=f"{help_text} (Can connect to: {', '.join(input_types_list)})",
|
|
139
|
+
value=default_value,
|
|
140
|
+
required=required,
|
|
141
|
+
input_types=input_types_list,
|
|
142
|
+
)
|
|
143
|
+
else:
|
|
144
|
+
build_config[dynamic_input_name] = MultilineInput(
|
|
145
|
+
name=dynamic_input_name,
|
|
146
|
+
display_name=display_name,
|
|
147
|
+
info=help_text,
|
|
148
|
+
value=default_value,
|
|
149
|
+
required=required,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
elif field_type == "number":
|
|
153
|
+
try:
|
|
154
|
+
default_int = int(default_value) if default_value else 0
|
|
155
|
+
except ValueError:
|
|
156
|
+
default_int = 0
|
|
157
|
+
|
|
158
|
+
if input_types_list:
|
|
159
|
+
build_config[dynamic_input_name] = IntInput(
|
|
160
|
+
name=dynamic_input_name,
|
|
161
|
+
display_name=display_name,
|
|
162
|
+
info=f"{help_text} (Can connect to: {', '.join(input_types_list)})",
|
|
163
|
+
value=default_int,
|
|
164
|
+
required=required,
|
|
165
|
+
input_types=input_types_list,
|
|
166
|
+
)
|
|
167
|
+
else:
|
|
168
|
+
build_config[dynamic_input_name] = IntInput(
|
|
169
|
+
name=dynamic_input_name,
|
|
170
|
+
display_name=display_name,
|
|
171
|
+
info=help_text,
|
|
172
|
+
value=default_int,
|
|
173
|
+
required=required,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
elif field_type == "float":
|
|
177
|
+
try:
|
|
178
|
+
default_float = float(default_value) if default_value else 0.0
|
|
179
|
+
except ValueError:
|
|
180
|
+
default_float = 0.0
|
|
181
|
+
|
|
182
|
+
if input_types_list:
|
|
183
|
+
build_config[dynamic_input_name] = FloatInput(
|
|
184
|
+
name=dynamic_input_name,
|
|
185
|
+
display_name=display_name,
|
|
186
|
+
info=f"{help_text} (Can connect to: {', '.join(input_types_list)})",
|
|
187
|
+
value=default_float,
|
|
188
|
+
required=required,
|
|
189
|
+
input_types=input_types_list,
|
|
190
|
+
)
|
|
191
|
+
else:
|
|
192
|
+
build_config[dynamic_input_name] = FloatInput(
|
|
193
|
+
name=dynamic_input_name,
|
|
194
|
+
display_name=display_name,
|
|
195
|
+
info=help_text,
|
|
196
|
+
value=default_float,
|
|
197
|
+
required=required,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
elif field_type == "boolean":
|
|
201
|
+
default_bool = default_value.lower() in ["true", "1", "yes"] if default_value else False
|
|
202
|
+
|
|
203
|
+
# Boolean fields don't use input_types parameter to avoid errors
|
|
204
|
+
build_config[dynamic_input_name] = BoolInput(
|
|
205
|
+
name=dynamic_input_name,
|
|
206
|
+
display_name=display_name,
|
|
207
|
+
info=help_text,
|
|
208
|
+
value=default_bool,
|
|
209
|
+
input_types=[],
|
|
210
|
+
required=required,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
elif field_type == "handle":
|
|
214
|
+
# HandleInput for generic data connections
|
|
215
|
+
build_config[dynamic_input_name] = HandleInput(
|
|
216
|
+
name=dynamic_input_name,
|
|
217
|
+
display_name=display_name,
|
|
218
|
+
info=f"{help_text} (Accepts: {', '.join(input_types_list) if input_types_list else 'Any'})",
|
|
219
|
+
input_types=input_types_list if input_types_list else ["Data", "Text", "Message"],
|
|
220
|
+
required=required,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
elif field_type == "data":
|
|
224
|
+
# Specialized for Data type connections
|
|
225
|
+
build_config[dynamic_input_name] = HandleInput(
|
|
226
|
+
name=dynamic_input_name,
|
|
227
|
+
display_name=display_name,
|
|
228
|
+
info=f"{help_text} (Data input)",
|
|
229
|
+
input_types=input_types_list if input_types_list else ["Data"],
|
|
230
|
+
required=required,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
else:
|
|
234
|
+
# Default to text input for unknown types
|
|
235
|
+
build_config[dynamic_input_name] = StrInput(
|
|
236
|
+
name=dynamic_input_name,
|
|
237
|
+
display_name=display_name,
|
|
238
|
+
info=f"{help_text} (Unknown type '{field_type}', defaulting to text)",
|
|
239
|
+
value=default_value,
|
|
240
|
+
required=required,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
return build_config
|
|
244
|
+
|
|
245
|
+
def get_dynamic_values(self) -> dict[str, Any]:
|
|
246
|
+
"""Extract simple values from all dynamic inputs, handling both manual and connected inputs."""
|
|
247
|
+
dynamic_values = {}
|
|
248
|
+
connection_info = {}
|
|
249
|
+
form_fields = getattr(self, "form_fields", [])
|
|
250
|
+
|
|
251
|
+
for field_config in form_fields:
|
|
252
|
+
# Safety check to ensure field_config is not None
|
|
253
|
+
if field_config is None:
|
|
254
|
+
continue
|
|
255
|
+
|
|
256
|
+
field_name = field_config.get("field_name", "")
|
|
257
|
+
if field_name:
|
|
258
|
+
dynamic_input_name = f"dynamic_{field_name}"
|
|
259
|
+
value = getattr(self, dynamic_input_name, None)
|
|
260
|
+
|
|
261
|
+
# Extract simple values from connections or manual input
|
|
262
|
+
if value is not None:
|
|
263
|
+
try:
|
|
264
|
+
extracted_value = self._extract_simple_value(value)
|
|
265
|
+
dynamic_values[field_name] = extracted_value
|
|
266
|
+
|
|
267
|
+
# Determine connection type for status
|
|
268
|
+
if hasattr(value, "text") and hasattr(value, "timestamp"):
|
|
269
|
+
connection_info[field_name] = "Connected (Message)"
|
|
270
|
+
elif hasattr(value, "data"):
|
|
271
|
+
connection_info[field_name] = "Connected (Data)"
|
|
272
|
+
elif isinstance(value, (str, int, float, bool, list, dict)):
|
|
273
|
+
connection_info[field_name] = "Manual input"
|
|
274
|
+
else:
|
|
275
|
+
connection_info[field_name] = "Connected (Object)"
|
|
276
|
+
|
|
277
|
+
except (AttributeError, TypeError, ValueError):
|
|
278
|
+
# Fallback to string representation if all else fails
|
|
279
|
+
dynamic_values[field_name] = str(value)
|
|
280
|
+
connection_info[field_name] = "Error"
|
|
281
|
+
else:
|
|
282
|
+
# Use empty default value if nothing connected
|
|
283
|
+
dynamic_values[field_name] = ""
|
|
284
|
+
connection_info[field_name] = "Empty default"
|
|
285
|
+
|
|
286
|
+
# Store connection info for status output
|
|
287
|
+
self._connection_info = connection_info
|
|
288
|
+
return dynamic_values
|
|
289
|
+
|
|
290
|
+
def _extract_simple_value(self, value: Any) -> Any:
|
|
291
|
+
"""Extract the simplest, most useful value from any input type."""
|
|
292
|
+
# Handle None
|
|
293
|
+
if value is None:
|
|
294
|
+
return None
|
|
295
|
+
|
|
296
|
+
# Handle simple types directly
|
|
297
|
+
if isinstance(value, (str, int, float, bool)):
|
|
298
|
+
return value
|
|
299
|
+
|
|
300
|
+
# Handle lists and tuples - keep simple
|
|
301
|
+
if isinstance(value, (list, tuple)):
|
|
302
|
+
return [self._extract_simple_value(item) for item in value]
|
|
303
|
+
|
|
304
|
+
# Handle dictionaries - keep simple
|
|
305
|
+
if isinstance(value, dict):
|
|
306
|
+
return {str(k): self._extract_simple_value(v) for k, v in value.items()}
|
|
307
|
+
|
|
308
|
+
# Handle Message objects - extract only the text
|
|
309
|
+
if hasattr(value, "text"):
|
|
310
|
+
return str(value.text) if value.text is not None else ""
|
|
311
|
+
|
|
312
|
+
# Handle Data objects - extract the data content
|
|
313
|
+
if hasattr(value, "data") and value.data is not None:
|
|
314
|
+
return self._extract_simple_value(value.data)
|
|
315
|
+
|
|
316
|
+
# For any other object, convert to string
|
|
317
|
+
return str(value)
|
|
318
|
+
|
|
319
|
+
def process_form(self) -> Data:
|
|
320
|
+
"""Process all dynamic form inputs and return clean data with just field values."""
|
|
321
|
+
# Get all dynamic values (just the key:value pairs)
|
|
322
|
+
dynamic_values = self.get_dynamic_values()
|
|
323
|
+
|
|
324
|
+
# Update status with connection info
|
|
325
|
+
connected_fields = len([v for v in getattr(self, "_connection_info", {}).values() if "Connected" in v])
|
|
326
|
+
total_fields = len(dynamic_values)
|
|
327
|
+
|
|
328
|
+
self.status = f"Form processed successfully. {connected_fields}/{total_fields} fields connected to components."
|
|
329
|
+
|
|
330
|
+
# Return clean Data object with just the field values
|
|
331
|
+
return Data(data=dynamic_values)
|
|
332
|
+
|
|
333
|
+
def get_message(self) -> Message:
|
|
334
|
+
"""Return form data as a formatted text message."""
|
|
335
|
+
# Get all dynamic values
|
|
336
|
+
dynamic_values = self.get_dynamic_values()
|
|
337
|
+
|
|
338
|
+
if not dynamic_values:
|
|
339
|
+
return Message(text="No form data available")
|
|
340
|
+
|
|
341
|
+
# Format as text message
|
|
342
|
+
message_lines = ["📋 Form Data:"]
|
|
343
|
+
message_lines.append("=" * 40)
|
|
344
|
+
|
|
345
|
+
for field_name, value in dynamic_values.items():
|
|
346
|
+
# Use field_name as display_name
|
|
347
|
+
display_name = field_name
|
|
348
|
+
|
|
349
|
+
message_lines.append(f"• {display_name}: {value}")
|
|
350
|
+
|
|
351
|
+
message_lines.append("=" * 40)
|
|
352
|
+
message_lines.append(f"Total fields: {len(dynamic_values)}")
|
|
353
|
+
|
|
354
|
+
message_text = "\n".join(message_lines)
|
|
355
|
+
self.status = f"Message formatted with {len(dynamic_values)} fields"
|
|
356
|
+
|
|
357
|
+
return Message(text=message_text)
|