lfx-nightly 0.2.0.dev25__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of lfx-nightly might be problematic. Click here for more details.

Files changed (769) hide show
  1. lfx/__init__.py +0 -0
  2. lfx/__main__.py +25 -0
  3. lfx/_assets/component_index.json +1 -0
  4. lfx/base/__init__.py +0 -0
  5. lfx/base/agents/__init__.py +0 -0
  6. lfx/base/agents/agent.py +375 -0
  7. lfx/base/agents/altk_base_agent.py +380 -0
  8. lfx/base/agents/altk_tool_wrappers.py +565 -0
  9. lfx/base/agents/callback.py +130 -0
  10. lfx/base/agents/context.py +109 -0
  11. lfx/base/agents/crewai/__init__.py +0 -0
  12. lfx/base/agents/crewai/crew.py +231 -0
  13. lfx/base/agents/crewai/tasks.py +12 -0
  14. lfx/base/agents/default_prompts.py +23 -0
  15. lfx/base/agents/errors.py +15 -0
  16. lfx/base/agents/events.py +430 -0
  17. lfx/base/agents/utils.py +237 -0
  18. lfx/base/astra_assistants/__init__.py +0 -0
  19. lfx/base/astra_assistants/util.py +171 -0
  20. lfx/base/chains/__init__.py +0 -0
  21. lfx/base/chains/model.py +19 -0
  22. lfx/base/composio/__init__.py +0 -0
  23. lfx/base/composio/composio_base.py +2584 -0
  24. lfx/base/compressors/__init__.py +0 -0
  25. lfx/base/compressors/model.py +60 -0
  26. lfx/base/constants.py +46 -0
  27. lfx/base/curl/__init__.py +0 -0
  28. lfx/base/curl/parse.py +188 -0
  29. lfx/base/data/__init__.py +5 -0
  30. lfx/base/data/base_file.py +810 -0
  31. lfx/base/data/docling_utils.py +338 -0
  32. lfx/base/data/storage_utils.py +192 -0
  33. lfx/base/data/utils.py +362 -0
  34. lfx/base/datastax/__init__.py +5 -0
  35. lfx/base/datastax/astradb_base.py +896 -0
  36. lfx/base/document_transformers/__init__.py +0 -0
  37. lfx/base/document_transformers/model.py +43 -0
  38. lfx/base/embeddings/__init__.py +0 -0
  39. lfx/base/embeddings/aiml_embeddings.py +62 -0
  40. lfx/base/embeddings/embeddings_class.py +113 -0
  41. lfx/base/embeddings/model.py +26 -0
  42. lfx/base/flow_processing/__init__.py +0 -0
  43. lfx/base/flow_processing/utils.py +86 -0
  44. lfx/base/huggingface/__init__.py +0 -0
  45. lfx/base/huggingface/model_bridge.py +133 -0
  46. lfx/base/io/__init__.py +0 -0
  47. lfx/base/io/chat.py +21 -0
  48. lfx/base/io/text.py +22 -0
  49. lfx/base/knowledge_bases/__init__.py +3 -0
  50. lfx/base/knowledge_bases/knowledge_base_utils.py +137 -0
  51. lfx/base/langchain_utilities/__init__.py +0 -0
  52. lfx/base/langchain_utilities/model.py +35 -0
  53. lfx/base/langchain_utilities/spider_constants.py +1 -0
  54. lfx/base/langwatch/__init__.py +0 -0
  55. lfx/base/langwatch/utils.py +18 -0
  56. lfx/base/mcp/__init__.py +0 -0
  57. lfx/base/mcp/constants.py +2 -0
  58. lfx/base/mcp/util.py +1659 -0
  59. lfx/base/memory/__init__.py +0 -0
  60. lfx/base/memory/memory.py +49 -0
  61. lfx/base/memory/model.py +38 -0
  62. lfx/base/models/__init__.py +3 -0
  63. lfx/base/models/aiml_constants.py +51 -0
  64. lfx/base/models/anthropic_constants.py +51 -0
  65. lfx/base/models/aws_constants.py +151 -0
  66. lfx/base/models/chat_result.py +76 -0
  67. lfx/base/models/cometapi_constants.py +54 -0
  68. lfx/base/models/google_generative_ai_constants.py +70 -0
  69. lfx/base/models/google_generative_ai_model.py +38 -0
  70. lfx/base/models/groq_constants.py +150 -0
  71. lfx/base/models/groq_model_discovery.py +265 -0
  72. lfx/base/models/model.py +375 -0
  73. lfx/base/models/model_input_constants.py +378 -0
  74. lfx/base/models/model_metadata.py +41 -0
  75. lfx/base/models/model_utils.py +108 -0
  76. lfx/base/models/novita_constants.py +35 -0
  77. lfx/base/models/ollama_constants.py +52 -0
  78. lfx/base/models/openai_constants.py +129 -0
  79. lfx/base/models/sambanova_constants.py +18 -0
  80. lfx/base/models/watsonx_constants.py +36 -0
  81. lfx/base/processing/__init__.py +0 -0
  82. lfx/base/prompts/__init__.py +0 -0
  83. lfx/base/prompts/api_utils.py +224 -0
  84. lfx/base/prompts/utils.py +61 -0
  85. lfx/base/textsplitters/__init__.py +0 -0
  86. lfx/base/textsplitters/model.py +28 -0
  87. lfx/base/tools/__init__.py +0 -0
  88. lfx/base/tools/base.py +26 -0
  89. lfx/base/tools/component_tool.py +325 -0
  90. lfx/base/tools/constants.py +49 -0
  91. lfx/base/tools/flow_tool.py +132 -0
  92. lfx/base/tools/run_flow.py +698 -0
  93. lfx/base/vectorstores/__init__.py +0 -0
  94. lfx/base/vectorstores/model.py +193 -0
  95. lfx/base/vectorstores/utils.py +22 -0
  96. lfx/base/vectorstores/vector_store_connection_decorator.py +52 -0
  97. lfx/cli/__init__.py +5 -0
  98. lfx/cli/commands.py +327 -0
  99. lfx/cli/common.py +650 -0
  100. lfx/cli/run.py +506 -0
  101. lfx/cli/script_loader.py +289 -0
  102. lfx/cli/serve_app.py +546 -0
  103. lfx/cli/validation.py +69 -0
  104. lfx/components/FAISS/__init__.py +34 -0
  105. lfx/components/FAISS/faiss.py +111 -0
  106. lfx/components/Notion/__init__.py +19 -0
  107. lfx/components/Notion/add_content_to_page.py +269 -0
  108. lfx/components/Notion/create_page.py +94 -0
  109. lfx/components/Notion/list_database_properties.py +68 -0
  110. lfx/components/Notion/list_pages.py +122 -0
  111. lfx/components/Notion/list_users.py +77 -0
  112. lfx/components/Notion/page_content_viewer.py +93 -0
  113. lfx/components/Notion/search.py +111 -0
  114. lfx/components/Notion/update_page_property.py +114 -0
  115. lfx/components/__init__.py +428 -0
  116. lfx/components/_importing.py +42 -0
  117. lfx/components/agentql/__init__.py +3 -0
  118. lfx/components/agentql/agentql_api.py +151 -0
  119. lfx/components/aiml/__init__.py +37 -0
  120. lfx/components/aiml/aiml.py +115 -0
  121. lfx/components/aiml/aiml_embeddings.py +37 -0
  122. lfx/components/altk/__init__.py +34 -0
  123. lfx/components/altk/altk_agent.py +193 -0
  124. lfx/components/amazon/__init__.py +36 -0
  125. lfx/components/amazon/amazon_bedrock_converse.py +195 -0
  126. lfx/components/amazon/amazon_bedrock_embedding.py +109 -0
  127. lfx/components/amazon/amazon_bedrock_model.py +130 -0
  128. lfx/components/amazon/s3_bucket_uploader.py +211 -0
  129. lfx/components/anthropic/__init__.py +34 -0
  130. lfx/components/anthropic/anthropic.py +187 -0
  131. lfx/components/apify/__init__.py +5 -0
  132. lfx/components/apify/apify_actor.py +325 -0
  133. lfx/components/arxiv/__init__.py +3 -0
  134. lfx/components/arxiv/arxiv.py +169 -0
  135. lfx/components/assemblyai/__init__.py +46 -0
  136. lfx/components/assemblyai/assemblyai_get_subtitles.py +83 -0
  137. lfx/components/assemblyai/assemblyai_lemur.py +183 -0
  138. lfx/components/assemblyai/assemblyai_list_transcripts.py +95 -0
  139. lfx/components/assemblyai/assemblyai_poll_transcript.py +72 -0
  140. lfx/components/assemblyai/assemblyai_start_transcript.py +188 -0
  141. lfx/components/azure/__init__.py +37 -0
  142. lfx/components/azure/azure_openai.py +95 -0
  143. lfx/components/azure/azure_openai_embeddings.py +83 -0
  144. lfx/components/baidu/__init__.py +32 -0
  145. lfx/components/baidu/baidu_qianfan_chat.py +113 -0
  146. lfx/components/bing/__init__.py +3 -0
  147. lfx/components/bing/bing_search_api.py +61 -0
  148. lfx/components/cassandra/__init__.py +40 -0
  149. lfx/components/cassandra/cassandra.py +264 -0
  150. lfx/components/cassandra/cassandra_chat.py +92 -0
  151. lfx/components/cassandra/cassandra_graph.py +238 -0
  152. lfx/components/chains/__init__.py +3 -0
  153. lfx/components/chroma/__init__.py +34 -0
  154. lfx/components/chroma/chroma.py +169 -0
  155. lfx/components/cleanlab/__init__.py +40 -0
  156. lfx/components/cleanlab/cleanlab_evaluator.py +155 -0
  157. lfx/components/cleanlab/cleanlab_rag_evaluator.py +254 -0
  158. lfx/components/cleanlab/cleanlab_remediator.py +131 -0
  159. lfx/components/clickhouse/__init__.py +34 -0
  160. lfx/components/clickhouse/clickhouse.py +135 -0
  161. lfx/components/cloudflare/__init__.py +32 -0
  162. lfx/components/cloudflare/cloudflare.py +81 -0
  163. lfx/components/cohere/__init__.py +40 -0
  164. lfx/components/cohere/cohere_embeddings.py +81 -0
  165. lfx/components/cohere/cohere_models.py +46 -0
  166. lfx/components/cohere/cohere_rerank.py +51 -0
  167. lfx/components/cometapi/__init__.py +32 -0
  168. lfx/components/cometapi/cometapi.py +166 -0
  169. lfx/components/composio/__init__.py +222 -0
  170. lfx/components/composio/agentql_composio.py +11 -0
  171. lfx/components/composio/agiled_composio.py +11 -0
  172. lfx/components/composio/airtable_composio.py +11 -0
  173. lfx/components/composio/apollo_composio.py +11 -0
  174. lfx/components/composio/asana_composio.py +11 -0
  175. lfx/components/composio/attio_composio.py +11 -0
  176. lfx/components/composio/bitbucket_composio.py +11 -0
  177. lfx/components/composio/bolna_composio.py +11 -0
  178. lfx/components/composio/brightdata_composio.py +11 -0
  179. lfx/components/composio/calendly_composio.py +11 -0
  180. lfx/components/composio/canva_composio.py +11 -0
  181. lfx/components/composio/canvas_composio.py +11 -0
  182. lfx/components/composio/coda_composio.py +11 -0
  183. lfx/components/composio/composio_api.py +278 -0
  184. lfx/components/composio/contentful_composio.py +11 -0
  185. lfx/components/composio/digicert_composio.py +11 -0
  186. lfx/components/composio/discord_composio.py +11 -0
  187. lfx/components/composio/dropbox_compnent.py +11 -0
  188. lfx/components/composio/elevenlabs_composio.py +11 -0
  189. lfx/components/composio/exa_composio.py +11 -0
  190. lfx/components/composio/figma_composio.py +11 -0
  191. lfx/components/composio/finage_composio.py +11 -0
  192. lfx/components/composio/firecrawl_composio.py +11 -0
  193. lfx/components/composio/fireflies_composio.py +11 -0
  194. lfx/components/composio/fixer_composio.py +11 -0
  195. lfx/components/composio/flexisign_composio.py +11 -0
  196. lfx/components/composio/freshdesk_composio.py +11 -0
  197. lfx/components/composio/github_composio.py +11 -0
  198. lfx/components/composio/gmail_composio.py +38 -0
  199. lfx/components/composio/googlebigquery_composio.py +11 -0
  200. lfx/components/composio/googlecalendar_composio.py +11 -0
  201. lfx/components/composio/googleclassroom_composio.py +11 -0
  202. lfx/components/composio/googledocs_composio.py +11 -0
  203. lfx/components/composio/googlemeet_composio.py +11 -0
  204. lfx/components/composio/googlesheets_composio.py +11 -0
  205. lfx/components/composio/googletasks_composio.py +8 -0
  206. lfx/components/composio/heygen_composio.py +11 -0
  207. lfx/components/composio/instagram_composio.py +11 -0
  208. lfx/components/composio/jira_composio.py +11 -0
  209. lfx/components/composio/jotform_composio.py +11 -0
  210. lfx/components/composio/klaviyo_composio.py +11 -0
  211. lfx/components/composio/linear_composio.py +11 -0
  212. lfx/components/composio/listennotes_composio.py +11 -0
  213. lfx/components/composio/mem0_composio.py +11 -0
  214. lfx/components/composio/miro_composio.py +11 -0
  215. lfx/components/composio/missive_composio.py +11 -0
  216. lfx/components/composio/notion_composio.py +11 -0
  217. lfx/components/composio/onedrive_composio.py +11 -0
  218. lfx/components/composio/outlook_composio.py +11 -0
  219. lfx/components/composio/pandadoc_composio.py +11 -0
  220. lfx/components/composio/peopledatalabs_composio.py +11 -0
  221. lfx/components/composio/perplexityai_composio.py +11 -0
  222. lfx/components/composio/reddit_composio.py +11 -0
  223. lfx/components/composio/serpapi_composio.py +11 -0
  224. lfx/components/composio/slack_composio.py +11 -0
  225. lfx/components/composio/slackbot_composio.py +11 -0
  226. lfx/components/composio/snowflake_composio.py +11 -0
  227. lfx/components/composio/supabase_composio.py +11 -0
  228. lfx/components/composio/tavily_composio.py +11 -0
  229. lfx/components/composio/timelinesai_composio.py +11 -0
  230. lfx/components/composio/todoist_composio.py +11 -0
  231. lfx/components/composio/wrike_composio.py +11 -0
  232. lfx/components/composio/youtube_composio.py +11 -0
  233. lfx/components/confluence/__init__.py +3 -0
  234. lfx/components/confluence/confluence.py +84 -0
  235. lfx/components/couchbase/__init__.py +34 -0
  236. lfx/components/couchbase/couchbase.py +102 -0
  237. lfx/components/crewai/__init__.py +49 -0
  238. lfx/components/crewai/crewai.py +108 -0
  239. lfx/components/crewai/hierarchical_crew.py +47 -0
  240. lfx/components/crewai/hierarchical_task.py +45 -0
  241. lfx/components/crewai/sequential_crew.py +53 -0
  242. lfx/components/crewai/sequential_task.py +74 -0
  243. lfx/components/crewai/sequential_task_agent.py +144 -0
  244. lfx/components/cuga/__init__.py +34 -0
  245. lfx/components/cuga/cuga_agent.py +730 -0
  246. lfx/components/custom_component/__init__.py +34 -0
  247. lfx/components/custom_component/custom_component.py +31 -0
  248. lfx/components/data/__init__.py +114 -0
  249. lfx/components/data_source/__init__.py +58 -0
  250. lfx/components/data_source/api_request.py +577 -0
  251. lfx/components/data_source/csv_to_data.py +101 -0
  252. lfx/components/data_source/json_to_data.py +106 -0
  253. lfx/components/data_source/mock_data.py +398 -0
  254. lfx/components/data_source/news_search.py +166 -0
  255. lfx/components/data_source/rss.py +71 -0
  256. lfx/components/data_source/sql_executor.py +101 -0
  257. lfx/components/data_source/url.py +311 -0
  258. lfx/components/data_source/web_search.py +326 -0
  259. lfx/components/datastax/__init__.py +76 -0
  260. lfx/components/datastax/astradb_assistant_manager.py +307 -0
  261. lfx/components/datastax/astradb_chatmemory.py +40 -0
  262. lfx/components/datastax/astradb_cql.py +288 -0
  263. lfx/components/datastax/astradb_graph.py +217 -0
  264. lfx/components/datastax/astradb_tool.py +378 -0
  265. lfx/components/datastax/astradb_vectorize.py +122 -0
  266. lfx/components/datastax/astradb_vectorstore.py +449 -0
  267. lfx/components/datastax/create_assistant.py +59 -0
  268. lfx/components/datastax/create_thread.py +33 -0
  269. lfx/components/datastax/dotenv.py +36 -0
  270. lfx/components/datastax/get_assistant.py +38 -0
  271. lfx/components/datastax/getenvvar.py +31 -0
  272. lfx/components/datastax/graph_rag.py +141 -0
  273. lfx/components/datastax/hcd.py +315 -0
  274. lfx/components/datastax/list_assistants.py +26 -0
  275. lfx/components/datastax/run.py +90 -0
  276. lfx/components/deactivated/__init__.py +15 -0
  277. lfx/components/deactivated/amazon_kendra.py +66 -0
  278. lfx/components/deactivated/chat_litellm_model.py +158 -0
  279. lfx/components/deactivated/code_block_extractor.py +26 -0
  280. lfx/components/deactivated/documents_to_data.py +22 -0
  281. lfx/components/deactivated/embed.py +16 -0
  282. lfx/components/deactivated/extract_key_from_data.py +46 -0
  283. lfx/components/deactivated/json_document_builder.py +57 -0
  284. lfx/components/deactivated/list_flows.py +20 -0
  285. lfx/components/deactivated/mcp_sse.py +61 -0
  286. lfx/components/deactivated/mcp_stdio.py +62 -0
  287. lfx/components/deactivated/merge_data.py +93 -0
  288. lfx/components/deactivated/message.py +37 -0
  289. lfx/components/deactivated/metal.py +54 -0
  290. lfx/components/deactivated/multi_query.py +59 -0
  291. lfx/components/deactivated/retriever.py +43 -0
  292. lfx/components/deactivated/selective_passthrough.py +77 -0
  293. lfx/components/deactivated/should_run_next.py +40 -0
  294. lfx/components/deactivated/split_text.py +63 -0
  295. lfx/components/deactivated/store_message.py +24 -0
  296. lfx/components/deactivated/sub_flow.py +124 -0
  297. lfx/components/deactivated/vectara_self_query.py +76 -0
  298. lfx/components/deactivated/vector_store.py +24 -0
  299. lfx/components/deepseek/__init__.py +34 -0
  300. lfx/components/deepseek/deepseek.py +136 -0
  301. lfx/components/docling/__init__.py +43 -0
  302. lfx/components/docling/chunk_docling_document.py +186 -0
  303. lfx/components/docling/docling_inline.py +238 -0
  304. lfx/components/docling/docling_remote.py +195 -0
  305. lfx/components/docling/export_docling_document.py +117 -0
  306. lfx/components/documentloaders/__init__.py +3 -0
  307. lfx/components/duckduckgo/__init__.py +3 -0
  308. lfx/components/duckduckgo/duck_duck_go_search_run.py +92 -0
  309. lfx/components/elastic/__init__.py +37 -0
  310. lfx/components/elastic/elasticsearch.py +267 -0
  311. lfx/components/elastic/opensearch.py +789 -0
  312. lfx/components/elastic/opensearch_multimodal.py +1575 -0
  313. lfx/components/embeddings/__init__.py +37 -0
  314. lfx/components/embeddings/similarity.py +77 -0
  315. lfx/components/embeddings/text_embedder.py +65 -0
  316. lfx/components/exa/__init__.py +3 -0
  317. lfx/components/exa/exa_search.py +68 -0
  318. lfx/components/files_and_knowledge/__init__.py +47 -0
  319. lfx/components/files_and_knowledge/directory.py +113 -0
  320. lfx/components/files_and_knowledge/file.py +841 -0
  321. lfx/components/files_and_knowledge/ingestion.py +694 -0
  322. lfx/components/files_and_knowledge/retrieval.py +264 -0
  323. lfx/components/files_and_knowledge/save_file.py +746 -0
  324. lfx/components/firecrawl/__init__.py +43 -0
  325. lfx/components/firecrawl/firecrawl_crawl_api.py +88 -0
  326. lfx/components/firecrawl/firecrawl_extract_api.py +136 -0
  327. lfx/components/firecrawl/firecrawl_map_api.py +89 -0
  328. lfx/components/firecrawl/firecrawl_scrape_api.py +73 -0
  329. lfx/components/flow_controls/__init__.py +58 -0
  330. lfx/components/flow_controls/conditional_router.py +208 -0
  331. lfx/components/flow_controls/data_conditional_router.py +126 -0
  332. lfx/components/flow_controls/flow_tool.py +111 -0
  333. lfx/components/flow_controls/listen.py +29 -0
  334. lfx/components/flow_controls/loop.py +163 -0
  335. lfx/components/flow_controls/notify.py +88 -0
  336. lfx/components/flow_controls/pass_message.py +36 -0
  337. lfx/components/flow_controls/run_flow.py +108 -0
  338. lfx/components/flow_controls/sub_flow.py +115 -0
  339. lfx/components/git/__init__.py +4 -0
  340. lfx/components/git/git.py +262 -0
  341. lfx/components/git/gitextractor.py +196 -0
  342. lfx/components/glean/__init__.py +3 -0
  343. lfx/components/glean/glean_search_api.py +173 -0
  344. lfx/components/google/__init__.py +17 -0
  345. lfx/components/google/gmail.py +193 -0
  346. lfx/components/google/google_bq_sql_executor.py +157 -0
  347. lfx/components/google/google_drive.py +92 -0
  348. lfx/components/google/google_drive_search.py +152 -0
  349. lfx/components/google/google_generative_ai.py +144 -0
  350. lfx/components/google/google_generative_ai_embeddings.py +141 -0
  351. lfx/components/google/google_oauth_token.py +89 -0
  352. lfx/components/google/google_search_api_core.py +68 -0
  353. lfx/components/google/google_serper_api_core.py +74 -0
  354. lfx/components/groq/__init__.py +34 -0
  355. lfx/components/groq/groq.py +143 -0
  356. lfx/components/helpers/__init__.py +154 -0
  357. lfx/components/homeassistant/__init__.py +7 -0
  358. lfx/components/homeassistant/home_assistant_control.py +152 -0
  359. lfx/components/homeassistant/list_home_assistant_states.py +137 -0
  360. lfx/components/huggingface/__init__.py +37 -0
  361. lfx/components/huggingface/huggingface.py +199 -0
  362. lfx/components/huggingface/huggingface_inference_api.py +106 -0
  363. lfx/components/ibm/__init__.py +34 -0
  364. lfx/components/ibm/watsonx.py +207 -0
  365. lfx/components/ibm/watsonx_embeddings.py +135 -0
  366. lfx/components/icosacomputing/__init__.py +5 -0
  367. lfx/components/icosacomputing/combinatorial_reasoner.py +84 -0
  368. lfx/components/input_output/__init__.py +40 -0
  369. lfx/components/input_output/chat.py +109 -0
  370. lfx/components/input_output/chat_output.py +184 -0
  371. lfx/components/input_output/text.py +27 -0
  372. lfx/components/input_output/text_output.py +29 -0
  373. lfx/components/input_output/webhook.py +56 -0
  374. lfx/components/jigsawstack/__init__.py +23 -0
  375. lfx/components/jigsawstack/ai_scrape.py +126 -0
  376. lfx/components/jigsawstack/ai_web_search.py +136 -0
  377. lfx/components/jigsawstack/file_read.py +115 -0
  378. lfx/components/jigsawstack/file_upload.py +94 -0
  379. lfx/components/jigsawstack/image_generation.py +205 -0
  380. lfx/components/jigsawstack/nsfw.py +60 -0
  381. lfx/components/jigsawstack/object_detection.py +124 -0
  382. lfx/components/jigsawstack/sentiment.py +112 -0
  383. lfx/components/jigsawstack/text_to_sql.py +90 -0
  384. lfx/components/jigsawstack/text_translate.py +77 -0
  385. lfx/components/jigsawstack/vocr.py +107 -0
  386. lfx/components/knowledge_bases/__init__.py +89 -0
  387. lfx/components/langchain_utilities/__init__.py +109 -0
  388. lfx/components/langchain_utilities/character.py +53 -0
  389. lfx/components/langchain_utilities/conversation.py +59 -0
  390. lfx/components/langchain_utilities/csv_agent.py +175 -0
  391. lfx/components/langchain_utilities/fake_embeddings.py +26 -0
  392. lfx/components/langchain_utilities/html_link_extractor.py +35 -0
  393. lfx/components/langchain_utilities/json_agent.py +100 -0
  394. lfx/components/langchain_utilities/langchain_hub.py +126 -0
  395. lfx/components/langchain_utilities/language_recursive.py +49 -0
  396. lfx/components/langchain_utilities/language_semantic.py +138 -0
  397. lfx/components/langchain_utilities/llm_checker.py +39 -0
  398. lfx/components/langchain_utilities/llm_math.py +42 -0
  399. lfx/components/langchain_utilities/natural_language.py +61 -0
  400. lfx/components/langchain_utilities/openai_tools.py +53 -0
  401. lfx/components/langchain_utilities/openapi.py +48 -0
  402. lfx/components/langchain_utilities/recursive_character.py +60 -0
  403. lfx/components/langchain_utilities/retrieval_qa.py +83 -0
  404. lfx/components/langchain_utilities/runnable_executor.py +137 -0
  405. lfx/components/langchain_utilities/self_query.py +80 -0
  406. lfx/components/langchain_utilities/spider.py +142 -0
  407. lfx/components/langchain_utilities/sql.py +40 -0
  408. lfx/components/langchain_utilities/sql_database.py +35 -0
  409. lfx/components/langchain_utilities/sql_generator.py +78 -0
  410. lfx/components/langchain_utilities/tool_calling.py +59 -0
  411. lfx/components/langchain_utilities/vector_store_info.py +49 -0
  412. lfx/components/langchain_utilities/vector_store_router.py +33 -0
  413. lfx/components/langchain_utilities/xml_agent.py +71 -0
  414. lfx/components/langwatch/__init__.py +3 -0
  415. lfx/components/langwatch/langwatch.py +278 -0
  416. lfx/components/link_extractors/__init__.py +3 -0
  417. lfx/components/llm_operations/__init__.py +46 -0
  418. lfx/components/llm_operations/batch_run.py +205 -0
  419. lfx/components/llm_operations/lambda_filter.py +218 -0
  420. lfx/components/llm_operations/llm_conditional_router.py +421 -0
  421. lfx/components/llm_operations/llm_selector.py +499 -0
  422. lfx/components/llm_operations/structured_output.py +244 -0
  423. lfx/components/lmstudio/__init__.py +34 -0
  424. lfx/components/lmstudio/lmstudioembeddings.py +89 -0
  425. lfx/components/lmstudio/lmstudiomodel.py +133 -0
  426. lfx/components/logic/__init__.py +181 -0
  427. lfx/components/maritalk/__init__.py +32 -0
  428. lfx/components/maritalk/maritalk.py +52 -0
  429. lfx/components/mem0/__init__.py +3 -0
  430. lfx/components/mem0/mem0_chat_memory.py +147 -0
  431. lfx/components/milvus/__init__.py +34 -0
  432. lfx/components/milvus/milvus.py +115 -0
  433. lfx/components/mistral/__init__.py +37 -0
  434. lfx/components/mistral/mistral.py +114 -0
  435. lfx/components/mistral/mistral_embeddings.py +58 -0
  436. lfx/components/models/__init__.py +89 -0
  437. lfx/components/models_and_agents/__init__.py +49 -0
  438. lfx/components/models_and_agents/agent.py +644 -0
  439. lfx/components/models_and_agents/embedding_model.py +423 -0
  440. lfx/components/models_and_agents/language_model.py +398 -0
  441. lfx/components/models_and_agents/mcp_component.py +594 -0
  442. lfx/components/models_and_agents/memory.py +268 -0
  443. lfx/components/models_and_agents/prompt.py +67 -0
  444. lfx/components/mongodb/__init__.py +34 -0
  445. lfx/components/mongodb/mongodb_atlas.py +213 -0
  446. lfx/components/needle/__init__.py +3 -0
  447. lfx/components/needle/needle.py +104 -0
  448. lfx/components/notdiamond/__init__.py +34 -0
  449. lfx/components/notdiamond/notdiamond.py +228 -0
  450. lfx/components/novita/__init__.py +32 -0
  451. lfx/components/novita/novita.py +130 -0
  452. lfx/components/nvidia/__init__.py +57 -0
  453. lfx/components/nvidia/nvidia.py +151 -0
  454. lfx/components/nvidia/nvidia_embedding.py +77 -0
  455. lfx/components/nvidia/nvidia_ingest.py +317 -0
  456. lfx/components/nvidia/nvidia_rerank.py +63 -0
  457. lfx/components/nvidia/system_assist.py +65 -0
  458. lfx/components/olivya/__init__.py +3 -0
  459. lfx/components/olivya/olivya.py +116 -0
  460. lfx/components/ollama/__init__.py +37 -0
  461. lfx/components/ollama/ollama.py +548 -0
  462. lfx/components/ollama/ollama_embeddings.py +103 -0
  463. lfx/components/openai/__init__.py +37 -0
  464. lfx/components/openai/openai.py +100 -0
  465. lfx/components/openai/openai_chat_model.py +176 -0
  466. lfx/components/openrouter/__init__.py +32 -0
  467. lfx/components/openrouter/openrouter.py +104 -0
  468. lfx/components/output_parsers/__init__.py +3 -0
  469. lfx/components/perplexity/__init__.py +34 -0
  470. lfx/components/perplexity/perplexity.py +75 -0
  471. lfx/components/pgvector/__init__.py +34 -0
  472. lfx/components/pgvector/pgvector.py +72 -0
  473. lfx/components/pinecone/__init__.py +34 -0
  474. lfx/components/pinecone/pinecone.py +134 -0
  475. lfx/components/processing/__init__.py +72 -0
  476. lfx/components/processing/alter_metadata.py +109 -0
  477. lfx/components/processing/combine_text.py +40 -0
  478. lfx/components/processing/converter.py +248 -0
  479. lfx/components/processing/create_data.py +111 -0
  480. lfx/components/processing/create_list.py +40 -0
  481. lfx/components/processing/data_operations.py +528 -0
  482. lfx/components/processing/data_to_dataframe.py +71 -0
  483. lfx/components/processing/dataframe_operations.py +313 -0
  484. lfx/components/processing/dataframe_to_toolset.py +259 -0
  485. lfx/components/processing/dynamic_create_data.py +357 -0
  486. lfx/components/processing/extract_key.py +54 -0
  487. lfx/components/processing/filter_data.py +43 -0
  488. lfx/components/processing/filter_data_values.py +89 -0
  489. lfx/components/processing/json_cleaner.py +104 -0
  490. lfx/components/processing/merge_data.py +91 -0
  491. lfx/components/processing/message_to_data.py +37 -0
  492. lfx/components/processing/output_parser.py +46 -0
  493. lfx/components/processing/parse_data.py +71 -0
  494. lfx/components/processing/parse_dataframe.py +69 -0
  495. lfx/components/processing/parse_json_data.py +91 -0
  496. lfx/components/processing/parser.py +148 -0
  497. lfx/components/processing/regex.py +83 -0
  498. lfx/components/processing/select_data.py +49 -0
  499. lfx/components/processing/split_text.py +141 -0
  500. lfx/components/processing/store_message.py +91 -0
  501. lfx/components/processing/update_data.py +161 -0
  502. lfx/components/prototypes/__init__.py +35 -0
  503. lfx/components/prototypes/python_function.py +73 -0
  504. lfx/components/qdrant/__init__.py +34 -0
  505. lfx/components/qdrant/qdrant.py +109 -0
  506. lfx/components/redis/__init__.py +37 -0
  507. lfx/components/redis/redis.py +89 -0
  508. lfx/components/redis/redis_chat.py +43 -0
  509. lfx/components/sambanova/__init__.py +32 -0
  510. lfx/components/sambanova/sambanova.py +84 -0
  511. lfx/components/scrapegraph/__init__.py +40 -0
  512. lfx/components/scrapegraph/scrapegraph_markdownify_api.py +64 -0
  513. lfx/components/scrapegraph/scrapegraph_search_api.py +64 -0
  514. lfx/components/scrapegraph/scrapegraph_smart_scraper_api.py +71 -0
  515. lfx/components/searchapi/__init__.py +34 -0
  516. lfx/components/searchapi/search.py +79 -0
  517. lfx/components/serpapi/__init__.py +3 -0
  518. lfx/components/serpapi/serp.py +115 -0
  519. lfx/components/supabase/__init__.py +34 -0
  520. lfx/components/supabase/supabase.py +76 -0
  521. lfx/components/tavily/__init__.py +4 -0
  522. lfx/components/tavily/tavily_extract.py +117 -0
  523. lfx/components/tavily/tavily_search.py +212 -0
  524. lfx/components/textsplitters/__init__.py +3 -0
  525. lfx/components/toolkits/__init__.py +3 -0
  526. lfx/components/tools/__init__.py +66 -0
  527. lfx/components/tools/calculator.py +109 -0
  528. lfx/components/tools/google_search_api.py +45 -0
  529. lfx/components/tools/google_serper_api.py +115 -0
  530. lfx/components/tools/python_code_structured_tool.py +328 -0
  531. lfx/components/tools/python_repl.py +98 -0
  532. lfx/components/tools/search_api.py +88 -0
  533. lfx/components/tools/searxng.py +145 -0
  534. lfx/components/tools/serp_api.py +120 -0
  535. lfx/components/tools/tavily_search_tool.py +345 -0
  536. lfx/components/tools/wikidata_api.py +103 -0
  537. lfx/components/tools/wikipedia_api.py +50 -0
  538. lfx/components/tools/yahoo_finance.py +130 -0
  539. lfx/components/twelvelabs/__init__.py +52 -0
  540. lfx/components/twelvelabs/convert_astra_results.py +84 -0
  541. lfx/components/twelvelabs/pegasus_index.py +311 -0
  542. lfx/components/twelvelabs/split_video.py +301 -0
  543. lfx/components/twelvelabs/text_embeddings.py +57 -0
  544. lfx/components/twelvelabs/twelvelabs_pegasus.py +408 -0
  545. lfx/components/twelvelabs/video_embeddings.py +100 -0
  546. lfx/components/twelvelabs/video_file.py +191 -0
  547. lfx/components/unstructured/__init__.py +3 -0
  548. lfx/components/unstructured/unstructured.py +121 -0
  549. lfx/components/upstash/__init__.py +34 -0
  550. lfx/components/upstash/upstash.py +124 -0
  551. lfx/components/utilities/__init__.py +43 -0
  552. lfx/components/utilities/calculator_core.py +89 -0
  553. lfx/components/utilities/current_date.py +42 -0
  554. lfx/components/utilities/id_generator.py +42 -0
  555. lfx/components/utilities/python_repl_core.py +98 -0
  556. lfx/components/vectara/__init__.py +37 -0
  557. lfx/components/vectara/vectara.py +97 -0
  558. lfx/components/vectara/vectara_rag.py +164 -0
  559. lfx/components/vectorstores/__init__.py +34 -0
  560. lfx/components/vectorstores/local_db.py +270 -0
  561. lfx/components/vertexai/__init__.py +37 -0
  562. lfx/components/vertexai/vertexai.py +71 -0
  563. lfx/components/vertexai/vertexai_embeddings.py +67 -0
  564. lfx/components/vlmrun/__init__.py +34 -0
  565. lfx/components/vlmrun/vlmrun_transcription.py +224 -0
  566. lfx/components/weaviate/__init__.py +34 -0
  567. lfx/components/weaviate/weaviate.py +89 -0
  568. lfx/components/wikipedia/__init__.py +4 -0
  569. lfx/components/wikipedia/wikidata.py +86 -0
  570. lfx/components/wikipedia/wikipedia.py +53 -0
  571. lfx/components/wolframalpha/__init__.py +3 -0
  572. lfx/components/wolframalpha/wolfram_alpha_api.py +54 -0
  573. lfx/components/xai/__init__.py +32 -0
  574. lfx/components/xai/xai.py +167 -0
  575. lfx/components/yahoosearch/__init__.py +3 -0
  576. lfx/components/yahoosearch/yahoo.py +137 -0
  577. lfx/components/youtube/__init__.py +52 -0
  578. lfx/components/youtube/channel.py +227 -0
  579. lfx/components/youtube/comments.py +231 -0
  580. lfx/components/youtube/playlist.py +33 -0
  581. lfx/components/youtube/search.py +120 -0
  582. lfx/components/youtube/trending.py +285 -0
  583. lfx/components/youtube/video_details.py +263 -0
  584. lfx/components/youtube/youtube_transcripts.py +206 -0
  585. lfx/components/zep/__init__.py +3 -0
  586. lfx/components/zep/zep.py +45 -0
  587. lfx/constants.py +6 -0
  588. lfx/custom/__init__.py +7 -0
  589. lfx/custom/attributes.py +87 -0
  590. lfx/custom/code_parser/__init__.py +3 -0
  591. lfx/custom/code_parser/code_parser.py +361 -0
  592. lfx/custom/custom_component/__init__.py +0 -0
  593. lfx/custom/custom_component/base_component.py +128 -0
  594. lfx/custom/custom_component/component.py +1890 -0
  595. lfx/custom/custom_component/component_with_cache.py +8 -0
  596. lfx/custom/custom_component/custom_component.py +650 -0
  597. lfx/custom/dependency_analyzer.py +165 -0
  598. lfx/custom/directory_reader/__init__.py +3 -0
  599. lfx/custom/directory_reader/directory_reader.py +359 -0
  600. lfx/custom/directory_reader/utils.py +171 -0
  601. lfx/custom/eval.py +12 -0
  602. lfx/custom/schema.py +32 -0
  603. lfx/custom/tree_visitor.py +21 -0
  604. lfx/custom/utils.py +877 -0
  605. lfx/custom/validate.py +523 -0
  606. lfx/events/__init__.py +1 -0
  607. lfx/events/event_manager.py +110 -0
  608. lfx/exceptions/__init__.py +0 -0
  609. lfx/exceptions/component.py +15 -0
  610. lfx/field_typing/__init__.py +91 -0
  611. lfx/field_typing/constants.py +216 -0
  612. lfx/field_typing/range_spec.py +35 -0
  613. lfx/graph/__init__.py +6 -0
  614. lfx/graph/edge/__init__.py +0 -0
  615. lfx/graph/edge/base.py +300 -0
  616. lfx/graph/edge/schema.py +119 -0
  617. lfx/graph/edge/utils.py +0 -0
  618. lfx/graph/graph/__init__.py +0 -0
  619. lfx/graph/graph/ascii.py +202 -0
  620. lfx/graph/graph/base.py +2298 -0
  621. lfx/graph/graph/constants.py +63 -0
  622. lfx/graph/graph/runnable_vertices_manager.py +133 -0
  623. lfx/graph/graph/schema.py +53 -0
  624. lfx/graph/graph/state_model.py +66 -0
  625. lfx/graph/graph/utils.py +1024 -0
  626. lfx/graph/schema.py +75 -0
  627. lfx/graph/state/__init__.py +0 -0
  628. lfx/graph/state/model.py +250 -0
  629. lfx/graph/utils.py +206 -0
  630. lfx/graph/vertex/__init__.py +0 -0
  631. lfx/graph/vertex/base.py +826 -0
  632. lfx/graph/vertex/constants.py +0 -0
  633. lfx/graph/vertex/exceptions.py +4 -0
  634. lfx/graph/vertex/param_handler.py +316 -0
  635. lfx/graph/vertex/schema.py +26 -0
  636. lfx/graph/vertex/utils.py +19 -0
  637. lfx/graph/vertex/vertex_types.py +489 -0
  638. lfx/helpers/__init__.py +141 -0
  639. lfx/helpers/base_model.py +71 -0
  640. lfx/helpers/custom.py +13 -0
  641. lfx/helpers/data.py +167 -0
  642. lfx/helpers/flow.py +308 -0
  643. lfx/inputs/__init__.py +68 -0
  644. lfx/inputs/constants.py +2 -0
  645. lfx/inputs/input_mixin.py +352 -0
  646. lfx/inputs/inputs.py +718 -0
  647. lfx/inputs/validators.py +19 -0
  648. lfx/interface/__init__.py +6 -0
  649. lfx/interface/components.py +897 -0
  650. lfx/interface/importing/__init__.py +5 -0
  651. lfx/interface/importing/utils.py +39 -0
  652. lfx/interface/initialize/__init__.py +3 -0
  653. lfx/interface/initialize/loading.py +317 -0
  654. lfx/interface/listing.py +26 -0
  655. lfx/interface/run.py +16 -0
  656. lfx/interface/utils.py +111 -0
  657. lfx/io/__init__.py +63 -0
  658. lfx/io/schema.py +295 -0
  659. lfx/load/__init__.py +8 -0
  660. lfx/load/load.py +256 -0
  661. lfx/load/utils.py +99 -0
  662. lfx/log/__init__.py +5 -0
  663. lfx/log/logger.py +411 -0
  664. lfx/logging/__init__.py +11 -0
  665. lfx/logging/logger.py +24 -0
  666. lfx/memory/__init__.py +70 -0
  667. lfx/memory/stubs.py +302 -0
  668. lfx/processing/__init__.py +1 -0
  669. lfx/processing/process.py +238 -0
  670. lfx/processing/utils.py +25 -0
  671. lfx/py.typed +0 -0
  672. lfx/schema/__init__.py +66 -0
  673. lfx/schema/artifact.py +83 -0
  674. lfx/schema/content_block.py +62 -0
  675. lfx/schema/content_types.py +91 -0
  676. lfx/schema/cross_module.py +80 -0
  677. lfx/schema/data.py +309 -0
  678. lfx/schema/dataframe.py +210 -0
  679. lfx/schema/dotdict.py +74 -0
  680. lfx/schema/encoders.py +13 -0
  681. lfx/schema/graph.py +47 -0
  682. lfx/schema/image.py +184 -0
  683. lfx/schema/json_schema.py +186 -0
  684. lfx/schema/log.py +62 -0
  685. lfx/schema/message.py +493 -0
  686. lfx/schema/openai_responses_schemas.py +74 -0
  687. lfx/schema/properties.py +41 -0
  688. lfx/schema/schema.py +180 -0
  689. lfx/schema/serialize.py +13 -0
  690. lfx/schema/table.py +142 -0
  691. lfx/schema/validators.py +114 -0
  692. lfx/serialization/__init__.py +5 -0
  693. lfx/serialization/constants.py +2 -0
  694. lfx/serialization/serialization.py +314 -0
  695. lfx/services/__init__.py +26 -0
  696. lfx/services/base.py +28 -0
  697. lfx/services/cache/__init__.py +6 -0
  698. lfx/services/cache/base.py +183 -0
  699. lfx/services/cache/service.py +166 -0
  700. lfx/services/cache/utils.py +169 -0
  701. lfx/services/chat/__init__.py +1 -0
  702. lfx/services/chat/config.py +2 -0
  703. lfx/services/chat/schema.py +10 -0
  704. lfx/services/database/__init__.py +5 -0
  705. lfx/services/database/service.py +25 -0
  706. lfx/services/deps.py +194 -0
  707. lfx/services/factory.py +19 -0
  708. lfx/services/initialize.py +19 -0
  709. lfx/services/interfaces.py +103 -0
  710. lfx/services/manager.py +185 -0
  711. lfx/services/mcp_composer/__init__.py +6 -0
  712. lfx/services/mcp_composer/factory.py +16 -0
  713. lfx/services/mcp_composer/service.py +1441 -0
  714. lfx/services/schema.py +21 -0
  715. lfx/services/session.py +87 -0
  716. lfx/services/settings/__init__.py +3 -0
  717. lfx/services/settings/auth.py +133 -0
  718. lfx/services/settings/base.py +668 -0
  719. lfx/services/settings/constants.py +43 -0
  720. lfx/services/settings/factory.py +23 -0
  721. lfx/services/settings/feature_flags.py +11 -0
  722. lfx/services/settings/service.py +35 -0
  723. lfx/services/settings/utils.py +40 -0
  724. lfx/services/shared_component_cache/__init__.py +1 -0
  725. lfx/services/shared_component_cache/factory.py +30 -0
  726. lfx/services/shared_component_cache/service.py +9 -0
  727. lfx/services/storage/__init__.py +5 -0
  728. lfx/services/storage/local.py +185 -0
  729. lfx/services/storage/service.py +177 -0
  730. lfx/services/tracing/__init__.py +1 -0
  731. lfx/services/tracing/service.py +21 -0
  732. lfx/settings.py +6 -0
  733. lfx/template/__init__.py +6 -0
  734. lfx/template/field/__init__.py +0 -0
  735. lfx/template/field/base.py +260 -0
  736. lfx/template/field/prompt.py +15 -0
  737. lfx/template/frontend_node/__init__.py +6 -0
  738. lfx/template/frontend_node/base.py +214 -0
  739. lfx/template/frontend_node/constants.py +65 -0
  740. lfx/template/frontend_node/custom_components.py +79 -0
  741. lfx/template/template/__init__.py +0 -0
  742. lfx/template/template/base.py +100 -0
  743. lfx/template/utils.py +217 -0
  744. lfx/type_extraction/__init__.py +19 -0
  745. lfx/type_extraction/type_extraction.py +75 -0
  746. lfx/type_extraction.py +80 -0
  747. lfx/utils/__init__.py +1 -0
  748. lfx/utils/async_helpers.py +42 -0
  749. lfx/utils/component_utils.py +154 -0
  750. lfx/utils/concurrency.py +60 -0
  751. lfx/utils/connection_string_parser.py +11 -0
  752. lfx/utils/constants.py +233 -0
  753. lfx/utils/data_structure.py +212 -0
  754. lfx/utils/exceptions.py +22 -0
  755. lfx/utils/helpers.py +34 -0
  756. lfx/utils/image.py +79 -0
  757. lfx/utils/langflow_utils.py +52 -0
  758. lfx/utils/lazy_load.py +15 -0
  759. lfx/utils/request_utils.py +18 -0
  760. lfx/utils/schemas.py +139 -0
  761. lfx/utils/ssrf_protection.py +384 -0
  762. lfx/utils/util.py +626 -0
  763. lfx/utils/util_strings.py +56 -0
  764. lfx/utils/validate_cloud.py +26 -0
  765. lfx/utils/version.py +24 -0
  766. lfx_nightly-0.2.0.dev25.dist-info/METADATA +312 -0
  767. lfx_nightly-0.2.0.dev25.dist-info/RECORD +769 -0
  768. lfx_nightly-0.2.0.dev25.dist-info/WHEEL +4 -0
  769. lfx_nightly-0.2.0.dev25.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,1890 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import asyncio
