lfx-nightly 0.2.0.dev0__py3-none-any.whl → 0.2.0.dev41__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 +21 -4
- lfx/base/agents/altk_base_agent.py +393 -0
- lfx/base/agents/altk_tool_wrappers.py +565 -0
- lfx/base/agents/events.py +2 -1
- lfx/base/composio/composio_base.py +159 -224
- lfx/base/data/base_file.py +97 -20
- lfx/base/data/docling_utils.py +61 -10
- lfx/base/data/storage_utils.py +301 -0
- lfx/base/data/utils.py +178 -14
- lfx/base/mcp/util.py +2 -2
- lfx/base/models/anthropic_constants.py +21 -12
- lfx/base/models/groq_constants.py +74 -58
- lfx/base/models/groq_model_discovery.py +265 -0
- lfx/base/models/model.py +1 -1
- lfx/base/models/model_utils.py +100 -0
- lfx/base/models/openai_constants.py +7 -0
- lfx/base/models/watsonx_constants.py +32 -8
- lfx/base/tools/run_flow.py +601 -129
- lfx/cli/commands.py +9 -4
- lfx/cli/common.py +2 -2
- lfx/cli/run.py +1 -1
- lfx/cli/script_loader.py +53 -11
- lfx/components/Notion/create_page.py +1 -1
- lfx/components/Notion/list_database_properties.py +1 -1
- lfx/components/Notion/list_pages.py +1 -1
- lfx/components/Notion/list_users.py +1 -1
- lfx/components/Notion/page_content_viewer.py +1 -1
- lfx/components/Notion/search.py +1 -1
- lfx/components/Notion/update_page_property.py +1 -1
- lfx/components/__init__.py +19 -5
- lfx/components/{agents → altk}/__init__.py +5 -9
- lfx/components/altk/altk_agent.py +193 -0
- lfx/components/apify/apify_actor.py +1 -1
- lfx/components/composio/__init__.py +70 -18
- lfx/components/composio/apollo_composio.py +11 -0
- lfx/components/composio/bitbucket_composio.py +11 -0
- lfx/components/composio/canva_composio.py +11 -0
- lfx/components/composio/coda_composio.py +11 -0
- lfx/components/composio/composio_api.py +10 -0
- lfx/components/composio/discord_composio.py +1 -1
- lfx/components/composio/elevenlabs_composio.py +11 -0
- lfx/components/composio/exa_composio.py +11 -0
- lfx/components/composio/firecrawl_composio.py +11 -0
- lfx/components/composio/fireflies_composio.py +11 -0
- lfx/components/composio/gmail_composio.py +1 -1
- lfx/components/composio/googlebigquery_composio.py +11 -0
- lfx/components/composio/googlecalendar_composio.py +1 -1
- lfx/components/composio/googledocs_composio.py +1 -1
- lfx/components/composio/googlemeet_composio.py +1 -1
- lfx/components/composio/googlesheets_composio.py +1 -1
- lfx/components/composio/googletasks_composio.py +1 -1
- lfx/components/composio/heygen_composio.py +11 -0
- lfx/components/composio/mem0_composio.py +11 -0
- lfx/components/composio/peopledatalabs_composio.py +11 -0
- lfx/components/composio/perplexityai_composio.py +11 -0
- lfx/components/composio/serpapi_composio.py +11 -0
- lfx/components/composio/slack_composio.py +3 -574
- lfx/components/composio/slackbot_composio.py +1 -1
- lfx/components/composio/snowflake_composio.py +11 -0
- lfx/components/composio/tavily_composio.py +11 -0
- lfx/components/composio/youtube_composio.py +2 -2
- lfx/components/cuga/__init__.py +34 -0
- lfx/components/cuga/cuga_agent.py +730 -0
- lfx/components/data/__init__.py +78 -28
- lfx/components/data_source/__init__.py +58 -0
- lfx/components/{data → data_source}/api_request.py +26 -3
- lfx/components/{data → data_source}/csv_to_data.py +15 -10
- lfx/components/{data → data_source}/json_to_data.py +15 -8
- lfx/components/{data → data_source}/news_search.py +1 -1
- lfx/components/{data → data_source}/rss.py +1 -1
- lfx/components/{data → data_source}/sql_executor.py +1 -1
- lfx/components/{data → data_source}/url.py +1 -1
- lfx/components/{data → data_source}/web_search.py +1 -1
- lfx/components/datastax/astradb_cql.py +1 -1
- lfx/components/datastax/astradb_graph.py +1 -1
- lfx/components/datastax/astradb_tool.py +1 -1
- lfx/components/datastax/astradb_vectorstore.py +1 -1
- lfx/components/datastax/hcd.py +1 -1
- lfx/components/deactivated/json_document_builder.py +1 -1
- lfx/components/docling/__init__.py +0 -3
- lfx/components/docling/chunk_docling_document.py +3 -1
- lfx/components/docling/export_docling_document.py +3 -1
- lfx/components/elastic/elasticsearch.py +1 -1
- lfx/components/files_and_knowledge/__init__.py +47 -0
- lfx/components/{data → files_and_knowledge}/directory.py +1 -1
- lfx/components/{data → files_and_knowledge}/file.py +304 -24
- lfx/components/{knowledge_bases → files_and_knowledge}/retrieval.py +2 -2
- lfx/components/{data → files_and_knowledge}/save_file.py +218 -31
- lfx/components/flow_controls/__init__.py +58 -0
- lfx/components/{logic → flow_controls}/conditional_router.py +1 -1
- lfx/components/{logic → flow_controls}/loop.py +43 -9
- lfx/components/flow_controls/run_flow.py +108 -0
- lfx/components/glean/glean_search_api.py +1 -1
- lfx/components/groq/groq.py +35 -28
- lfx/components/helpers/__init__.py +102 -0
- lfx/components/ibm/watsonx.py +7 -1
- lfx/components/input_output/__init__.py +3 -1
- lfx/components/input_output/chat.py +4 -3
- lfx/components/input_output/chat_output.py +10 -4
- lfx/components/input_output/text.py +1 -1
- lfx/components/input_output/text_output.py +1 -1
- lfx/components/{data → input_output}/webhook.py +1 -1
- lfx/components/knowledge_bases/__init__.py +59 -4
- lfx/components/langchain_utilities/character.py +1 -1
- lfx/components/langchain_utilities/csv_agent.py +84 -16
- lfx/components/langchain_utilities/json_agent.py +67 -12
- lfx/components/langchain_utilities/language_recursive.py +1 -1
- lfx/components/llm_operations/__init__.py +46 -0
- lfx/components/{processing → llm_operations}/batch_run.py +17 -8
- lfx/components/{processing → llm_operations}/lambda_filter.py +1 -1
- lfx/components/{logic → llm_operations}/llm_conditional_router.py +1 -1
- lfx/components/{processing/llm_router.py → llm_operations/llm_selector.py} +3 -3
- lfx/components/{processing → llm_operations}/structured_output.py +1 -1
- lfx/components/logic/__init__.py +126 -0
- lfx/components/mem0/mem0_chat_memory.py +11 -0
- lfx/components/models/__init__.py +64 -9
- lfx/components/models_and_agents/__init__.py +49 -0
- lfx/components/{agents → models_and_agents}/agent.py +6 -4
- lfx/components/models_and_agents/embedding_model.py +353 -0
- lfx/components/models_and_agents/language_model.py +398 -0
- lfx/components/{agents → models_and_agents}/mcp_component.py +53 -44
- lfx/components/{helpers → models_and_agents}/memory.py +1 -1
- lfx/components/nvidia/system_assist.py +1 -1
- lfx/components/olivya/olivya.py +1 -1
- lfx/components/ollama/ollama.py +24 -5
- lfx/components/processing/__init__.py +9 -60
- lfx/components/processing/converter.py +1 -1
- lfx/components/processing/dataframe_operations.py +1 -1
- lfx/components/processing/parse_json_data.py +2 -2
- lfx/components/processing/parser.py +1 -1
- lfx/components/processing/split_text.py +1 -1
- lfx/components/qdrant/qdrant.py +1 -1
- lfx/components/redis/redis.py +1 -1
- lfx/components/twelvelabs/split_video.py +10 -0
- lfx/components/twelvelabs/video_file.py +12 -0
- lfx/components/utilities/__init__.py +43 -0
- lfx/components/{helpers → utilities}/calculator_core.py +1 -1
- lfx/components/{helpers → utilities}/current_date.py +1 -1
- lfx/components/{processing → utilities}/python_repl_core.py +1 -1
- lfx/components/vectorstores/local_db.py +9 -0
- lfx/components/youtube/youtube_transcripts.py +118 -30
- lfx/custom/custom_component/component.py +57 -1
- lfx/custom/custom_component/custom_component.py +68 -6
- lfx/custom/directory_reader/directory_reader.py +5 -2
- lfx/graph/edge/base.py +43 -20
- lfx/graph/state/model.py +15 -2
- lfx/graph/utils.py +6 -0
- lfx/graph/vertex/param_handler.py +10 -7
- lfx/helpers/__init__.py +12 -0
- lfx/helpers/flow.py +117 -0
- lfx/inputs/input_mixin.py +24 -1
- lfx/inputs/inputs.py +13 -1
- lfx/interface/components.py +161 -83
- lfx/log/logger.py +5 -3
- lfx/schema/image.py +2 -12
- lfx/services/database/__init__.py +5 -0
- lfx/services/database/service.py +25 -0
- lfx/services/deps.py +87 -22
- lfx/services/interfaces.py +5 -0
- lfx/services/manager.py +24 -10
- lfx/services/mcp_composer/service.py +1029 -162
- lfx/services/session.py +5 -0
- lfx/services/settings/auth.py +18 -11
- lfx/services/settings/base.py +56 -30
- lfx/services/settings/constants.py +8 -0
- lfx/services/storage/local.py +108 -46
- lfx/services/storage/service.py +171 -29
- lfx/template/field/base.py +3 -0
- lfx/utils/image.py +29 -11
- lfx/utils/ssrf_protection.py +384 -0
- lfx/utils/validate_cloud.py +26 -0
- {lfx_nightly-0.2.0.dev0.dist-info → lfx_nightly-0.2.0.dev41.dist-info}/METADATA +38 -22
- {lfx_nightly-0.2.0.dev0.dist-info → lfx_nightly-0.2.0.dev41.dist-info}/RECORD +189 -160
- {lfx_nightly-0.2.0.dev0.dist-info → lfx_nightly-0.2.0.dev41.dist-info}/WHEEL +1 -1
- lfx/components/agents/altk_agent.py +0 -366
- lfx/components/agents/cuga_agent.py +0 -1013
- lfx/components/docling/docling_remote_vlm.py +0 -284
- lfx/components/logic/run_flow.py +0 -71
- lfx/components/models/embedding_model.py +0 -195
- lfx/components/models/language_model.py +0 -144
- lfx/components/processing/dataframe_to_toolset.py +0 -259
- /lfx/components/{data → data_source}/mock_data.py +0 -0
- /lfx/components/{knowledge_bases → files_and_knowledge}/ingestion.py +0 -0
- /lfx/components/{logic → flow_controls}/data_conditional_router.py +0 -0
- /lfx/components/{logic → flow_controls}/flow_tool.py +0 -0
- /lfx/components/{logic → flow_controls}/listen.py +0 -0
- /lfx/components/{logic → flow_controls}/notify.py +0 -0
- /lfx/components/{logic → flow_controls}/pass_message.py +0 -0
- /lfx/components/{logic → flow_controls}/sub_flow.py +0 -0
- /lfx/components/{processing → models_and_agents}/prompt.py +0 -0
- /lfx/components/{helpers → processing}/create_list.py +0 -0
- /lfx/components/{helpers → processing}/output_parser.py +0 -0
- /lfx/components/{helpers → processing}/store_message.py +0 -0
- /lfx/components/{helpers → utilities}/id_generator.py +0 -0
- {lfx_nightly-0.2.0.dev0.dist-info → lfx_nightly-0.2.0.dev41.dist-info}/entry_points.txt +0 -0
lfx/base/agents/agent.py
CHANGED
|
@@ -165,11 +165,27 @@ class LCAgentComponent(Component):
|
|
|
165
165
|
lc_message = None
|
|
166
166
|
if isinstance(self.input_value, Message):
|
|
167
167
|
lc_message = self.input_value.to_lc_message()
|
|
168
|
-
|
|
168
|
+
# Extract text content from the LangChain message for agent input
|
|
169
|
+
# Agents expect a string input, not a Message object
|
|
170
|
+
if hasattr(lc_message, "content"):
|
|
171
|
+
if isinstance(lc_message.content, str):
|
|
172
|
+
input_dict: dict[str, str | list[BaseMessage] | BaseMessage] = {"input": lc_message.content}
|
|
173
|
+
elif isinstance(lc_message.content, list):
|
|
174
|
+
# For multimodal content, extract text parts
|
|
175
|
+
text_parts = [item.get("text", "") for item in lc_message.content if item.get("type") == "text"]
|
|
176
|
+
input_dict = {"input": " ".join(text_parts) if text_parts else ""}
|
|
177
|
+
else:
|
|
178
|
+
input_dict = {"input": str(lc_message.content)}
|
|
179
|
+
else:
|
|
180
|
+
input_dict = {"input": str(lc_message)}
|
|
169
181
|
else:
|
|
170
182
|
input_dict = {"input": self.input_value}
|
|
171
183
|
|
|
172
|
-
|
|
184
|
+
# Ensure input_dict is initialized
|
|
185
|
+
if "input" not in input_dict:
|
|
186
|
+
input_dict = {"input": self.input_value}
|
|
187
|
+
|
|
188
|
+
if hasattr(self, "system_prompt") and self.system_prompt and self.system_prompt.strip():
|
|
173
189
|
input_dict["system_prompt"] = self.system_prompt
|
|
174
190
|
|
|
175
191
|
if hasattr(self, "chat_history") and self.chat_history:
|
|
@@ -184,8 +200,9 @@ class LCAgentComponent(Component):
|
|
|
184
200
|
# Note: Agent input must be a string, so we extract text and move images to chat_history
|
|
185
201
|
if lc_message is not None and hasattr(lc_message, "content") and isinstance(lc_message.content, list):
|
|
186
202
|
# Extract images and text from the text content items
|
|
187
|
-
|
|
188
|
-
|
|
203
|
+
# Support both "image" (legacy) and "image_url" (standard) types
|
|
204
|
+
image_dicts = [item for item in lc_message.content if item.get("type") in ("image", "image_url")]
|
|
205
|
+
text_content = [item for item in lc_message.content if item.get("type") not in ("image", "image_url")]
|
|
189
206
|
|
|
190
207
|
text_strings = [
|
|
191
208
|
item.get("text", "")
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
"""Reusable base classes for ALTK agent components and tool wrappers.
|
|
2
|
+
|
|
3
|
+
This module abstracts common orchestration so concrete components can focus
|
|
4
|
+
on user-facing configuration and small customizations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import uuid
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
12
|
+
|
|
13
|
+
from altk.core.llm import get_llm
|
|
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.components.models_and_agents import AgentComponent
|
|
27
|
+
from lfx.log.logger import logger
|
|
28
|
+
from lfx.memory import delete_message
|
|
29
|
+
from lfx.schema.content_block import ContentBlock
|
|
30
|
+
from lfx.schema.data import Data
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from collections.abc import Sequence
|
|
34
|
+
|
|
35
|
+
from lfx.schema.log import SendMessageFunctionType
|
|
36
|
+
|
|
37
|
+
from lfx.schema.message import Message
|
|
38
|
+
from lfx.utils.constants import MESSAGE_SENDER_AI
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def normalize_message_content(message: BaseMessage) -> str:
|
|
42
|
+
"""Normalize message content to handle inconsistent formats from Data.to_lc_message().
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
message: A BaseMessage that may have content as either:
|
|
46
|
+
- str (for AI messages)
|
|
47
|
+
- list[dict] (for User messages in format [{"type": "text", "text": "..."}])
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
str: The extracted text content
|
|
51
|
+
|
|
52
|
+
Note:
|
|
53
|
+
This addresses the inconsistency in lfx.schema.data.Data.to_lc_message() where:
|
|
54
|
+
- User messages: content = [{"type": "text", "text": text}] (list format)
|
|
55
|
+
- AI messages: content = text (string format)
|
|
56
|
+
"""
|
|
57
|
+
content = message.content
|
|
58
|
+
|
|
59
|
+
# Handle string format (AI messages)
|
|
60
|
+
if isinstance(content, str):
|
|
61
|
+
return content
|
|
62
|
+
|
|
63
|
+
# Handle list format (User messages)
|
|
64
|
+
if isinstance(content, list) and len(content) > 0:
|
|
65
|
+
# Extract text from first content block that has 'text' field
|
|
66
|
+
for item in content:
|
|
67
|
+
if isinstance(item, dict) and item.get("type") == "text" and "text" in item:
|
|
68
|
+
return item["text"]
|
|
69
|
+
# If no text found, return empty string (e.g., image-only messages)
|
|
70
|
+
return ""
|
|
71
|
+
|
|
72
|
+
# Handle empty list or other formats
|
|
73
|
+
if isinstance(content, list):
|
|
74
|
+
return ""
|
|
75
|
+
|
|
76
|
+
# Fallback for any other format
|
|
77
|
+
return str(content)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# === Base Tool Wrapper Architecture ===
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class BaseToolWrapper(ABC):
|
|
84
|
+
"""Base class for all tool wrappers in the pipeline.
|
|
85
|
+
|
|
86
|
+
Tool wrappers can enhance tools by adding pre-execution validation,
|
|
87
|
+
post-execution processing, or other capabilities.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
@abstractmethod
|
|
91
|
+
def wrap_tool(self, tool: BaseTool, **kwargs) -> BaseTool:
|
|
92
|
+
"""Wrap a tool with enhanced functionality."""
|
|
93
|
+
|
|
94
|
+
def initialize(self, **_kwargs) -> bool: # pragma: no cover - trivial
|
|
95
|
+
"""Initialize any resources needed by the wrapper."""
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def is_available(self) -> bool: # pragma: no cover - trivial
|
|
100
|
+
"""Check if the wrapper is available for use."""
|
|
101
|
+
return True
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class ALTKBaseTool(BaseTool):
|
|
105
|
+
"""Base class for tools that need agent interaction and ALTK LLM access.
|
|
106
|
+
|
|
107
|
+
Provides common functionality for tool execution and ALTK LLM object creation.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
name: str = Field(...)
|
|
111
|
+
description: str = Field(...)
|
|
112
|
+
wrapped_tool: BaseTool = Field(...)
|
|
113
|
+
agent: Runnable | BaseSingleActionAgent | BaseMultiActionAgent | AgentExecutor = Field(...)
|
|
114
|
+
|
|
115
|
+
def _run(self, *args, **kwargs) -> str:
|
|
116
|
+
"""Abstract method implementation that uses the wrapped tool execution."""
|
|
117
|
+
return self._execute_tool(*args, **kwargs)
|
|
118
|
+
|
|
119
|
+
def _execute_tool(self, *args, **kwargs) -> str:
|
|
120
|
+
"""Execute the wrapped tool with compatibility across LC versions."""
|
|
121
|
+
# BaseTool.run() expects tool_input as first argument
|
|
122
|
+
if args:
|
|
123
|
+
# Use first arg as tool_input, pass remaining args
|
|
124
|
+
tool_input = args[0]
|
|
125
|
+
return self.wrapped_tool.run(tool_input, *args[1:])
|
|
126
|
+
if kwargs:
|
|
127
|
+
# Use kwargs dict as tool_input
|
|
128
|
+
return self.wrapped_tool.run(kwargs)
|
|
129
|
+
# No arguments - pass empty dict as tool_input
|
|
130
|
+
return self.wrapped_tool.run({})
|
|
131
|
+
|
|
132
|
+
def _get_altk_llm_object(self, *, use_output_val: bool = True) -> Any:
|
|
133
|
+
"""Extract the underlying LLM and map it to an ALTK client object."""
|
|
134
|
+
llm_object: BaseChatModel | None = None
|
|
135
|
+
steps = getattr(self.agent, "steps", None)
|
|
136
|
+
if steps:
|
|
137
|
+
for step in steps:
|
|
138
|
+
if isinstance(step, RunnableBinding) and isinstance(step.bound, BaseChatModel):
|
|
139
|
+
llm_object = step.bound
|
|
140
|
+
break
|
|
141
|
+
|
|
142
|
+
if isinstance(llm_object, ChatAnthropic):
|
|
143
|
+
model_name = f"anthropic/{llm_object.model}"
|
|
144
|
+
api_key = llm_object.anthropic_api_key.get_secret_value()
|
|
145
|
+
llm_client_type = "litellm.output_val" if use_output_val else "litellm"
|
|
146
|
+
llm_client = get_llm(llm_client_type)
|
|
147
|
+
llm_client_obj = llm_client(model_name=model_name, api_key=api_key)
|
|
148
|
+
elif isinstance(llm_object, ChatOpenAI):
|
|
149
|
+
model_name = llm_object.model_name
|
|
150
|
+
api_key = llm_object.openai_api_key.get_secret_value()
|
|
151
|
+
llm_client_type = "openai.sync.output_val" if use_output_val else "openai.sync"
|
|
152
|
+
llm_client = get_llm(llm_client_type)
|
|
153
|
+
llm_client_obj = llm_client(model=model_name, api_key=api_key)
|
|
154
|
+
else:
|
|
155
|
+
logger.info("ALTK currently only supports OpenAI and Anthropic models through Langflow.")
|
|
156
|
+
llm_client_obj = None
|
|
157
|
+
|
|
158
|
+
return llm_client_obj
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class ToolPipelineManager:
|
|
162
|
+
"""Manages a sequence of tool wrappers and applies them to tools."""
|
|
163
|
+
|
|
164
|
+
def __init__(self):
|
|
165
|
+
self.wrappers: list[BaseToolWrapper] = []
|
|
166
|
+
|
|
167
|
+
def clear(self) -> None:
|
|
168
|
+
self.wrappers.clear()
|
|
169
|
+
|
|
170
|
+
def add_wrapper(self, wrapper: BaseToolWrapper) -> None:
|
|
171
|
+
self.wrappers.append(wrapper)
|
|
172
|
+
|
|
173
|
+
def configure_wrappers(self, wrappers: list[BaseToolWrapper]) -> None:
|
|
174
|
+
"""Replace current wrappers with new configuration."""
|
|
175
|
+
self.clear()
|
|
176
|
+
for wrapper in wrappers:
|
|
177
|
+
self.add_wrapper(wrapper)
|
|
178
|
+
|
|
179
|
+
def process_tools(self, tools: list[BaseTool], **kwargs) -> list[BaseTool]:
|
|
180
|
+
return [self._apply_wrappers_to_tool(tool, **kwargs) for tool in tools]
|
|
181
|
+
|
|
182
|
+
def _apply_wrappers_to_tool(self, tool: BaseTool, **kwargs) -> BaseTool:
|
|
183
|
+
wrapped_tool = tool
|
|
184
|
+
for wrapper in reversed(self.wrappers):
|
|
185
|
+
if wrapper.is_available:
|
|
186
|
+
wrapped_tool = wrapper.wrap_tool(wrapped_tool, **kwargs)
|
|
187
|
+
return wrapped_tool
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# === Base Agent Component Orchestration ===
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class ALTKBaseAgentComponent(AgentComponent):
|
|
194
|
+
"""Base agent component that centralizes orchestration and hooks.
|
|
195
|
+
|
|
196
|
+
Subclasses should override `get_tool_wrappers` to provide their wrappers
|
|
197
|
+
and can customize context building if needed.
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
def __init__(self, **kwargs):
|
|
201
|
+
super().__init__(**kwargs)
|
|
202
|
+
self.pipeline_manager = ToolPipelineManager()
|
|
203
|
+
|
|
204
|
+
# ---- Hooks for subclasses ----
|
|
205
|
+
def configure_tool_pipeline(self) -> None:
|
|
206
|
+
"""Configure the tool pipeline with wrappers. Subclasses override this."""
|
|
207
|
+
# Default: no wrappers
|
|
208
|
+
self.pipeline_manager.clear()
|
|
209
|
+
|
|
210
|
+
def build_conversation_context(self) -> list[BaseMessage]:
|
|
211
|
+
"""Create conversation context from input and chat history."""
|
|
212
|
+
context: list[BaseMessage] = []
|
|
213
|
+
|
|
214
|
+
# Add chat history to maintain chronological order
|
|
215
|
+
if hasattr(self, "chat_history") and self.chat_history:
|
|
216
|
+
if isinstance(self.chat_history, Data):
|
|
217
|
+
context.append(self.chat_history.to_lc_message())
|
|
218
|
+
elif isinstance(self.chat_history, list):
|
|
219
|
+
if all(isinstance(m, Message) for m in self.chat_history):
|
|
220
|
+
context.extend([m.to_lc_message() for m in self.chat_history])
|
|
221
|
+
else:
|
|
222
|
+
# Assume list of Data objects, let data_to_messages handle validation
|
|
223
|
+
try:
|
|
224
|
+
context.extend(data_to_messages(self.chat_history))
|
|
225
|
+
except (AttributeError, TypeError) as e:
|
|
226
|
+
error_message = f"Invalid chat_history list contents: {e}"
|
|
227
|
+
raise ValueError(error_message) from e
|
|
228
|
+
else:
|
|
229
|
+
# Reject all other types (strings, numbers, etc.)
|
|
230
|
+
type_name = type(self.chat_history).__name__
|
|
231
|
+
error_message = (
|
|
232
|
+
f"chat_history must be a Data object, list of Data/Message objects, or None. Got: {type_name}"
|
|
233
|
+
)
|
|
234
|
+
raise ValueError(error_message)
|
|
235
|
+
|
|
236
|
+
# Then add current input to maintain chronological order
|
|
237
|
+
if hasattr(self, "input_value") and self.input_value:
|
|
238
|
+
if isinstance(self.input_value, Message):
|
|
239
|
+
context.append(self.input_value.to_lc_message())
|
|
240
|
+
else:
|
|
241
|
+
context.append(HumanMessage(content=str(self.input_value)))
|
|
242
|
+
|
|
243
|
+
return context
|
|
244
|
+
|
|
245
|
+
def get_user_query(self) -> str:
|
|
246
|
+
if hasattr(self.input_value, "get_text") and callable(self.input_value.get_text):
|
|
247
|
+
return self.input_value.get_text()
|
|
248
|
+
return str(self.input_value)
|
|
249
|
+
|
|
250
|
+
# ---- Internal helpers reused by run/update ----
|
|
251
|
+
def _initialize_tool_pipeline(self) -> None:
|
|
252
|
+
"""Initialize the tool pipeline by calling the subclass configuration."""
|
|
253
|
+
self.configure_tool_pipeline()
|
|
254
|
+
|
|
255
|
+
def update_runnable_instance(
|
|
256
|
+
self, agent: AgentExecutor, runnable: AgentExecutor, tools: Sequence[BaseTool]
|
|
257
|
+
) -> AgentExecutor:
|
|
258
|
+
"""Update the runnable instance with processed tools.
|
|
259
|
+
|
|
260
|
+
Subclasses can override this method to customize tool processing.
|
|
261
|
+
The default implementation applies the tool wrapper pipeline.
|
|
262
|
+
"""
|
|
263
|
+
user_query = self.get_user_query()
|
|
264
|
+
conversation_context = self.build_conversation_context()
|
|
265
|
+
|
|
266
|
+
self._initialize_tool_pipeline()
|
|
267
|
+
processed_tools = self.pipeline_manager.process_tools(
|
|
268
|
+
list(tools or []),
|
|
269
|
+
agent=agent,
|
|
270
|
+
user_query=user_query,
|
|
271
|
+
conversation_context=conversation_context,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
runnable.tools = processed_tools
|
|
275
|
+
return runnable
|
|
276
|
+
|
|
277
|
+
async def run_agent(
|
|
278
|
+
self,
|
|
279
|
+
agent: Runnable | BaseSingleActionAgent | BaseMultiActionAgent | AgentExecutor,
|
|
280
|
+
) -> Message:
|
|
281
|
+
if isinstance(agent, AgentExecutor):
|
|
282
|
+
runnable = agent
|
|
283
|
+
else:
|
|
284
|
+
# note the tools are not required to run the agent, hence the validation removed.
|
|
285
|
+
handle_parsing_errors = hasattr(self, "handle_parsing_errors") and self.handle_parsing_errors
|
|
286
|
+
verbose = hasattr(self, "verbose") and self.verbose
|
|
287
|
+
max_iterations = hasattr(self, "max_iterations") and self.max_iterations
|
|
288
|
+
runnable = AgentExecutor.from_agent_and_tools(
|
|
289
|
+
agent=agent,
|
|
290
|
+
tools=self.tools or [],
|
|
291
|
+
handle_parsing_errors=handle_parsing_errors,
|
|
292
|
+
verbose=verbose,
|
|
293
|
+
max_iterations=max_iterations,
|
|
294
|
+
)
|
|
295
|
+
runnable = self.update_runnable_instance(agent, runnable, self.tools)
|
|
296
|
+
|
|
297
|
+
# Convert input_value to proper format for agent
|
|
298
|
+
if hasattr(self.input_value, "to_lc_message") and callable(self.input_value.to_lc_message):
|
|
299
|
+
lc_message = self.input_value.to_lc_message()
|
|
300
|
+
input_text = lc_message.content if hasattr(lc_message, "content") else str(lc_message)
|
|
301
|
+
else:
|
|
302
|
+
lc_message = None
|
|
303
|
+
input_text = self.input_value
|
|
304
|
+
|
|
305
|
+
input_dict: dict[str, str | list[BaseMessage]] = {}
|
|
306
|
+
if hasattr(self, "system_prompt"):
|
|
307
|
+
input_dict["system_prompt"] = self.system_prompt
|
|
308
|
+
if hasattr(self, "chat_history") and self.chat_history:
|
|
309
|
+
if (
|
|
310
|
+
hasattr(self.chat_history, "to_data")
|
|
311
|
+
and callable(self.chat_history.to_data)
|
|
312
|
+
and self.chat_history.__class__.__name__ == "Data"
|
|
313
|
+
):
|
|
314
|
+
input_dict["chat_history"] = data_to_messages(self.chat_history)
|
|
315
|
+
# Handle both lfx.schema.message.Message and langflow.schema.message.Message types
|
|
316
|
+
if all(hasattr(m, "to_data") and callable(m.to_data) and "text" in m.data for m in self.chat_history):
|
|
317
|
+
input_dict["chat_history"] = data_to_messages(self.chat_history)
|
|
318
|
+
if all(isinstance(m, Message) for m in self.chat_history):
|
|
319
|
+
input_dict["chat_history"] = data_to_messages([m.to_data() for m in self.chat_history])
|
|
320
|
+
if hasattr(lc_message, "content") and isinstance(lc_message.content, list):
|
|
321
|
+
# ! Because the input has to be a string, we must pass the images in the chat_history
|
|
322
|
+
# Support both "image" (legacy) and "image_url" (standard) types
|
|
323
|
+
image_dicts = [item for item in lc_message.content if item.get("type") in ("image", "image_url")]
|
|
324
|
+
lc_message.content = [item for item in lc_message.content if item.get("type") not in ("image", "image_url")]
|
|
325
|
+
|
|
326
|
+
if "chat_history" not in input_dict:
|
|
327
|
+
input_dict["chat_history"] = []
|
|
328
|
+
if isinstance(input_dict["chat_history"], list):
|
|
329
|
+
input_dict["chat_history"].extend(HumanMessage(content=[image_dict]) for image_dict in image_dicts)
|
|
330
|
+
else:
|
|
331
|
+
input_dict["chat_history"] = [HumanMessage(content=[image_dict]) for image_dict in image_dicts]
|
|
332
|
+
input_dict["input"] = input_text
|
|
333
|
+
|
|
334
|
+
# Copied from agent.py
|
|
335
|
+
# Final safety check: ensure input is never empty (prevents Anthropic API errors)
|
|
336
|
+
current_input = input_dict.get("input", "")
|
|
337
|
+
if isinstance(current_input, list):
|
|
338
|
+
current_input = " ".join(map(str, current_input))
|
|
339
|
+
elif not isinstance(current_input, str):
|
|
340
|
+
current_input = str(current_input)
|
|
341
|
+
if not current_input.strip():
|
|
342
|
+
input_dict["input"] = "Continue the conversation."
|
|
343
|
+
else:
|
|
344
|
+
input_dict["input"] = current_input
|
|
345
|
+
|
|
346
|
+
if hasattr(self, "graph"):
|
|
347
|
+
session_id = self.graph.session_id
|
|
348
|
+
elif hasattr(self, "_session_id"):
|
|
349
|
+
session_id = self._session_id
|
|
350
|
+
else:
|
|
351
|
+
session_id = None
|
|
352
|
+
|
|
353
|
+
try:
|
|
354
|
+
sender_name = get_chat_output_sender_name(self)
|
|
355
|
+
except AttributeError:
|
|
356
|
+
sender_name = self.display_name or "AI"
|
|
357
|
+
|
|
358
|
+
agent_message = Message(
|
|
359
|
+
sender=MESSAGE_SENDER_AI,
|
|
360
|
+
sender_name=sender_name,
|
|
361
|
+
properties={"icon": "Bot", "state": "partial"},
|
|
362
|
+
content_blocks=[ContentBlock(title="Agent Steps", contents=[])],
|
|
363
|
+
session_id=session_id or uuid.uuid4(),
|
|
364
|
+
)
|
|
365
|
+
try:
|
|
366
|
+
result = await process_agent_events(
|
|
367
|
+
runnable.astream_events(
|
|
368
|
+
input_dict,
|
|
369
|
+
config={
|
|
370
|
+
"callbacks": [
|
|
371
|
+
AgentAsyncHandler(self.log),
|
|
372
|
+
*self.get_langchain_callbacks(),
|
|
373
|
+
]
|
|
374
|
+
},
|
|
375
|
+
version="v2",
|
|
376
|
+
),
|
|
377
|
+
agent_message,
|
|
378
|
+
cast("SendMessageFunctionType", self.send_message),
|
|
379
|
+
)
|
|
380
|
+
except ExceptionWithMessageError as e:
|
|
381
|
+
if hasattr(e, "agent_message") and hasattr(e.agent_message, "id"):
|
|
382
|
+
msg_id = e.agent_message.id
|
|
383
|
+
await delete_message(id_=msg_id)
|
|
384
|
+
await self._send_message_event(e.agent_message, category="remove_message")
|
|
385
|
+
logger.error(f"ExceptionWithMessageError: {e}")
|
|
386
|
+
raise
|
|
387
|
+
except Exception as e:
|
|
388
|
+
# Log or handle any other exceptions
|
|
389
|
+
logger.error(f"Error: {e}")
|
|
390
|
+
raise
|
|
391
|
+
|
|
392
|
+
self.status = result
|
|
393
|
+
return result
|