lfx-nightly 0.1.13.dev6__py3-none-any.whl → 0.1.13.dev8__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 -1
- lfx/components/agents/__init__.py +3 -1
- lfx/components/agents/altk_agent.py +366 -0
- lfx/components/agents/mcp_component.py +16 -2
- lfx/components/processing/structured_output.py +55 -17
- {lfx_nightly-0.1.13.dev6.dist-info → lfx_nightly-0.1.13.dev8.dist-info}/METADATA +1 -1
- {lfx_nightly-0.1.13.dev6.dist-info → lfx_nightly-0.1.13.dev8.dist-info}/RECORD +9 -8
- {lfx_nightly-0.1.13.dev6.dist-info → lfx_nightly-0.1.13.dev8.dist-info}/WHEEL +0 -0
- {lfx_nightly-0.1.13.dev6.dist-info → lfx_nightly-0.1.13.dev8.dist-info}/entry_points.txt +0 -0
|
@@ -6,6 +6,7 @@ from lfx.components._importing import import_mod
|
|
|
6
6
|
|
|
7
7
|
if TYPE_CHECKING:
|
|
8
8
|
from lfx.components.agents.agent import AgentComponent
|
|
9
|
+
from lfx.components.agents.altk_agent import ALTKAgentComponent
|
|
9
10
|
from lfx.components.agents.cuga_agent import CugaComponent
|
|
10
11
|
from lfx.components.agents.mcp_component import MCPToolsComponent
|
|
11
12
|
|
|
@@ -13,9 +14,10 @@ _dynamic_imports = {
|
|
|
13
14
|
"AgentComponent": "agent",
|
|
14
15
|
"CugaComponent": "cuga_agent",
|
|
15
16
|
"MCPToolsComponent": "mcp_component",
|
|
17
|
+
"ALTKAgentComponent": "altk_agent",
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
__all__ = ["AgentComponent", "CugaComponent", "MCPToolsComponent"]
|
|
20
|
+
__all__ = ["ALTKAgentComponent", "AgentComponent", "CugaComponent", "MCPToolsComponent"]
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
def __getattr__(attr_name: str) -> Any:
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import json
|
|
3
|
+
import uuid
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
6
|
+
|
|
7
|
+
from altk.core.llm import get_llm
|
|
8
|
+
from altk.core.toolkit import AgentPhase
|
|
9
|
+
from altk.post_tool.code_generation.code_generation import (
|
|
10
|
+
CodeGenerationComponent,
|
|
11
|
+
CodeGenerationComponentConfig,
|
|
12
|
+
)
|
|
13
|
+
from altk.post_tool.core.toolkit import CodeGenerationRunInput
|
|
14
|
+
from langchain.agents import AgentExecutor, BaseMultiActionAgent, BaseSingleActionAgent
|
|
15
|
+
from langchain_anthropic.chat_models import ChatAnthropic
|
|
16
|
+
from langchain_core.language_models.chat_models import BaseChatModel
|
|
17
|
+
from langchain_core.messages import BaseMessage, HumanMessage
|
|
18
|
+
from langchain_core.runnables import Runnable, RunnableBinding
|
|
19
|
+
from langchain_core.tools import BaseTool
|
|
20
|
+
from langchain_openai.chat_models.base import ChatOpenAI
|
|
21
|
+
from pydantic import Field
|
|
22
|
+
|
|
23
|
+
from lfx.base.agents.callback import AgentAsyncHandler
|
|
24
|
+
from lfx.base.agents.events import ExceptionWithMessageError, process_agent_events
|
|
25
|
+
from lfx.base.agents.utils import data_to_messages, get_chat_output_sender_name
|
|
26
|
+
from lfx.base.models.model_input_constants import (
|
|
27
|
+
MODEL_PROVIDERS_DICT,
|
|
28
|
+
MODELS_METADATA,
|
|
29
|
+
)
|
|
30
|
+
from lfx.components.agents import AgentComponent
|
|
31
|
+
from lfx.inputs.inputs import BoolInput
|
|
32
|
+
from lfx.io import DropdownInput, IntInput, Output
|
|
33
|
+
from lfx.log.logger import logger
|
|
34
|
+
from lfx.memory import delete_message
|
|
35
|
+
from lfx.schema.content_block import ContentBlock
|
|
36
|
+
from lfx.schema.data import Data
|
|
37
|
+
from lfx.schema.message import Message
|
|
38
|
+
from lfx.utils.constants import MESSAGE_SENDER_AI
|
|
39
|
+
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
from lfx.schema.log import SendMessageFunctionType
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def set_advanced_true(component_input):
|
|
45
|
+
component_input.advanced = True
|
|
46
|
+
return component_input
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
MODEL_PROVIDERS_LIST = ["Anthropic", "OpenAI"]
|
|
50
|
+
INPUT_NAMES_TO_BE_OVERRIDDEN = ["agent_llm"]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_parent_agent_inputs():
|
|
54
|
+
return [
|
|
55
|
+
input_field for input_field in AgentComponent.inputs if input_field.name not in INPUT_NAMES_TO_BE_OVERRIDDEN
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class PostToolProcessor(BaseTool):
|
|
60
|
+
"""A tool output processor to process tool outputs.
|
|
61
|
+
|
|
62
|
+
This wrapper intercepts the tool execution output and
|
|
63
|
+
if the tool output is a JSON, it invokes an ALTK component
|
|
64
|
+
to extract information from the JSON by generating Python code.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
name: str = Field(...)
|
|
68
|
+
description: str = Field(...)
|
|
69
|
+
wrapped_tool: BaseTool = Field(...)
|
|
70
|
+
user_query: str = Field(...)
|
|
71
|
+
agent: Runnable | BaseSingleActionAgent | BaseMultiActionAgent | AgentExecutor = Field(...)
|
|
72
|
+
response_processing_size_threshold: int = Field(...)
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self, wrapped_tool: BaseTool, user_query: str, agent, response_processing_size_threshold: int, **kwargs
|
|
76
|
+
):
|
|
77
|
+
super().__init__(
|
|
78
|
+
name=wrapped_tool.name,
|
|
79
|
+
description=wrapped_tool.description,
|
|
80
|
+
wrapped_tool=wrapped_tool,
|
|
81
|
+
user_query=user_query,
|
|
82
|
+
agent=agent,
|
|
83
|
+
response_processing_size_threshold=response_processing_size_threshold,
|
|
84
|
+
**kwargs,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def _execute_tool(self, *args, **kwargs) -> str:
|
|
88
|
+
"""Execute the wrapped tool with proper error handling."""
|
|
89
|
+
try:
|
|
90
|
+
# Try with config parameter first (newer LangChain versions)
|
|
91
|
+
if hasattr(self.wrapped_tool, "_run"):
|
|
92
|
+
# Ensure config is provided for StructuredTool
|
|
93
|
+
if "config" not in kwargs:
|
|
94
|
+
kwargs["config"] = {}
|
|
95
|
+
return self.wrapped_tool._run(*args, **kwargs) # noqa: SLF001
|
|
96
|
+
return self.wrapped_tool.run(*args, **kwargs)
|
|
97
|
+
except TypeError as e:
|
|
98
|
+
if "config" in str(e):
|
|
99
|
+
# Fallback: try without config for older tools
|
|
100
|
+
kwargs.pop("config", None)
|
|
101
|
+
if hasattr(self.wrapped_tool, "_run"):
|
|
102
|
+
return self.wrapped_tool._run(*args, **kwargs) # noqa: SLF001
|
|
103
|
+
return self.wrapped_tool.run(*args, **kwargs)
|
|
104
|
+
raise
|
|
105
|
+
|
|
106
|
+
def _run(self, *args: Any, **kwargs: Any) -> str:
|
|
107
|
+
# Run the wrapped tool
|
|
108
|
+
result = self._execute_tool(*args, **kwargs)
|
|
109
|
+
|
|
110
|
+
# Run postprocessing and return the output
|
|
111
|
+
return self.process_tool_response(result)
|
|
112
|
+
|
|
113
|
+
def _get_tool_response_str(self, tool_response) -> str | None:
|
|
114
|
+
if isinstance(tool_response, str):
|
|
115
|
+
tool_response_str = tool_response
|
|
116
|
+
elif isinstance(tool_response, Data):
|
|
117
|
+
tool_response_str = str(tool_response.data)
|
|
118
|
+
elif isinstance(tool_response, list) and all(isinstance(item, Data) for item in tool_response):
|
|
119
|
+
# get only the first element, not 100% sure if it should be the first or the last
|
|
120
|
+
tool_response_str = str(tool_response[0].data)
|
|
121
|
+
elif isinstance(tool_response, (dict, list)):
|
|
122
|
+
tool_response_str = str(tool_response)
|
|
123
|
+
else:
|
|
124
|
+
tool_response_str = None
|
|
125
|
+
return tool_response_str
|
|
126
|
+
|
|
127
|
+
def _get_altk_llm_object(self) -> Any:
|
|
128
|
+
# Extract the LLM model and map it to altk model inputs
|
|
129
|
+
llm_object: BaseChatModel | None = None
|
|
130
|
+
steps = getattr(self.agent, "steps", None)
|
|
131
|
+
if steps:
|
|
132
|
+
for step in steps:
|
|
133
|
+
if isinstance(step, RunnableBinding) and isinstance(step.bound, BaseChatModel):
|
|
134
|
+
llm_object = step.bound
|
|
135
|
+
break
|
|
136
|
+
if isinstance(llm_object, ChatAnthropic):
|
|
137
|
+
# litellm needs the prefix to the model name for anthropic
|
|
138
|
+
model_name = f"anthropic/{llm_object.model}"
|
|
139
|
+
api_key = llm_object.anthropic_api_key.get_secret_value()
|
|
140
|
+
llm_client = get_llm("litellm")
|
|
141
|
+
llm_client_obj = llm_client(model_name=model_name, api_key=api_key)
|
|
142
|
+
elif isinstance(llm_object, ChatOpenAI):
|
|
143
|
+
model_name = llm_object.model_name
|
|
144
|
+
api_key = llm_object.openai_api_key.get_secret_value()
|
|
145
|
+
llm_client = get_llm("openai.sync")
|
|
146
|
+
llm_client_obj = llm_client(model=model_name, api_key=api_key)
|
|
147
|
+
else:
|
|
148
|
+
logger.info("ALTK currently only supports OpenAI and Anthropic models through Langflow.")
|
|
149
|
+
llm_client_obj = None
|
|
150
|
+
|
|
151
|
+
return llm_client_obj
|
|
152
|
+
|
|
153
|
+
def process_tool_response(self, tool_response: str, **_kwargs):
|
|
154
|
+
logger.info("Calling process_tool_response of PostToolProcessor")
|
|
155
|
+
tool_response_str = self._get_tool_response_str(tool_response)
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
tool_response_json = ast.literal_eval(tool_response_str)
|
|
159
|
+
if not isinstance(tool_response_json, (list, dict)):
|
|
160
|
+
tool_response_json = None
|
|
161
|
+
except (json.JSONDecodeError, TypeError) as e:
|
|
162
|
+
logger.info(
|
|
163
|
+
f"An error in converting the tool response to json, this will skip the code generation component: {e}"
|
|
164
|
+
)
|
|
165
|
+
tool_response_json = None
|
|
166
|
+
|
|
167
|
+
if tool_response_json is not None and len(str(tool_response_json)) > self.response_processing_size_threshold:
|
|
168
|
+
llm_client_obj = self._get_altk_llm_object()
|
|
169
|
+
if llm_client_obj is not None:
|
|
170
|
+
config = CodeGenerationComponentConfig(llm_client=llm_client_obj, use_docker_sandbox=False)
|
|
171
|
+
|
|
172
|
+
middleware = CodeGenerationComponent(config=config)
|
|
173
|
+
input_data = CodeGenerationRunInput(
|
|
174
|
+
messages=[], nl_query=self.user_query, tool_response=tool_response_json
|
|
175
|
+
)
|
|
176
|
+
output = None
|
|
177
|
+
try:
|
|
178
|
+
output = middleware.process(input_data, AgentPhase.RUNTIME)
|
|
179
|
+
except Exception as e: # noqa: BLE001
|
|
180
|
+
logger.error(f"Exception in executing CodeGenerationComponent: {e}")
|
|
181
|
+
logger.info(f"Output of CodeGenerationComponent: {output.result}")
|
|
182
|
+
return output.result
|
|
183
|
+
return tool_response
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class ALTKAgentComponent(AgentComponent):
|
|
187
|
+
"""An advanced tool calling agent.
|
|
188
|
+
|
|
189
|
+
The ALTKAgent is an advanced AI agent that enhances the tool calling capabilities of LLMs
|
|
190
|
+
by performing special checks and processing around tool calls.
|
|
191
|
+
It uses components from the Agent Lifecycle ToolKit (https://github.com/AgentToolkit/agent-lifecycle-toolkit)
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
display_name: str = "ALTK Agent"
|
|
195
|
+
description: str = "Agent with enhanced tool calling capabilities. For more information on ALTK, visit https://github.com/AgentToolkit/agent-lifecycle-toolkit"
|
|
196
|
+
documentation: str = "https://docs.langflow.org/agents"
|
|
197
|
+
icon = "bot"
|
|
198
|
+
beta = True
|
|
199
|
+
name = "ALTKAgent"
|
|
200
|
+
|
|
201
|
+
# Filter out json_mode from OpenAI inputs since we handle structured output differently
|
|
202
|
+
if "OpenAI" in MODEL_PROVIDERS_DICT:
|
|
203
|
+
openai_inputs_filtered = [
|
|
204
|
+
input_field
|
|
205
|
+
for input_field in MODEL_PROVIDERS_DICT["OpenAI"]["inputs"]
|
|
206
|
+
if not (hasattr(input_field, "name") and input_field.name == "json_mode")
|
|
207
|
+
]
|
|
208
|
+
else:
|
|
209
|
+
openai_inputs_filtered = []
|
|
210
|
+
|
|
211
|
+
inputs = [
|
|
212
|
+
DropdownInput(
|
|
213
|
+
name="agent_llm",
|
|
214
|
+
display_name="Model Provider",
|
|
215
|
+
info="The provider of the language model that the agent will use to generate responses.",
|
|
216
|
+
options=[*MODEL_PROVIDERS_LIST],
|
|
217
|
+
value="OpenAI",
|
|
218
|
+
real_time_refresh=True,
|
|
219
|
+
refresh_button=False,
|
|
220
|
+
input_types=[],
|
|
221
|
+
options_metadata=[MODELS_METADATA[key] for key in MODEL_PROVIDERS_LIST if key in MODELS_METADATA],
|
|
222
|
+
),
|
|
223
|
+
*get_parent_agent_inputs(),
|
|
224
|
+
BoolInput(
|
|
225
|
+
name="enable_post_tool_reflection",
|
|
226
|
+
display_name="Post Tool JSON Processing",
|
|
227
|
+
info="If true, it passes the tool output to a json processing (if json) step.",
|
|
228
|
+
value=True,
|
|
229
|
+
),
|
|
230
|
+
# Post Tool Processing is applied only when the number of characters in the response
|
|
231
|
+
# exceed the following threshold
|
|
232
|
+
IntInput(
|
|
233
|
+
name="response_processing_size_threshold",
|
|
234
|
+
display_name="Response Processing Size Threshold",
|
|
235
|
+
value=100,
|
|
236
|
+
info="Tool output is post-processed only if the response length exceeds a specified character threshold.",
|
|
237
|
+
advanced=True,
|
|
238
|
+
show=True,
|
|
239
|
+
),
|
|
240
|
+
]
|
|
241
|
+
outputs = [
|
|
242
|
+
Output(name="response", display_name="Response", method="message_response"),
|
|
243
|
+
]
|
|
244
|
+
|
|
245
|
+
def update_runnable_instance(
|
|
246
|
+
self, agent: AgentExecutor, runnable: AgentExecutor, tools: Sequence[BaseTool]
|
|
247
|
+
) -> AgentExecutor:
|
|
248
|
+
user_query = self.input_value.get_text() if hasattr(self.input_value, "get_text") else self.input_value
|
|
249
|
+
if self.enable_post_tool_reflection:
|
|
250
|
+
wrapped_tools = [
|
|
251
|
+
PostToolProcessor(
|
|
252
|
+
wrapped_tool=tool,
|
|
253
|
+
user_query=user_query,
|
|
254
|
+
agent=agent,
|
|
255
|
+
response_processing_size_threshold=self.response_processing_size_threshold,
|
|
256
|
+
)
|
|
257
|
+
if not isinstance(tool, PostToolProcessor)
|
|
258
|
+
else tool
|
|
259
|
+
for tool in tools
|
|
260
|
+
]
|
|
261
|
+
else:
|
|
262
|
+
wrapped_tools = tools
|
|
263
|
+
|
|
264
|
+
runnable.tools = wrapped_tools
|
|
265
|
+
|
|
266
|
+
return runnable
|
|
267
|
+
|
|
268
|
+
async def run_agent(
|
|
269
|
+
self,
|
|
270
|
+
agent: Runnable | BaseSingleActionAgent | BaseMultiActionAgent | AgentExecutor,
|
|
271
|
+
) -> Message:
|
|
272
|
+
if isinstance(agent, AgentExecutor):
|
|
273
|
+
runnable = agent
|
|
274
|
+
else:
|
|
275
|
+
# note the tools are not required to run the agent, hence the validation removed.
|
|
276
|
+
handle_parsing_errors = hasattr(self, "handle_parsing_errors") and self.handle_parsing_errors
|
|
277
|
+
verbose = hasattr(self, "verbose") and self.verbose
|
|
278
|
+
max_iterations = hasattr(self, "max_iterations") and self.max_iterations
|
|
279
|
+
runnable = AgentExecutor.from_agent_and_tools(
|
|
280
|
+
agent=agent,
|
|
281
|
+
tools=self.tools or [],
|
|
282
|
+
handle_parsing_errors=handle_parsing_errors,
|
|
283
|
+
verbose=verbose,
|
|
284
|
+
max_iterations=max_iterations,
|
|
285
|
+
)
|
|
286
|
+
runnable = self.update_runnable_instance(agent, runnable, self.tools)
|
|
287
|
+
|
|
288
|
+
# Convert input_value to proper format for agent
|
|
289
|
+
if hasattr(self.input_value, "to_lc_message") and callable(self.input_value.to_lc_message):
|
|
290
|
+
lc_message = self.input_value.to_lc_message()
|
|
291
|
+
input_text = lc_message.content if hasattr(lc_message, "content") else str(lc_message)
|
|
292
|
+
else:
|
|
293
|
+
lc_message = None
|
|
294
|
+
input_text = self.input_value
|
|
295
|
+
|
|
296
|
+
input_dict: dict[str, str | list[BaseMessage]] = {}
|
|
297
|
+
if hasattr(self, "system_prompt"):
|
|
298
|
+
input_dict["system_prompt"] = self.system_prompt
|
|
299
|
+
if hasattr(self, "chat_history") and self.chat_history:
|
|
300
|
+
if (
|
|
301
|
+
hasattr(self.chat_history, "to_data")
|
|
302
|
+
and callable(self.chat_history.to_data)
|
|
303
|
+
and self.chat_history.__class__.__name__ == "Data"
|
|
304
|
+
):
|
|
305
|
+
input_dict["chat_history"] = data_to_messages(self.chat_history)
|
|
306
|
+
# Handle both lfx.schema.message.Message and langflow.schema.message.Message types
|
|
307
|
+
if all(hasattr(m, "to_data") and callable(m.to_data) and "text" in m.data for m in self.chat_history):
|
|
308
|
+
input_dict["chat_history"] = data_to_messages(self.chat_history)
|
|
309
|
+
if all(isinstance(m, Message) for m in self.chat_history):
|
|
310
|
+
input_dict["chat_history"] = data_to_messages([m.to_data() for m in self.chat_history])
|
|
311
|
+
if hasattr(lc_message, "content") and isinstance(lc_message.content, list):
|
|
312
|
+
# ! Because the input has to be a string, we must pass the images in the chat_history
|
|
313
|
+
|
|
314
|
+
image_dicts = [item for item in lc_message.content if item.get("type") == "image"]
|
|
315
|
+
lc_message.content = [item for item in lc_message.content if item.get("type") != "image"]
|
|
316
|
+
|
|
317
|
+
if "chat_history" not in input_dict:
|
|
318
|
+
input_dict["chat_history"] = []
|
|
319
|
+
if isinstance(input_dict["chat_history"], list):
|
|
320
|
+
input_dict["chat_history"].extend(HumanMessage(content=[image_dict]) for image_dict in image_dicts)
|
|
321
|
+
else:
|
|
322
|
+
input_dict["chat_history"] = [HumanMessage(content=[image_dict]) for image_dict in image_dicts]
|
|
323
|
+
input_dict["input"] = input_text
|
|
324
|
+
if hasattr(self, "graph"):
|
|
325
|
+
session_id = self.graph.session_id
|
|
326
|
+
elif hasattr(self, "_session_id"):
|
|
327
|
+
session_id = self._session_id
|
|
328
|
+
else:
|
|
329
|
+
session_id = None
|
|
330
|
+
|
|
331
|
+
try:
|
|
332
|
+
sender_name = get_chat_output_sender_name(self)
|
|
333
|
+
except AttributeError:
|
|
334
|
+
sender_name = self.display_name or "AI"
|
|
335
|
+
|
|
336
|
+
agent_message = Message(
|
|
337
|
+
sender=MESSAGE_SENDER_AI,
|
|
338
|
+
sender_name=sender_name,
|
|
339
|
+
properties={"icon": "Bot", "state": "partial"},
|
|
340
|
+
content_blocks=[ContentBlock(title="Agent Steps", contents=[])],
|
|
341
|
+
session_id=session_id or uuid.uuid4(),
|
|
342
|
+
)
|
|
343
|
+
try:
|
|
344
|
+
result = await process_agent_events(
|
|
345
|
+
runnable.astream_events(
|
|
346
|
+
input_dict,
|
|
347
|
+
config={"callbacks": [AgentAsyncHandler(self.log), *self.get_langchain_callbacks()]},
|
|
348
|
+
version="v2",
|
|
349
|
+
),
|
|
350
|
+
agent_message,
|
|
351
|
+
cast("SendMessageFunctionType", self.send_message),
|
|
352
|
+
)
|
|
353
|
+
except ExceptionWithMessageError as e:
|
|
354
|
+
if hasattr(e, "agent_message") and hasattr(e.agent_message, "id"):
|
|
355
|
+
msg_id = e.agent_message.id
|
|
356
|
+
await delete_message(id_=msg_id)
|
|
357
|
+
await self._send_message_event(e.agent_message, category="remove_message")
|
|
358
|
+
logger.error(f"ExceptionWithMessageError: {e}")
|
|
359
|
+
raise
|
|
360
|
+
except Exception as e:
|
|
361
|
+
# Log or handle any other exceptions
|
|
362
|
+
logger.error(f"Error: {e}")
|
|
363
|
+
raise
|
|
364
|
+
|
|
365
|
+
self.status = result
|
|
366
|
+
return result
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import json
|
|
4
5
|
import uuid
|
|
5
6
|
from typing import Any
|
|
6
7
|
|
|
@@ -520,7 +521,6 @@ class MCPToolsComponent(ComponentWithCache):
|
|
|
520
521
|
if session_context:
|
|
521
522
|
self.stdio_client.set_session_context(session_context)
|
|
522
523
|
self.streamable_http_client.set_session_context(session_context)
|
|
523
|
-
|
|
524
524
|
exec_tool = self._tool_cache[self.tool]
|
|
525
525
|
tool_args = self.get_inputs_for_all_tools(self.tools)[self.tool]
|
|
526
526
|
kwargs = {}
|
|
@@ -535,11 +535,14 @@ class MCPToolsComponent(ComponentWithCache):
|
|
|
535
535
|
unflattened_kwargs = maybe_unflatten_dict(kwargs)
|
|
536
536
|
|
|
537
537
|
output = await exec_tool.coroutine(**unflattened_kwargs)
|
|
538
|
-
|
|
539
538
|
tool_content = []
|
|
540
539
|
for item in output.content:
|
|
541
540
|
item_dict = item.model_dump()
|
|
541
|
+
item_dict = self.process_output_item(item_dict)
|
|
542
542
|
tool_content.append(item_dict)
|
|
543
|
+
|
|
544
|
+
if isinstance(tool_content, list) and all(isinstance(x, dict) for x in tool_content):
|
|
545
|
+
return DataFrame(tool_content)
|
|
543
546
|
return DataFrame(data=tool_content)
|
|
544
547
|
return DataFrame(data=[{"error": "You must select a tool"}])
|
|
545
548
|
except Exception as e:
|
|
@@ -547,6 +550,17 @@ class MCPToolsComponent(ComponentWithCache):
|
|
|
547
550
|
await logger.aexception(msg)
|
|
548
551
|
raise ValueError(msg) from e
|
|
549
552
|
|
|
553
|
+
def process_output_item(self, item_dict):
|
|
554
|
+
"""Process the output of a tool."""
|
|
555
|
+
if item_dict.get("type") == "text":
|
|
556
|
+
text = item_dict.get("text")
|
|
557
|
+
try:
|
|
558
|
+
return json.loads(text)
|
|
559
|
+
# convert it to dict
|
|
560
|
+
except json.JSONDecodeError:
|
|
561
|
+
return item_dict
|
|
562
|
+
return item_dict
|
|
563
|
+
|
|
550
564
|
def _get_session_context(self) -> str | None:
|
|
551
565
|
"""Get the Langflow session ID for MCP session caching."""
|
|
552
566
|
# Try to get session ID from the component's execution context
|
|
@@ -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=(
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lfx-nightly
|
|
3
|
-
Version: 0.1.13.
|
|
3
|
+
Version: 0.1.13.dev8
|
|
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
|
|
@@ -4,7 +4,7 @@ lfx/constants.py,sha256=Ert_SpwXhutgcTKEvtDArtkONXgyE5x68opMoQfukMA,203
|
|
|
4
4
|
lfx/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
lfx/settings.py,sha256=wnx4zkOLQ8mvampYsnnvVV9GvEnRUuWQpKFSbFTCIp4,181
|
|
6
6
|
lfx/type_extraction.py,sha256=eCZNl9nAQivKdaPv_9BK71N0JV9Rtr--veAht0dnQ4A,2921
|
|
7
|
-
lfx/_assets/component_index.json,sha256=
|
|
7
|
+
lfx/_assets/component_index.json,sha256=iFhI2IAaOrFMIB-NI1GrezFq6uTRjP83yqSIaGpgUbM,3558913
|
|
8
8
|
lfx/base/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
9
|
lfx/base/constants.py,sha256=v9vo0Ifg8RxDu__XqgGzIXHlsnUFyWM-SSux0uHHoz8,1187
|
|
10
10
|
lfx/base/agents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -115,10 +115,11 @@ lfx/components/Notion/search.py,sha256=WEDYbNotXdlgTXrmRtpZ9_proiOJl1zouNPRpFllg
|
|
|
115
115
|
lfx/components/Notion/update_page_property.py,sha256=tgmPMbD1eX58dQQNXv1w5FzDec7QwFiBuOqh4RazoJ0,4541
|
|
116
116
|
lfx/components/agentql/__init__.py,sha256=Erl669Dzsk-SegsDPWTtkKbprMXVuv8UTCo5REzZGTc,56
|
|
117
117
|
lfx/components/agentql/agentql_api.py,sha256=N94yEK7ZuQCIsFBlr_8dqrJY-K1-KNb6QEEYfDIsDME,5569
|
|
118
|
-
lfx/components/agents/__init__.py,sha256=
|
|
118
|
+
lfx/components/agents/__init__.py,sha256=O5ng90Dn9mSlpCdeGPXM4tvdWYVfEMv_7PGDKShyBKg,1294
|
|
119
119
|
lfx/components/agents/agent.py,sha256=-pGSOYS-2pHoYq5VyNdDCnkQVzVz52as4hdF_BB1hVI,26993
|
|
120
|
+
lfx/components/agents/altk_agent.py,sha256=D_ChOwqYao0QZBfiLpiMVoMOgCwzcb_q0MIPDULLHW8,15939
|
|
120
121
|
lfx/components/agents/cuga_agent.py,sha256=JNi4MsSziTJQI3z_0KGzNWxm5RDaMk_W9zcTW2KcTtI,44499
|
|
121
|
-
lfx/components/agents/mcp_component.py,sha256=
|
|
122
|
+
lfx/components/agents/mcp_component.py,sha256=gbEzi-hZMqVmIGO5DjJDOlmpisPkylOSZSGgaDjiQLY,26809
|
|
122
123
|
lfx/components/aiml/__init__.py,sha256=DNKB-HMFGFYmsdkON-s8557ttgBXVXADmS-BcuSQiIQ,1087
|
|
123
124
|
lfx/components/aiml/aiml.py,sha256=23Ineg1ajlCoqXgWgp50I20OnQbaleRNsw1c6IzPu3A,3877
|
|
124
125
|
lfx/components/aiml/aiml_embeddings.py,sha256=2uNwORuj55mxn2SfLbh7oAIfjuXwHbsnOqRjfMtQRqc,1095
|
|
@@ -476,7 +477,7 @@ lfx/components/processing/python_repl_core.py,sha256=6kOu64pWyBwBpTqOTM9LPnSsnTX
|
|
|
476
477
|
lfx/components/processing/regex.py,sha256=9n171_Ze--5gpKFJJyJlYafuEOwbPQPiyjhdLY3SUrY,2689
|
|
477
478
|
lfx/components/processing/select_data.py,sha256=BRK9mM5NuHveCrMOyIXjzzpEsNMEiA7oQXvk1DZLHM4,1788
|
|
478
479
|
lfx/components/processing/split_text.py,sha256=8oZ-_aYfjxEdzFFr2reKeBVPjMrAeAauZiQkM9J7Syc,5293
|
|
479
|
-
lfx/components/processing/structured_output.py,sha256=
|
|
480
|
+
lfx/components/processing/structured_output.py,sha256=mtjY2PwWKjAFYd4CYOo1Nbxv4umVTYgj6O9xszbN2IA,10015
|
|
480
481
|
lfx/components/processing/update_data.py,sha256=0HkK86ybp0vWc_KKMWD0j0dnaxS6zQMOjAY8lLwNkr4,6090
|
|
481
482
|
lfx/components/prototypes/__init__.py,sha256=uJRmThNAq6Tr_mBppcD95dV07WkOF6BYdy-zq7uXUhY,1061
|
|
482
483
|
lfx/components/prototypes/python_function.py,sha256=fafYVqVFqusIkJP4jgmHMZnwgVkYdhYBmD51Dtst9Z4,2419
|
|
@@ -729,7 +730,7 @@ lfx/utils/schemas.py,sha256=NbOtVQBrn4d0BAu-0H_eCTZI2CXkKZlRY37XCSmuJwc,3865
|
|
|
729
730
|
lfx/utils/util.py,sha256=Ww85wbr1-vjh2pXVtmTqoUVr6MXAW8S7eDx_Ys6HpE8,20696
|
|
730
731
|
lfx/utils/util_strings.py,sha256=nU_IcdphNaj6bAPbjeL-c1cInQPfTBit8mp5Y57lwQk,1686
|
|
731
732
|
lfx/utils/version.py,sha256=cHpbO0OJD2JQAvVaTH_6ibYeFbHJV0QDHs_YXXZ-bT8,671
|
|
732
|
-
lfx_nightly-0.1.13.
|
|
733
|
-
lfx_nightly-0.1.13.
|
|
734
|
-
lfx_nightly-0.1.13.
|
|
735
|
-
lfx_nightly-0.1.13.
|
|
733
|
+
lfx_nightly-0.1.13.dev8.dist-info/METADATA,sha256=OuXsV_znOj-UTr--bes0-v4clOA1MVVgLl2ygj8ok94,8289
|
|
734
|
+
lfx_nightly-0.1.13.dev8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
735
|
+
lfx_nightly-0.1.13.dev8.dist-info/entry_points.txt,sha256=1724p3RHDQRT2CKx_QRzEIa7sFuSVO0Ux70YfXfoMT4,42
|
|
736
|
+
lfx_nightly-0.1.13.dev8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|