lfx-nightly 0.1.13.dev0__py3-none-any.whl → 0.2.0.dev0__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 (86) hide show
  1. lfx/_assets/component_index.json +1 -1
  2. lfx/base/agents/agent.py +109 -29
  3. lfx/base/agents/events.py +102 -35
  4. lfx/base/agents/utils.py +15 -2
  5. lfx/base/composio/composio_base.py +24 -9
  6. lfx/base/datastax/__init__.py +5 -0
  7. lfx/{components/vectorstores/astradb.py → base/datastax/astradb_base.py} +84 -473
  8. lfx/base/io/chat.py +5 -4
  9. lfx/base/mcp/util.py +101 -15
  10. lfx/base/models/model_input_constants.py +74 -7
  11. lfx/base/models/ollama_constants.py +3 -0
  12. lfx/base/models/watsonx_constants.py +12 -0
  13. lfx/cli/commands.py +1 -1
  14. lfx/components/agents/__init__.py +3 -1
  15. lfx/components/agents/agent.py +47 -4
  16. lfx/components/agents/altk_agent.py +366 -0
  17. lfx/components/agents/cuga_agent.py +1 -1
  18. lfx/components/agents/mcp_component.py +32 -2
  19. lfx/components/amazon/amazon_bedrock_converse.py +1 -1
  20. lfx/components/apify/apify_actor.py +3 -3
  21. lfx/components/datastax/__init__.py +12 -6
  22. lfx/components/datastax/{astra_assistant_manager.py → astradb_assistant_manager.py} +1 -0
  23. lfx/components/datastax/astradb_chatmemory.py +40 -0
  24. lfx/components/datastax/astradb_cql.py +5 -31
  25. lfx/components/datastax/astradb_graph.py +9 -123
  26. lfx/components/datastax/astradb_tool.py +12 -52
  27. lfx/components/datastax/astradb_vectorstore.py +133 -976
  28. lfx/components/datastax/create_assistant.py +1 -0
  29. lfx/components/datastax/create_thread.py +1 -0
  30. lfx/components/datastax/dotenv.py +1 -0
  31. lfx/components/datastax/get_assistant.py +1 -0
  32. lfx/components/datastax/getenvvar.py +1 -0
  33. lfx/components/datastax/graph_rag.py +1 -1
  34. lfx/components/datastax/list_assistants.py +1 -0
  35. lfx/components/datastax/run.py +1 -0
  36. lfx/components/docling/__init__.py +3 -0
  37. lfx/components/docling/docling_remote_vlm.py +284 -0
  38. lfx/components/ibm/watsonx.py +25 -21
  39. lfx/components/input_output/chat.py +8 -0
  40. lfx/components/input_output/chat_output.py +8 -0
  41. lfx/components/knowledge_bases/ingestion.py +17 -9
  42. lfx/components/knowledge_bases/retrieval.py +16 -8
  43. lfx/components/logic/loop.py +4 -0
  44. lfx/components/mistral/mistral_embeddings.py +1 -1
  45. lfx/components/models/embedding_model.py +88 -7
  46. lfx/components/ollama/ollama.py +221 -14
  47. lfx/components/openrouter/openrouter.py +49 -147
  48. lfx/components/processing/parser.py +6 -1
  49. lfx/components/processing/structured_output.py +55 -17
  50. lfx/components/vectorstores/__init__.py +0 -6
  51. lfx/custom/custom_component/component.py +3 -2
  52. lfx/field_typing/constants.py +1 -0
  53. lfx/graph/edge/base.py +2 -2
  54. lfx/graph/graph/base.py +1 -1
  55. lfx/graph/graph/schema.py +3 -2
  56. lfx/graph/vertex/vertex_types.py +1 -1
  57. lfx/io/schema.py +6 -0
  58. lfx/schema/schema.py +5 -0
  59. {lfx_nightly-0.1.13.dev0.dist-info → lfx_nightly-0.2.0.dev0.dist-info}/METADATA +1 -1
  60. {lfx_nightly-0.1.13.dev0.dist-info → lfx_nightly-0.2.0.dev0.dist-info}/RECORD +63 -81
  61. lfx/components/datastax/astra_db.py +0 -77
  62. lfx/components/datastax/cassandra.py +0 -92
  63. lfx/components/vectorstores/astradb_graph.py +0 -326
  64. lfx/components/vectorstores/cassandra.py +0 -264
  65. lfx/components/vectorstores/cassandra_graph.py +0 -238
  66. lfx/components/vectorstores/chroma.py +0 -167
  67. lfx/components/vectorstores/clickhouse.py +0 -135
  68. lfx/components/vectorstores/couchbase.py +0 -102
  69. lfx/components/vectorstores/elasticsearch.py +0 -267
  70. lfx/components/vectorstores/faiss.py +0 -111
  71. lfx/components/vectorstores/graph_rag.py +0 -141
  72. lfx/components/vectorstores/hcd.py +0 -314
  73. lfx/components/vectorstores/milvus.py +0 -115
  74. lfx/components/vectorstores/mongodb_atlas.py +0 -213
  75. lfx/components/vectorstores/opensearch.py +0 -243
  76. lfx/components/vectorstores/pgvector.py +0 -72
  77. lfx/components/vectorstores/pinecone.py +0 -134
  78. lfx/components/vectorstores/qdrant.py +0 -109
  79. lfx/components/vectorstores/supabase.py +0 -76
  80. lfx/components/vectorstores/upstash.py +0 -124
  81. lfx/components/vectorstores/vectara.py +0 -97
  82. lfx/components/vectorstores/vectara_rag.py +0 -164
  83. lfx/components/vectorstores/weaviate.py +0 -89
  84. /lfx/components/datastax/{astra_vectorize.py → astradb_vectorize.py} +0 -0
  85. {lfx_nightly-0.1.13.dev0.dist-info → lfx_nightly-0.2.0.dev0.dist-info}/WHEEL +0 -0
  86. {lfx_nightly-0.1.13.dev0.dist-info → lfx_nightly-0.2.0.dev0.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,3 @@
1
- from collections import defaultdict
2
- from typing import Any
3
-
4
1
  import httpx
5
2
  from langchain_openai import ChatOpenAI
6
3
  from pydantic.v1 import SecretStr
@@ -8,13 +5,7 @@ from pydantic.v1 import SecretStr
8
5
  from lfx.base.models.model import LCModelComponent
9
6
  from lfx.field_typing import LanguageModel
10
7
  from lfx.field_typing.range_spec import RangeSpec
11
- from lfx.inputs.inputs import (
12
- DropdownInput,
13
- IntInput,
14
- SecretStrInput,
15
- SliderInput,
16
- StrInput,
17
- )
8
+ from lfx.inputs.inputs import DropdownInput, IntInput, SecretStrInput, SliderInput, StrInput
18
9
 
19
10
 
20
11
  class OpenRouterComponent(LCModelComponent):
@@ -28,36 +19,13 @@ class OpenRouterComponent(LCModelComponent):
28
19
 
29
20
  inputs = [
30
21
  *LCModelComponent.get_base_inputs(),
31
- SecretStrInput(
32
- name="api_key", display_name="OpenRouter API Key", required=True, info="Your OpenRouter API key"
33
- ),
34
- StrInput(
35
- name="site_url",
36
- display_name="Site URL",
37
- info="Your site URL for OpenRouter rankings",
38
- advanced=True,
39
- ),
40
- StrInput(
41
- name="app_name",
42
- display_name="App Name",
43
- info="Your app name for OpenRouter rankings",
44
- advanced=True,
45
- ),
46
- DropdownInput(
47
- name="provider",
48
- display_name="Provider",
49
- info="The AI model provider",
50
- options=["Loading providers..."],
51
- value="Loading providers...",
52
- real_time_refresh=True,
53
- required=True,
54
- ),
22
+ SecretStrInput(name="api_key", display_name="API Key", required=True),
55
23
  DropdownInput(
56
24
  name="model_name",
57
25
  display_name="Model",
58
- info="The model to use for chat completion",
59
- options=["Select a provider first"],
60
- value="Select a provider first",
26
+ options=[],
27
+ value="",
28
+ refresh_button=True,
61
29
  real_time_refresh=True,
62
30
  required=True,
63
31
  ),
@@ -66,137 +34,71 @@ class OpenRouterComponent(LCModelComponent):
66
34
  display_name="Temperature",
67
35
  value=0.7,
68
36
  range_spec=RangeSpec(min=0, max=2, step=0.01),
69
- info="Controls randomness. Lower values are more deterministic, higher values are more creative.",
70
- advanced=True,
71
- ),
72
- IntInput(
73
- name="max_tokens",
74
- display_name="Max Tokens",
75
- info="Maximum number of tokens to generate",
76
37
  advanced=True,
77
38
  ),
39
+ IntInput(name="max_tokens", display_name="Max Tokens", advanced=True),
40
+ StrInput(name="site_url", display_name="Site URL", advanced=True),
41
+ StrInput(name="app_name", display_name="App Name", advanced=True),
78
42
  ]