5
+ import inspect
6
+ from collections.abc import AsyncIterator, Iterator
7
+ from copy import deepcopy
8
+ from textwrap import dedent
9
+ from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple, get_type_hints
10
+ from uuid import UUID
11
+
12
+ import nanoid
13
+ import pandas as pd
14
+ import yaml
15
+ from langchain_core.tools import StructuredTool
16
+ from pydantic import BaseModel, ValidationError
17
+
18
+ from lfx.base.tools.constants import (
19
+ TOOL_OUTPUT_DISPLAY_NAME,
20
+ TOOL_OUTPUT_NAME,
21
+ TOOLS_METADATA_INFO,
22
+ TOOLS_METADATA_INPUT_NAME,
23
+ )
24
+ from lfx.custom.tree_visitor import RequiredInputsVisitor
25
+ from lfx.exceptions.component import StreamingError
26
+ from lfx.field_typing import Tool # noqa: TC001
27
+
28
+ # Lazy import to avoid circular dependency
29
+ # from lfx.graph.state.model import create_state_model
30
+ # Lazy import to avoid circular dependency
31
+ # from lfx.graph.utils import has_chat_output
32
+ from lfx.helpers.custom import format_type
33
+ from lfx.memory import astore_message, aupdate_messages, delete_message
34
+ from lfx.schema.artifact import get_artifact_type, post_process_raw
35
+ from lfx.schema.data import Data
36
+ from lfx.schema.log import Log
37
+ from lfx.schema.message import ErrorMessage, Message
38
+ from lfx.schema.properties import Source
39
+ from lfx.serialization.serialization import serialize
40
+ from lfx.template.field.base import UNDEFINED, Input, Output
41
+ from lfx.template.frontend_node.custom_components import ComponentFrontendNode
42
+ from lfx.utils.async_helpers import run_until_complete
43
+ from lfx.utils.util import find_closest_match
44
+
45
+ from .custom_component import CustomComponent
46
+
47
+ if TYPE_CHECKING:
48
+ from collections.abc import Callable
49
+
50
+ from lfx.base.tools.component_tool import ComponentToolkit
51
+ from lfx.events.event_manager import EventManager
52
+ from lfx.graph.edge.schema import EdgeData
53
+ from lfx.graph.vertex.base import Vertex
54
+ from lfx.inputs.inputs import InputTypes
55
+ from lfx.schema.dataframe import DataFrame
56
+ from lfx.schema.log import LoggableType
57
+
58
+
59
+ _ComponentToolkit = None
60
+
61
+
62
+ def get_component_toolkit():
63
+ global _ComponentToolkit # noqa: PLW0603
64
+ if _ComponentToolkit is None:
65
+ from lfx.base.tools.component_tool import ComponentToolkit
66
+
67
+ _ComponentToolkit = ComponentToolkit
68
+ return _ComponentToolkit
69
+
70
+
71
+ BACKWARDS_COMPATIBLE_ATTRIBUTES = ["user_id", "vertex", "tracing_service"]
72
+ CONFIG_ATTRIBUTES = ["_display_name", "_description", "_icon", "_name", "_metadata"]
73
+
74
+
75
+ class PlaceholderGraph(NamedTuple):
76
+ """A placeholder graph structure for components, providing backwards compatibility.
77
+
78
+ and enabling component execution without a full graph object.
79
+
80
+ This lightweight structure contains essential information typically found in a complete graph,
81
+ allowing components to function in isolation or in simplified contexts.
82
+
83
+ Attributes:
84
+ flow_id (str | None): Unique identifier for the flow, if applicable.
85
+ user_id (str | None): Identifier of the user associated with the flow, if any.
86
+ session_id (str | None): Identifier for the current session, if applicable.
87
+ context (dict): Additional contextual information for the component's execution.
88
+ flow_name (str | None): Name of the flow, if available.
89
+ """
90
+
91
+ flow_id: str | None
92
+ user_id: str | None
93
+ session_id: str | None
94
+ context: dict
95
+ flow_name: str | None
96
+
97
+ def get_vertex_neighbors(self, _vertex) -> dict:
98
+ """Returns an empty dictionary since PlaceholderGraph has no edges or neighbors.
99
+
100
+ This method exists for compatibility with real Graph objects, allowing components
101
+ to check graph connectivity even when running in isolation (e.g., in tests).
102
+
103
+ Args:
104
+ _vertex: The vertex to check neighbors for (ignored in placeholder context).
105
+
106
+ Returns:
107
+ An empty dictionary, indicating no neighbors exist.
108
+ """
109
+ return {}
110
+
111
+
112
+ class Component(CustomComponent):
113
+ inputs: list[InputTypes] = []
114
+ outputs: list[Output] = []
115
+ selected_output: str | None = None
116
+ code_class_base_inheritance: ClassVar[str] = "Component"
117
+
118
+ def __init__(self, **kwargs) -> None:
119
+ # Initialize instance-specific attributes first
120
+ if overlap := self._there_is_overlap_in_inputs_and_outputs():
121
+ msg = f"Inputs and outputs have overlapping names: {overlap}"
122
+ raise ValueError(msg)
123
+ self._output_logs: dict[str, list[Log]] = {}
124
+ self._current_output: str = ""
125
+ self._metadata: dict = {}
126
+ self._ctx: dict = {}
127
+ self._code: str | None = None
128
+ self._logs: list[Log] = []
129
+
130
+ # Initialize component-specific collections
131
+ self._inputs: dict[str, InputTypes] = {}
132
+ self._outputs_map: dict[str, Output] = {}
133
+ self._results: dict[str, Any] = {}
134
+ self._attributes: dict[str, Any] = {}
135
+ self._edges: list[EdgeData] = []
136
+ self._components: list[Component] = []
137
+ self._event_manager: EventManager | None = None
138
+ self._state_model = None
139
+ self._telemetry_input_values: dict[str, Any] | None = None
140
+
141
+ # Process input kwargs
142
+ inputs = {}
143
+ config = {}
144
+ for key, value in kwargs.items():
145
+ if key.startswith("_"):
146
+ config[key] = value
147
+ elif key in CONFIG_ATTRIBUTES:
148
+ config[key[1:]] = value
149
+ else:
150
+ inputs[key] = value
151
+
152
+ self._parameters = inputs or {}
153
+ self.set_attributes(self._parameters)
154
+
155
+ # Store original inputs and config for reference
156
+ self.__inputs = inputs
157
+ self.__config = config or {}
158
+
159
+ # Add unique ID if not provided
160
+ if "_id" not in self.__config:
161
+ self.__config |= {"_id": f"{self.__class__.__name__}-{nanoid.generate(size=5)}"}
162
+
163
+ # Initialize base class
164
+ super().__init__(**self.__config)
165
+
166
+ # Post-initialization setup
167
+ if hasattr(self, "_trace_type"):
168
+ self.trace_type = self._trace_type
169
+ if not hasattr(self, "trace_type"):
170
+ self.trace_type = "chain"
171
+
172
+ # Setup inputs and outputs
173
+ self.reset_all_output_values()
174
+ if self.inputs is not None:
175
+ self.map_inputs(self.inputs)
176
+ self.map_outputs()
177
+
178
+ # Final setup
179
+ self._set_output_types(list(self._outputs_map.values()))
180
+ self.set_class_code()
181
+
182
+ @classmethod
183
+ def get_base_inputs(cls):
184
+ if not hasattr(cls, "_base_inputs"):
185
+ return []
186
+ return cls._base_inputs
187
+
188
+ @classmethod
189
+ def get_base_outputs(cls):
190
+ if not hasattr(cls, "_base_outputs"):
191
+ return []
192
+ return cls._base_outputs
193
+
194
+ def get_results(self) -> dict[str, Any]:
195
+ return self._results
196
+
197
+ def get_artifacts(self) -> dict[str, Any]:
198
+ return self._artifacts
199
+
200
+ def get_event_manager(self) -> EventManager | None:
201
+ return self._event_manager
202
+
203
+ def get_undesrcore_inputs(self) -> dict[str, InputTypes]:
204
+ return self._inputs
205
+
206
+ def get_id(self) -> str:
207
+ return self._id
208
+
209
+ def set_id(self, id_: str) -> None:
210
+ self._id = id_
211
+
212
+ def get_edges(self) -> list[EdgeData]:
213
+ return self._edges
214
+
215
+ def get_components(self) -> list[Component]:
216
+ return self._components
217
+
218
+ def get_outputs_map(self) -> dict[str, Output]:
219
+ return self._outputs_map
220
+
221
+ def get_output_logs(self) -> dict[str, Any]:
222
+ return self._output_logs
223
+
224
+ def _build_source(self, id_: str | None, display_name: str | None, source: str | None) -> Source:
225
+ source_dict = {}
226
+ if id_:
227
+ source_dict["id"] = id_
228
+ if display_name:
229
+ source_dict["display_name"] = display_name
230
+ if source:
231
+ # Handle case where source is a ChatOpenAI and other models objects
232
+ if hasattr(source, "model_name"):
233
+ source_dict["source"] = source.model_name
234
+ elif hasattr(source, "model"):
235
+ source_dict["source"] = str(source.model)
236
+ else:
237
+ source_dict["source"] = str(source)
238
+ return Source(**source_dict)
239
+
240
+ def get_incoming_edge_by_target_param(self, target_param: str) -> str | None:
241
+ """Get the source vertex ID for an incoming edge that targets a specific parameter.
242
+
243
+ This method delegates to the underlying vertex to find an incoming edge that connects
244
+ to the specified target parameter.
245
+
246
+ Args:
247
+ target_param (str): The name of the target parameter to find an incoming edge for
248
+
249
+ Returns:
250
+ str | None: The ID of the source vertex if an incoming edge is found, None otherwise
251
+ """
252
+ if self._vertex is None:
253
+ msg = "Vertex not found. Please build the graph first."
254
+ raise ValueError(msg)
255
+ return self._vertex.get_incoming_edge_by_target_param(target_param)
256
+
257
+ @property
258
+ def enabled_tools(self) -> list[str] | None:
259
+ """Dynamically determine which tools should be enabled.
260
+
261
+ This property can be overridden by subclasses to provide custom tool filtering.
262
+ By default, it returns None, which means all tools are enabled.
263
+
264
+ Returns:
265
+ list[str] | None: List of tool names or tags to enable, or None to enable all tools.
266
+ """
267
+ # Default implementation returns None (all tools enabled)
268
+ # Subclasses can override this to provide custom filtering
269
+ return None
270
+
271
+ def _there_is_overlap_in_inputs_and_outputs(self) -> set[str]:
272
+ """Check the `.name` of inputs and outputs to see if there is overlap.
273
+
274
+ Returns:
275
+ set[str]: Set of names that overlap between inputs and outputs.
276
+ """
277
+ # Create sets of input and output names for O(1) lookup
278
+ input_names = {input_.name for input_ in self.inputs if input_.name is not None}
279
+ output_names = {output.name for output in self.outputs}
280
+
281
+ # Return the intersection of the sets
282
+ return input_names & output_names
283
+
284
+ def get_base_args(self):
285
+ """Get the base arguments required for component initialization.
286
+
287
+ Returns:
288
+ dict: A dictionary containing the base arguments:
289
+ - _user_id: The ID of the current user
290
+ - _session_id: The ID of the current session
291
+ - _tracing_service: The tracing service instance for logging/monitoring
292
+ """
293
+ return {
294
+ "_user_id": self.user_id,
295
+ "_session_id": self.graph.session_id,
296
+ "_tracing_service": self.tracing_service,
297
+ }
298
+
299
+ @property
300
+ def ctx(self):
301
+ if not hasattr(self, "graph") or self.graph is None:
302
+ msg = "Graph not found. Please build the graph first."
303
+ raise ValueError(msg)
304
+ return self.graph.context
305
+
306
+ def add_to_ctx(self, key: str, value: Any, *, overwrite: bool = False) -> None:
307
+ """Add a key-value pair to the context.
308
+
309
+ Args:
310
+ key (str): The key to add.
311
+ value (Any): The value to associate with the key.
312
+ overwrite (bool, optional): Whether to overwrite the existing value. Defaults to False.
313
+
314
+ Raises:
315
+ ValueError: If the graph is not built.
316
+ """
317
+ if not hasattr(self, "graph") or self.graph is None:
318
+ msg = "Graph not found. Please build the graph first."
319
+ raise ValueError(msg)
320
+ if key in self.graph.context and not overwrite:
321
+ msg = f"Key {key} already exists in context. Set overwrite=True to overwrite."
322
+ raise ValueError(msg)
323
+ self.graph.context.update({key: value})
324
+
325
+ def update_ctx(self, value_dict: dict[str, Any]) -> None:
326
+ """Update the context with a dictionary of values.
327
+
328
+ Args:
329
+ value_dict (dict[str, Any]): The dictionary of values to update.
330
+
331
+ Raises:
332
+ ValueError: If the graph is not built.
333
+ """
334
+ if not hasattr(self, "graph") or self.graph is None:
335
+ msg = "Graph not found. Please build the graph first."
336
+ raise ValueError(msg)
337
+ if not isinstance(value_dict, dict):
338
+ msg = "Value dict must be a dictionary"
339
+ raise TypeError(msg)
340
+
341
+ self.graph.context.update(value_dict)
342
+
343
+ def _pre_run_setup(self):
344
+ pass
345
+
346
+ def set_event_manager(self, event_manager: EventManager | None = None) -> None:
347
+ self._event_manager = event_manager
348
+
349
+ def reset_all_output_values(self) -> None:
350
+ """Reset all output values to UNDEFINED."""
351
+ if isinstance(self._outputs_map, dict):
352
+ for output in self._outputs_map.values():
353
+ output.value = UNDEFINED
354
+
355
+ def _build_state_model(self):
356
+ if self._state_model:
357
+ return self._state_model
358
+ name = self.name or self.__class__.__name__
359
+ model_name = f"{name}StateModel"
360
+ fields = {}
361
+ for output in self._outputs_map.values():
362
+ fields[output.name] = getattr(self, output.method)
363
+ # Lazy import to avoid circular dependency
364
+ from lfx.graph.state.model import create_state_model
365
+
366
+ self._state_model = create_state_model(model_name=model_name, **fields)
367
+ return self._state_model
368
+
369
+ def get_state_model_instance_getter(self):
370
+ state_model = self._build_state_model()
371
+
372
+ def _instance_getter(_):
373
+ return state_model()
374
+
375
+ _instance_getter.__annotations__["return"] = state_model
376
+ return _instance_getter
377
+
378
+ def __deepcopy__(self, memo: dict) -> Component:
379
+ if id(self) in memo:
380
+ return memo[id(self)]
381
+ kwargs = deepcopy(self.__config, memo)
382
+ kwargs["inputs"] = deepcopy(self.__inputs, memo)
383
+ new_component = type(self)(**kwargs)
384
+ new_component._code = self._code
385
+ new_component._outputs_map = self._outputs_map
386
+ new_component._inputs = self._inputs
387
+ new_component._edges = self._edges
388
+ new_component._components = self._components
389
+ new_component._parameters = self._parameters
390
+ new_component._attributes = self._attributes
391
+ new_component._output_logs = self._output_logs
392
+ new_component._logs = self._logs # type: ignore[attr-defined]
393
+ memo[id(self)] = new_component
394
+ return new_component
395
+
396
+ def set_class_code(self) -> None:
397
+ # Get the source code of the calling class
398
+ if self._code:
399
+ return
400
+ try:
401
+ module = inspect.getmodule(self.__class__)
402
+ if module is None:
403
+ msg = "Could not find module for class"
404
+ raise ValueError(msg)
405
+
406
+ class_code = inspect.getsource(module)
407
+ self._code = class_code
408
+ except (OSError, TypeError) as e:
409
+ msg = f"Could not find source code for {self.__class__.__name__}"
410
+ raise ValueError(msg) from e
411
+
412
+ def set(self, **kwargs):
413
+ """Connects the component to other components or sets parameters and attributes.
414
+
415
+ Args:
416
+ **kwargs: Keyword arguments representing the connections, parameters, and attributes.
417
+
418
+ Returns:
419
+ None
420
+
421
+ Raises:
422
+ KeyError: If the specified input name does not exist.
423
+ """
424
+ for key, value in kwargs.items():
425
+ self._process_connection_or_parameters(key, value)
426
+ return self
427
+
428
+ def list_inputs(self):
429
+ """Returns a list of input names."""
430
+ return [_input.name for _input in self.inputs]
431
+
432
+ def list_outputs(self):
433
+ """Returns a list of output names."""
434
+ return [_output.name for _output in self._outputs_map.values()]
435
+
436
+ async def run(self):
437
+ """Executes the component's logic and returns the result.
438
+
439
+ Returns:
440
+ The result of executing the component's logic.
441
+ """
442
+ return await self._run()
443
+
444
+ def set_vertex(self, vertex: Vertex) -> None:
445
+ """Sets the vertex for the component.
446
+
447
+ Args:
448
+ vertex (Vertex): The vertex to set.
449
+
450
+ Returns:
451
+ None
452
+ """
453
+ self._vertex = vertex
454
+
455
+ def get_input(self, name: str) -> Any:
456
+ """Retrieves the value of the input with the specified name.
457
+
458
+ Args:
459
+ name (str): The name of the input.
460
+
461
+ Returns:
462
+ Any: The value of the input.
463
+
464
+ Raises:
465
+ ValueError: If the input with the specified name is not found.
466
+ """
467
+ if name in self._inputs:
468
+ return self._inputs[name]
469
+ msg = f"Input {name} not found in {self.__class__.__name__}"
470
+ raise ValueError(msg)
471
+
472
+ def get_output(self, name: str) -> Any:
473
+ """Retrieves the output with the specified name.
474
+
475
+ Args:
476
+ name (str): The name of the output to retrieve.
477
+
478
+ Returns:
479
+ Any: The output value.
480
+
481
+ Raises:
482
+ ValueError: If the output with the specified name is not found.
483
+ """
484
+ if name in self._outputs_map:
485
+ return self._outputs_map[name]
486
+ msg = f"Output {name} not found in {self.__class__.__name__}"
487
+ raise ValueError(msg)
488
+
489
+ def set_on_output(self, name: str, **kwargs) -> None:
490
+ output = self.get_output(name)
491
+ for key, value in kwargs.items():
492
+ if not hasattr(output, key):
493
+ msg = f"Output {name} does not have a method {key}"
494
+ raise ValueError(msg)
495
+ setattr(output, key, value)
496
+
497
+ def set_output_value(self, name: str, value: Any) -> None:
498
+ if name in self._outputs_map:
499
+ self._outputs_map[name].value = value
500
+ else:
501
+ msg = f"Output {name} not found in {self.__class__.__name__}"
502
+ raise ValueError(msg)
503
+
504
+ def map_outputs(self) -> None:
505
+ """Maps the given list of outputs to the component.
506
+
507
+ Args:
508
+ outputs (List[Output]): The list of outputs to be mapped.
509
+
510
+ Raises:
511
+ ValueError: If the output name is None.
512
+
513
+ Returns:
514
+ None
515
+ """
516
+ # override outputs (generated from the class code) with vertex outputs
517
+ # if they exist (generated from the frontend)
518
+ outputs = []
519
+ if self._vertex and self._vertex.outputs:
520
+ for output in self._vertex.outputs:
521
+ try:
522
+ output_ = Output(**output)
523
+ outputs.append(output_)
524
+ except ValidationError as e:
525
+ msg = f"Invalid output: {e}"
526
+ raise ValueError(msg) from e
527
+ else:
528
+ outputs = self.outputs
529
+ for output in outputs:
530
+ if output.name is None:
531
+ msg = "Output name cannot be None."
532
+ raise ValueError(msg)
533
+ # Deepcopy is required to avoid modifying the original component;
534
+ # allows each instance of each component to modify its own output
535
+ self._outputs_map[output.name] = deepcopy(output)
536
+
537
+ def map_inputs(self, inputs: list[InputTypes]) -> None:
538
+ """Maps the given inputs to the component.
539
+
540
+ Args:
541
+ inputs (List[InputTypes]): A list of InputTypes objects representing the inputs.
542
+
543
+ Raises:
544
+ ValueError: If the input name is None.
545
+
546
+ """
547
+ telemetry_values = {}
548
+
549
+ for input_ in inputs:
550
+ if input_.name is None:
551
+ msg = self.build_component_error_message("Input name cannot be None")
552
+ raise ValueError(msg)
553
+ try:
554
+ self._inputs[input_.name] = deepcopy(input_)
555
+ except TypeError:
556
+ self._inputs[input_.name] = input_
557
+
558
+ # Build telemetry data during existing iteration (no performance impact)
559
+ if self._should_track_input(input_):
560
+ telemetry_values[input_.name] = serialize(input_.value)
561
+
562
+ # Cache for later O(1) retrieval
563
+ self._telemetry_input_values = telemetry_values if telemetry_values else None
564
+
565
+ def _should_track_input(self, input_obj: InputTypes) -> bool:
566
+ """Check if input should be tracked in telemetry."""
567
+ from lfx.inputs.input_mixin import SENSITIVE_FIELD_TYPES
568
+
569
+ # Respect opt-in flag (default: False for privacy)
570
+ if not getattr(input_obj, "track_in_telemetry", False):
571
+ return False
572
+ # Auto-exclude sensitive field types
573
+ return not (hasattr(input_obj, "field_type") and input_obj.field_type in SENSITIVE_FIELD_TYPES)
574
+
575
+ def get_telemetry_input_values(self) -> dict[str, Any] | None:
576
+ """Get cached telemetry input values. O(1) lookup, no iteration."""
577
+ # Return all values including descriptive strings and None
578
+ return self._telemetry_input_values if self._telemetry_input_values else None
579
+
580
+ def validate(self, params: dict) -> None:
581
+ """Validates the component parameters.
582
+
583
+ Args:
584
+ params (dict): A dictionary containing the component parameters.
585
+
586
+ Raises:
587
+ ValueError: If the inputs are not valid.
588
+ ValueError: If the outputs are not valid.
589
+ """
590
+ self._validate_inputs(params)
591
+ self._validate_outputs()
592
+
593
+ async def run_and_validate_update_outputs(self, frontend_node: dict, field_name: str, field_value: Any):
594
+ if inspect.iscoroutinefunction(self.update_outputs):
595
+ frontend_node = await self.update_outputs(frontend_node, field_name, field_value)
596
+ else:
597
+ frontend_node = self.update_outputs(frontend_node, field_name, field_value)
598
+ if field_name == "tool_mode" or frontend_node.get("tool_mode"):
599
+ is_tool_mode = field_value or frontend_node.get("tool_mode")
600
+ frontend_node["outputs"] = [self._build_tool_output()] if is_tool_mode else frontend_node["outputs"]
601
+ if is_tool_mode:
602
+ frontend_node.setdefault("template", {})
603
+ frontend_node["tool_mode"] = True
604
+ tools_metadata_input = await self._build_tools_metadata_input()
605
+ frontend_node["template"][TOOLS_METADATA_INPUT_NAME] = tools_metadata_input.to_dict()
606
+ self._append_tool_to_outputs_map()
607
+ elif "template" in frontend_node:
608
+ frontend_node["template"].pop(TOOLS_METADATA_INPUT_NAME, None)
609
+ self.tools_metadata = frontend_node.get("template", {}).get(TOOLS_METADATA_INPUT_NAME, {}).get("value")
610
+ return self._validate_frontend_node(frontend_node)
611
+
612
+ def _validate_frontend_node(self, frontend_node: dict):
613
+ # Check if all outputs are either Output or a valid Output model
614
+ for index, output in enumerate(frontend_node["outputs"]):
615
+ if isinstance(output, dict):
616
+ try:
617
+ output_ = Output(**output)
618
+ self._set_output_return_type(output_)
619
+ output_dict = output_.model_dump()
620
+ except ValidationError as e:
621
+ msg = f"Invalid output: {e}"
622
+ raise ValueError(msg) from e
623
+ elif isinstance(output, Output):
624
+ # we need to serialize it
625
+ self._set_output_return_type(output)
626
+ output_dict = output.model_dump()
627
+ else:
628
+ msg = f"Invalid output type: {type(output)}"
629
+ raise TypeError(msg)
630
+ frontend_node["outputs"][index] = output_dict
631
+ return frontend_node
632
+
633
+ def update_outputs(self, frontend_node: dict, field_name: str, field_value: Any) -> dict: # noqa: ARG002
634
+ """Default implementation for updating outputs based on field changes.
635
+
636
+ Subclasses can override this to modify outputs based on field_name and field_value.
637
+ """
638
+ return frontend_node
639
+
640
+ def _set_output_types(self, outputs: list[Output]) -> None:
641
+ for output in outputs:
642
+ self._set_output_return_type(output)
643
+
644
+ def _set_output_return_type(self, output: Output) -> None:
645
+ if output.method is None:
646
+ msg = f"Output {output.name} does not have a method"
647
+ raise ValueError(msg)
648
+ return_types = self._get_method_return_type(output.method)
649
+ output.add_types(return_types)
650
+
651
+ def _set_output_required_inputs(self) -> None:
652
+ for output in self.outputs:
653
+ if not output.method:
654
+ continue
655
+ method = getattr(self, output.method, None)
656
+ if not method or not callable(method):
657
+ continue
658
+ try:
659
+ source_code = inspect.getsource(method)
660
+ ast_tree = ast.parse(dedent(source_code))
661
+ except Exception: # noqa: BLE001
662
+ ast_tree = ast.parse(dedent(self._code or ""))
663
+
664
+ visitor = RequiredInputsVisitor(self._inputs)
665
+ visitor.visit(ast_tree)
666
+ output.required_inputs = sorted(visitor.required_inputs)
667
+
668
+ def get_output_by_method(self, method: Callable):
669
+ # method is a callable and output.method is a string
670
+ # we need to find the output that has the same method
671
+ output = next((output for output in self._outputs_map.values() if output.method == method.__name__), None)
672
+ if output is None:
673
+ method_name = method.__name__ if hasattr(method, "__name__") else str(method)
674
+ msg = f"Output with method {method_name} not found"
675
+ raise ValueError(msg)
676
+ return output
677
+
678
+ def _inherits_from_component(self, method: Callable):
679
+ # check if the method is a method from a class that inherits from Component
680
+ # and that it is an output of that class
681
+ return hasattr(method, "__self__") and isinstance(method.__self__, Component)
682
+
683
+ def _method_is_valid_output(self, method: Callable):
684
+ # check if the method is a method from a class that inherits from Component
685
+ # and that it is an output of that class
686
+ return (
687
+ hasattr(method, "__self__")
688
+ and isinstance(method.__self__, Component)
689
+ and method.__self__.get_output_by_method(method)
690
+ )
691
+
692
+ def _build_error_string_from_matching_pairs(self, matching_pairs: list[tuple[Output, Input]]):
693
+ text = ""
694
+ for output, input_ in matching_pairs:
695
+ text += f"{output.name}[{','.join(output.types)}]->{input_.name}[{','.join(input_.input_types or [])}]\n"
696
+ return text
697
+
698
+ def _find_matching_output_method(self, input_name: str, value: Component):
699
+ """Find the output method from the given component and input name.
700
+
701
+ Find the output method from the given component (`value`) that matches the specified input (`input_name`)
702
+ in the current component.
703
+ This method searches through all outputs of the provided component to find outputs whose types match
704
+ the input types of the specified input in the current component. If exactly one matching output is found,
705
+ it returns the corresponding method. If multiple matching outputs are found, it raises an error indicating
706
+ ambiguity. If no matching outputs are found, it raises an error indicating that no suitable output was found.
707
+
708
+ Args:
709
+ input_name (str): The name of the input in the current component to match.
710
+ value (Component): The component whose outputs are to be considered.
711
+
712
+ Returns:
713
+ Callable: The method corresponding to the matching output.
714
+
715
+ Raises:
716
+ ValueError: If multiple matching outputs are found, if no matching outputs are found,
717
+ or if the output method is invalid.
718
+ """
719
+ # Retrieve all outputs from the given component
720
+ outputs = value._outputs_map.values()
721
+ # Prepare to collect matching output-input pairs
722
+ matching_pairs = []
723
+ # Get the input object from the current component
724
+ input_ = self._inputs[input_name]
725
+ # Iterate over outputs to find matches based on types
726
+ matching_pairs = [
727
+ (output, input_)
728
+ for output in outputs
729
+ for output_type in output.types
730
+ # Check if the output type matches the input's accepted types
731
+ if input_.input_types and output_type in input_.input_types
732
+ ]
733
+ # If multiple matches are found, raise an error indicating ambiguity
734
+ if len(matching_pairs) > 1:
735
+ matching_pairs_str = self._build_error_string_from_matching_pairs(matching_pairs)
736
+ msg = self.build_component_error_message(
737
+ f"There are multiple outputs from {value.display_name} that can connect to inputs: {matching_pairs_str}"
738
+ )
739
+ raise ValueError(msg)
740
+ # If no matches are found, raise an error indicating no suitable output
741
+ if not matching_pairs:
742
+ msg = self.build_input_error_message(input_name, f"No matching output from {value.display_name} found")
743
+ raise ValueError(msg)
744
+ # Get the matching output and input pair
745
+ output, input_ = matching_pairs[0]
746
+ # Ensure that the output method is a valid method name (string)
747
+ if not isinstance(output.method, str):
748
+ msg = self.build_component_error_message(
749
+ f"Method {output.method} is not a valid output of {value.display_name}"
750
+ )
751
+ raise TypeError(msg)
752
+ return getattr(value, output.method)
753
+
754
+ def _process_connection_or_parameter(self, key, value) -> None:
755
+ # Special handling for Loop components: check if we're setting a loop-enabled output
756
+ if self._is_loop_connection(key, value):
757
+ self._process_loop_connection(key, value)
758
+ return
759
+
760
+ input_ = self._get_or_create_input(key)
761
+ # We need to check if callable AND if it is a method from a class that inherits from Component
762
+ if isinstance(value, Component):
763
+ # We need to find the Output that can connect to an input of the current component
764
+ # if there's more than one output that matches, we need to raise an error
765
+ # because we don't know which one to connect to
766
+ value = self._find_matching_output_method(key, value)
767
+ if callable(value) and self._inherits_from_component(value):
768
+ try:
769
+ self._method_is_valid_output(value)
770
+ except ValueError as e:
771
+ msg = f"Method {value.__name__} is not a valid output of {value.__self__.__class__.__name__}"
772
+ raise ValueError(msg) from e
773
+ self._connect_to_component(key, value, input_)
774
+ else:
775
+ self._set_parameter_or_attribute(key, value)
776
+
777
+ def _is_loop_connection(self, key: str, value) -> bool:
778
+ """Check if this is a loop feedback connection.
779
+
780
+ A loop connection occurs when:
781
+ 1. The key matches an output name of this component
782
+ 2. That output has allows_loop=True
783
+ 3. The value is a callable method from another component
784
+ """
785
+ # Check if key matches a loop-enabled output
786
+ if key not in self._outputs_map:
787
+ return False
788
+
789
+ output = self._outputs_map[key]
790
+ if not getattr(output, "allows_loop", False):
791
+ return False
792
+
793
+ # Check if value is a callable method from a Component
794
+ return callable(value) and self._inherits_from_component(value)
795
+
796
+ def _process_loop_connection(self, key: str, value) -> None:
797
+ """Process a loop feedback connection.
798
+
799
+ Creates a special edge that connects the source component's output
800
+ to this Loop component's loop-enabled output (not an input).
801
+ """
802
+ try:
803
+ self._method_is_valid_output(value)
804
+ except ValueError as e:
805
+ msg = f"Method {value.__name__} is not a valid output of {value.__self__.__class__.__name__}"
806
+ raise ValueError(msg) from e
807
+
808
+ source_component = value.__self__
809
+ self._components.append(source_component)
810
+ source_output = source_component.get_output_by_method(value)
811
+ target_output = self._outputs_map[key]
812
+
813
+ # Create special loop feedback edge
814
+ self._add_loop_edge(source_component, source_output, target_output)
815
+
816
+ def _add_loop_edge(self, source_component, source_output, target_output) -> None:
817
+ """Add a special loop feedback edge that targets an output instead of an input."""
818
+ self._edges.append(
819
+ {
820
+ "source": source_component._id,
821
+ "target": self._id,
822
+ "data": {
823
+ "sourceHandle": {
824
+ "dataType": source_component.name or source_component.__class__.__name__,
825
+ "id": source_component._id,
826
+ "name": source_output.name,
827
+ "output_types": source_output.types,
828
+ },
829
+ "targetHandle": {
830
+ # Special loop edge structure - targets an output, not an input
831
+ "dataType": self.name or self.__class__.__name__,
832
+ "id": self._id,
833
+ "name": target_output.name,
834
+ "output_types": target_output.types,
835
+ },
836
+ },
837
+ }
838
+ )
839
+
840
+ def _process_connection_or_parameters(self, key, value) -> None:
841
+ # if value is a list of components, we need to process each component
842
+ # Note this update make sure it is not a list str | int | float | bool | type(None)
843
+ if isinstance(value, list) and not any(
844
+ isinstance(val, str | int | float | bool | type(None) | Message | Data | StructuredTool) for val in value
845
+ ):
846
+ for val in value:
847
+ self._process_connection_or_parameter(key, val)
848
+ else:
849
+ self._process_connection_or_parameter(key, value)
850
+
851
+ def _get_or_create_input(self, key):
852
+ try:
853
+ return self._inputs[key]
854
+ except KeyError:
855
+ input_ = self._get_fallback_input(name=key, display_name=key)
856
+ self._inputs[key] = input_
857
+ self.inputs.append(input_)
858
+ return input_
859
+
860
+ def _connect_to_component(self, key, value, input_) -> None:
861
+ component = value.__self__
862
+ self._components.append(component)
863
+ output = component.get_output_by_method(value)
864
+ self._add_edge(component, key, output, input_)
865
+
866
+ def _add_edge(self, component, key, output, input_) -> None:
867
+ self._edges.append(
868
+ {
869
+ "source": component._id,
870
+ "target": self._id,
871
+ "data": {
872
+ "sourceHandle": {
873
+ "dataType": component.name or component.__class__.__name__,
874
+ "id": component._id,
875
+ "name": output.name,
876
+ "output_types": output.types,
877
+ },
878
+ "targetHandle": {
879
+ "fieldName": key,
880
+ "id": self._id,
881
+ "inputTypes": input_.input_types,
882
+ "type": input_.field_type,
883
+ },
884
+ },
885
+ }
886
+ )
887
+
888
+ def _set_parameter_or_attribute(self, key, value) -> None:
889
+ if isinstance(value, Component):
890
+ methods = ", ".join([f"'{output.method}'" for output in value.outputs])
891
+ msg = f"You set {value.display_name} as value for `{key}`. You should pass one of the following: {methods}"
892
+ raise TypeError(msg)
893
+ self.set_input_value(key, value)
894
+ self._parameters[key] = value
895
+ self._attributes[key] = value
896
+
897
+ def __call__(self, **kwargs):
898
+ self.set(**kwargs)
899
+
900
+ return run_until_complete(self.run())
901
+
902
+ async def _run(self):
903
+ # Resolve callable inputs
904
+ for key, _input in self._inputs.items():
905
+ if asyncio.iscoroutinefunction(_input.value):
906
+ self._inputs[key].value = await _input.value()
907
+ elif callable(_input.value):
908
+ self._inputs[key].value = await asyncio.to_thread(_input.value)
909
+
910
+ self.set_attributes({})
911
+
912
+ return await self.build_results()
913
+
914
+ def __getattr__(self, name: str) -> Any:
915
+ if "_attributes" in self.__dict__ and name in self.__dict__["_attributes"]:
916
+ # It is a dict of attributes that are not inputs or outputs all the raw data it should have the loop input.
917
+ return self.__dict__["_attributes"][name]
918
+ if "_inputs" in self.__dict__ and name in self.__dict__["_inputs"]:
919
+ return self.__dict__["_inputs"][name].value
920
+ if "_outputs_map" in self.__dict__ and name in self.__dict__["_outputs_map"]:
921
+ return self.__dict__["_outputs_map"][name]
922
+ if name in BACKWARDS_COMPATIBLE_ATTRIBUTES:
923
+ return self.__dict__[f"_{name}"]
924
+ if name.startswith("_") and name[1:] in BACKWARDS_COMPATIBLE_ATTRIBUTES:
925
+ return self.__dict__[name]
926
+ if name == "graph":
927
+ # If it got up to here it means it was going to raise
928
+ session_id = self._session_id if hasattr(self, "_session_id") else None
929
+ user_id = self._user_id if hasattr(self, "_user_id") else None
930
+ flow_name = self._flow_name if hasattr(self, "_flow_name") else None
931
+ flow_id = self._flow_id if hasattr(self, "_flow_id") else None
932
+ return PlaceholderGraph(
933
+ flow_id=flow_id, user_id=str(user_id), session_id=session_id, context={}, flow_name=flow_name
934
+ )
935
+ msg = f"Attribute {name} not found in {self.__class__.__name__}"
936
+ raise AttributeError(msg)
937
+
938
+ def set_input_value(self, name: str, value: Any) -> None:
939
+ if name in self._inputs:
940
+ input_value = self._inputs[name].value
941
+ if isinstance(input_value, Component):
942
+ methods = ", ".join([f"'{output.method}'" for output in input_value.outputs])
943
+ msg = self.build_input_error_message(
944
+ name,
945
+ f"You set {input_value.display_name} as value. You should pass one of the following: {methods}",
946
+ )
947
+ raise ValueError(msg)
948
+ if callable(input_value) and hasattr(input_value, "__self__"):
949
+ msg = self.build_input_error_message(
950
+ name, f"Input is connected to {input_value.__self__.display_name}.{input_value.__name__}"
951
+ )
952
+ raise ValueError(msg)
953
+ try:
954
+ self._inputs[name].value = value
955
+ except Exception as e:
956
+ msg = f"Error setting input value for {name}: {e}"
957
+ raise ValueError(msg) from e
958
+ if hasattr(self._inputs[name], "load_from_db"):
959
+ self._inputs[name].load_from_db = False
960
+ else:
961
+ msg = self.build_component_error_message(f"Input {name} not found")
962
+ raise ValueError(msg)
963
+
964
+ def _validate_outputs(self) -> None:
965
+ # Raise Error if some rule isn't met
966
+ if self.selected_output is not None and self.selected_output not in self._outputs_map:
967
+ output_names = ", ".join(list(self._outputs_map.keys()))
968
+ msg = f"selected_output '{self.selected_output}' is not valid. Must be one of: {output_names}"
969
+ raise ValueError(msg)
970
+
971
+ def _map_parameters_on_frontend_node(self, frontend_node: ComponentFrontendNode) -> None:
972
+ for name, value in self._parameters.items():
973
+ frontend_node.set_field_value_in_template(name, value)
974
+
975
+ def _map_parameters_on_template(self, template: dict) -> None:
976
+ for name, value in self._parameters.items():
977
+ try:
978
+ template[name]["value"] = value
979
+ except KeyError as e:
980
+ close_match = find_closest_match(name, list(template.keys()))
981
+ if close_match:
982
+ msg = f"Parameter '{name}' not found in {self.__class__.__name__}. Did you mean '{close_match}'?"
983
+ raise ValueError(msg) from e
984
+ msg = f"Parameter {name} not found in {self.__class__.__name__}. "
985
+ raise ValueError(msg) from e
986
+
987
+ def _get_method_return_type(self, method_name: str) -> list[str]:
988
+ method = getattr(self, method_name)
989
+ return_type = get_type_hints(method).get("return")
990
+ if return_type is None:
991
+ return []
992
+ extracted_return_types = self._extract_return_type(return_type)
993
+ return [format_type(extracted_return_type) for extracted_return_type in extracted_return_types]
994
+
995
+ def _update_template(self, frontend_node: dict):
996
+ return frontend_node
997
+
998
+ def to_frontend_node(self):
999
+ # ! This part here is clunky but we need it like this for
1000
+ # ! backwards compatibility. We can change how prompt component
1001
+ # ! works and then update this later
1002
+ field_config = self.get_template_config(self)
1003
+ frontend_node = ComponentFrontendNode.from_inputs(**field_config)
1004
+ # for key in self._inputs:
1005
+ # frontend_node.set_field_load_from_db_in_template(key, value=False)
1006
+ self._map_parameters_on_frontend_node(frontend_node)
1007
+
1008
+ frontend_node_dict = frontend_node.to_dict(keep_name=False)
1009
+ frontend_node_dict = self._update_template(frontend_node_dict)
1010
+ self._map_parameters_on_template(frontend_node_dict["template"])
1011
+
1012
+ frontend_node = ComponentFrontendNode.from_dict(frontend_node_dict)
1013
+ if not self._code:
1014
+ self.set_class_code()
1015
+ code_field = Input(
1016
+ dynamic=True,
1017
+ required=True,
1018
+ placeholder="",
1019
+ multiline=True,
1020
+ value=self._code,
1021
+ password=False,
1022
+ name="code",
1023
+ advanced=True,
1024
+ field_type="code",
1025
+ is_list=False,
1026
+ )
1027
+ frontend_node.template.add_field(code_field)
1028
+
1029
+ for output in frontend_node.outputs:
1030
+ if output.types:
1031
+ continue
1032
+ return_types = self._get_method_return_type(output.method)
1033
+ output.add_types(return_types)
1034
+
1035
+ frontend_node.validate_component()
1036
+ frontend_node.set_base_classes_from_outputs()
1037
+
1038
+ # Get the node dictionary and add selected_output if specified
1039
+ node_dict = frontend_node.to_dict(keep_name=False)
1040
+ if self.selected_output is not None:
1041
+ node_dict["selected_output"] = self.selected_output
1042
+
1043
+ return {
1044
+ "data": {
1045
+ "node": node_dict,
1046
+ "type": self.name or self.__class__.__name__,
1047
+ "id": self._id,
1048
+ },
1049
+ "id": self._id,
1050
+ }
1051
+
1052
+ def _validate_inputs(self, params: dict) -> None:
1053
+ # Params keys are the `name` attribute of the Input objects
1054
+ """Validates and assigns input values from the provided parameters dictionary.
1055
+
1056
+ For each parameter matching a defined input, sets the input's value and updates the parameter
1057
+ dictionary with the validated value.
1058
+ """
1059
+ for key, value in params.copy().items():
1060
+ if key not in self._inputs:
1061
+ continue
1062
+ input_ = self._inputs[key]
1063
+ # BaseInputMixin has a `validate_assignment=True`
1064
+
1065
+ input_.value = value
1066
+ params[input_.name] = input_.value
1067
+
1068
+ def set_attributes(self, params: dict) -> None:
1069
+ """Sets component attributes from the given parameters, preventing conflicts with reserved attribute names.
1070
+
1071
+ Raises:
1072
+ ValueError: If a parameter name matches a reserved attribute not managed in _attributes and its
1073
+ value differs from the current attribute value.
1074
+ """
1075
+ self._validate_inputs(params)
1076
+ attributes = {}
1077
+ for key, value in params.items():
1078
+ if key in self.__dict__ and key not in self._attributes and value != getattr(self, key):
1079
+ msg = (
1080
+ f"{self.__class__.__name__} defines an input parameter named '{key}' "
1081
+ f"that is a reserved word and cannot be used."
1082
+ )
1083
+ raise ValueError(msg)
1084
+ attributes[key] = value
1085
+ for key, input_obj in self._inputs.items():
1086
+ if key not in attributes and key not in self._attributes:
1087
+ attributes[key] = input_obj.value or None
1088
+
1089
+ self._attributes.update(attributes)
1090
+
1091
+ def _set_outputs(self, outputs: list[dict]) -> None:
1092
+ self.outputs = [Output(**output) for output in outputs]
1093
+ for output in self.outputs:
1094
+ setattr(self, output.name, output)
1095
+ self._outputs_map[output.name] = output
1096
+
1097
+ def get_trace_as_inputs(self):
1098
+ predefined_inputs = {
1099
+ input_.name: input_.value
1100
+ for input_ in self.inputs
1101
+ if hasattr(input_, "trace_as_input") and input_.trace_as_input
1102
+ }
1103
+ # Runtime inputs
1104
+ runtime_inputs = {name: input_.value for name, input_ in self._inputs.items() if hasattr(input_, "value")}
1105
+ return {**predefined_inputs, **runtime_inputs}
1106
+
1107
+ def get_trace_as_metadata(self):
1108
+ return {
1109
+ input_.name: input_.value
1110
+ for input_ in self.inputs
1111
+ if hasattr(input_, "trace_as_metadata") and input_.trace_as_metadata
1112
+ }
1113
+
1114
+ async def _build_with_tracing(self):
1115
+ inputs = self.get_trace_as_inputs()
1116
+ metadata = self.get_trace_as_metadata()
1117
+ async with self.tracing_service.trace_component(self, self.trace_name, inputs, metadata):
1118
+ results, artifacts = await self._build_results()
1119
+ self.tracing_service.set_outputs(self.trace_name, results)
1120
+
1121
+ return results, artifacts
1122
+
1123
+ async def _build_without_tracing(self):
1124
+ return await self._build_results()
1125
+
1126
+ async def build_results(self):
1127
+ """Build the results of the component."""
1128
+ if hasattr(self, "graph"):
1129
+ session_id = self.graph.session_id
1130
+ elif hasattr(self, "_session_id"):
1131
+ session_id = self._session_id
1132
+ else:
1133
+ session_id = None
1134
+ try:
1135
+ if self.tracing_service:
1136
+ return await self._build_with_tracing()
1137
+ return await self._build_without_tracing()
1138
+ except StreamingError as e:
1139
+ await self.send_error(
1140
+ exception=e.cause,
1141
+ session_id=session_id,
1142
+ trace_name=getattr(self, "trace_name", None),
1143
+ source=e.source,
1144
+ )
1145
+ raise e.cause # noqa: B904
1146
+ except Exception as e:
1147
+ await self.send_error(
1148
+ exception=e,
1149
+ session_id=session_id,
1150
+ source=Source(id=self._id, display_name=self.display_name, source=self.display_name),
1151
+ trace_name=getattr(self, "trace_name", None),
1152
+ )
1153
+ raise
1154
+
1155
+ async def _build_results(self) -> tuple[dict, dict]:
1156
+ results, artifacts = {}, {}
1157
+
1158
+ self._pre_run_setup_if_needed()
1159
+ self._handle_tool_mode()
1160
+
1161
+ for output in self._get_outputs_to_process():
1162
+ self._current_output = output.name
1163
+ result = await self._get_output_result(output)
1164
+ results[output.name] = result
1165
+ artifacts[output.name] = self._build_artifact(result)
1166
+ self._log_output(output)
1167
+
1168
+ self._finalize_results(results, artifacts)
1169
+ return results, artifacts
1170
+
1171
+ def _pre_run_setup_if_needed(self):
1172
+ if hasattr(self, "_pre_run_setup"):
1173
+ self._pre_run_setup()
1174
+
1175
+ def _handle_tool_mode(self):
1176
+ if (
1177
+ hasattr(self, "outputs") and any(getattr(_input, "tool_mode", False) for _input in self.inputs)
1178
+ ) or self.add_tool_output:
1179
+ self._append_tool_to_outputs_map()
1180
+
1181
+ def _should_process_output(self, output):
1182
+ """Determines whether a given output should be processed based on vertex edge configuration.
1183
+
1184
+ Returns True if the component has no vertex or outgoing edges, or if the output's name is among
1185
+ the vertex's source edge names.
1186
+ """
1187
+ if not self._vertex or not self._vertex.outgoing_edges:
1188
+ return True
1189
+ return output.name in self._vertex.edges_source_names
1190
+
1191
+ def _get_outputs_to_process(self):
1192
+ """Returns a list of outputs to process, ordered according to self.outputs.
1193
+
1194
+ Outputs are included only if they should be processed, as determined by _should_process_output.
1195
+ First processes outputs in the order defined by self.outputs, then processes any remaining outputs
1196
+ from _outputs_map that weren't in self.outputs.
1197
+
1198
+ Returns:
1199
+ list: Outputs to be processed in the defined order.
1200
+
1201
+ Raises:
1202
+ ValueError: If an output name in self.outputs is not present in _outputs_map.
1203
+ """
1204
+ result = []
1205
+ processed_names = set()
1206
+
1207
+ # First process outputs in the order defined by self.outputs
1208
+ for output in self.outputs:
1209
+ output_obj = self._outputs_map.get(output.name, deepcopy(output))
1210
+ if self._should_process_output(output_obj):
1211
+ result.append(output_obj)
1212
+ processed_names.add(output_obj.name)
1213
+
1214
+ # Then process any remaining outputs from _outputs_map
1215
+ for name, output_obj in self._outputs_map.items():
1216
+ if name not in processed_names and self._should_process_output(output_obj):
1217
+ result.append(output_obj)
1218
+
1219
+ return result
1220
+
1221
+ async def _get_output_result(self, output):
1222
+ """Computes and returns the result for a given output, applying caching and output options.
1223
+
1224
+ If the output is cached and a value is already defined, returns the cached value. Otherwise,
1225
+ invokes the associated output method asynchronously, applies output options, updates the cache,
1226
+ and returns the result. Raises a ValueError if the output method is not defined, or a TypeError
1227
+ if the method invocation fails.
1228
+ """
1229
+ if output.cache and output.value != UNDEFINED:
1230
+ return output.value
1231
+
1232
+ if output.method is None:
1233
+ msg = f'Output "{output.name}" does not have a method defined.'
1234
+ raise ValueError(msg)
1235
+
1236
+ method = getattr(self, output.method)
1237
+ try:
1238
+ result = await method() if inspect.iscoroutinefunction(method) else await asyncio.to_thread(method)
1239
+ except TypeError as e:
1240
+ msg = f'Error running method "{output.method}": {e}'
1241
+ raise TypeError(msg) from e
1242
+
1243
+ if (
1244
+ self._vertex is not None
1245
+ and isinstance(result, Message)
1246
+ and result.flow_id is None
1247
+ and self._vertex.graph.flow_id is not None
1248
+ ):
1249
+ result.set_flow_id(self._vertex.graph.flow_id)
1250
+ result = output.apply_options(result)
1251
+ output.value = result
1252
+
1253
+ return result
1254
+
1255
+ async def resolve_output(self, output_name: str) -> Any:
1256
+ """Resolves and returns the value for a specified output by name.
1257
+
1258
+ If output caching is enabled and a value is already available, returns the cached value;
1259
+ otherwise, computes and returns the output result. Raises a KeyError if the output name
1260
+ does not exist.
1261
+ """
1262
+ output = self._outputs_map.get(output_name)
1263
+ if output is None:
1264
+ msg = (
1265
+ f"Sorry, an output named '{output_name}' could not be found. "
1266
+ "Please ensure that the output is correctly configured and try again."
1267
+ )
1268
+ raise KeyError(msg)
1269
+ if output.cache and output.value != UNDEFINED:
1270
+ return output.value
1271
+ return await self._get_output_result(output)
1272
+
1273
+ def _build_artifact(self, result):
1274
+ """Builds an artifact dictionary containing a string representation, raw data, and type for a result.
1275
+
1276
+ The artifact includes a human-readable representation, the processed raw result, and its determined type.
1277
+ """
1278
+ custom_repr = self.custom_repr()
1279
+ if custom_repr is None and isinstance(result, dict | Data | str):
1280
+ custom_repr = result
1281
+ if not isinstance(custom_repr, str):
1282
+ custom_repr = str(custom_repr)
1283
+
1284
+ raw = self._process_raw_result(result)
1285
+ artifact_type = get_artifact_type(self.status or raw, result)
1286
+ raw, artifact_type = post_process_raw(raw, artifact_type)
1287
+ return {"repr": custom_repr, "raw": raw, "type": artifact_type}
1288
+
1289
+ def _process_raw_result(self, result):
1290
+ return self.extract_data(result)
1291
+
1292
+ def extract_data(self, result):
1293
+ """Extract the data from the result. this is where the self.status is set."""
1294
+ if isinstance(result, Message):
1295
+ self.status = result.get_text()
1296
+ return (
1297
+ self.status if self.status is not None else "No text available"
1298
+ ) # Provide a default message if .text_key is missing
1299
+ if hasattr(result, "data"):
1300
+ return result.data
1301
+ if hasattr(result, "model_dump"):
1302
+ return result.model_dump()
1303
+ if isinstance(result, Data | dict | str):
1304
+ return result.data if isinstance(result, Data) else result
1305
+
1306
+ if self.status:
1307
+ return self.status
1308
+ return result
1309
+
1310
+ def _log_output(self, output):
1311
+ self._output_logs[output.name] = self._logs
1312
+ self._logs = []
1313
+ self._current_output = ""
1314
+
1315
+ def _finalize_results(self, results, artifacts):
1316
+ self._artifacts = artifacts
1317
+ self._results = results
1318
+ if self.tracing_service:
1319
+ self.tracing_service.set_outputs(self.trace_name, results)
1320
+
1321
+ def custom_repr(self):
1322
+ if self.repr_value == "":
1323
+ self.repr_value = self.status
1324
+ if isinstance(self.repr_value, dict):
1325
+ return yaml.dump(self.repr_value)
1326
+ if isinstance(self.repr_value, str):
1327
+ return self.repr_value
1328
+ if isinstance(self.repr_value, BaseModel) and not isinstance(self.repr_value, Data):
1329
+ return str(self.repr_value)
1330
+ return self.repr_value
1331
+
1332
+ def build_inputs(self):
1333
+ """Builds the inputs for the custom component.
1334
+
1335
+ Returns:
1336
+ List[Input]: The list of inputs.
1337
+ """
1338
+ # This function is similar to build_config, but it will process the inputs
1339
+ # and return them as a dict with keys being the Input.name and values being the Input.model_dump()
1340
+ self.inputs = self.template_config.get("inputs", [])
1341
+ if not self.inputs:
1342
+ return {}
1343
+ return {_input.name: _input.model_dump(by_alias=True, exclude_none=True) for _input in self.inputs}
1344
+
1345
+ def _get_field_order(self):
1346
+ try:
1347
+ inputs = self.template_config["inputs"]
1348
+ return [field.name for field in inputs]
1349
+ except KeyError:
1350
+ return []
1351
+
1352
+ def build(self, **kwargs) -> None:
1353
+ self.set_attributes(kwargs)
1354
+
1355
+ def _get_fallback_input(self, **kwargs):
1356
+ return Input(**kwargs)
1357
+
1358
+ async def to_toolkit(self) -> list[Tool]:
1359
+ """Convert component to a list of tools.
1360
+
1361
+ This is a template method that defines the skeleton of the toolkit creation
1362
+ algorithm. Subclasses can override _get_tools() to provide custom tool
1363
+ implementations while maintaining the metadata update functionality.
1364
+
1365
+ Returns:
1366
+ list[Tool]: A list of tools with updated metadata. Each tool contains:
1367
+ - name: The name of the tool
1368
+ - description: A description of what the tool does
1369
+ - tags: List of tags associated with the tool
1370
+ """
1371
+ # Get tools from subclass implementation
1372
+ # Handle both sync and async _get_tools methods
1373
+ if asyncio.iscoroutinefunction(self._get_tools):
1374
+ tools = await self._get_tools()
1375
+ else:
1376
+ tools = self._get_tools()
1377
+
1378
+ if hasattr(self, TOOLS_METADATA_INPUT_NAME):
1379
+ tools = self._filter_tools_by_status(tools=tools, metadata=self.tools_metadata)
1380
+ return self._update_tools_with_metadata(tools=tools, metadata=self.tools_metadata)
1381
+
1382
+ # If no metadata exists yet, filter based on enabled_tools
1383
+ return self._filter_tools_by_status(tools=tools, metadata=None)
1384
+
1385
+ async def _get_tools(self) -> list[Tool]:
1386
+ """Get the list of tools for this component.
1387
+
1388
+ This method can be overridden by subclasses to provide custom tool implementations.
1389
+ The default implementation uses ComponentToolkit.
1390
+
1391
+ Returns:
1392
+ list[Tool]: List of tools provided by this component
1393
+ """
1394
+ component_toolkit: type[ComponentToolkit] = get_component_toolkit()
1395
+ return component_toolkit(component=self).get_tools(callbacks=self.get_langchain_callbacks())
1396
+
1397
+ def _extract_tools_tags(self, tools_metadata: list[dict]) -> list[str]:
1398
+ """Extract the first tag from each tool's metadata."""
1399
+ return [tool["tags"][0] for tool in tools_metadata if tool["tags"]]
1400
+
1401
+ def _update_tools_with_metadata(self, tools: list[Tool], metadata: DataFrame | None) -> list[Tool]:
1402
+ """Update tools with provided metadata."""
1403
+ component_toolkit: type[ComponentToolkit] = get_component_toolkit()
1404
+ return component_toolkit(component=self, metadata=metadata).update_tools_metadata(tools=tools)
1405
+
1406
+ def check_for_tool_tag_change(self, old_tags: list[str], new_tags: list[str]) -> bool:
1407
+ # First check length - if different lengths, they can't be equal
1408
+ if len(old_tags) != len(new_tags):
1409
+ return True
1410
+ # Use set comparison for O(n) average case complexity, earlier the old_tags.sort() != new_tags.sort() was used
1411
+ return set(old_tags) != set(new_tags)
1412
+
1413
+ def _filter_tools_by_status(self, tools: list[Tool], metadata: pd.DataFrame | None) -> list[Tool]:
1414
+ """Filter tools based on their status in metadata.
1415
+
1416
+ Args:
1417
+ tools (list[Tool]): List of tools to filter.
1418
+ metadata (list[dict] | None): Tools metadata containing status information.
1419
+
1420
+ Returns:
1421
+ list[Tool]: Filtered list of tools.
1422
+ """
1423
+ # Convert metadata to a list of dicts if it's a DataFrame
1424
+ metadata_dict = None # Initialize as None to avoid lint issues with empty dict
1425
+ if isinstance(metadata, pd.DataFrame):
1426
+ metadata_dict = metadata.to_dict(orient="records")
1427
+
1428
+ # If metadata is None or empty, use enabled_tools
1429
+ if not metadata_dict:
1430
+ enabled = self.enabled_tools
1431
+ return (
1432
+ tools
1433
+ if enabled is None
1434
+ else [
1435
+ tool for tool in tools if any(enabled_name in [tool.name, *tool.tags] for enabled_name in enabled)
1436
+ ]
1437
+ )
1438
+
1439
+ # Ensure metadata is a list of dicts
1440
+ if not isinstance(metadata_dict, list):
1441
+ return tools
1442
+
1443
+ # Create a mapping of tool names to their status
1444
+ tool_status = {item["name"]: item.get("status", True) for item in metadata_dict}
1445
+ return [tool for tool in tools if tool_status.get(tool.name, True)]
1446
+
1447
+ def _build_tool_data(self, tool: Tool) -> dict:
1448
+ if tool.metadata is None:
1449
+ tool.metadata = {}
1450
+ return {
1451
+ "name": tool.name,
1452
+ "description": tool.description,
1453
+ "tags": tool.tags if hasattr(tool, "tags") and tool.tags else [tool.name],
1454
+ "status": True, # Initialize all tools with status True
1455
+ "display_name": tool.metadata.get("display_name", tool.name),
1456
+ "display_description": tool.metadata.get("display_description", tool.description),
1457
+ "readonly": tool.metadata.get("readonly", False),
1458
+ "args": tool.args,
1459
+ # "args_schema": tool.args_schema,
1460
+ }
1461
+
1462
+ async def _build_tools_metadata_input(self):
1463
+ try:
1464
+ from lfx.inputs.inputs import ToolsInput
1465
+ except ImportError as e:
1466
+ msg = "Failed to import ToolsInput from lfx.inputs.inputs"
1467
+ raise ImportError(msg) from e
1468
+ placeholder = None
1469
+ tools = []
1470
+ try:
1471
+ # Handle both sync and async _get_tools methods
1472
+ # TODO: this check can be remomved ince get tools is async
1473
+ if asyncio.iscoroutinefunction(self._get_tools):
1474
+ tools = await self._get_tools()
1475
+ else:
1476
+ tools = self._get_tools()
1477
+
1478
+ placeholder = "Loading actions..." if len(tools) == 0 else ""
1479
+ except (TimeoutError, asyncio.TimeoutError):
1480
+ placeholder = "Timeout loading actions"
1481
+ except (ConnectionError, OSError, ValueError):
1482
+ placeholder = "Error loading actions"
1483
+ # Always use the latest tool data
1484
+ tool_data = [self._build_tool_data(tool) for tool in tools]
1485
+ # print(tool_data)
1486
+ if hasattr(self, TOOLS_METADATA_INPUT_NAME):
1487
+ old_tags = self._extract_tools_tags(self.tools_metadata)
1488
+ new_tags = self._extract_tools_tags(tool_data)
1489
+ if self.check_for_tool_tag_change(old_tags, new_tags):
1490
+ # If enabled tools are set, update status based on them
1491
+ enabled = self.enabled_tools
1492
+ if enabled is not None:
1493
+ for item in tool_data:
1494
+ item["status"] = any(enabled_name in [item["name"], *item["tags"]] for enabled_name in enabled)
1495
+ self.tools_metadata = tool_data
1496
+ else:
1497
+ # Preserve existing status values
1498
+ existing_status = {item["name"]: item.get("status", True) for item in self.tools_metadata}
1499
+ for item in tool_data:
1500
+ item["status"] = existing_status.get(item["name"], True)
1501
+ tool_data = self.tools_metadata
1502
+ else:
1503
+ # If enabled tools are set, update status based on them
1504
+ enabled = self.enabled_tools
1505
+ if enabled is not None:
1506
+ for item in tool_data:
1507
+ item["status"] = any(enabled_name in [item["name"], *item["tags"]] for enabled_name in enabled)
1508
+ self.tools_metadata = tool_data
1509
+
1510
+ return ToolsInput(
1511
+ name=TOOLS_METADATA_INPUT_NAME,
1512
+ placeholder=placeholder,
1513
+ display_name="Actions",
1514
+ info=TOOLS_METADATA_INFO,
1515
+ value=tool_data,
1516
+ )
1517
+
1518
+ def get_project_name(self):
1519
+ if hasattr(self, "_tracing_service") and self.tracing_service:
1520
+ return self.tracing_service.project_name
1521
+ return "Langflow"
1522
+
1523
+ def log(self, message: LoggableType | list[LoggableType], name: str | None = None) -> None:
1524
+ """Logs a message.
1525
+
1526
+ Args:
1527
+ message (LoggableType | list[LoggableType]): The message to log.
1528
+ name (str, optional): The name of the log. Defaults to None.
1529
+ """
1530
+ if name is None:
1531
+ name = f"Log {len(self._logs) + 1}"
1532
+ log = Log(message=message, type=get_artifact_type(message), name=name)
1533
+ self._logs.append(log)
1534
+ if self.tracing_service and self._vertex:
1535
+ self.tracing_service.add_log(trace_name=self.trace_name, log=log)
1536
+ if self._event_manager is not None and self._current_output:
1537
+ data = log.model_dump()
1538
+ data["output"] = self._current_output
1539
+ data["component_id"] = self._id
1540
+ self._event_manager.on_log(data=data)
1541
+
1542
+ def _append_tool_output(self) -> None:
1543
+ if next((output for output in self.outputs if output.name == TOOL_OUTPUT_NAME), None) is None:
1544
+ self.outputs.append(
1545
+ Output(
1546
+ name=TOOL_OUTPUT_NAME,
1547
+ display_name=TOOL_OUTPUT_DISPLAY_NAME,
1548
+ method="to_toolkit",
1549
+ types=["Tool"],
1550
+ )
1551
+ )
1552
+
1553
+ def is_connected_to_chat_output(self) -> bool:
1554
+ # Lazy import to avoid circular dependency
1555
+ from lfx.graph.utils import has_chat_output
1556
+
1557
+ return has_chat_output(self.graph.get_vertex_neighbors(self._vertex))
1558
+
1559
+ def is_connected_to_chat_input(self) -> bool:
1560
+ # Lazy import to avoid circular dependency
1561
+ from lfx.graph.utils import has_chat_input
1562
+
1563
+ if self.graph is None:
1564
+ return False
1565
+ return has_chat_input(self.graph.get_vertex_neighbors(self._vertex))
1566
+
1567
+ def _should_skip_message(self, message: Message) -> bool:
1568
+ """Check if the message should be skipped based on vertex configuration and message type."""
1569
+ return (
1570
+ self._vertex is not None
1571
+ and not (self._vertex.is_output or self._vertex.is_input)
1572
+ and not self.is_connected_to_chat_output()
1573
+ and not isinstance(message, ErrorMessage)
1574
+ )
1575
+
1576
+ def _ensure_message_required_fields(self, message: Message) -> None:
1577
+ """Ensure message has required fields for storage (session_id, sender, sender_name).
1578
+
1579
+ Only sets default values if the fields are not already provided.
1580
+ """
1581
+ from lfx.utils.constants import MESSAGE_SENDER_AI, MESSAGE_SENDER_NAME_AI
1582
+
1583
+ # Set default session_id from graph if not already set
1584
+ if (
1585
+ not message.session_id
1586
+ and hasattr(self, "graph")
1587
+ and hasattr(self.graph, "session_id")
1588
+ and self.graph.session_id
1589
+ ):
1590
+ session_id = (
1591
+ UUID(self.graph.session_id) if isinstance(self.graph.session_id, str) else self.graph.session_id
1592
+ )
1593
+ message.session_id = session_id
1594
+
1595
+ # Set default sender if not set (preserves existing values)
1596
+ if not message.sender:
1597
+ message.sender = MESSAGE_SENDER_AI
1598
+
1599
+ # Set default sender_name if not set (preserves existing values)
1600
+ if not message.sender_name:
1601
+ message.sender_name = MESSAGE_SENDER_NAME_AI
1602
+
1603
+ async def send_message(self, message: Message, id_: str | None = None, *, skip_db_update: bool = False):
1604
+ """Send a message with optional database update control.
1605
+
1606
+ Args:
1607
+ message: The message to send
1608
+ id_: Optional message ID
1609
+ skip_db_update: If True, only update in-memory and send event, skip DB write.
1610
+ Useful during streaming to avoid excessive DB round-trips.
1611
+ Note: This assumes the message already exists in the database with message.id set.
1612
+ """
1613
+ if self._should_skip_message(message):
1614
+ return message
1615
+
1616
+ if hasattr(message, "flow_id") and isinstance(message.flow_id, str):
1617
+ message.flow_id = UUID(message.flow_id)
1618
+
1619
+ # Ensure required fields for message storage are set
1620
+ self._ensure_message_required_fields(message)
1621
+
1622
+ # If skip_db_update is True and message already has an ID, skip the DB write
1623
+ # This path is used during agent streaming to avoid excessive DB round-trips
1624
+ if skip_db_update and message.id:
1625
+ # Create a fresh Message instance for consistency with normal flow
1626
+ stored_message = await Message.create(**message.model_dump())
1627
+ self._stored_message_id = stored_message.id
1628
+ # Still send the event to update the client in real-time
1629
+ # Note: If this fails, we don't need DB cleanup since we didn't write to DB
1630
+ await self._send_message_event(stored_message, id_=id_)
1631
+ else:
1632
+ # Normal flow: store/update in database
1633
+ stored_message = await self._store_message(message)
1634
+
1635
+ self._stored_message_id = stored_message.id
1636
+ try:
1637
+ complete_message = ""
1638
+ if (
1639
+ self._should_stream_message(stored_message, message)
1640
+ and message is not None
1641
+ and isinstance(message.text, AsyncIterator | Iterator)
1642
+ ):
1643
+ complete_message = await self._stream_message(message.text, stored_message)
1644
+ stored_message.text = complete_message
1645
+ if complete_message:
1646
+ stored_message.properties.state = "complete"
1647
+ stored_message = await self._update_stored_message(stored_message)
1648
+ # Note: We intentionally do NOT send a message event here with state="complete"
1649
+ # The frontend already has all the content from streaming tokens
1650
+ # Only the database is updated with the complete state
1651
+ else:
1652
+ # Only send message event for non-streaming messages
1653
+ await self._send_message_event(stored_message, id_=id_)
1654
+ except Exception:
1655
+ # remove the message from the database
1656
+ await delete_message(stored_message.id)
1657
+ raise
1658
+ self.status = stored_message
1659
+ return stored_message
1660
+
1661
+ async def _store_message(self, message: Message) -> Message:
1662
+ flow_id: str | None = None
1663
+ if hasattr(self, "graph"):
1664
+ # Convert UUID to str if needed
1665
+ flow_id = str(self.graph.flow_id) if self.graph.flow_id else None
1666
+ stored_messages = await astore_message(message, flow_id=flow_id)
1667
+ if len(stored_messages) != 1:
1668
+ msg = "Only one message can be stored at a time."
1669
+ raise ValueError(msg)
1670
+ stored_message = stored_messages[0]
1671
+ return await Message.create(**stored_message.model_dump())
1672
+
1673
+ async def _send_message_event(self, message: Message, id_: str | None = None, category: str | None = None) -> None:
1674
+ if hasattr(self, "_event_manager") and self._event_manager:
1675
+ data_dict = message.model_dump()["data"] if hasattr(message, "data") else message.model_dump()
1676
+ if id_ and not data_dict.get("id"):
1677
+ data_dict["id"] = id_
1678
+ category = category or data_dict.get("category", None)
1679
+
1680
+ def _send_event():
1681
+ match category:
1682
+ case "error":
1683
+ self._event_manager.on_error(data=data_dict)
1684
+ case "remove_message":
1685
+ # Check if id exists in data_dict before accessing it
1686
+ if "id" in data_dict:
1687
+ self._event_manager.on_remove_message(data={"id": data_dict["id"]})
1688
+ else:
1689
+ # If no id, try to get it from the message object or id_ parameter
1690
+ message_id = getattr(message, "id", None) or id_
1691
+ if message_id:
1692
+ self._event_manager.on_remove_message(data={"id": message_id})
1693
+ case _:
1694
+ self._event_manager.on_message(data=data_dict)
1695
+
1696
+ await asyncio.to_thread(_send_event)
1697
+
1698
+ def _should_stream_message(self, stored_message: Message, original_message: Message) -> bool:
1699
+ return bool(
1700
+ hasattr(self, "_event_manager")
1701
+ and self._event_manager
1702
+ and stored_message.id
1703
+ and not isinstance(original_message.text, str)
1704
+ )
1705
+
1706
+ async def _update_stored_message(self, message: Message) -> Message:
1707
+ """Update the stored message."""
1708
+ if hasattr(self, "_vertex") and self._vertex is not None and hasattr(self._vertex, "graph"):
1709
+ flow_id = (
1710
+ UUID(self._vertex.graph.flow_id)
1711
+ if isinstance(self._vertex.graph.flow_id, str)
1712
+ else self._vertex.graph.flow_id
1713
+ )
1714
+
1715
+ message.flow_id = flow_id
1716
+
1717
+ message_tables = await aupdate_messages(message)
1718
+ if not message_tables:
1719
+ msg = "Failed to update message"
1720
+ raise ValueError(msg)
1721
+ message_table = message_tables[0]
1722
+ return await Message.create(**message_table.model_dump())
1723
+
1724
+ async def _stream_message(self, iterator: AsyncIterator | Iterator, message: Message) -> str:
1725
+ if not isinstance(iterator, AsyncIterator | Iterator):
1726
+ msg = "The message must be an iterator or an async iterator."
1727
+ raise TypeError(msg)
1728
+
1729
+ if isinstance(iterator, AsyncIterator):
1730
+ return await self._handle_async_iterator(iterator, message.id, message)
1731
+ try:
1732
+ complete_message = ""
1733
+ first_chunk = True
1734
+ for chunk in iterator:
1735
+ complete_message = await self._process_chunk(
1736
+ chunk.content, complete_message, message.id, message, first_chunk=first_chunk
1737
+ )
1738
+ first_chunk = False
1739
+ except Exception as e:
1740
+ raise StreamingError(cause=e, source=message.properties.source) from e
1741
+ else:
1742
+ return complete_message
1743
+
1744
+ async def _handle_async_iterator(self, iterator: AsyncIterator, message_id: str, message: Message) -> str:
1745
+ complete_message = ""
1746
+ first_chunk = True
1747
+ async for chunk in iterator:
1748
+ complete_message = await self._process_chunk(
1749
+ chunk.content, complete_message, message_id, message, first_chunk=first_chunk
1750
+ )
1751
+ first_chunk = False
1752
+ return complete_message
1753
+
1754
+ async def _process_chunk(
1755
+ self, chunk: str, complete_message: str, message_id: str, message: Message, *, first_chunk: bool = False
1756
+ ) -> str:
1757
+ complete_message += chunk
1758
+ if self._event_manager:
1759
+ if first_chunk:
1760
+ # Send the initial message only on the first chunk
1761
+ msg_copy = message.model_copy()
1762
+ msg_copy.text = complete_message
1763
+ await self._send_message_event(msg_copy, id_=message_id)
1764
+ await asyncio.to_thread(
1765
+ self._event_manager.on_token,
1766
+ data={
1767
+ "chunk": chunk,
1768
+ "id": str(message_id),
1769
+ },
1770
+ )
1771
+ return complete_message
1772
+
1773
+ async def send_error(
1774
+ self,
1775
+ exception: Exception,
1776
+ session_id: str,
1777
+ trace_name: str,
1778
+ source: Source,
1779
+ ) -> Message | None:
1780
+ """Send an error message to the frontend."""
1781
+ flow_id = self.graph.flow_id if hasattr(self, "graph") else None
1782
+ if not session_id:
1783
+ return None
1784
+ error_message = ErrorMessage(
1785
+ flow_id=flow_id,
1786
+ exception=exception,
1787
+ session_id=session_id,
1788
+ trace_name=trace_name,
1789
+ source=source,
1790
+ )
1791
+ await self.send_message(error_message)
1792
+ return error_message
1793
+
1794
+ def _append_tool_to_outputs_map(self):
1795
+ self._outputs_map[TOOL_OUTPUT_NAME] = self._build_tool_output()
1796
+ # add a new input for the tool schema
1797
+ # self.inputs.append(self._build_tool_schema())
1798
+
1799
+ def _build_tool_output(self) -> Output:
1800
+ return Output(name=TOOL_OUTPUT_NAME, display_name=TOOL_OUTPUT_DISPLAY_NAME, method="to_toolkit", types=["Tool"])
1801
+
1802
+ def get_input_display_name(self, input_name: str) -> str:
1803
+ """Get the display name of an input.
1804
+
1805
+ This is a public utility method that subclasses can use to get user-friendly
1806
+ display names for inputs when building error messages or UI elements.
1807
+
1808
+ Usage:
1809
+ msg = f"Input {self.get_input_display_name(input_name)} not found"
1810
+
1811
+ Args:
1812
+ input_name (str): The name of the input.
1813
+
1814
+ Returns:
1815
+ str: The display name of the input, or the input name if not found.
1816
+ """
1817
+ if input_name in self._inputs:
1818
+ return getattr(self._inputs[input_name], "display_name", input_name)
1819
+ return input_name
1820
+
1821
+ def get_output_display_name(self, output_name: str) -> str:
1822
+ """Get the display name of an output.
1823
+
1824
+ This is a public utility method that subclasses can use to get user-friendly
1825
+ display names for outputs when building error messages or UI elements.
1826
+
1827
+ Args:
1828
+ output_name (str): The name of the output.
1829
+
1830
+ Returns:
1831
+ str: The display name of the output, or the output name if not found.
1832
+ """
1833
+ if output_name in self._outputs_map:
1834
+ return getattr(self._outputs_map[output_name], "display_name", output_name)
1835
+ return output_name
1836
+
1837
+ def build_input_error_message(self, input_name: str, message: str) -> str:
1838
+ """Build an error message for an input.
1839
+
1840
+ This is a public utility method that subclasses can use to create consistent,
1841
+ user-friendly error messages that reference inputs by their display names.
1842
+ The input name is placed at the beginning to ensure it's visible even if the message is truncated.
1843
+
1844
+ Args:
1845
+ input_name (str): The name of the input.
1846
+ message (str): The error message.
1847
+
1848
+ Returns:
1849
+ str: The formatted error message with display name.
1850
+ """
1851
+ display_name = self.get_input_display_name(input_name)
1852
+ return f"[Input: {display_name}] {message}"
1853
+
1854
+ def build_output_error_message(self, output_name: str, message: str) -> str:
1855
+ """Build an error message for an output.
1856
+
1857
+ This is a public utility method that subclasses can use to create consistent,
1858
+ user-friendly error messages that reference outputs by their display names.
1859
+ The output name is placed at the beginning to ensure it's visible even if the message is truncated.
1860
+
1861
+ Args:
1862
+ output_name (str): The name of the output.
1863
+ message (str): The error message.
1864
+
1865
+ Returns:
1866
+ str: The formatted error message with display name.
1867
+ """
1868
+ display_name = self.get_output_display_name(output_name)
1869
+ return f"[Output: {display_name}] {message}"
1870
+
1871
+ def build_component_error_message(self, message: str) -> str:
1872
+ """Build an error message for the component.
1873
+
1874
+ This is a public utility method that subclasses can use to create consistent,
1875
+ user-friendly error messages that reference the component by its display name.
1876
+ The component name is placed at the beginning to ensure it's visible even if the message is truncated.
1877
+
1878
+ Args:
1879
+ message (str): The error message.
1880
+
1881
+ Returns:
1882
+ str: The formatted error message with component display name.
1883
+ """
1884
+ return f"[Component: {self.display_name or self.__class__.__name__}] {message}"
1885
+
1886
+
1887
+ def _get_component_toolkit():
1888
+ from lfx.base.tools.component_tool import ComponentToolkit
1889
+
1890
+ return ComponentToolkit