lfx-nightly 0.1.13.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.
Files changed (237) hide show
  1. lfx/_assets/component_index.json +1 -1
  2. lfx/base/agents/agent.py +121 -29
  3. lfx/base/agents/altk_base_agent.py +380 -0
  4. lfx/base/agents/altk_tool_wrappers.py +565 -0
  5. lfx/base/agents/events.py +103 -35
  6. lfx/base/agents/utils.py +15 -2
  7. lfx/base/composio/composio_base.py +183 -233
  8. lfx/base/data/base_file.py +88 -21
  9. lfx/base/data/storage_utils.py +192 -0
  10. lfx/base/data/utils.py +178 -14
  11. lfx/base/datastax/__init__.py +5 -0
  12. lfx/{components/vectorstores/astradb.py → base/datastax/astradb_base.py} +84 -473
  13. lfx/base/embeddings/embeddings_class.py +113 -0
  14. lfx/base/io/chat.py +5 -4
  15. lfx/base/mcp/util.py +101 -15
  16. lfx/base/models/groq_constants.py +74 -58
  17. lfx/base/models/groq_model_discovery.py +265 -0
  18. lfx/base/models/model.py +1 -1
  19. lfx/base/models/model_input_constants.py +74 -7
  20. lfx/base/models/model_utils.py +100 -0
  21. lfx/base/models/ollama_constants.py +3 -0
  22. lfx/base/models/openai_constants.py +7 -0
  23. lfx/base/models/watsonx_constants.py +36 -0
  24. lfx/base/tools/run_flow.py +601 -129
  25. lfx/cli/commands.py +7 -4
  26. lfx/cli/common.py +2 -2
  27. lfx/cli/run.py +1 -1
  28. lfx/cli/script_loader.py +53 -11
  29. lfx/components/Notion/create_page.py +1 -1
  30. lfx/components/Notion/list_database_properties.py +1 -1
  31. lfx/components/Notion/list_pages.py +1 -1
  32. lfx/components/Notion/list_users.py +1 -1
  33. lfx/components/Notion/page_content_viewer.py +1 -1
  34. lfx/components/Notion/search.py +1 -1
  35. lfx/components/Notion/update_page_property.py +1 -1
  36. lfx/components/__init__.py +19 -5
  37. lfx/components/altk/__init__.py +34 -0
  38. lfx/components/altk/altk_agent.py +193 -0
  39. lfx/components/amazon/amazon_bedrock_converse.py +1 -1
  40. lfx/components/apify/apify_actor.py +4 -4
  41. lfx/components/composio/__init__.py +70 -18
  42. lfx/components/composio/apollo_composio.py +11 -0
  43. lfx/components/composio/bitbucket_composio.py +11 -0
  44. lfx/components/composio/canva_composio.py +11 -0
  45. lfx/components/composio/coda_composio.py +11 -0
  46. lfx/components/composio/composio_api.py +10 -0
  47. lfx/components/composio/discord_composio.py +1 -1
  48. lfx/components/composio/elevenlabs_composio.py +11 -0
  49. lfx/components/composio/exa_composio.py +11 -0
  50. lfx/components/composio/firecrawl_composio.py +11 -0
  51. lfx/components/composio/fireflies_composio.py +11 -0
  52. lfx/components/composio/gmail_composio.py +1 -1
  53. lfx/components/composio/googlebigquery_composio.py +11 -0
  54. lfx/components/composio/googlecalendar_composio.py +1 -1
  55. lfx/components/composio/googledocs_composio.py +1 -1
  56. lfx/components/composio/googlemeet_composio.py +1 -1
  57. lfx/components/composio/googlesheets_composio.py +1 -1
  58. lfx/components/composio/googletasks_composio.py +1 -1
  59. lfx/components/composio/heygen_composio.py +11 -0
  60. lfx/components/composio/mem0_composio.py +11 -0
  61. lfx/components/composio/peopledatalabs_composio.py +11 -0
  62. lfx/components/composio/perplexityai_composio.py +11 -0
  63. lfx/components/composio/serpapi_composio.py +11 -0
  64. lfx/components/composio/slack_composio.py +3 -574
  65. lfx/components/composio/slackbot_composio.py +1 -1
  66. lfx/components/composio/snowflake_composio.py +11 -0
  67. lfx/components/composio/tavily_composio.py +11 -0
  68. lfx/components/composio/youtube_composio.py +2 -2
  69. lfx/components/{agents → cuga}/__init__.py +5 -7
  70. lfx/components/cuga/cuga_agent.py +730 -0
  71. lfx/components/data/__init__.py +78 -28
  72. lfx/components/data_source/__init__.py +58 -0
  73. lfx/components/{data → data_source}/api_request.py +26 -3
  74. lfx/components/{data → data_source}/csv_to_data.py +15 -10
  75. lfx/components/{data → data_source}/json_to_data.py +15 -8
  76. lfx/components/{data → data_source}/news_search.py +1 -1
  77. lfx/components/{data → data_source}/rss.py +1 -1
  78. lfx/components/{data → data_source}/sql_executor.py +1 -1
  79. lfx/components/{data → data_source}/url.py +1 -1
  80. lfx/components/{data → data_source}/web_search.py +1 -1
  81. lfx/components/datastax/__init__.py +12 -6
  82. lfx/components/datastax/{astra_assistant_manager.py → astradb_assistant_manager.py} +1 -0
  83. lfx/components/datastax/astradb_chatmemory.py +40 -0
  84. lfx/components/datastax/astradb_cql.py +6 -32
  85. lfx/components/datastax/astradb_graph.py +10 -124
  86. lfx/components/datastax/astradb_tool.py +13 -53
  87. lfx/components/datastax/astradb_vectorstore.py +134 -977
  88. lfx/components/datastax/create_assistant.py +1 -0
  89. lfx/components/datastax/create_thread.py +1 -0
  90. lfx/components/datastax/dotenv.py +1 -0
  91. lfx/components/datastax/get_assistant.py +1 -0
  92. lfx/components/datastax/getenvvar.py +1 -0
  93. lfx/components/datastax/graph_rag.py +1 -1
  94. lfx/components/datastax/hcd.py +1 -1
  95. lfx/components/datastax/list_assistants.py +1 -0
  96. lfx/components/datastax/run.py +1 -0
  97. lfx/components/deactivated/json_document_builder.py +1 -1
  98. lfx/components/elastic/elasticsearch.py +1 -1
  99. lfx/components/elastic/opensearch_multimodal.py +1575 -0
  100. lfx/components/files_and_knowledge/__init__.py +47 -0
  101. lfx/components/{data → files_and_knowledge}/directory.py +1 -1
  102. lfx/components/{data → files_and_knowledge}/file.py +246 -18
  103. lfx/components/{knowledge_bases → files_and_knowledge}/ingestion.py +17 -9
  104. lfx/components/{knowledge_bases → files_and_knowledge}/retrieval.py +18 -10
  105. lfx/components/{data → files_and_knowledge}/save_file.py +142 -22
  106. lfx/components/flow_controls/__init__.py +58 -0
  107. lfx/components/{logic → flow_controls}/conditional_router.py +1 -1
  108. lfx/components/{logic → flow_controls}/loop.py +47 -9
  109. lfx/components/flow_controls/run_flow.py +108 -0
  110. lfx/components/glean/glean_search_api.py +1 -1
  111. lfx/components/groq/groq.py +35 -28
  112. lfx/components/helpers/__init__.py +102 -0
  113. lfx/components/ibm/watsonx.py +25 -21
  114. lfx/components/input_output/__init__.py +3 -1
  115. lfx/components/input_output/chat.py +12 -3
  116. lfx/components/input_output/chat_output.py +12 -4
  117. lfx/components/input_output/text.py +1 -1
  118. lfx/components/input_output/text_output.py +1 -1
  119. lfx/components/{data → input_output}/webhook.py +1 -1
  120. lfx/components/knowledge_bases/__init__.py +59 -4
  121. lfx/components/langchain_utilities/character.py +1 -1
  122. lfx/components/langchain_utilities/csv_agent.py +84 -16
  123. lfx/components/langchain_utilities/json_agent.py +67 -12
  124. lfx/components/langchain_utilities/language_recursive.py +1 -1
  125. lfx/components/llm_operations/__init__.py +46 -0
  126. lfx/components/{processing → llm_operations}/batch_run.py +1 -1
  127. lfx/components/{processing → llm_operations}/lambda_filter.py +1 -1
  128. lfx/components/{logic → llm_operations}/llm_conditional_router.py +1 -1
  129. lfx/components/{processing/llm_router.py → llm_operations/llm_selector.py} +3 -3
  130. lfx/components/{processing → llm_operations}/structured_output.py +56 -18
  131. lfx/components/logic/__init__.py +126 -0
  132. lfx/components/mem0/mem0_chat_memory.py +11 -0
  133. lfx/components/mistral/mistral_embeddings.py +1 -1
  134. lfx/components/models/__init__.py +64 -9
  135. lfx/components/models_and_agents/__init__.py +49 -0
  136. lfx/components/{agents → models_and_agents}/agent.py +49 -6
  137. lfx/components/models_and_agents/embedding_model.py +423 -0
  138. lfx/components/models_and_agents/language_model.py +398 -0
  139. lfx/components/{agents → models_and_agents}/mcp_component.py +84 -45
  140. lfx/components/{helpers → models_and_agents}/memory.py +1 -1
  141. lfx/components/nvidia/system_assist.py +1 -1
  142. lfx/components/olivya/olivya.py +1 -1
  143. lfx/components/ollama/ollama.py +235 -14
  144. lfx/components/openrouter/openrouter.py +49 -147
  145. lfx/components/processing/__init__.py +9 -57
  146. lfx/components/processing/converter.py +1 -1
  147. lfx/components/processing/dataframe_operations.py +1 -1
  148. lfx/components/processing/parse_json_data.py +2 -2
  149. lfx/components/processing/parser.py +7 -2
  150. lfx/components/processing/split_text.py +1 -1
  151. lfx/components/qdrant/qdrant.py +1 -1
  152. lfx/components/redis/redis.py +1 -1
  153. lfx/components/twelvelabs/split_video.py +10 -0
  154. lfx/components/twelvelabs/video_file.py +12 -0
  155. lfx/components/utilities/__init__.py +43 -0
  156. lfx/components/{helpers → utilities}/calculator_core.py +1 -1
  157. lfx/components/{helpers → utilities}/current_date.py +1 -1
  158. lfx/components/{processing → utilities}/python_repl_core.py +1 -1
  159. lfx/components/vectorstores/__init__.py +0 -6
  160. lfx/components/vectorstores/local_db.py +9 -0
  161. lfx/components/youtube/youtube_transcripts.py +118 -30
  162. lfx/custom/custom_component/component.py +60 -3
  163. lfx/custom/custom_component/custom_component.py +68 -6
  164. lfx/field_typing/constants.py +1 -0
  165. lfx/graph/edge/base.py +45 -22
  166. lfx/graph/graph/base.py +5 -2
  167. lfx/graph/graph/schema.py +3 -2
  168. lfx/graph/state/model.py +15 -2
  169. lfx/graph/utils.py +6 -0
  170. lfx/graph/vertex/base.py +4 -1
  171. lfx/graph/vertex/param_handler.py +10 -7
  172. lfx/graph/vertex/vertex_types.py +1 -1
  173. lfx/helpers/__init__.py +12 -0
  174. lfx/helpers/flow.py +117 -0
  175. lfx/inputs/input_mixin.py +24 -1
  176. lfx/inputs/inputs.py +13 -1
  177. lfx/interface/components.py +161 -83
  178. lfx/io/schema.py +6 -0
  179. lfx/log/logger.py +5 -3
  180. lfx/schema/schema.py +5 -0
  181. lfx/services/database/__init__.py +5 -0
  182. lfx/services/database/service.py +25 -0
  183. lfx/services/deps.py +87 -22
  184. lfx/services/manager.py +19 -6
  185. lfx/services/mcp_composer/service.py +998 -157
  186. lfx/services/session.py +5 -0
  187. lfx/services/settings/base.py +51 -7
  188. lfx/services/settings/constants.py +8 -0
  189. lfx/services/storage/local.py +76 -46
  190. lfx/services/storage/service.py +152 -29
  191. lfx/template/field/base.py +3 -0
  192. lfx/utils/ssrf_protection.py +384 -0
  193. lfx/utils/validate_cloud.py +26 -0
  194. {lfx_nightly-0.1.13.dev0.dist-info → lfx_nightly-0.2.0.dev26.dist-info}/METADATA +38 -22
  195. {lfx_nightly-0.1.13.dev0.dist-info → lfx_nightly-0.2.0.dev26.dist-info}/RECORD +210 -196
  196. {lfx_nightly-0.1.13.dev0.dist-info → lfx_nightly-0.2.0.dev26.dist-info}/WHEEL +1 -1
  197. lfx/components/agents/cuga_agent.py +0 -1013
  198. lfx/components/datastax/astra_db.py +0 -77
  199. lfx/components/datastax/cassandra.py +0 -92
  200. lfx/components/logic/run_flow.py +0 -71
  201. lfx/components/models/embedding_model.py +0 -114
  202. lfx/components/models/language_model.py +0 -144
  203. lfx/components/vectorstores/astradb_graph.py +0 -326
  204. lfx/components/vectorstores/cassandra.py +0 -264
  205. lfx/components/vectorstores/cassandra_graph.py +0 -238
  206. lfx/components/vectorstores/chroma.py +0 -167
  207. lfx/components/vectorstores/clickhouse.py +0 -135
  208. lfx/components/vectorstores/couchbase.py +0 -102
  209. lfx/components/vectorstores/elasticsearch.py +0 -267
  210. lfx/components/vectorstores/faiss.py +0 -111
  211. lfx/components/vectorstores/graph_rag.py +0 -141
  212. lfx/components/vectorstores/hcd.py +0 -314
  213. lfx/components/vectorstores/milvus.py +0 -115
  214. lfx/components/vectorstores/mongodb_atlas.py +0 -213
  215. lfx/components/vectorstores/opensearch.py +0 -243
  216. lfx/components/vectorstores/pgvector.py +0 -72
  217. lfx/components/vectorstores/pinecone.py +0 -134
  218. lfx/components/vectorstores/qdrant.py +0 -109
  219. lfx/components/vectorstores/supabase.py +0 -76
  220. lfx/components/vectorstores/upstash.py +0 -124
  221. lfx/components/vectorstores/vectara.py +0 -97
  222. lfx/components/vectorstores/vectara_rag.py +0 -164
  223. lfx/components/vectorstores/weaviate.py +0 -89
  224. /lfx/components/{data → data_source}/mock_data.py +0 -0
  225. /lfx/components/datastax/{astra_vectorize.py → astradb_vectorize.py} +0 -0
  226. /lfx/components/{logic → flow_controls}/data_conditional_router.py +0 -0
  227. /lfx/components/{logic → flow_controls}/flow_tool.py +0 -0
  228. /lfx/components/{logic → flow_controls}/listen.py +0 -0
  229. /lfx/components/{logic → flow_controls}/notify.py +0 -0
  230. /lfx/components/{logic → flow_controls}/pass_message.py +0 -0
  231. /lfx/components/{logic → flow_controls}/sub_flow.py +0 -0
  232. /lfx/components/{processing → models_and_agents}/prompt.py +0 -0
  233. /lfx/components/{helpers → processing}/create_list.py +0 -0
  234. /lfx/components/{helpers → processing}/output_parser.py +0 -0
  235. /lfx/components/{helpers → processing}/store_message.py +0 -0
  236. /lfx/components/{helpers → utilities}/id_generator.py +0 -0
  237. {lfx_nightly-0.1.13.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
+ )