79
43
 
80
- def fetch_models(self) -> dict[str, list]:
81
- """Fetch available models from OpenRouter API and organize them by provider."""
82
- url = "https://openrouter.ai/api/v1/models"
83
-
44
+ def fetch_models(self) -> list[dict]:
45
+ """Fetch available models from OpenRouter."""
84
46
  try:
85
- with httpx.Client() as client:
86
- response = client.get(url)
87
- response.raise_for_status()
88
-
89
- models_data = response.json().get("data", [])
90
- provider_models = defaultdict(list)
91
-
92
- for model in models_data:
93
- model_id = model.get("id", "")
94
- if "/" in model_id:
95
- provider = model_id.split("/")[0].title()
96
- provider_models[provider].append(
97
- {
98
- "id": model_id,
99
- "name": model.get("name", ""),
100
- "description": model.get("description", ""),
101
- "context_length": model.get("context_length", 0),
102
- }
103
- )
104
-
105
- return dict(provider_models)
106
-
107
- except httpx.HTTPError as e:
108
- self.log(f"Error fetching models: {e!s}")
109
- return {"Error": [{"id": "error", "name": f"Error fetching models: {e!s}"}]}
47
+ response = httpx.get("https://openrouter.ai/api/v1/models", timeout=10.0)
48
+ response.raise_for_status()
49
+ models = response.json().get("data", [])
50
+ return sorted(
51
+ [
52
+ {
53
+ "id": m["id"],
54
+ "name": m.get("name", m["id"]),
55
+ "context": m.get("context_length", 0),
56
+ }
57
+ for m in models
58
+ if m.get("id")
59
+ ],
60
+ key=lambda x: x["name"],
61
+ )
62
+ except (httpx.RequestError, httpx.HTTPStatusError) as e:
63
+ self.log(f"Error fetching models: {e}")
64
+ return []
65
+
66
+ def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None) -> dict: # noqa: ARG002
67
+ """Update model options."""
68
+ models = self.fetch_models()
69
+ if models:
70
+ build_config["model_name"]["options"] = [m["id"] for m in models]
71
+ build_config["model_name"]["tooltips"] = {m["id"]: f"{m['name']} ({m['context']:,} tokens)" for m in models}
72
+ else:
73
+ build_config["model_name"]["options"] = ["Failed to load models"]
74
+ build_config["model_name"]["value"] = "Failed to load models"
75
+ return build_config
110
76
 
