lfx-nightly 0.1.12.dev42__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.
- lfx/_assets/component_index.json +1 -1
- lfx/base/agents/agent.py +109 -29
- lfx/base/agents/events.py +102 -35
- lfx/base/agents/utils.py +15 -2
- lfx/base/composio/composio_base.py +24 -9
- lfx/base/datastax/__init__.py +5 -0
- lfx/{components/vectorstores/astradb.py → base/datastax/astradb_base.py} +84 -473
- lfx/base/io/chat.py +5 -4
- lfx/base/mcp/util.py +101 -15
- lfx/base/models/cometapi_constants.py +54 -0
- lfx/base/models/model_input_constants.py +74 -7
- lfx/base/models/ollama_constants.py +3 -0
- lfx/base/models/watsonx_constants.py +12 -0
- lfx/cli/commands.py +1 -1
- lfx/components/agents/__init__.py +3 -1
- lfx/components/agents/agent.py +47 -4
- lfx/components/agents/altk_agent.py +366 -0
- lfx/components/agents/cuga_agent.py +1 -1
- lfx/components/agents/mcp_component.py +32 -2
- lfx/components/amazon/amazon_bedrock_converse.py +1 -1
- lfx/components/apify/apify_actor.py +3 -3
- lfx/components/cometapi/__init__.py +32 -0
- lfx/components/cometapi/cometapi.py +166 -0
- lfx/components/datastax/__init__.py +12 -6
- lfx/components/datastax/{astra_assistant_manager.py → astradb_assistant_manager.py} +1 -0
- lfx/components/datastax/astradb_chatmemory.py +40 -0
- lfx/components/datastax/astradb_cql.py +5 -31
- lfx/components/datastax/astradb_graph.py +9 -123
- lfx/components/datastax/astradb_tool.py +12 -52
- lfx/components/datastax/astradb_vectorstore.py +133 -976
- lfx/components/datastax/create_assistant.py +1 -0
- lfx/components/datastax/create_thread.py +1 -0
- lfx/components/datastax/dotenv.py +1 -0
- lfx/components/datastax/get_assistant.py +1 -0
- lfx/components/datastax/getenvvar.py +1 -0
- lfx/components/datastax/graph_rag.py +1 -1
- lfx/components/datastax/list_assistants.py +1 -0
- lfx/components/datastax/run.py +1 -0
- lfx/components/docling/__init__.py +3 -0
- lfx/components/docling/docling_remote_vlm.py +284 -0
- lfx/components/helpers/memory.py +19 -4
- lfx/components/ibm/watsonx.py +25 -21
- lfx/components/input_output/chat.py +8 -0
- lfx/components/input_output/chat_output.py +8 -0
- lfx/components/knowledge_bases/ingestion.py +17 -9
- lfx/components/knowledge_bases/retrieval.py +16 -8
- lfx/components/logic/loop.py +4 -0
- lfx/components/mistral/mistral_embeddings.py +1 -1
- lfx/components/models/embedding_model.py +88 -7
- lfx/components/ollama/ollama.py +221 -14
- lfx/components/openrouter/openrouter.py +49 -147
- lfx/components/processing/parser.py +6 -1
- lfx/components/processing/structured_output.py +55 -17
- lfx/components/vectorstores/__init__.py +0 -6
- lfx/custom/custom_component/component.py +3 -2
- lfx/field_typing/constants.py +1 -0
- lfx/graph/edge/base.py +2 -2
- lfx/graph/graph/base.py +1 -1
- lfx/graph/graph/schema.py +3 -2
- lfx/graph/vertex/vertex_types.py +1 -1
- lfx/io/schema.py +6 -0
- lfx/memory/stubs.py +26 -7
- lfx/schema/message.py +6 -0
- lfx/schema/schema.py +5 -0
- lfx/services/settings/constants.py +1 -0
- {lfx_nightly-0.1.12.dev42.dist-info → lfx_nightly-0.2.0.dev0.dist-info}/METADATA +1 -1
- {lfx_nightly-0.1.12.dev42.dist-info → lfx_nightly-0.2.0.dev0.dist-info}/RECORD +70 -85
- lfx/components/datastax/astra_db.py +0 -77
- lfx/components/datastax/cassandra.py +0 -92
- lfx/components/vectorstores/astradb_graph.py +0 -326
- lfx/components/vectorstores/cassandra.py +0 -264
- lfx/components/vectorstores/cassandra_graph.py +0 -238
- lfx/components/vectorstores/chroma.py +0 -167
- lfx/components/vectorstores/clickhouse.py +0 -135
- lfx/components/vectorstores/couchbase.py +0 -102
- lfx/components/vectorstores/elasticsearch.py +0 -267
- lfx/components/vectorstores/faiss.py +0 -111
- lfx/components/vectorstores/graph_rag.py +0 -141
- lfx/components/vectorstores/hcd.py +0 -314
- lfx/components/vectorstores/milvus.py +0 -115
- lfx/components/vectorstores/mongodb_atlas.py +0 -213
- lfx/components/vectorstores/opensearch.py +0 -243
- lfx/components/vectorstores/pgvector.py +0 -72
- lfx/components/vectorstores/pinecone.py +0 -134
- lfx/components/vectorstores/qdrant.py +0 -109
- lfx/components/vectorstores/supabase.py +0 -76
- lfx/components/vectorstores/upstash.py +0 -124
- lfx/components/vectorstores/vectara.py +0 -97
- lfx/components/vectorstores/vectara_rag.py +0 -164
- lfx/components/vectorstores/weaviate.py +0 -89
- /lfx/components/datastax/{astra_vectorize.py → astradb_vectorize.py} +0 -0
- {lfx_nightly-0.1.12.dev42.dist-info → lfx_nightly-0.2.0.dev0.dist-info}/WHEEL +0 -0
- {lfx_nightly-0.1.12.dev42.dist-info → lfx_nightly-0.2.0.dev0.dist-info}/entry_points.txt +0 -0
lfx/base/agents/agent.py
CHANGED
|
@@ -5,12 +5,13 @@ from typing import TYPE_CHECKING, cast
|
|
|
5
5
|
|
|
6
6
|
from langchain.agents import AgentExecutor, BaseMultiActionAgent, BaseSingleActionAgent
|
|
7
7
|
from langchain.agents.agent import RunnableAgent
|
|
8
|
-
from
|
|
8
|
+
from langchain.callbacks.base import BaseCallbackHandler
|
|
9
|
+
from langchain_core.messages import BaseMessage, HumanMessage
|
|
9
10
|
from langchain_core.runnables import Runnable
|
|
10
11
|
|
|
11
12
|
from lfx.base.agents.callback import AgentAsyncHandler
|
|
12
13
|
from lfx.base.agents.events import ExceptionWithMessageError, process_agent_events
|
|
13
|
-
from lfx.base.agents.utils import
|
|
14
|
+
from lfx.base.agents.utils import get_chat_output_sender_name
|
|
14
15
|
from lfx.custom.custom_component.component import Component, _get_component_toolkit
|
|
15
16
|
from lfx.field_typing import Tool
|
|
16
17
|
from lfx.inputs.inputs import InputTypes, MultilineInput
|
|
@@ -19,14 +20,13 @@ from lfx.log.logger import logger
|
|
|
19
20
|
from lfx.memory import delete_message
|
|
20
21
|
from lfx.schema.content_block import ContentBlock
|
|
21
22
|
from lfx.schema.data import Data
|
|
23
|
+
from lfx.schema.log import OnTokenFunctionType
|
|
22
24
|
from lfx.schema.message import Message
|
|
23
25
|
from lfx.template.field.base import Output
|
|
24
26
|
from lfx.utils.constants import MESSAGE_SENDER_AI
|
|
25
27
|
|
|
26
28
|
if TYPE_CHECKING:
|
|
27
|
-
from
|
|
28
|
-
|
|
29
|
-
from lfx.schema.log import SendMessageFunctionType
|
|
29
|
+
from lfx.schema.log import OnTokenFunctionType, SendMessageFunctionType
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
DEFAULT_TOOLS_DESCRIPTION = "A helpful assistant with access to the following tools:"
|
|
@@ -75,6 +75,12 @@ class LCAgentComponent(Component):
|
|
|
75
75
|
Output(display_name="Response", name="response", method="message_response"),
|
|
76
76
|
]
|
|
77
77
|
|
|
78
|
+
# Get shared callbacks for tracing and save them to self.shared_callbacks
|
|
79
|
+
def _get_shared_callbacks(self) -> list[BaseCallbackHandler]:
|
|
80
|
+
if not hasattr(self, "shared_callbacks"):
|
|
81
|
+
self.shared_callbacks = self.get_langchain_callbacks()
|
|
82
|
+
return self.shared_callbacks
|
|
83
|
+
|
|
78
84
|
@abstractmethod
|
|
79
85
|
def build_agent(self) -> AgentExecutor:
|
|
80
86
|
"""Create the agent."""
|
|
@@ -119,6 +125,24 @@ class LCAgentComponent(Component):
|
|
|
119
125
|
# might be overridden in subclasses
|
|
120
126
|
return None
|
|
121
127
|
|
|
128
|
+
def _data_to_messages_skip_empty(self, data: list[Data]) -> list[BaseMessage]:
|
|
129
|
+
"""Convert data to messages, filtering only empty text while preserving non-text content.
|
|
130
|
+
|
|
131
|
+
Note: added to fix issue with certain providers failing when given empty text as input.
|
|
132
|
+
"""
|
|
133
|
+
messages = []
|
|
134
|
+
for value in data:
|
|
135
|
+
# Only skip if the message has a text attribute that is empty/whitespace
|
|
136
|
+
text = getattr(value, "text", None)
|
|
137
|
+
if isinstance(text, str) and not text.strip():
|
|
138
|
+
# Skip only messages with empty/whitespace-only text strings
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
lc_message = value.to_lc_message()
|
|
142
|
+
messages.append(lc_message)
|
|
143
|
+
|
|
144
|
+
return messages
|
|
145
|
+
|
|
122
146
|
async def run_agent(
|
|
123
147
|
self,
|
|
124
148
|
agent: Runnable | BaseSingleActionAgent | BaseMultiActionAgent | AgentExecutor,
|
|
@@ -138,41 +162,64 @@ class LCAgentComponent(Component):
|
|
|
138
162
|
max_iterations=max_iterations,
|
|
139
163
|
)
|
|
140
164
|
# Convert input_value to proper format for agent
|
|
141
|
-
|
|
165
|
+
lc_message = None
|
|
166
|
+
if isinstance(self.input_value, Message):
|
|
142
167
|
lc_message = self.input_value.to_lc_message()
|
|
143
|
-
|
|
168
|
+
input_dict: dict[str, str | list[BaseMessage] | BaseMessage] = {"input": lc_message}
|
|
144
169
|
else:
|
|
145
|
-
|
|
146
|
-
input_text = self.input_value
|
|
170
|
+
input_dict = {"input": self.input_value}
|
|
147
171
|
|
|
148
|
-
input_dict: dict[str, str | list[BaseMessage]] = {}
|
|
149
172
|
if hasattr(self, "system_prompt"):
|
|
150
173
|
input_dict["system_prompt"] = self.system_prompt
|
|
151
|
-
if hasattr(self, "chat_history") and self.chat_history:
|
|
152
|
-
if (
|
|
153
|
-
hasattr(self.chat_history, "to_data")
|
|
154
|
-
and callable(self.chat_history.to_data)
|
|
155
|
-
and self.chat_history.__class__.__name__ == "Data"
|
|
156
|
-
):
|
|
157
|
-
input_dict["chat_history"] = data_to_messages(self.chat_history)
|
|
158
|
-
# Handle both lfx.schema.message.Message and langflow.schema.message.Message types
|
|
159
|
-
if all(hasattr(m, "to_data") and callable(m.to_data) and "text" in m.data for m in self.chat_history):
|
|
160
|
-
input_dict["chat_history"] = data_to_messages(self.chat_history)
|
|
161
|
-
if all(isinstance(m, Message) for m in self.chat_history):
|
|
162
|
-
input_dict["chat_history"] = data_to_messages([m.to_data() for m in self.chat_history])
|
|
163
|
-
if hasattr(lc_message, "content") and isinstance(lc_message.content, list):
|
|
164
|
-
# ! Because the input has to be a string, we must pass the images in the chat_history
|
|
165
174
|
|
|
175
|
+
if hasattr(self, "chat_history") and self.chat_history:
|
|
176
|
+
if isinstance(self.chat_history, Data):
|
|
177
|
+
input_dict["chat_history"] = self._data_to_messages_skip_empty([self.chat_history])
|
|
178
|
+
elif all(hasattr(m, "to_data") and callable(m.to_data) and "text" in m.data for m in self.chat_history):
|
|
179
|
+
input_dict["chat_history"] = self._data_to_messages_skip_empty(self.chat_history)
|
|
180
|
+
elif all(isinstance(m, Message) for m in self.chat_history):
|
|
181
|
+
input_dict["chat_history"] = self._data_to_messages_skip_empty([m.to_data() for m in self.chat_history])
|
|
182
|
+
|
|
183
|
+
# Handle multimodal input (images + text)
|
|
184
|
+
# Note: Agent input must be a string, so we extract text and move images to chat_history
|
|
185
|
+
if lc_message is not None and hasattr(lc_message, "content") and isinstance(lc_message.content, list):
|
|
186
|
+
# Extract images and text from the text content items
|
|
166
187
|
image_dicts = [item for item in lc_message.content if item.get("type") == "image"]
|
|
167
|
-
|
|
188
|
+
text_content = [item for item in lc_message.content if item.get("type") != "image"]
|
|
189
|
+
|
|
190
|
+
text_strings = [
|
|
191
|
+
item.get("text", "")
|
|
192
|
+
for item in text_content
|
|
193
|
+
if item.get("type") == "text" and item.get("text", "").strip()
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
# Set input to concatenated text or empty string
|
|
197
|
+
input_dict["input"] = " ".join(text_strings) if text_strings else ""
|
|
198
|
+
|
|
199
|
+
# If input is still a list or empty, provide a default
|
|
200
|
+
if isinstance(input_dict["input"], list) or not input_dict["input"]:
|
|
201
|
+
input_dict["input"] = "Process the provided images."
|
|
168
202
|
|
|
169
203
|
if "chat_history" not in input_dict:
|
|
170
204
|
input_dict["chat_history"] = []
|
|
205
|
+
|
|
171
206
|
if isinstance(input_dict["chat_history"], list):
|
|
172
207
|
input_dict["chat_history"].extend(HumanMessage(content=[image_dict]) for image_dict in image_dicts)
|
|
173
208
|
else:
|
|
174
209
|
input_dict["chat_history"] = [HumanMessage(content=[image_dict]) for image_dict in image_dicts]
|
|
175
|
-
|
|
210
|
+
|
|
211
|
+
# Final safety check: ensure input is never empty (prevents Anthropic API errors)
|
|
212
|
+
current_input = input_dict.get("input", "")
|
|
213
|
+
if isinstance(current_input, list):
|
|
214
|
+
current_input = " ".join(map(str, current_input))
|
|
215
|
+
elif not isinstance(current_input, str):
|
|
216
|
+
current_input = str(current_input)
|
|
217
|
+
|
|
218
|
+
if not current_input.strip():
|
|
219
|
+
input_dict["input"] = "Continue the conversation."
|
|
220
|
+
else:
|
|
221
|
+
input_dict["input"] = current_input
|
|
222
|
+
|
|
176
223
|
if hasattr(self, "graph"):
|
|
177
224
|
session_id = self.graph.session_id
|
|
178
225
|
elif hasattr(self, "_session_id"):
|
|
@@ -181,7 +228,6 @@ class LCAgentComponent(Component):
|
|
|
181
228
|
session_id = None
|
|
182
229
|
|
|
183
230
|
sender_name = get_chat_output_sender_name(self) or self.display_name or "AI"
|
|
184
|
-
|
|
185
231
|
agent_message = Message(
|
|
186
232
|
sender=MESSAGE_SENDER_AI,
|
|
187
233
|
sender_name=sender_name,
|
|
@@ -189,15 +235,24 @@ class LCAgentComponent(Component):
|
|
|
189
235
|
content_blocks=[ContentBlock(title="Agent Steps", contents=[])],
|
|
190
236
|
session_id=session_id or uuid.uuid4(),
|
|
191
237
|
)
|
|
238
|
+
|
|
239
|
+
# Create token callback if event_manager is available
|
|
240
|
+
# This wraps the event_manager's on_token method to match OnTokenFunctionType Protocol
|
|
241
|
+
on_token_callback: OnTokenFunctionType | None = None
|
|
242
|
+
if self._event_manager:
|
|
243
|
+
on_token_callback = cast("OnTokenFunctionType", self._event_manager.on_token)
|
|
244
|
+
|
|
192
245
|
try:
|
|
193
246
|
result = await process_agent_events(
|
|
194
247
|
runnable.astream_events(
|
|
195
248
|
input_dict,
|
|
196
|
-
|
|
249
|
+
# here we use the shared callbacks because the AgentExecutor uses the tools
|
|
250
|
+
config={"callbacks": [AgentAsyncHandler(self.log), *self._get_shared_callbacks()]},
|
|
197
251
|
version="v2",
|
|
198
252
|
),
|
|
199
253
|
agent_message,
|
|
200
254
|
cast("SendMessageFunctionType", self.send_message),
|
|
255
|
+
on_token_callback,
|
|
201
256
|
)
|
|
202
257
|
except ExceptionWithMessageError as e:
|
|
203
258
|
if hasattr(e, "agent_message") and hasattr(e.agent_message, "id"):
|
|
@@ -269,15 +324,40 @@ class LCToolsAgentComponent(LCAgentComponent):
|
|
|
269
324
|
tools_names = ", ".join([tool.name for tool in self.tools])
|
|
270
325
|
return tools_names
|
|
271
326
|
|
|
327
|
+
# Set shared callbacks for tracing
|
|
328
|
+
def set_tools_callbacks(self, tools_list: list[Tool], callbacks_list: list[BaseCallbackHandler]):
|
|
329
|
+
"""Set shared callbacks for tracing to the tools.
|
|
330
|
+
|
|
331
|
+
If we do not pass down the same callbacks to each tool
|
|
332
|
+
used by the agent, then each tool will instantiate a new callback.
|
|
333
|
+
For some tracing services, this will cause
|
|
334
|
+
the callback handler to lose the id of its parent run (Agent)
|
|
335
|
+
and thus throw an error in the tracing service client.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
tools_list: list of tools to set the callbacks for
|
|
339
|
+
callbacks_list: list of callbacks to set for the tools
|
|
340
|
+
Returns:
|
|
341
|
+
None
|
|
342
|
+
"""
|
|
343
|
+
for tool in tools_list or []:
|
|
344
|
+
if hasattr(tool, "callbacks"):
|
|
345
|
+
tool.callbacks = callbacks_list
|
|
346
|
+
|
|
272
347
|
async def _get_tools(self) -> list[Tool]:
|
|
273
348
|
component_toolkit = _get_component_toolkit()
|
|
274
349
|
tools_names = self._build_tools_names()
|
|
275
350
|
agent_description = self.get_tool_description()
|
|
276
351
|
# TODO: Agent Description Depreciated Feature to be removed
|
|
277
352
|
description = f"{agent_description}{tools_names}"
|
|
353
|
+
|
|
278
354
|
tools = component_toolkit(component=self).get_tools(
|
|
279
|
-
tool_name=self.get_tool_name(),
|
|
355
|
+
tool_name=self.get_tool_name(),
|
|
356
|
+
tool_description=description,
|
|
357
|
+
# here we do not use the shared callbacks as we are exposing the agent as a tool
|
|
358
|
+
callbacks=self.get_langchain_callbacks(),
|
|
280
359
|
)
|
|
281
360
|
if hasattr(self, "tools_metadata"):
|
|
282
361
|
tools = component_toolkit(component=self, metadata=self.tools_metadata).update_tools_metadata(tools=tools)
|
|
362
|
+
|
|
283
363
|
return tools
|
lfx/base/agents/events.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# Add helper functions for each event type
|
|
2
|
+
import asyncio
|
|
2
3
|
from collections.abc import AsyncIterator
|
|
3
4
|
from time import perf_counter
|
|
4
5
|
from typing import Any, Protocol
|
|
@@ -9,7 +10,7 @@ from typing_extensions import TypedDict
|
|
|
9
10
|
|
|
10
11
|
from lfx.schema.content_block import ContentBlock
|
|
11
12
|
from lfx.schema.content_types import TextContent, ToolContent
|
|
12
|
-
from lfx.schema.log import SendMessageFunctionType
|
|
13
|
+
from lfx.schema.log import OnTokenFunctionType, SendMessageFunctionType
|
|
13
14
|
from lfx.schema.message import Message
|
|
14
15
|
|
|
15
16
|
|
|
@@ -53,7 +54,14 @@ def _calculate_duration(start_time: float) -> int:
|
|
|
53
54
|
|
|
54
55
|
|
|
55
56
|
async def handle_on_chain_start(
|
|
56
|
-
event: dict[str, Any],
|
|
57
|
+
event: dict[str, Any],
|
|
58
|
+
agent_message: Message,
|
|
59
|
+
send_message_callback: SendMessageFunctionType,
|
|
60
|
+
send_token_callback: OnTokenFunctionType | None, # noqa: ARG001
|
|
61
|
+
start_time: float,
|
|
62
|
+
*,
|
|
63
|
+
had_streaming: bool = False, # noqa: ARG001
|
|
64
|
+
message_id: str | None = None, # noqa: ARG001
|
|
57
65
|
) -> tuple[Message, float]:
|
|
58
66
|
# Create content blocks if they don't exist
|
|
59
67
|
if not agent_message.content_blocks:
|
|
@@ -80,7 +88,7 @@ async def handle_on_chain_start(
|
|
|
80
88
|
header={"title": "Input", "icon": "MessageSquare"},
|
|
81
89
|
)
|
|
82
90
|
agent_message.content_blocks[0].contents.append(text_content)
|
|
83
|
-
agent_message = await
|
|
91
|
+
agent_message = await send_message_callback(message=agent_message, skip_db_update=True)
|
|
84
92
|
start_time = perf_counter()
|
|
85
93
|
return agent_message, start_time
|
|
86
94
|
|
|
@@ -101,15 +109,23 @@ def _extract_output_text(output: str | list) -> str:
|
|
|
101
109
|
if isinstance(item, dict):
|
|
102
110
|
if "text" in item:
|
|
103
111
|
return item["text"] or ""
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
+
if "content" in item:
|
|
113
|
+
return str(item["content"])
|
|
114
|
+
if "message" in item:
|
|
115
|
+
return str(item["message"])
|
|
116
|
+
|
|
117
|
+
# Special case handling for non-text-like dicts
|
|
118
|
+
if (
|
|
119
|
+
item.get("type") == "tool_use" # Handle tool use items
|
|
120
|
+
or ("index" in item and len(item) == 1) # Handle index-only items
|
|
121
|
+
or "partial_json" in item # Handle partial json items
|
|
122
|
+
# Handle index-only items
|
|
123
|
+
or ("index" in item and not any(k in item for k in ("text", "content", "message")))
|
|
124
|
+
# Handle other metadata-only chunks that don't contain meaningful text
|
|
125
|
+
or not any(key in item for key in ["text", "content", "message"])
|
|
126
|
+
):
|
|
112
127
|
return ""
|
|
128
|
+
|
|
113
129
|
# For any other dict format, return empty string
|
|
114
130
|
return ""
|
|
115
131
|
# For any other single item type (not str or dict), return empty string
|
|
@@ -133,7 +149,14 @@ def _extract_output_text(output: str | list) -> str:
|
|
|
133
149
|
|
|
134
150
|
|
|
135
151
|
async def handle_on_chain_end(
|
|
136
|
-
event: dict[str, Any],
|
|
152
|
+
event: dict[str, Any],
|
|
153
|
+
agent_message: Message,
|
|
154
|
+
send_message_callback: SendMessageFunctionType,
|
|
155
|
+
send_token_callback: OnTokenFunctionType | None, # noqa: ARG001
|
|
156
|
+
start_time: float,
|
|
157
|
+
*,
|
|
158
|
+
had_streaming: bool = False,
|
|
159
|
+
message_id: str | None = None, # noqa: ARG001
|
|
137
160
|
) -> tuple[Message, float]:
|
|
138
161
|
data_output = event["data"].get("output")
|
|
139
162
|
if data_output and isinstance(data_output, AgentFinish) and data_output.return_values.get("output"):
|
|
@@ -151,7 +174,11 @@ async def handle_on_chain_end(
|
|
|
151
174
|
header={"title": "Output", "icon": "MessageSquare"},
|
|
152
175
|
)
|
|
153
176
|
agent_message.content_blocks[0].contents.append(text_content)
|
|
154
|
-
|
|
177
|
+
|
|
178
|
+
# Only send final message if we didn't have streaming chunks
|
|
179
|
+
# If we had streaming, frontend already accumulated the chunks
|
|
180
|
+
if not had_streaming:
|
|
181
|
+
agent_message = await send_message_callback(message=agent_message)
|
|
155
182
|
start_time = perf_counter()
|
|
156
183
|
return agent_message, start_time
|
|
157
184
|
|
|
@@ -160,7 +187,7 @@ async def handle_on_tool_start(
|
|
|
160
187
|
event: dict[str, Any],
|
|
161
188
|
agent_message: Message,
|
|
162
189
|
tool_blocks_map: dict[str, ToolContent],
|
|
163
|
-
|
|
190
|
+
send_message_callback: SendMessageFunctionType,
|
|
164
191
|
start_time: float,
|
|
165
192
|
) -> tuple[Message, float]:
|
|
166
193
|
tool_name = event["name"]
|
|
@@ -190,7 +217,7 @@ async def handle_on_tool_start(
|
|
|
190
217
|
tool_blocks_map[tool_key] = tool_content
|
|
191
218
|
agent_message.content_blocks[0].contents.append(tool_content)
|
|
192
219
|
|
|
193
|
-
agent_message = await
|
|
220
|
+
agent_message = await send_message_callback(message=agent_message, skip_db_update=True)
|
|
194
221
|
if agent_message.content_blocks and agent_message.content_blocks[0].contents:
|
|
195
222
|
tool_blocks_map[tool_key] = agent_message.content_blocks[0].contents[-1]
|
|
196
223
|
return agent_message, new_start_time
|
|
@@ -200,7 +227,7 @@ async def handle_on_tool_end(
|
|
|
200
227
|
event: dict[str, Any],
|
|
201
228
|
agent_message: Message,
|
|
202
229
|
tool_blocks_map: dict[str, ToolContent],
|
|
203
|
-
|
|
230
|
+
send_message_callback: SendMessageFunctionType,
|
|
204
231
|
start_time: float,
|
|
205
232
|
) -> tuple[Message, float]:
|
|
206
233
|
run_id = event.get("run_id", "")
|
|
@@ -209,8 +236,8 @@ async def handle_on_tool_end(
|
|
|
209
236
|
tool_content = tool_blocks_map.get(tool_key)
|
|
210
237
|
|
|
211
238
|
if tool_content and isinstance(tool_content, ToolContent):
|
|
212
|
-
# Call
|
|
213
|
-
agent_message = await
|
|
239
|
+
# Call send_message_callback first to get the updated message structure
|
|
240
|
+
agent_message = await send_message_callback(message=agent_message, skip_db_update=True)
|
|
214
241
|
new_start_time = perf_counter()
|
|
215
242
|
|
|
216
243
|
# Now find and update the tool content in the current message
|
|
@@ -246,7 +273,7 @@ async def handle_on_tool_error(
|
|
|
246
273
|
event: dict[str, Any],
|
|
247
274
|
agent_message: Message,
|
|
248
275
|
tool_blocks_map: dict[str, ToolContent],
|
|
249
|
-
|
|
276
|
+
send_message_callback: SendMessageFunctionType,
|
|
250
277
|
start_time: float,
|
|
251
278
|
) -> tuple[Message, float]:
|
|
252
279
|
run_id = event.get("run_id", "")
|
|
@@ -258,7 +285,7 @@ async def handle_on_tool_error(
|
|
|
258
285
|
tool_content.error = event["data"].get("error", "Unknown error")
|
|
259
286
|
tool_content.duration = _calculate_duration(start_time)
|
|
260
287
|
tool_content.header = {"title": f"Error using **{tool_content.name}**", "icon": "Hammer"}
|
|
261
|
-
agent_message = await
|
|
288
|
+
agent_message = await send_message_callback(message=agent_message, skip_db_update=True)
|
|
262
289
|
start_time = perf_counter()
|
|
263
290
|
return agent_message, start_time
|
|
264
291
|
|
|
@@ -266,8 +293,12 @@ async def handle_on_tool_error(
|
|
|
266
293
|
async def handle_on_chain_stream(
|
|
267
294
|
event: dict[str, Any],
|
|
268
295
|
agent_message: Message,
|
|
269
|
-
|
|
296
|
+
send_message_callback: SendMessageFunctionType, # noqa: ARG001
|
|
297
|
+
send_token_callback: OnTokenFunctionType | None,
|
|
270
298
|
start_time: float,
|
|
299
|
+
*,
|
|
300
|
+
had_streaming: bool = False, # noqa: ARG001
|
|
301
|
+
message_id: str | None = None,
|
|
271
302
|
) -> tuple[Message, float]:
|
|
272
303
|
data_chunk = event["data"].get("chunk", {})
|
|
273
304
|
if isinstance(data_chunk, dict) and data_chunk.get("output"):
|
|
@@ -275,15 +306,26 @@ async def handle_on_chain_stream(
|
|
|
275
306
|
if output and isinstance(output, str | list):
|
|
276
307
|
agent_message.text = _extract_output_text(output)
|
|
277
308
|
agent_message.properties.state = "complete"
|
|
278
|
-
|
|
309
|
+
# Don't call send_message_callback here - we must update in place
|
|
310
|
+
# in order to keep the message id consistent throughout the stream.
|
|
311
|
+
# The final message will be sent after the loop completes
|
|
279
312
|
start_time = perf_counter()
|
|
280
313
|
elif isinstance(data_chunk, AIMessageChunk):
|
|
281
314
|
output_text = _extract_output_text(data_chunk.content)
|
|
282
|
-
if
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
315
|
+
# For streaming, send token event if callback is available
|
|
316
|
+
# Note: we should expect the callback, but we keep it optional for backwards compatibility
|
|
317
|
+
# as of v1.6.5
|
|
318
|
+
if output_text and output_text.strip() and send_token_callback and message_id:
|
|
319
|
+
await asyncio.to_thread(
|
|
320
|
+
send_token_callback,
|
|
321
|
+
data={
|
|
322
|
+
"chunk": output_text,
|
|
323
|
+
"id": str(message_id),
|
|
324
|
+
},
|
|
325
|
+
)
|
|
326
|
+
|
|
286
327
|
if not agent_message.text:
|
|
328
|
+
# Starts the timer when the first message is starting to be generated
|
|
287
329
|
start_time = perf_counter()
|
|
288
330
|
return agent_message, start_time
|
|
289
331
|
|
|
@@ -294,7 +336,7 @@ class ToolEventHandler(Protocol):
|
|
|
294
336
|
event: dict[str, Any],
|
|
295
337
|
agent_message: Message,
|
|
296
338
|
tool_blocks_map: dict[str, ContentBlock],
|
|
297
|
-
|
|
339
|
+
send_message_callback: SendMessageFunctionType,
|
|
298
340
|
start_time: float,
|
|
299
341
|
) -> tuple[Message, float]: ...
|
|
300
342
|
|
|
@@ -304,8 +346,12 @@ class ChainEventHandler(Protocol):
|
|
|
304
346
|
self,
|
|
305
347
|
event: dict[str, Any],
|
|
306
348
|
agent_message: Message,
|
|
307
|
-
|
|
349
|
+
send_message_callback: SendMessageFunctionType,
|
|
350
|
+
send_token_callback: OnTokenFunctionType | None,
|
|
308
351
|
start_time: float,
|
|
352
|
+
*,
|
|
353
|
+
had_streaming: bool = False,
|
|
354
|
+
message_id: str | None = None,
|
|
309
355
|
) -> tuple[Message, float]: ...
|
|
310
356
|
|
|
311
357
|
|
|
@@ -329,7 +375,8 @@ TOOL_EVENT_HANDLERS: dict[str, ToolEventHandler] = {
|
|
|
329
375
|
async def process_agent_events(
|
|
330
376
|
agent_executor: AsyncIterator[dict[str, Any]],
|
|
331
377
|
agent_message: Message,
|
|
332
|
-
|
|
378
|
+
send_message_callback: SendMessageFunctionType,
|
|
379
|
+
send_token_callback: OnTokenFunctionType | None = None,
|
|
333
380
|
) -> Message:
|
|
334
381
|
"""Process agent events and return the final output."""
|
|
335
382
|
if isinstance(agent_message.properties, dict):
|
|
@@ -337,26 +384,46 @@ async def process_agent_events(
|
|
|
337
384
|
else:
|
|
338
385
|
agent_message.properties.icon = "Bot"
|
|
339
386
|
agent_message.properties.state = "partial"
|
|
340
|
-
# Store the initial message
|
|
341
|
-
agent_message = await
|
|
387
|
+
# Store the initial message and capture the message id
|
|
388
|
+
agent_message = await send_message_callback(message=agent_message)
|
|
389
|
+
# Capture the original message id - this must stay consistent throughout if streaming
|
|
390
|
+
initial_message_id = agent_message.id
|
|
342
391
|
try:
|
|
343
392
|
# Create a mapping of run_ids to tool contents
|
|
344
393
|
tool_blocks_map: dict[str, ToolContent] = {}
|
|
394
|
+
had_streaming = False
|
|
345
395
|
start_time = perf_counter()
|
|
396
|
+
|
|
346
397
|
async for event in agent_executor:
|
|
347
398
|
if event["event"] in TOOL_EVENT_HANDLERS:
|
|
348
399
|
tool_handler = TOOL_EVENT_HANDLERS[event["event"]]
|
|
349
400
|
# Use skip_db_update=True during streaming to avoid DB round-trips
|
|
350
401
|
agent_message, start_time = await tool_handler(
|
|
351
|
-
event, agent_message, tool_blocks_map,
|
|
402
|
+
event, agent_message, tool_blocks_map, send_message_callback, start_time
|
|
352
403
|
)
|
|
353
404
|
elif event["event"] in CHAIN_EVENT_HANDLERS:
|
|
354
405
|
chain_handler = CHAIN_EVENT_HANDLERS[event["event"]]
|
|
355
|
-
|
|
356
|
-
|
|
406
|
+
|
|
407
|
+
# Check if this is a streaming event
|
|
408
|
+
if event["event"] in ("on_chain_stream", "on_chat_model_stream"):
|
|
409
|
+
had_streaming = True
|
|
410
|
+
agent_message, start_time = await chain_handler(
|
|
411
|
+
event,
|
|
412
|
+
agent_message,
|
|
413
|
+
send_message_callback,
|
|
414
|
+
send_token_callback,
|
|
415
|
+
start_time,
|
|
416
|
+
had_streaming=had_streaming,
|
|
417
|
+
message_id=initial_message_id,
|
|
418
|
+
)
|
|
419
|
+
else:
|
|
420
|
+
agent_message, start_time = await chain_handler(
|
|
421
|
+
event, agent_message, send_message_callback, None, start_time, had_streaming=had_streaming
|
|
422
|
+
)
|
|
423
|
+
|
|
357
424
|
agent_message.properties.state = "complete"
|
|
358
425
|
# Final DB update with the complete message (skip_db_update=False by default)
|
|
359
|
-
agent_message = await
|
|
426
|
+
agent_message = await send_message_callback(message=agent_message)
|
|
360
427
|
except Exception as e:
|
|
361
428
|
raise ExceptionWithMessageError(agent_message, str(e)) from e
|
|
362
429
|
return await Message.create(**agent_message.model_dump())
|
lfx/base/agents/utils.py
CHANGED
|
@@ -47,9 +47,22 @@ def data_to_messages(data: list[Data | Message]) -> list[BaseMessage]:
|
|
|
47
47
|
data (List[Data | Message]): The data to convert.
|
|
48
48
|
|
|
49
49
|
Returns:
|
|
50
|
-
List[BaseMessage]: The data as messages.
|
|
50
|
+
List[BaseMessage]: The data as messages, filtering out any with empty content.
|
|
51
51
|
"""
|
|
52
|
-
|
|
52
|
+
messages = []
|
|
53
|
+
for value in data:
|
|
54
|
+
try:
|
|
55
|
+
lc_message = value.to_lc_message()
|
|
56
|
+
# Only add messages with non-empty content (prevents Anthropic API errors)
|
|
57
|
+
content = lc_message.content
|
|
58
|
+
if content and ((isinstance(content, str) and content.strip()) or (isinstance(content, list) and content)):
|
|
59
|
+
messages.append(lc_message)
|
|
60
|
+
else:
|
|
61
|
+
logger.warning("Skipping message with empty content in chat history")
|
|
62
|
+
except (ValueError, AttributeError) as e:
|
|
63
|
+
logger.warning(f"Failed to convert message to BaseMessage: {e}")
|
|
64
|
+
continue
|
|
65
|
+
return messages
|
|
53
66
|
|
|
54
67
|
|
|
55
68
|
def validate_and_create_xml_agent(
|
|
@@ -284,6 +284,21 @@ class ComposioBaseComponent(Component):
|
|
|
284
284
|
# Track all auth field names discovered across all toolkits
|
|
285
285
|
_all_auth_field_names: set[str] = set()
|
|
286
286
|
|
|
287
|
+
@classmethod
|
|
288
|
+
def get_actions_cache(cls) -> dict[str, dict[str, Any]]:
|
|
289
|
+
"""Get the class-level actions cache."""
|
|
290
|
+
return cls._actions_cache
|
|
291
|
+
|
|
292
|
+
@classmethod
|
|
293
|
+
def get_action_schema_cache(cls) -> dict[str, dict[str, Any]]:
|
|
294
|
+
"""Get the class-level action schema cache."""
|
|
295
|
+
return cls._action_schema_cache
|
|
296
|
+
|
|
297
|
+
@classmethod
|
|
298
|
+
def get_all_auth_field_names(cls) -> set[str]:
|
|
299
|
+
"""Get all auth field names discovered across toolkits."""
|
|
300
|
+
return cls._all_auth_field_names
|
|
301
|
+
|
|
287
302
|
outputs = [
|
|
288
303
|
Output(name="dataFrame", display_name="DataFrame", method="as_dataframe"),
|
|
289
304
|
]
|
|
@@ -403,11 +418,11 @@ class ComposioBaseComponent(Component):
|
|
|
403
418
|
|
|
404
419
|
# Try to load from the class-level cache
|
|
405
420
|
toolkit_slug = self.app_name.lower()
|
|
406
|
-
if toolkit_slug in self.__class__.
|
|
421
|
+
if toolkit_slug in self.__class__.get_actions_cache():
|
|
407
422
|
# Deep-copy so that any mutation on this instance does not affect the
|
|
408
423
|
# cached master copy.
|
|
409
|
-
self._actions_data = copy.deepcopy(self.__class__.
|
|
410
|
-
self._action_schemas = copy.deepcopy(self.__class__.
|
|
424
|
+
self._actions_data = copy.deepcopy(self.__class__.get_actions_cache()[toolkit_slug])
|
|
425
|
+
self._action_schemas = copy.deepcopy(self.__class__.get_action_schema_cache().get(toolkit_slug, {}))
|
|
411
426
|
logger.debug(f"Loaded actions for {toolkit_slug} from in-process cache")
|
|
412
427
|
return
|
|
413
428
|
|
|
@@ -630,8 +645,8 @@ class ComposioBaseComponent(Component):
|
|
|
630
645
|
|
|
631
646
|
# Cache actions for this toolkit so subsequent component instances
|
|
632
647
|
# can reuse them without hitting the Composio API again.
|
|
633
|
-
self.__class__.
|
|
634
|
-
self.__class__.
|
|
648
|
+
self.__class__.get_actions_cache()[toolkit_slug] = copy.deepcopy(self._actions_data)
|
|
649
|
+
self.__class__.get_action_schema_cache()[toolkit_slug] = copy.deepcopy(self._action_schemas)
|
|
635
650
|
|
|
636
651
|
except ValueError as e:
|
|
637
652
|
logger.debug(f"Could not populate Composio actions for {self.app_name}: {e}")
|
|
@@ -1313,7 +1328,7 @@ class ComposioBaseComponent(Component):
|
|
|
1313
1328
|
|
|
1314
1329
|
self._auth_dynamic_fields.add(name)
|
|
1315
1330
|
# Also add to class-level cache for better tracking
|
|
1316
|
-
self.__class__.
|
|
1331
|
+
self.__class__.get_all_auth_field_names().add(name)
|
|
1317
1332
|
|
|
1318
1333
|
def _render_custom_auth_fields(self, build_config: dict, schema: dict[str, Any], mode: str) -> None:
|
|
1319
1334
|
"""Render fields for custom auth based on schema auth_config_details sections."""
|
|
@@ -1378,7 +1393,7 @@ class ComposioBaseComponent(Component):
|
|
|
1378
1393
|
if name:
|
|
1379
1394
|
names.add(name)
|
|
1380
1395
|
# Add to class-level cache for tracking all discovered auth fields
|
|
1381
|
-
self.__class__.
|
|
1396
|
+
self.__class__.get_all_auth_field_names().add(name)
|
|
1382
1397
|
# Only use names discovered from the toolkit schema; do not add aliases
|
|
1383
1398
|
return names
|
|
1384
1399
|
|
|
@@ -1443,7 +1458,7 @@ class ComposioBaseComponent(Component):
|
|
|
1443
1458
|
# Check if we need to populate actions - but also check cache availability
|
|
1444
1459
|
actions_available = bool(self._actions_data)
|
|
1445
1460
|
toolkit_slug = getattr(self, "app_name", "").lower()
|
|
1446
|
-
cached_actions_available = toolkit_slug in self.__class__.
|
|
1461
|
+
cached_actions_available = toolkit_slug in self.__class__.get_actions_cache()
|
|
1447
1462
|
|
|
1448
1463
|
should_populate = False
|
|
1449
1464
|
|
|
@@ -2623,7 +2638,7 @@ class ComposioBaseComponent(Component):
|
|
|
2623
2638
|
# Add all dynamic auth fields to protected set
|
|
2624
2639
|
protected.update(self._auth_dynamic_fields)
|
|
2625
2640
|
# Also protect any auth fields discovered across all instances
|
|
2626
|
-
protected.update(self.__class__.
|
|
2641
|
+
protected.update(self.__class__.get_all_auth_field_names())
|
|
2627
2642
|
|
|
2628
2643
|
for key, cfg in list(build_config.items()):
|
|
2629
2644
|
if key in protected:
|