lfx-nightly 0.1.12.dev33__py3-none-any.whl → 0.1.12.dev35__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of lfx-nightly might be problematic. Click here for more details.
- lfx/base/agents/events.py +11 -7
- lfx/base/mcp/util.py +202 -133
- lfx/base/models/model.py +1 -1
- lfx/base/tools/flow_tool.py +1 -1
- lfx/base/tools/run_flow.py +1 -1
- lfx/cli/commands.py +17 -12
- lfx/cli/run.py +156 -95
- lfx/components/agents/mcp_component.py +11 -4
- lfx/components/deactivated/sub_flow.py +1 -1
- lfx/components/logic/flow_tool.py +1 -1
- lfx/components/logic/run_flow.py +1 -1
- lfx/components/logic/sub_flow.py +1 -1
- lfx/components/nvidia/nvidia.py +2 -5
- lfx/custom/custom_component/component.py +40 -20
- lfx/custom/custom_component/custom_component.py +1 -1
- lfx/graph/vertex/param_handler.py +2 -2
- lfx/helpers/__init__.py +129 -1
- lfx/helpers/flow.py +0 -3
- lfx/inputs/input_mixin.py +2 -1
- lfx/inputs/inputs.py +5 -14
- lfx/interface/components.py +4 -2
- lfx/log/logger.py +5 -1
- lfx/memory/__init__.py +10 -30
- lfx/schema/cross_module.py +80 -0
- lfx/schema/data.py +2 -1
- lfx/schema/log.py +1 -0
- lfx/schema/message.py +7 -3
- lfx/utils/image.py +7 -1
- lfx/utils/langflow_utils.py +52 -0
- {lfx_nightly-0.1.12.dev33.dist-info → lfx_nightly-0.1.12.dev35.dist-info}/METADATA +1 -1
- {lfx_nightly-0.1.12.dev33.dist-info → lfx_nightly-0.1.12.dev35.dist-info}/RECORD +33 -31
- {lfx_nightly-0.1.12.dev33.dist-info → lfx_nightly-0.1.12.dev35.dist-info}/WHEEL +0 -0
- {lfx_nightly-0.1.12.dev33.dist-info → lfx_nightly-0.1.12.dev35.dist-info}/entry_points.txt +0 -0
|
@@ -1548,7 +1548,16 @@ class Component(CustomComponent):
|
|
|
1548
1548
|
if not message.sender_name:
|
|
1549
1549
|
message.sender_name = MESSAGE_SENDER_NAME_AI
|
|
1550
1550
|
|
|
1551
|
-
async def send_message(self, message: Message, id_: str | None = None):
|
|
1551
|
+
async def send_message(self, message: Message, id_: str | None = None, *, skip_db_update: bool = False):
|
|
1552
|
+
"""Send a message with optional database update control.
|
|
1553
|
+
|
|
1554
|
+
Args:
|
|
1555
|
+
message: The message to send
|
|
1556
|
+
id_: Optional message ID
|
|
1557
|
+
skip_db_update: If True, only update in-memory and send event, skip DB write.
|
|
1558
|
+
Useful during streaming to avoid excessive DB round-trips.
|
|
1559
|
+
Note: This assumes the message already exists in the database with message.id set.
|
|
1560
|
+
"""
|
|
1552
1561
|
if self._should_skip_message(message):
|
|
1553
1562
|
return message
|
|
1554
1563
|
|
|
@@ -1558,26 +1567,37 @@ class Component(CustomComponent):
|
|
|
1558
1567
|
# Ensure required fields for message storage are set
|
|
1559
1568
|
self._ensure_message_required_fields(message)
|
|
1560
1569
|
|
|
1561
|
-
|
|
1570
|
+
# If skip_db_update is True and message already has an ID, skip the DB write
|
|
1571
|
+
# This path is used during agent streaming to avoid excessive DB round-trips
|
|
1572
|
+
if skip_db_update and message.id:
|
|
1573
|
+
# Create a fresh Message instance for consistency with normal flow
|
|
1574
|
+
stored_message = await Message.create(**message.model_dump())
|
|
1575
|
+
self._stored_message_id = stored_message.id
|
|
1576
|
+
# Still send the event to update the client in real-time
|
|
1577
|
+
# Note: If this fails, we don't need DB cleanup since we didn't write to DB
|
|
1578
|
+
await self._send_message_event(stored_message, id_=id_)
|
|
1579
|
+
else:
|
|
1580
|
+
# Normal flow: store/update in database
|
|
1581
|
+
stored_message = await self._store_message(message)
|
|
1562
1582
|
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1583
|
+
self._stored_message_id = stored_message.id
|
|
1584
|
+
try:
|
|
1585
|
+
complete_message = ""
|
|
1586
|
+
if (
|
|
1587
|
+
self._should_stream_message(stored_message, message)
|
|
1588
|
+
and message is not None
|
|
1589
|
+
and isinstance(message.text, AsyncIterator | Iterator)
|
|
1590
|
+
):
|
|
1591
|
+
complete_message = await self._stream_message(message.text, stored_message)
|
|
1592
|
+
stored_message.text = complete_message
|
|
1593
|
+
stored_message = await self._update_stored_message(stored_message)
|
|
1594
|
+
else:
|
|
1595
|
+
# Only send message event for non-streaming messages
|
|
1596
|
+
await self._send_message_event(stored_message, id_=id_)
|
|
1597
|
+
except Exception:
|
|
1598
|
+
# remove the message from the database
|
|
1599
|
+
await delete_message(stored_message.id)
|
|
1600
|
+
raise
|
|
1581
1601
|
self.status = stored_message
|
|
1582
1602
|
return stored_message
|
|
1583
1603
|
|
|
@@ -12,7 +12,7 @@ from pydantic import BaseModel
|
|
|
12
12
|
|
|
13
13
|
from lfx.custom import validate
|
|
14
14
|
from lfx.custom.custom_component.base_component import BaseComponent
|
|
15
|
-
from lfx.helpers
|
|
15
|
+
from lfx.helpers import list_flows, load_flow, run_flow
|
|
16
16
|
from lfx.log.logger import logger
|
|
17
17
|
from lfx.schema.data import Data
|
|
18
18
|
from lfx.services.deps import get_storage_service, get_variable_service, session_scope
|
|
@@ -161,7 +161,7 @@ class ParameterHandler:
|
|
|
161
161
|
elif field.get("required"):
|
|
162
162
|
field_display_name = field.get("display_name")
|
|
163
163
|
logger.warning(
|
|
164
|
-
"File path not found for
|
|
164
|
+
"File path not found for %s in component %s. Setting to None.",
|
|
165
165
|
field_display_name,
|
|
166
166
|
self.vertex.display_name,
|
|
167
167
|
)
|
|
@@ -255,7 +255,7 @@ class ParameterHandler:
|
|
|
255
255
|
else:
|
|
256
256
|
params[field_name] = ast.literal_eval(val) if val else None
|
|
257
257
|
except Exception: # noqa: BLE001
|
|
258
|
-
logger.debug("Error evaluating code for
|
|
258
|
+
logger.debug("Error evaluating code for %s", field_name)
|
|
259
259
|
params[field_name] = val
|
|
260
260
|
return params
|
|
261
261
|
|
lfx/helpers/__init__.py
CHANGED
|
@@ -1 +1,129 @@
|
|
|
1
|
-
"""Helpers module for lfx package.
|
|
1
|
+
"""Helpers module for the lfx package.
|
|
2
|
+
|
|
3
|
+
This module automatically chooses between the full langflow implementation
|
|
4
|
+
(when available) and the lfx implementation (when standalone).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from lfx.utils.langflow_utils import has_langflow_memory
|
|
8
|
+
|
|
9
|
+
# Import the appropriate implementation
|
|
10
|
+
if has_langflow_memory():
|
|
11
|
+
try:
|
|
12
|
+
# Import full langflow implementation
|
|
13
|
+
# Base Model
|
|
14
|
+
from langflow.helpers.base_model import (
|
|
15
|
+
BaseModel,
|
|
16
|
+
SchemaField,
|
|
17
|
+
build_model_from_schema,
|
|
18
|
+
coalesce_bool,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Custom
|
|
22
|
+
from langflow.helpers.custom import (
|
|
23
|
+
format_type,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Data
|
|
27
|
+
from langflow.helpers.data import (
|
|
28
|
+
clean_string,
|
|
29
|
+
data_to_text,
|
|
30
|
+
data_to_text_list,
|
|
31
|
+
docs_to_data,
|
|
32
|
+
safe_convert,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Flow
|
|
36
|
+
from langflow.helpers.flow import (
|
|
37
|
+
build_schema_from_inputs,
|
|
38
|
+
get_arg_names,
|
|
39
|
+
get_flow_inputs,
|
|
40
|
+
list_flows,
|
|
41
|
+
load_flow,
|
|
42
|
+
run_flow,
|
|
43
|
+
)
|
|
44
|
+
except ImportError:
|
|
45
|
+
# Fallback to lfx implementation if langflow import fails
|
|
46
|
+
# Base Model
|
|
47
|
+
from lfx.helpers.base_model import (
|
|
48
|
+
BaseModel,
|
|
49
|
+
SchemaField,
|
|
50
|
+
build_model_from_schema,
|
|
51
|
+
coalesce_bool,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Custom
|
|
55
|
+
from lfx.helpers.custom import (
|
|
56
|
+
format_type,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Data
|
|
60
|
+
from lfx.helpers.data import (
|
|
61
|
+
clean_string,
|
|
62
|
+
data_to_text,
|
|
63
|
+
data_to_text_list,
|
|
64
|
+
docs_to_data,
|
|
65
|
+
safe_convert,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Flow
|
|
69
|
+
from lfx.helpers.flow import (
|
|
70
|
+
build_schema_from_inputs,
|
|
71
|
+
get_arg_names,
|
|
72
|
+
get_flow_inputs,
|
|
73
|
+
list_flows,
|
|
74
|
+
load_flow,
|
|
75
|
+
run_flow,
|
|
76
|
+
)
|
|
77
|
+
else:
|
|
78
|
+
# Use lfx implementation
|
|
79
|
+
# Base Model
|
|
80
|
+
from lfx.helpers.base_model import (
|
|
81
|
+
BaseModel,
|
|
82
|
+
SchemaField,
|
|
83
|
+
build_model_from_schema,
|
|
84
|
+
coalesce_bool,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Custom
|
|
88
|
+
from lfx.helpers.custom import (
|
|
89
|
+
format_type,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Data
|
|
93
|
+
from lfx.helpers.data import (
|
|
94
|
+
clean_string,
|
|
95
|
+
data_to_text,
|
|
96
|
+
data_to_text_list,
|
|
97
|
+
docs_to_data,
|
|
98
|
+
safe_convert,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Flow
|
|
102
|
+
from lfx.helpers.flow import (
|
|
103
|
+
build_schema_from_inputs,
|
|
104
|
+
get_arg_names,
|
|
105
|
+
get_flow_inputs,
|
|
106
|
+
list_flows,
|
|
107
|
+
load_flow,
|
|
108
|
+
run_flow,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Export the available functions
|
|
112
|
+
__all__ = [
|
|
113
|
+
"BaseModel",
|
|
114
|
+
"SchemaField",
|
|
115
|
+
"build_model_from_schema",
|
|
116
|
+
"build_schema_from_inputs",
|
|
117
|
+
"clean_string",
|
|
118
|
+
"coalesce_bool",
|
|
119
|
+
"data_to_text",
|
|
120
|
+
"data_to_text_list",
|
|
121
|
+
"docs_to_data",
|
|
122
|
+
"format_type",
|
|
123
|
+
"get_arg_names",
|
|
124
|
+
"get_flow_inputs",
|
|
125
|
+
"list_flows",
|
|
126
|
+
"load_flow",
|
|
127
|
+
"run_flow",
|
|
128
|
+
"safe_convert",
|
|
129
|
+
]
|
lfx/helpers/flow.py
CHANGED
lfx/inputs/input_mixin.py
CHANGED
|
@@ -12,6 +12,7 @@ from pydantic import (
|
|
|
12
12
|
|
|
13
13
|
from lfx.field_typing.range_spec import RangeSpec
|
|
14
14
|
from lfx.inputs.validators import CoalesceBool
|
|
15
|
+
from lfx.schema.cross_module import CrossModuleModel
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
class FieldTypes(str, Enum):
|
|
@@ -42,7 +43,7 @@ SerializableFieldTypes = Annotated[FieldTypes, PlainSerializer(lambda v: v.value
|
|
|
42
43
|
|
|
43
44
|
|
|
44
45
|
# Base mixin for common input field attributes and methods
|
|
45
|
-
class BaseInputMixin(
|
|
46
|
+
class BaseInputMixin(CrossModuleModel, validate_assignment=True): # type: ignore[call-arg]
|
|
46
47
|
model_config = ConfigDict(
|
|
47
48
|
arbitrary_types_allowed=True,
|
|
48
49
|
extra="forbid",
|
lfx/inputs/inputs.py
CHANGED
|
@@ -190,24 +190,15 @@ class MessageInput(StrInput, InputTraceMixin):
|
|
|
190
190
|
# If v is a instance of Message, then its fine
|
|
191
191
|
if isinstance(v, dict):
|
|
192
192
|
return Message(**v)
|
|
193
|
+
# Duck-typed Message check - works across module boundaries
|
|
193
194
|
if isinstance(v, Message):
|
|
195
|
+
# If it's from a different module (e.g., langflow.schema.Message),
|
|
196
|
+
# convert it to ensure we have the right type
|
|
197
|
+
if type(v).__module__ != Message.__module__:
|
|
198
|
+
return Message(**v.model_dump())
|
|
194
199
|
return v
|
|
195
|
-
# Check for Message-like objects by examining their fields
|
|
196
|
-
# This handles both langflow and lfx Message instances
|
|
197
|
-
if hasattr(v, "text") and hasattr(v, "model_dump") and callable(v.model_dump):
|
|
198
|
-
# Check if it has other Message-specific attributes
|
|
199
|
-
message_fields = {"text", "data", "sender", "session_id", "properties"}
|
|
200
|
-
obj_attrs = set(dir(v))
|
|
201
|
-
min_message_fields = 3
|
|
202
|
-
if len(message_fields.intersection(obj_attrs)) >= min_message_fields:
|
|
203
|
-
try:
|
|
204
|
-
return Message(**v.model_dump())
|
|
205
|
-
except (TypeError, ValueError):
|
|
206
|
-
# Fallback to text only if model_dump fails
|
|
207
|
-
return Message(text=v.text)
|
|
208
200
|
if isinstance(v, str | AsyncIterator | Iterator):
|
|
209
201
|
return Message(text=v)
|
|
210
|
-
# For simplified implementation, we'll skip MessageBase handling
|
|
211
202
|
msg = f"Invalid value type {type(v)}"
|
|
212
203
|
raise ValueError(msg)
|
|
213
204
|
|
lfx/interface/components.py
CHANGED
|
@@ -95,8 +95,10 @@ def _process_single_module(modname: str) -> tuple[str, dict] | None:
|
|
|
95
95
|
"""
|
|
96
96
|
try:
|
|
97
97
|
module = importlib.import_module(modname)
|
|
98
|
-
except
|
|
99
|
-
|
|
98
|
+
except Exception as e: # noqa: BLE001
|
|
99
|
+
# Catch all exceptions during import to prevent component failures from crashing startup
|
|
100
|
+
# TODO: Surface these errors to the UI in a friendly manner
|
|
101
|
+
logger.error(f"Failed to import module {modname}: {e}", exc_info=True)
|
|
100
102
|
return None
|
|
101
103
|
# Extract the top-level subpackage name after "langflow.components."
|
|
102
104
|
# e.g., "langflow.components.Notion.add_content_to_page" -> "Notion"
|
lfx/log/logger.py
CHANGED
|
@@ -209,6 +209,7 @@ def configure(
|
|
|
209
209
|
log_format: str | None = None,
|
|
210
210
|
log_rotation: str | None = None,
|
|
211
211
|
cache: bool | None = None,
|
|
212
|
+
output_file=None,
|
|
212
213
|
) -> None:
|
|
213
214
|
"""Configure the logger."""
|
|
214
215
|
# Early-exit only if structlog is configured AND current min level matches the requested one.
|
|
@@ -297,11 +298,14 @@ def configure(
|
|
|
297
298
|
wrapper_class.min_level = numeric_level
|
|
298
299
|
|
|
299
300
|
# Configure structlog
|
|
301
|
+
# Default to stdout for backward compatibility, unless output_file is specified
|
|
302
|
+
log_output_file = output_file if output_file is not None else sys.stdout
|
|
303
|
+
|
|
300
304
|
structlog.configure(
|
|
301
305
|
processors=processors,
|
|
302
306
|
wrapper_class=wrapper_class,
|
|
303
307
|
context_class=dict,
|
|
304
|
-
logger_factory=structlog.PrintLoggerFactory(file=
|
|
308
|
+
logger_factory=structlog.PrintLoggerFactory(file=log_output_file)
|
|
305
309
|
if not log_file
|
|
306
310
|
else structlog.stdlib.LoggerFactory(),
|
|
307
311
|
cache_logger_on_first_use=cache if cache is not None else True,
|
lfx/memory/__init__.py
CHANGED
|
@@ -1,35 +1,15 @@
|
|
|
1
1
|
"""Memory management for lfx with dynamic loading.
|
|
2
2
|
|
|
3
|
-
This module automatically chooses between full langflow
|
|
4
|
-
(when available) and lfx
|
|
3
|
+
This module automatically chooses between the full langflow implementation
|
|
4
|
+
(when available) and the lfx implementation (when standalone).
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
from lfx.utils.langflow_utils import has_langflow_memory
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def _has_langflow_memory():
|
|
13
|
-
"""Check if langflow.memory with database support is available."""
|
|
14
|
-
try:
|
|
15
|
-
# Check if langflow.memory and MessageTable are available
|
|
16
|
-
return importlib.util.find_spec("langflow") is not None
|
|
17
|
-
except (ImportError, ModuleNotFoundError):
|
|
18
|
-
pass
|
|
19
|
-
except Exception as e: # noqa: BLE001
|
|
20
|
-
logger.error(f"Error checking for langflow.memory: {e}")
|
|
21
|
-
return False
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
#### TODO: This _LANGFLOW_AVAILABLE implementation should be changed later ####
|
|
25
|
-
# Consider refactoring to lazy loading or a more robust service discovery mechanism
|
|
26
|
-
# that can handle runtime availability changes.
|
|
27
|
-
_LANGFLOW_AVAILABLE = _has_langflow_memory()
|
|
28
|
-
|
|
29
|
-
# Import the appropriate implementations
|
|
30
|
-
if _LANGFLOW_AVAILABLE:
|
|
9
|
+
# Import the appropriate implementation
|
|
10
|
+
if has_langflow_memory():
|
|
31
11
|
try:
|
|
32
|
-
# Import
|
|
12
|
+
# Import full langflow implementation
|
|
33
13
|
from langflow.memory import (
|
|
34
14
|
aadd_messages,
|
|
35
15
|
aadd_messagetables,
|
|
@@ -43,8 +23,8 @@ if _LANGFLOW_AVAILABLE:
|
|
|
43
23
|
get_messages,
|
|
44
24
|
store_message,
|
|
45
25
|
)
|
|
46
|
-
except
|
|
47
|
-
#
|
|
26
|
+
except ImportError:
|
|
27
|
+
# Fallback to lfx implementation if langflow import fails
|
|
48
28
|
from lfx.memory.stubs import (
|
|
49
29
|
aadd_messages,
|
|
50
30
|
aadd_messagetables,
|
|
@@ -59,7 +39,7 @@ if _LANGFLOW_AVAILABLE:
|
|
|
59
39
|
store_message,
|
|
60
40
|
)
|
|
61
41
|
else:
|
|
62
|
-
# Use lfx
|
|
42
|
+
# Use lfx implementation
|
|
63
43
|
from lfx.memory.stubs import (
|
|
64
44
|
aadd_messages,
|
|
65
45
|
aadd_messagetables,
|
|
@@ -74,7 +54,7 @@ else:
|
|
|
74
54
|
store_message,
|
|
75
55
|
)
|
|
76
56
|
|
|
77
|
-
# Export the available functions
|
|
57
|
+
# Export the available functions
|
|
78
58
|
__all__ = [
|
|
79
59
|
"aadd_messages",
|
|
80
60
|
"aadd_messagetables",
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Cross-module BaseModel for handling re-exported classes.
|
|
2
|
+
|
|
3
|
+
This module provides a metaclass and base model that enable isinstance checks
|
|
4
|
+
to work across module boundaries for Pydantic models. This is particularly useful
|
|
5
|
+
when the same class is re-exported from different modules (e.g., lfx.Message vs
|
|
6
|
+
langflow.schema.Message) but Python's isinstance() checks fail due to different
|
|
7
|
+
module paths.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CrossModuleMeta(type(BaseModel)): # type: ignore[misc]
|
|
18
|
+
"""Metaclass that enables cross-module isinstance checks for Pydantic models.
|
|
19
|
+
|
|
20
|
+
This metaclass overrides __instancecheck__ to perform structural type checking
|
|
21
|
+
based on the model's fields rather than strict class identity. This allows
|
|
22
|
+
instances of the same model from different module paths to be recognized as
|
|
23
|
+
compatible.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __instancecheck__(cls, instance: Any) -> bool:
|
|
27
|
+
"""Check if instance is compatible with this class across module boundaries.
|
|
28
|
+
|
|
29
|
+
First performs a standard isinstance check. If that fails, falls back to
|
|
30
|
+
checking if the instance has all required Pydantic model attributes and
|
|
31
|
+
a compatible set of model fields.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
instance: The object to check.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
bool: True if instance is compatible with this class.
|
|
38
|
+
"""
|
|
39
|
+
# First try standard isinstance check
|
|
40
|
+
if type.__instancecheck__(cls, instance):
|
|
41
|
+
return True
|
|
42
|
+
|
|
43
|
+
# If that fails, check for cross-module compatibility
|
|
44
|
+
# An object is cross-module compatible if it:
|
|
45
|
+
# 1. Has model_fields attribute (is a Pydantic model)
|
|
46
|
+
# 2. Has the same __class__.__name__
|
|
47
|
+
# 3. Has compatible model fields
|
|
48
|
+
if not hasattr(instance, "model_fields"):
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
# Check if class names match
|
|
52
|
+
if instance.__class__.__name__ != cls.__name__:
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
# Check if the instance has all required fields from cls
|
|
56
|
+
cls_fields = set(cls.model_fields.keys()) if hasattr(cls, "model_fields") else set()
|
|
57
|
+
instance_fields = set(instance.model_fields.keys())
|
|
58
|
+
|
|
59
|
+
# The instance must have at least the same fields as the class
|
|
60
|
+
# (it can have more, but not fewer required fields)
|
|
61
|
+
return cls_fields.issubset(instance_fields)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class CrossModuleModel(BaseModel, metaclass=CrossModuleMeta):
|
|
65
|
+
"""Base Pydantic model with cross-module isinstance support.
|
|
66
|
+
|
|
67
|
+
This class should be used as the base for models that may be re-exported
|
|
68
|
+
from different modules. It enables isinstance() checks to work across
|
|
69
|
+
module boundaries by using structural type checking.
|
|
70
|
+
|
|
71
|
+
Example:
|
|
72
|
+
>>> class Message(CrossModuleModel):
|
|
73
|
+
... text: str
|
|
74
|
+
...
|
|
75
|
+
>>> # Even if Message is imported from different paths:
|
|
76
|
+
>>> from lfx.schema.message import Message as LfxMessage
|
|
77
|
+
>>> from langflow.schema import Message as LangflowMessage
|
|
78
|
+
>>> msg = LfxMessage(text="hello")
|
|
79
|
+
>>> isinstance(msg, LangflowMessage) # True (with cross-module support)
|
|
80
|
+
"""
|
lfx/schema/data.py
CHANGED
|
@@ -14,6 +14,7 @@ from langchain_core.messages import AIMessage, BaseMessage, HumanMessage
|
|
|
14
14
|
from pydantic import BaseModel, ConfigDict, model_serializer, model_validator
|
|
15
15
|
|
|
16
16
|
from lfx.log.logger import logger
|
|
17
|
+
from lfx.schema.cross_module import CrossModuleModel
|
|
17
18
|
from lfx.utils.constants import MESSAGE_SENDER_AI, MESSAGE_SENDER_USER
|
|
18
19
|
from lfx.utils.image import create_image_content_dict
|
|
19
20
|
|
|
@@ -22,7 +23,7 @@ if TYPE_CHECKING:
|
|
|
22
23
|
from lfx.schema.message import Message
|
|
23
24
|
|
|
24
25
|
|
|
25
|
-
class Data(
|
|
26
|
+
class Data(CrossModuleModel):
|
|
26
27
|
"""Represents a record with text and optional data.
|
|
27
28
|
|
|
28
29
|
Attributes:
|
lfx/schema/log.py
CHANGED
lfx/schema/message.py
CHANGED
|
@@ -121,9 +121,13 @@ class Message(Data):
|
|
|
121
121
|
|
|
122
122
|
def to_lc_message(
|
|
123
123
|
self,
|
|
124
|
+
model_name: str | None = None,
|
|
124
125
|
) -> BaseMessage:
|
|
125
126
|
"""Converts the Data to a BaseMessage.
|
|
126
127
|
|
|
128
|
+
Args:
|
|
129
|
+
model_name: The model name to use for conversion. Optional.
|
|
130
|
+
|
|
127
131
|
Returns:
|
|
128
132
|
BaseMessage: The converted BaseMessage.
|
|
129
133
|
"""
|
|
@@ -139,7 +143,7 @@ class Message(Data):
|
|
|
139
143
|
if self.sender == MESSAGE_SENDER_USER or not self.sender:
|
|
140
144
|
if self.files:
|
|
141
145
|
contents = [{"type": "text", "text": text}]
|
|
142
|
-
file_contents = self.get_file_content_dicts()
|
|
146
|
+
file_contents = self.get_file_content_dicts(model_name)
|
|
143
147
|
contents.extend(file_contents)
|
|
144
148
|
human_message = HumanMessage(content=contents)
|
|
145
149
|
else:
|
|
@@ -197,7 +201,7 @@ class Message(Data):
|
|
|
197
201
|
return value
|
|
198
202
|
|
|
199
203
|
# Keep this async method for backwards compatibility
|
|
200
|
-
def get_file_content_dicts(self):
|
|
204
|
+
def get_file_content_dicts(self, model_name: str | None = None):
|
|
201
205
|
content_dicts = []
|
|
202
206
|
try:
|
|
203
207
|
files = get_file_paths(self.files)
|
|
@@ -209,7 +213,7 @@ class Message(Data):
|
|
|
209
213
|
if isinstance(file, Image):
|
|
210
214
|
content_dicts.append(file.to_content_dict())
|
|
211
215
|
else:
|
|
212
|
-
content_dicts.append(create_image_content_dict(file))
|
|
216
|
+
content_dicts.append(create_image_content_dict(file, None, model_name))
|
|
213
217
|
return content_dicts
|
|
214
218
|
|
|
215
219
|
def load_lc_prompt(self):
|
lfx/utils/image.py
CHANGED
|
@@ -56,12 +56,15 @@ def create_data_url(image_path: str | Path, mime_type: str | None = None) -> str
|
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
@lru_cache(maxsize=50)
|
|
59
|
-
def create_image_content_dict(
|
|
59
|
+
def create_image_content_dict(
|
|
60
|
+
image_path: str | Path, mime_type: str | None = None, model_name: str | None = None
|
|
61
|
+
) -> dict:
|
|
60
62
|
"""Create a content dictionary for multimodal inputs from an image file.
|
|
61
63
|
|
|
62
64
|
Args:
|
|
63
65
|
image_path: Path to the image file
|
|
64
66
|
mime_type: MIME type of the image. If None, will be auto-detected
|
|
67
|
+
model_name: Optional model parameter to determine content dict structure
|
|
65
68
|
|
|
66
69
|
Returns:
|
|
67
70
|
Content dictionary with type and image_url fields
|
|
@@ -70,4 +73,7 @@ def create_image_content_dict(image_path: str | Path, mime_type: str | None = No
|
|
|
70
73
|
FileNotFoundError: If the image file doesn't exist
|
|
71
74
|
"""
|
|
72
75
|
data_url = create_data_url(image_path, mime_type)
|
|
76
|
+
|
|
77
|
+
if model_name == "OllamaModel":
|
|
78
|
+
return {"type": "image_url", "source_type": "url", "image_url": data_url}
|
|
73
79
|
return {"type": "image", "source_type": "url", "url": data_url}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Langflow environment utility functions."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
|
|
5
|
+
from lfx.log.logger import logger
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class _LangflowModule:
|
|
9
|
+
# Static variable
|
|
10
|
+
# Tri-state:
|
|
11
|
+
# - None: Langflow check not performed yet
|
|
12
|
+
# - True: Langflow is available
|
|
13
|
+
# - False: Langflow is not available
|
|
14
|
+
_available = None
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def is_available(cls):
|
|
18
|
+
return cls._available
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def set_available(cls, value):
|
|
22
|
+
cls._available = value
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def has_langflow_memory():
|
|
26
|
+
"""Check if langflow.memory (with database support) and MessageTable are available."""
|
|
27
|
+
# TODO: REVISIT: Optimize this implementation later
|
|
28
|
+
# - Consider refactoring to use lazy loading or a more robust service discovery mechanism
|
|
29
|
+
# that can handle runtime availability changes.
|
|
30
|
+
|
|
31
|
+
# Use cached check from previous invocation (if applicable)
|
|
32
|
+
|
|
33
|
+
is_langflow_available = _LangflowModule.is_available()
|
|
34
|
+
|
|
35
|
+
if is_langflow_available is not None:
|
|
36
|
+
return is_langflow_available
|
|
37
|
+
|
|
38
|
+
# First check (lazy load and cache check)
|
|
39
|
+
|
|
40
|
+
module_spec = None
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
module_spec = importlib.util.find_spec("langflow")
|
|
44
|
+
except ImportError:
|
|
45
|
+
pass
|
|
46
|
+
except (TypeError, ValueError) as e:
|
|
47
|
+
logger.error(f"Error encountered checking for langflow.memory: {e}")
|
|
48
|
+
|
|
49
|
+
is_langflow_available = module_spec is not None
|
|
50
|
+
_LangflowModule.set_available(is_langflow_available)
|
|
51
|
+
|
|
52
|
+
return is_langflow_available
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lfx-nightly
|
|
3
|
-
Version: 0.1.12.
|
|
3
|
+
Version: 0.1.12.dev35
|
|
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
|