111
77
  def build_model(self) -> LanguageModel:
112
- """Build and return the OpenRouter language model."""
113
- model_not_selected = "Please select a model"
114
- api_key_required = "API key is required"
115
-
116
- if not self.model_name or self.model_name == "Select a provider first":
117
- raise ValueError(model_not_selected)
118
-
78
+ """Build the OpenRouter model."""
119
79
  if not self.api_key:
120
- raise ValueError(api_key_required)
121
-
122
- api_key = SecretStr(self.api_key).get_secret_value()
80
+ msg = "API key is required"
81
+ raise ValueError(msg)
82
+ if not self.model_name or self.model_name == "Loading...":
83
+ msg = "Please select a model"
84
+ raise ValueError(msg)
123
85
 
124
- # Build base configuration
125
- kwargs: dict[str, Any] = {
86
+ kwargs = {
126
87
  "model": self.model_name,
127
- "openai_api_key": api_key,
88
+ "openai_api_key": SecretStr(self.api_key).get_secret_value(),
128
89
  "openai_api_base": "https://openrouter.ai/api/v1",
129
90
  "temperature": self.temperature if self.temperature is not None else 0.7,
130
91
  }
131
92
 
132
- # Add optional parameters
133
93
  if self.max_tokens:
134
- kwargs["max_tokens"] = self.max_tokens
94
+ kwargs["max_tokens"] = int(self.max_tokens)
135
95
 
136
96
  headers = {}
137
97
  if self.site_url:
138
98
  headers["HTTP-Referer"] = self.site_url
139
99
  if self.app_name:
140
100
  headers["X-Title"] = self.app_name
141
-
142
101
  if headers:
143
102
  kwargs["default_headers"] = headers
144
103
 
145
- try:
146
- return ChatOpenAI(**kwargs)
147
- except (ValueError, httpx.HTTPError) as err:
148
- error_msg = f"Failed to build model: {err!s}"
149
- self.log(error_msg)
150
- raise ValueError(error_msg) from err
151
-
152
- def _get_exception_message(self, e: Exception) -> str | None:
153
- """Get a message from an OpenRouter exception.
154
-
155
- Args:
156
- e (Exception): The exception to get the message from.
157
-
158
- Returns:
159
- str | None: The message from the exception, or None if no specific message can be extracted.
160
- """
161
- try:
162
- from openai import BadRequestError
163
-
164
- if isinstance(e, BadRequestError):
165
- message = e.body.get("message")
166
- if message:
167
- return message
168
- except ImportError:
169
- pass
170
- return None
171
-
172
- def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None) -> dict:
173
- """Update build configuration based on field updates."""
174
- try:
175
- if field_name is None or field_name == "provider":
176
- provider_models = self.fetch_models()
177
- build_config["provider"]["options"] = sorted(provider_models.keys())
178
- if build_config["provider"]["value"] not in provider_models:
179
- build_config["provider"]["value"] = build_config["provider"]["options"][0]
180
-
181
- if field_name == "provider" and field_value in self.fetch_models():
182
- provider_models = self.fetch_models()
183
- models = provider_models[field_value]
184
-
185
- build_config["model_name"]["options"] = [model["id"] for model in models]
186
- if models:
187
- build_config["model_name"]["value"] = models[0]["id"]
188
-
189
- tooltips = {
190
- model["id"]: (f"{model['name']}\nContext Length: {model['context_length']}\n{model['description']}")
191
- for model in models
192
- }
193
- build_config["model_name"]["tooltips"] = tooltips
194
-
195
- except httpx.HTTPError as e:
196
- self.log(f"Error updating build config: {e!s}")
197
- build_config["provider"]["options"] = ["Error loading providers"]
198
- build_config["provider"]["value"] = "Error loading providers"
199
- build_config["model_name"]["options"] = ["Error loading models"]
200
- build_config["model_name"]["value"] = "Error loading models"
201
-
202
- return build_config
104
+ return ChatOpenAI(**kwargs)
@@ -122,7 +122,12 @@ class ParserComponent(Component):
122
122
  formatted_text = self.pattern.format(**row.to_dict())
