lfx-nightly 0.2.0.dev0__py3-none-any.whl → 0.2.0.dev26__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 +13 -1
- lfx/base/agents/altk_base_agent.py +380 -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 +88 -21
- lfx/base/data/storage_utils.py +192 -0
- lfx/base/data/utils.py +178 -14
- lfx/base/embeddings/embeddings_class.py +113 -0
- 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 +6 -3
- 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/elastic/elasticsearch.py +1 -1
- lfx/components/elastic/opensearch_multimodal.py +1575 -0
- 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 +246 -18
- lfx/components/{knowledge_bases → files_and_knowledge}/retrieval.py +2 -2
- lfx/components/{data → files_and_knowledge}/save_file.py +142 -22
- 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/input_output/__init__.py +3 -1
- lfx/components/input_output/chat.py +4 -3
- lfx/components/input_output/chat_output.py +4 -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 +1 -1
- 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 +2 -2
- lfx/components/models_and_agents/embedding_model.py +423 -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 +17 -3
- lfx/components/processing/__init__.py +9 -57
- 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/graph/edge/base.py +43 -20
- lfx/graph/graph/base.py +4 -1
- lfx/graph/state/model.py +15 -2
- lfx/graph/utils.py +6 -0
- lfx/graph/vertex/base.py +4 -1
- 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/services/database/__init__.py +5 -0
- lfx/services/database/service.py +25 -0
- lfx/services/deps.py +87 -22
- lfx/services/manager.py +19 -6
- lfx/services/mcp_composer/service.py +998 -157
- lfx/services/session.py +5 -0
- lfx/services/settings/base.py +51 -7
- lfx/services/settings/constants.py +8 -0
- lfx/services/storage/local.py +76 -46
- lfx/services/storage/service.py +152 -29
- lfx/template/field/base.py +3 -0
- 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.dev26.dist-info}/METADATA +38 -22
- {lfx_nightly-0.2.0.dev0.dist-info → lfx_nightly-0.2.0.dev26.dist-info}/RECORD +182 -150
- {lfx_nightly-0.2.0.dev0.dist-info → lfx_nightly-0.2.0.dev26.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/{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.dev26.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import json
|
|
3
|
+
import uuid
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from altk.core.toolkit import AgentPhase, ComponentConfig
|
|
7
|
+
from altk.post_tool.code_generation.code_generation import (
|
|
8
|
+
CodeGenerationComponent,
|
|
9
|
+
CodeGenerationComponentConfig,
|
|
10
|
+
)
|
|
11
|
+
from altk.post_tool.core.toolkit import CodeGenerationRunInput
|
|
12
|
+
from altk.pre_tool.core import SPARCExecutionMode, SPARCReflectionRunInput, Track
|
|
13
|
+
from altk.pre_tool.sparc import SPARCReflectionComponent
|
|
14
|
+
from langchain_core.messages import BaseMessage
|
|
15
|
+
from langchain_core.messages.base import message_to_dict
|
|
16
|
+
from langchain_core.tools import BaseTool
|
|
17
|
+
from pydantic import Field
|
|
18
|
+
|
|
19
|
+
from lfx.base.agents.altk_base_agent import ALTKBaseTool, BaseToolWrapper
|
|
20
|
+
from lfx.log.logger import logger
|
|
21
|
+
from lfx.schema.data import Data
|
|
22
|
+
|
|
23
|
+
# Maximum wrapper nesting depth to prevent infinite loops
|
|
24
|
+
_MAX_WRAPPER_DEPTH = 10
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _convert_pydantic_type_to_json_schema_type(param_info: dict) -> dict:
|
|
28
|
+
"""Convert Pydantic parameter info to OpenAI function calling JSON schema format.
|
|
29
|
+
|
|
30
|
+
SPARC expects tools to be in OpenAI's function calling format, which uses
|
|
31
|
+
JSON Schema for parameter specifications.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
param_info: Parameter info from LangChain tool.args
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Dict with 'type' and optionally other JSON schema properties compatible
|
|
38
|
+
with OpenAI function calling format
|
|
39
|
+
"""
|
|
40
|
+
# Handle simple types first
|
|
41
|
+
if "type" in param_info:
|
|
42
|
+
schema_type = param_info["type"]
|
|
43
|
+
|
|
44
|
+
# Direct type mappings
|
|
45
|
+
if schema_type in ("string", "number", "integer", "boolean", "null", "object"):
|
|
46
|
+
return {
|
|
47
|
+
"type": schema_type,
|
|
48
|
+
"description": param_info.get("description", ""),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Array type
|
|
52
|
+
if schema_type == "array":
|
|
53
|
+
result = {"type": "array", "description": param_info.get("description", "")}
|
|
54
|
+
# Add items schema if available
|
|
55
|
+
if "items" in param_info:
|
|
56
|
+
items_schema = _convert_pydantic_type_to_json_schema_type(param_info["items"])
|
|
57
|
+
result["items"] = items_schema
|
|
58
|
+
return result
|
|
59
|
+
|
|
60
|
+
# Handle complex types with anyOf (unions like list[str] | None)
|
|
61
|
+
if "anyOf" in param_info:
|
|
62
|
+
# Find the most specific non-null type
|
|
63
|
+
for variant in param_info["anyOf"]:
|
|
64
|
+
if variant.get("type") == "null":
|
|
65
|
+
continue # Skip null variants
|
|
66
|
+
|
|
67
|
+
# Process the non-null variant
|
|
68
|
+
converted = _convert_pydantic_type_to_json_schema_type(variant)
|
|
69
|
+
converted["description"] = param_info.get("description", "")
|
|
70
|
+
|
|
71
|
+
# If it has a default value, it's optional
|
|
72
|
+
if "default" in param_info:
|
|
73
|
+
converted["default"] = param_info["default"]
|
|
74
|
+
|
|
75
|
+
return converted
|
|
76
|
+
|
|
77
|
+
# Handle oneOf (similar to anyOf)
|
|
78
|
+
if "oneOf" in param_info:
|
|
79
|
+
# Take the first non-null option
|
|
80
|
+
for variant in param_info["oneOf"]:
|
|
81
|
+
if variant.get("type") != "null":
|
|
82
|
+
converted = _convert_pydantic_type_to_json_schema_type(variant)
|
|
83
|
+
converted["description"] = param_info.get("description", "")
|
|
84
|
+
return converted
|
|
85
|
+
|
|
86
|
+
# Handle allOf (intersection types)
|
|
87
|
+
if param_info.get("allOf"):
|
|
88
|
+
# For now, take the first schema
|
|
89
|
+
converted = _convert_pydantic_type_to_json_schema_type(param_info["allOf"][0])
|
|
90
|
+
converted["description"] = param_info.get("description", "")
|
|
91
|
+
return converted
|
|
92
|
+
|
|
93
|
+
# Fallback: try to infer from title or default to string
|
|
94
|
+
logger.debug(f"Could not determine type for param_info: {param_info}")
|
|
95
|
+
return {
|
|
96
|
+
"type": "string", # Safe fallback
|
|
97
|
+
"description": param_info.get("description", ""),
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class ValidatedTool(ALTKBaseTool):
|
|
102
|
+
"""A wrapper tool that validates calls before execution using SPARC reflection.
|
|
103
|
+
|
|
104
|
+
Falls back to simple validation if SPARC is not available.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
sparc_component: Any | None = Field(default=None)
|
|
108
|
+
conversation_context: list[BaseMessage] = Field(default_factory=list)
|
|
109
|
+
tool_specs: list[dict] = Field(default_factory=list)
|
|
110
|
+
validation_attempts: dict[str, int] = Field(default_factory=dict)
|
|
111
|
+
current_conversation_context: list[BaseMessage] = Field(default_factory=list)
|
|
112
|
+
previous_tool_calls_in_current_step: list[dict] = Field(default_factory=list)
|
|
113
|
+
previous_reflection_messages: dict[str, str] = Field(default_factory=list)
|
|
114
|
+
|
|
115
|
+
def __init__(
|
|
116
|
+
self,
|
|
117
|
+
wrapped_tool: BaseTool,
|
|
118
|
+
agent,
|
|
119
|
+
sparc_component=None,
|
|
120
|
+
conversation_context=None,
|
|
121
|
+
tool_specs=None,
|
|
122
|
+
**kwargs,
|
|
123
|
+
):
|
|
124
|
+
super().__init__(
|
|
125
|
+
name=wrapped_tool.name,
|
|
126
|
+
description=wrapped_tool.description,
|
|
127
|
+
wrapped_tool=wrapped_tool,
|
|
128
|
+
sparc_component=sparc_component,
|
|
129
|
+
conversation_context=conversation_context or [],
|
|
130
|
+
tool_specs=tool_specs or [],
|
|
131
|
+
agent=agent,
|
|
132
|
+
**kwargs,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def _run(self, *args, **kwargs) -> str:
|
|
136
|
+
"""Execute the tool with validation."""
|
|
137
|
+
self.sparc_component = SPARCReflectionComponent(
|
|
138
|
+
config=ComponentConfig(llm_client=self._get_altk_llm_object()),
|
|
139
|
+
track=Track.FAST_TRACK, # Use fast track for performance
|
|
140
|
+
execution_mode=SPARCExecutionMode.SYNC, # Use SYNC to avoid event loop conflicts
|
|
141
|
+
)
|
|
142
|
+
return self._validate_and_run(*args, **kwargs)
|
|
143
|
+
|
|
144
|
+
@staticmethod
|
|
145
|
+
def _custom_message_to_dict(message: BaseMessage) -> dict:
|
|
146
|
+
"""Convert a BaseMessage to a dictionary."""
|
|
147
|
+
if isinstance(message, BaseMessage):
|
|
148
|
+
return message_to_dict(message)
|
|
149
|
+
msg = f"Invalid message type: {type(message)}"
|
|
150
|
+
logger.error(msg, exc_info=True)
|
|
151
|
+
raise ValueError(msg) from None
|
|
152
|
+
|
|
153
|
+
def _validate_and_run(self, *args, **kwargs) -> str:
|
|
154
|
+
"""Validate the tool call using SPARC and execute if valid."""
|
|
155
|
+
# Check if validation should be bypassed
|
|
156
|
+
if not self.sparc_component:
|
|
157
|
+
return self._execute_tool(*args, **kwargs)
|
|
158
|
+
|
|
159
|
+
# Prepare tool call for SPARC validation
|
|
160
|
+
tool_call = {
|
|
161
|
+
"id": str(uuid.uuid4()),
|
|
162
|
+
"type": "function",
|
|
163
|
+
"function": {
|
|
164
|
+
"name": self.name,
|
|
165
|
+
"arguments": json.dumps(self._prepare_arguments(*args, **kwargs)),
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (
|
|
170
|
+
isinstance(self.conversation_context, list)
|
|
171
|
+
and self.conversation_context
|
|
172
|
+
and isinstance(self.conversation_context[0], BaseMessage)
|
|
173
|
+
):
|
|
174
|
+
logger.debug("Converting BaseMessages to list of dictionaries for conversation context of SPARC")
|
|
175
|
+
self.conversation_context = [self._custom_message_to_dict(msg) for msg in self.conversation_context]
|
|
176
|
+
|
|
177
|
+
logger.debug(
|
|
178
|
+
f"Converted conversation context for SPARC for tool call:\n"
|
|
179
|
+
f"{json.dumps(tool_call, indent=2)}\n{self.conversation_context=}"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
# Run SPARC validation
|
|
184
|
+
run_input = SPARCReflectionRunInput(
|
|
185
|
+
messages=self.conversation_context + self.previous_tool_calls_in_current_step,
|
|
186
|
+
tool_specs=self.tool_specs,
|
|
187
|
+
tool_calls=[tool_call],
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
if self.current_conversation_context != self.conversation_context:
|
|
191
|
+
logger.info("Updating conversation context for SPARC validation")
|
|
192
|
+
self.current_conversation_context = self.conversation_context
|
|
193
|
+
self.previous_tool_calls_in_current_step = []
|
|
194
|
+
else:
|
|
195
|
+
logger.info("Using existing conversation context for SPARC validation")
|
|
196
|
+
self.previous_tool_calls_in_current_step.append(tool_call)
|
|
197
|
+
|
|
198
|
+
# Check for missing tool specs and bypass if necessary
|
|
199
|
+
if not self.tool_specs:
|
|
200
|
+
logger.warning(f"No tool specs available for SPARC validation of {self.name}, executing directly")
|
|
201
|
+
return self._execute_tool(*args, **kwargs)
|
|
202
|
+
|
|
203
|
+
result = self.sparc_component.process(run_input, phase=AgentPhase.RUNTIME)
|
|
204
|
+
logger.debug(f"SPARC validation result for tool {self.name}: {result.output.reflection_result}")
|
|
205
|
+
|
|
206
|
+
# Check validation result
|
|
207
|
+
if result.output.reflection_result.decision.name == "APPROVE":
|
|
208
|
+
logger.info(f"✅ SPARC approved tool call for {self.name}")
|
|
209
|
+
return self._execute_tool(*args, **kwargs)
|
|
210
|
+
logger.info(f"❌ SPARC rejected tool call for {self.name}")
|
|
211
|
+
return self._format_sparc_rejection(result.output.reflection_result)
|
|
212
|
+
|
|
213
|
+
except (AttributeError, TypeError, ValueError, RuntimeError) as e:
|
|
214
|
+
logger.error(f"Error during SPARC validation: {e}")
|
|
215
|
+
# Execute directly on error
|
|
216
|
+
return self._execute_tool(*args, **kwargs)
|
|
217
|
+
|
|
218
|
+
def _prepare_arguments(self, *args, **kwargs) -> dict[str, Any]:
|
|
219
|
+
"""Prepare arguments for SPARC validation."""
|
|
220
|
+
# Remove config parameter if present (not needed for validation)
|
|
221
|
+
clean_kwargs = {k: v for k, v in kwargs.items() if k != "config"}
|
|
222
|
+
|
|
223
|
+
# If we have positional args, try to map them to parameter names
|
|
224
|
+
if args and hasattr(self.wrapped_tool, "args_schema"):
|
|
225
|
+
try:
|
|
226
|
+
schema = self.wrapped_tool.args_schema
|
|
227
|
+
field_source = None
|
|
228
|
+
if hasattr(schema, "__fields__"):
|
|
229
|
+
field_source = schema.__fields__
|
|
230
|
+
elif hasattr(schema, "model_fields"):
|
|
231
|
+
field_source = schema.model_fields
|
|
232
|
+
if field_source:
|
|
233
|
+
field_names = list(field_source.keys())
|
|
234
|
+
for i, arg in enumerate(args):
|
|
235
|
+
if i < len(field_names):
|
|
236
|
+
clean_kwargs[field_names[i]] = arg
|
|
237
|
+
except (AttributeError, KeyError, TypeError):
|
|
238
|
+
# If schema parsing fails, just use kwargs
|
|
239
|
+
pass
|
|
240
|
+
|
|
241
|
+
return clean_kwargs
|
|
242
|
+
|
|
243
|
+
def _format_sparc_rejection(self, reflection_result) -> str:
|
|
244
|
+
"""Format SPARC rejection into a helpful error message."""
|
|
245
|
+
if not reflection_result.issues:
|
|
246
|
+
return "Error: Tool call validation failed - please review your approach and try again"
|
|
247
|
+
|
|
248
|
+
error_parts = ["Tool call validation failed:"]
|
|
249
|
+
|
|
250
|
+
for issue in reflection_result.issues:
|
|
251
|
+
error_parts.append(f"\n• {issue.explanation}")
|
|
252
|
+
if issue.correction:
|
|
253
|
+
try:
|
|
254
|
+
correction_data = issue.correction
|
|
255
|
+
if isinstance(correction_data, dict):
|
|
256
|
+
if "corrected_function_name" in correction_data:
|
|
257
|
+
error_parts.append(f" 💡 Suggested function: {correction_data['corrected_function_name']}")
|
|
258
|
+
elif "tool_call" in correction_data:
|
|
259
|
+
suggested_args = correction_data["tool_call"].get("arguments", {})
|
|
260
|
+
error_parts.append(f" 💡 Suggested parameters: {suggested_args}")
|
|
261
|
+
except (AttributeError, KeyError, TypeError):
|
|
262
|
+
# If correction parsing fails, skip it
|
|
263
|
+
pass
|
|
264
|
+
|
|
265
|
+
error_parts.append("\nPlease adjust your approach and try again.")
|
|
266
|
+
return "\n".join(error_parts)
|
|
267
|
+
|
|
268
|
+
def update_context(self, conversation_context: list[BaseMessage]):
|
|
269
|
+
"""Update the conversation context."""
|
|
270
|
+
self.conversation_context = conversation_context
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class PreToolValidationWrapper(BaseToolWrapper):
|
|
274
|
+
"""Tool wrapper that adds pre-tool validation capabilities.
|
|
275
|
+
|
|
276
|
+
This wrapper validates tool calls before execution using the SPARC
|
|
277
|
+
reflection component to check for appropriateness and correctness.
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
def __init__(self):
|
|
281
|
+
self.tool_specs = []
|
|
282
|
+
|
|
283
|
+
def wrap_tool(self, tool: BaseTool, **kwargs) -> BaseTool:
|
|
284
|
+
"""Wrap a tool with validation functionality.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
tool: The BaseTool to wrap
|
|
288
|
+
**kwargs: May contain 'conversation_context' for improved validation
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
A wrapped BaseTool with validation capabilities
|
|
292
|
+
"""
|
|
293
|
+
if isinstance(tool, ValidatedTool):
|
|
294
|
+
# Already wrapped, update context and tool specs
|
|
295
|
+
tool.tool_specs = self.tool_specs
|
|
296
|
+
if "conversation_context" in kwargs:
|
|
297
|
+
tool.update_context(kwargs["conversation_context"])
|
|
298
|
+
logger.debug(f"Updated existing ValidatedTool {tool.name} with {len(self.tool_specs)} tool specs")
|
|
299
|
+
return tool
|
|
300
|
+
|
|
301
|
+
agent = kwargs.get("agent")
|
|
302
|
+
|
|
303
|
+
if not agent:
|
|
304
|
+
logger.warning("Cannot wrap tool with PreToolValidationWrapper: missing 'agent'")
|
|
305
|
+
return tool
|
|
306
|
+
|
|
307
|
+
# Wrap with validation
|
|
308
|
+
return ValidatedTool(
|
|
309
|
+
wrapped_tool=tool,
|
|
310
|
+
agent=agent,
|
|
311
|
+
tool_specs=self.tool_specs,
|
|
312
|
+
conversation_context=kwargs.get("conversation_context", []),
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
@staticmethod
|
|
316
|
+
def convert_langchain_tools_to_sparc_tool_specs_format(
|
|
317
|
+
tools: list[BaseTool],
|
|
318
|
+
) -> list[dict]:
|
|
319
|
+
"""Convert LangChain tools to OpenAI function calling format for SPARC validation.
|
|
320
|
+
|
|
321
|
+
SPARC expects tools in OpenAI's function calling format, which is the standard
|
|
322
|
+
format used by OpenAI, Anthropic, Google, and other LLM providers for tool integration.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
tools: List of LangChain BaseTool instances to convert
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
List of tool specifications in OpenAI function calling format:
|
|
329
|
+
[
|
|
330
|
+
{
|
|
331
|
+
"type": "function",
|
|
332
|
+
"function": {
|
|
333
|
+
"name": "tool_name",
|
|
334
|
+
"description": "Tool description",
|
|
335
|
+
"parameters": {
|
|
336
|
+
"type": "object",
|
|
337
|
+
"properties": {...},
|
|
338
|
+
"required": [...]
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
]
|
|
343
|
+
"""
|
|
344
|
+
tool_specs = []
|
|
345
|
+
|
|
346
|
+
for i, tool in enumerate(tools):
|
|
347
|
+
try:
|
|
348
|
+
# Handle nested wrappers
|
|
349
|
+
unwrapped_tool = tool
|
|
350
|
+
wrapper_count = 0
|
|
351
|
+
|
|
352
|
+
# Unwrap to get to the actual tool
|
|
353
|
+
while hasattr(unwrapped_tool, "wrapped_tool") and not isinstance(unwrapped_tool, ValidatedTool):
|
|
354
|
+
unwrapped_tool = unwrapped_tool.wrapped_tool
|
|
355
|
+
wrapper_count += 1
|
|
356
|
+
if wrapper_count > _MAX_WRAPPER_DEPTH: # Prevent infinite loops
|
|
357
|
+
break
|
|
358
|
+
|
|
359
|
+
# Build tool spec from LangChain tool
|
|
360
|
+
tool_spec = {
|
|
361
|
+
"type": "function",
|
|
362
|
+
"function": {
|
|
363
|
+
"name": unwrapped_tool.name,
|
|
364
|
+
"description": unwrapped_tool.description or f"Tool: {unwrapped_tool.name}",
|
|
365
|
+
"parameters": {
|
|
366
|
+
"type": "object",
|
|
367
|
+
"properties": {},
|
|
368
|
+
"required": [],
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
# Extract parameters from tool schema if available
|
|
374
|
+
args_dict = unwrapped_tool.args
|
|
375
|
+
if isinstance(args_dict, dict):
|
|
376
|
+
for param_name, param_info in args_dict.items():
|
|
377
|
+
logger.debug(f"Processing parameter: {param_name}")
|
|
378
|
+
logger.debug(f"Parameter info: {param_info}")
|
|
379
|
+
|
|
380
|
+
# Use the new conversion function
|
|
381
|
+
param_spec = _convert_pydantic_type_to_json_schema_type(param_info)
|
|
382
|
+
|
|
383
|
+
# Check if parameter is required using Pydantic model fields
|
|
384
|
+
if unwrapped_tool.args_schema and hasattr(unwrapped_tool.args_schema, "model_fields"):
|
|
385
|
+
field_info = unwrapped_tool.args_schema.model_fields.get(param_name)
|
|
386
|
+
if field_info and field_info.is_required():
|
|
387
|
+
tool_spec["function"]["parameters"]["required"].append(param_name)
|
|
388
|
+
|
|
389
|
+
tool_spec["function"]["parameters"]["properties"][param_name] = param_spec
|
|
390
|
+
|
|
391
|
+
tool_specs.append(tool_spec)
|
|
392
|
+
|
|
393
|
+
except (AttributeError, KeyError, TypeError, ValueError) as e:
|
|
394
|
+
logger.warning(f"Could not convert tool {getattr(tool, 'name', 'unknown')} to spec: {e}")
|
|
395
|
+
# Create minimal spec
|
|
396
|
+
minimal_spec = {
|
|
397
|
+
"type": "function",
|
|
398
|
+
"function": {
|
|
399
|
+
"name": getattr(tool, "name", f"unknown_tool_{i}"),
|
|
400
|
+
"description": getattr(
|
|
401
|
+
tool,
|
|
402
|
+
"description",
|
|
403
|
+
f"Tool: {getattr(tool, 'name', 'unknown')}",
|
|
404
|
+
),
|
|
405
|
+
"parameters": {
|
|
406
|
+
"type": "object",
|
|
407
|
+
"properties": {},
|
|
408
|
+
"required": [],
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
}
|
|
412
|
+
tool_specs.append(minimal_spec)
|
|
413
|
+
|
|
414
|
+
if not tool_specs:
|
|
415
|
+
logger.error("⚠️ No tool specs were generated! This will cause SPARC validation to fail")
|
|
416
|
+
return tool_specs
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
class PostToolProcessor(ALTKBaseTool):
|
|
420
|
+
"""A tool output processor to process tool outputs.
|
|
421
|
+
|
|
422
|
+
This wrapper intercepts the tool execution output and
|
|
423
|
+
if the tool output is a JSON, it invokes an ALTK component
|
|
424
|
+
to extract information from the JSON by generating Python code.
|
|
425
|
+
"""
|
|
426
|
+
|
|
427
|
+
user_query: str = Field(...)
|
|
428
|
+
response_processing_size_threshold: int = Field(...)
|
|
429
|
+
|
|
430
|
+
def __init__(
|
|
431
|
+
self,
|
|
432
|
+
wrapped_tool: BaseTool,
|
|
433
|
+
user_query: str,
|
|
434
|
+
agent,
|
|
435
|
+
response_processing_size_threshold: int,
|
|
436
|
+
**kwargs,
|
|
437
|
+
):
|
|
438
|
+
super().__init__(
|
|
439
|
+
name=wrapped_tool.name,
|
|
440
|
+
description=wrapped_tool.description,
|
|
441
|
+
wrapped_tool=wrapped_tool,
|
|
442
|
+
user_query=user_query,
|
|
443
|
+
agent=agent,
|
|
444
|
+
response_processing_size_threshold=response_processing_size_threshold,
|
|
445
|
+
**kwargs,
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
def _run(self, *args: Any, **kwargs: Any) -> str:
|
|
449
|
+
# Run the wrapped tool
|
|
450
|
+
result = self._execute_tool(*args, **kwargs)
|
|
451
|
+
|
|
452
|
+
try:
|
|
453
|
+
# Run postprocessing and return the output
|
|
454
|
+
return self.process_tool_response(result)
|
|
455
|
+
except (AttributeError, TypeError, ValueError, RuntimeError) as e:
|
|
456
|
+
# If post-processing fails, log the error and return the original result
|
|
457
|
+
logger.error(f"Error in post-processing tool response: {e}")
|
|
458
|
+
return result
|
|
459
|
+
|
|
460
|
+
def _get_tool_response_str(self, tool_response) -> str:
|
|
461
|
+
"""Convert various tool response formats to a string representation."""
|
|
462
|
+
if isinstance(tool_response, str):
|
|
463
|
+
tool_response_str = tool_response
|
|
464
|
+
elif isinstance(tool_response, Data):
|
|
465
|
+
tool_response_str = str(tool_response.data)
|
|
466
|
+
elif isinstance(tool_response, list) and all(isinstance(item, Data) for item in tool_response):
|
|
467
|
+
# get only the first element, not 100% sure if it should be the first or the last
|
|
468
|
+
tool_response_str = str(tool_response[0].data)
|
|
469
|
+
elif isinstance(tool_response, (dict, list)):
|
|
470
|
+
tool_response_str = str(tool_response)
|
|
471
|
+
else:
|
|
472
|
+
# Return empty string instead of None to avoid type errors
|
|
473
|
+
tool_response_str = str(tool_response) if tool_response is not None else ""
|
|
474
|
+
|
|
475
|
+
return tool_response_str
|
|
476
|
+
|
|
477
|
+
def process_tool_response(self, tool_response: str, **_kwargs) -> str:
|
|
478
|
+
logger.info("Calling process_tool_response of PostToolProcessor")
|
|
479
|
+
tool_response_str = self._get_tool_response_str(tool_response)
|
|
480
|
+
|
|
481
|
+
# First check if this looks like an error message with bullet points (SPARC rejection)
|
|
482
|
+
if "❌" in tool_response_str or "•" in tool_response_str:
|
|
483
|
+
logger.info("Detected error message with special characters, skipping JSON parsing")
|
|
484
|
+
return tool_response_str
|
|
485
|
+
|
|
486
|
+
try:
|
|
487
|
+
# Only attempt to parse content that looks like JSON
|
|
488
|
+
if (tool_response_str.startswith("{") and tool_response_str.endswith("}")) or (
|
|
489
|
+
tool_response_str.startswith("[") and tool_response_str.endswith("]")
|
|
490
|
+
):
|
|
491
|
+
tool_response_json = ast.literal_eval(tool_response_str)
|
|
492
|
+
if not isinstance(tool_response_json, (list, dict)):
|
|
493
|
+
tool_response_json = None
|
|
494
|
+
else:
|
|
495
|
+
tool_response_json = None
|
|
496
|
+
except (json.JSONDecodeError, TypeError, SyntaxError, ValueError) as e:
|
|
497
|
+
logger.info(
|
|
498
|
+
f"An error in converting the tool response to json, this will skip the code generation component: {e}"
|
|
499
|
+
)
|
|
500
|
+
tool_response_json = None
|
|
501
|
+
|
|
502
|
+
if tool_response_json is not None and len(str(tool_response_json)) > self.response_processing_size_threshold:
|
|
503
|
+
llm_client_obj = self._get_altk_llm_object(use_output_val=False)
|
|
504
|
+
if llm_client_obj is not None:
|
|
505
|
+
config = CodeGenerationComponentConfig(llm_client=llm_client_obj, use_docker_sandbox=False)
|
|
506
|
+
|
|
507
|
+
middleware = CodeGenerationComponent(config=config)
|
|
508
|
+
input_data = CodeGenerationRunInput(
|
|
509
|
+
messages=[],
|
|
510
|
+
nl_query=self.user_query,
|
|
511
|
+
tool_response=tool_response_json,
|
|
512
|
+
)
|
|
513
|
+
output = None
|
|
514
|
+
try:
|
|
515
|
+
output = middleware.process(input_data, AgentPhase.RUNTIME)
|
|
516
|
+
except (AttributeError, TypeError, ValueError, RuntimeError) as e:
|
|
517
|
+
logger.error(f"Exception in executing CodeGenerationComponent: {e}")
|
|
518
|
+
if output is not None and hasattr(output, "result"):
|
|
519
|
+
logger.info(f"Output of CodeGenerationComponent: {output.result}")
|
|
520
|
+
return output.result
|
|
521
|
+
return tool_response
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
class PostToolProcessingWrapper(BaseToolWrapper):
|
|
525
|
+
"""Tool wrapper that adds post-tool processing capabilities.
|
|
526
|
+
|
|
527
|
+
This wrapper processes the output of tool calls, particularly JSON responses,
|
|
528
|
+
using the ALTK code generation component to extract useful information.
|
|
529
|
+
"""
|
|
530
|
+
|
|
531
|
+
def __init__(self, response_processing_size_threshold: int = 100):
|
|
532
|
+
self.response_processing_size_threshold = response_processing_size_threshold
|
|
533
|
+
|
|
534
|
+
def wrap_tool(self, tool: BaseTool, **kwargs) -> BaseTool:
|
|
535
|
+
"""Wrap a tool with post-processing functionality.
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
tool: The BaseTool to wrap
|
|
539
|
+
**kwargs: Must contain 'agent' and 'user_query'
|
|
540
|
+
|
|
541
|
+
Returns:
|
|
542
|
+
A wrapped BaseTool with post-processing capabilities
|
|
543
|
+
"""
|
|
544
|
+
logger.info(f"Post-tool reflection enabled for {tool.name}")
|
|
545
|
+
if isinstance(tool, PostToolProcessor):
|
|
546
|
+
# Already wrapped with this wrapper, just return it
|
|
547
|
+
return tool
|
|
548
|
+
|
|
549
|
+
# Required kwargs
|
|
550
|
+
agent = kwargs.get("agent")
|
|
551
|
+
user_query = kwargs.get("user_query", "")
|
|
552
|
+
|
|
553
|
+
if not agent:
|
|
554
|
+
logger.warning("Cannot wrap tool with PostToolProcessor: missing 'agent'")
|
|
555
|
+
return tool
|
|
556
|
+
|
|
557
|
+
# If the tool is already wrapped by another wrapper, we need to get the innermost tool
|
|
558
|
+
actual_tool = tool
|
|
559
|
+
|
|
560
|
+
return PostToolProcessor(
|
|
561
|
+
wrapped_tool=actual_tool,
|
|
562
|
+
user_query=user_query,
|
|
563
|
+
agent=agent,
|
|
564
|
+
response_processing_size_threshold=self.response_processing_size_threshold,
|
|
565
|
+
)
|
lfx/base/agents/events.py
CHANGED
|
@@ -387,7 +387,8 @@ async def process_agent_events(
|
|
|
387
387
|
# Store the initial message and capture the message id
|
|
388
388
|
agent_message = await send_message_callback(message=agent_message)
|
|
389
389
|
# Capture the original message id - this must stay consistent throughout if streaming
|
|
390
|
-
|
|
390
|
+
# Message may not contain id if the Agent is not connected to a Chat Output (_should_skip_message is True)
|
|
391
|
+
initial_message_id = agent_message.id if hasattr(agent_message, "id") else None
|
|
391
392
|
try:
|
|
392
393
|
# Create a mapping of run_ids to tool contents
|
|
393
394
|
tool_blocks_map: dict[str, ToolContent] = {}
|