123
123
  lines.append(formatted_text)
124
124
  elif data is not None:
125
- formatted_text = self.pattern.format(**data.data)
125
+ # Use format_map with a dict that returns default_value for missing keys
126
+ class DefaultDict(dict):
127
+ def __missing__(self, key):
128
+ return data.default_value or ""
129
+
130
+ formatted_text = self.pattern.format_map(DefaultDict(data.data))
126
131
  lines.append(formatted_text)
127
132
 
128
133
  combined_text = self.sep.join(lines)
@@ -11,6 +11,7 @@ from lfx.io import (
11
11
  Output,
12
12
  TableInput,
13
13
  )
14
+ from lfx.log.logger import logger
14
15
  from lfx.schema.data import Data
15
16
  from lfx.schema.dataframe import DataFrame
16
17
  from lfx.schema.table import EditMode
@@ -136,30 +137,27 @@ class StructuredOutputComponent(Component):
136
137
  raise ValueError(msg)
137
138
 
138
139
  output_model_ = build_model_from_schema(self.output_schema)
139
-
140
140
  output_model = create_model(
141
141
  schema_name,
142
142
  __doc__=f"A list of {schema_name}.",
143
- objects=(list[output_model_], Field(description=f"A list of {schema_name}.")), # type: ignore[valid-type]
143
+ objects=(
144
+ list[output_model_],
145
+ Field(
146
+ description=f"A list of {schema_name}.", # type: ignore[valid-type]
147
+ min_length=1, # help ensure non-empty output
148
+ ),
149
+ ),
144
150
  )
145
-
146
- try:
147
- llm_with_structured_output = create_extractor(self.llm, tools=[output_model])
148
- except NotImplementedError as exc:
149
- msg = f"{self.llm.__class__.__name__} does not support structured output."
150
- raise TypeError(msg) from exc
151
-
151
+ # Tracing config
152
152
  config_dict = {
153
153
  "run_name": self.display_name,
154
154
  "project_name": self.get_project_name(),
155
155
  "callbacks": self.get_langchain_callbacks(),
156
156
  }
157
- result = get_chat_result(
158
- runnable=llm_with_structured_output,
159
- system_message=self.system_prompt,
160
- input_value=self.input_value,
161
- config=config_dict,
162
- )
157
+ # Generate structured output using Trustcall first, then fallback to Langchain if it fails
158
+ result = self._extract_output_with_trustcall(output_model, config_dict)
159
+ if result is None:
160
+ result = self._extract_output_with_langchain(output_model, config_dict)
163
161
 
164
162
  # OPTIMIZATION NOTE: Simplified processing based on trustcall response structure
165
163
  # Handle non-dict responses (shouldn't happen with trustcall, but defensive)
@@ -173,8 +171,9 @@ class StructuredOutputComponent(Component):
173
171
 
174
172
  # Convert BaseModel to dict (creates the "objects" key)
175
173
  first_response = responses[0]
176
- structured_data = first_response.model_dump() if isinstance(first_response, BaseModel) else first_response
177
-
174
+ structured_data = first_response
175
+ if isinstance(first_response, BaseModel):
176
+ structured_data = first_response.model_dump()
178
177
  # Extract the objects array (guaranteed to exist due to our Pydantic model structure)
179
178
  return structured_data.get("objects", structured_data)
180
179
 
@@ -204,3 +203,42 @@ class StructuredOutputComponent(Component):
204
203
  # Multiple outputs - convert to DataFrame directly
205
204
  return DataFrame(output)
206
205
  return DataFrame()
206
+
207
+ def _extract_output_with_trustcall(self, schema: BaseModel, config_dict: dict) -> list[BaseModel] | None:
208
+ try:
209
+ llm_with_structured_output = create_extractor(self.llm, tools=[schema], tool_choice=schema.__name__)
210
+ result = get_chat_result(
211
+ runnable=llm_with_structured_output,
212
+ system_message=self.system_prompt,
213
+ input_value=self.input_value,
214
+ config=config_dict,
215
+ )
216
+ except Exception as e: # noqa: BLE001
217
+ logger.warning(
218
+ f"Trustcall extraction failed, falling back to Langchain: {e} "
219
+ "(Note: This may not be an error—some models or configurations do not support tool calling. "
220
+ "Falling back is normal in such cases.)"
221
+ )
222
+ return None
223
+ return result or None # langchain fallback is used if error occurs or the result is empty
224
+
225
+ def _extract_output_with_langchain(self, schema: BaseModel, config_dict: dict) -> list[BaseModel] | None:
226
+ try:
227
+ llm_with_structured_output = self.llm.with_structured_output(schema)
228
+ result = get_chat_result(
229
+ runnable=llm_with_structured_output,
230
+ system_message=self.system_prompt,
231
+ input_value=self.input_value,
232
+ config=config_dict,
233
+ )
234
+ if isinstance(result, BaseModel):
235
+ result = result.model_dump()
236
+ result = result.get("objects", result)
237
+ except Exception as fallback_error:
238
+ msg = (
239
+ f"Model does not support tool calling (trustcall failed) "
240
+ f"and fallback with_structured_output also failed: {fallback_error}"
241
+ )
242
+ raise ValueError(msg) from fallback_error
243
+
244
+ return result or None
@@ -5,20 +5,14 @@ from typing import TYPE_CHECKING, Any
5
5
  from lfx.components._importing import import_mod
6
6
 
7
7
  if TYPE_CHECKING:
8
- from .astradb import AstraDBVectorStoreComponent
9
8
  from .local_db import LocalDBComponent
10
- from .mongodb_atlas import MongoVectorStoreComponent
11
9
 
12
10
  _dynamic_imports = {
13
11
  "LocalDBComponent": "local_db",
14
- "AstraDBVectorStoreComponent": "astradb",
15
- "MongoVectorStoreComponent": "mongodb_atlas",
16
12
  }
17
13
 
18
14
  __all__ = [
19
- "AstraDBVectorStoreComponent",
20
15
  "LocalDBComponent",
21
- "MongoVectorStoreComponent",
22
16
  ]
23
17
 
24
18
 
@@ -154,7 +154,7 @@ class Component(CustomComponent):
154
154
  self.trace_type = "chain"
155
155
 
156
156
  # Setup inputs and outputs
157
- self._reset_all_output_values()
157
+ self.reset_all_output_values()
158
158
  if self.inputs is not None:
159
159
  self.map_inputs(self.inputs)
160
160
  self.map_outputs()
@@ -330,7 +330,8 @@ class Component(CustomComponent):
330
330
  def set_event_manager(self, event_manager: EventManager | None = None) -> None:
331
331
  self._event_manager = event_manager
332
332
 
333
- def _reset_all_output_values(self) -> None:
333
+ def reset_all_output_values(self) -> None:
334
+ """Reset all output values to UNDEFINED."""
334
335
  if isinstance(self._outputs_map, dict):
335
336
  for output in self._outputs_map.values():
336
337
  output.value = UNDEFINED
@@ -209,6 +209,7 @@ from lfx.io import (
209
209
  TableInput,
210
210
  )
211
211
  from lfx.schema.data import Data
212
+ from lfx.schema.dataframe import DataFrame
212
213
  """
213
214
 
214
215
  if importlib.util.find_spec("langchain") is not None:
lfx/graph/edge/base.py CHANGED
@@ -63,8 +63,8 @@ class Edge:
63
63
  # target_param is documents
64
64
  if isinstance(self._target_handle, str):
65
65
  self.target_param = self._target_handle.split("|")[1]
66
- self.source_handle = None
67
- self.target_handle = None
66
+ self.source_handle = None # type: ignore[assignment]
67
+ self.target_handle = None # type: ignore[assignment]
68
68
  else:
69
69
  msg = "Target handle is not a string"
70
70
  raise ValueError(msg)
lfx/graph/graph/base.py CHANGED
@@ -402,7 +402,7 @@ class Graph:
402
402
  for vertex in self.vertices:
403
403
  if vertex.custom_component is None:
404
404
  continue
405
- vertex.custom_component._reset_all_output_values()
405
+ vertex.custom_component.reset_all_output_values()
406
406
 
407
407
  def start(
408
408
  self,
lfx/graph/graph/schema.py CHANGED
@@ -4,11 +4,12 @@ from typing import TYPE_CHECKING, NamedTuple, Protocol
4
4
 
5
5
  from typing_extensions import NotRequired, TypedDict
6
6
 
7
+ from lfx.graph.edge.schema import EdgeData
8
+ from lfx.graph.vertex.schema import NodeData
9
+
7
10
  if TYPE_CHECKING:
8
- from lfx.graph.edge.schema import EdgeData
9
11
  from lfx.graph.schema import ResultData
10
12
  from lfx.graph.vertex.base import Vertex
11
- from lfx.graph.vertex.schema import NodeData
12
13
  from lfx.schema.log import LoggableType
13
14
 
14
15
 
@@ -65,7 +65,7 @@ class ComponentVertex(Vertex):
65
65
  self.built_object, self.artifacts = result
66
66
  elif len(result) == 3: # noqa: PLR2004
67
67
  self.custom_component, self.built_object, self.artifacts = result
68
- self.logs = self.custom_component._output_logs
68
+ self.logs = self.custom_component.get_output_logs()
69
69
  for key in self.artifacts:
70
70
  if self.artifacts_raw is None:
71
71
  self.artifacts_raw = {}
lfx/io/schema.py CHANGED
@@ -137,6 +137,12 @@ def schema_to_langflow_inputs(schema: type[BaseModel]) -> list[InputTypes]:
137
137
 
138
138
  is_list = False
139
139
 
140
+ # Handle unparameterized list (e.g., coming from nullable array schemas)
141
+ # Treat it as a list of strings for input purposes
142
+ if ann is list:
143
+ is_list = True
144
+ ann = str
145
+
140
146
  if get_origin(ann) is list:
141
147
  is_list = True
142
148
  ann = get_args(ann)[0]
lfx/schema/schema.py CHANGED
@@ -149,6 +149,11 @@ class InputValueRequest(BaseModel):
149
149
  description="Defines on which components the input value should be applied. "
150
150
  "'any' applies to all input components.",
151
151
  )
152
+ client_request_time: int | None = Field(
153
+ None,
154
+ description="Client-side timestamp in milliseconds when the request was initiated. "
155
+ "Used to calculate accurate end-to-end duration.",
156
+ )
152
157
 
153
158
  # add an example
154
159
  model_config = ConfigDict(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lfx-nightly
3
- Version: 0.1.13.dev0
3
+ Version: 0.2.0.dev0
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