lfx-nightly 0.1.11.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lfx/__init__.py +0 -0
- lfx/__main__.py +25 -0
- lfx/base/__init__.py +0 -0
- lfx/base/agents/__init__.py +0 -0
- lfx/base/agents/agent.py +268 -0
- lfx/base/agents/callback.py +130 -0
- lfx/base/agents/context.py +109 -0
- lfx/base/agents/crewai/__init__.py +0 -0
- lfx/base/agents/crewai/crew.py +231 -0
- lfx/base/agents/crewai/tasks.py +12 -0
- lfx/base/agents/default_prompts.py +23 -0
- lfx/base/agents/errors.py +15 -0
- lfx/base/agents/events.py +346 -0
- lfx/base/agents/utils.py +205 -0
- lfx/base/astra_assistants/__init__.py +0 -0
- lfx/base/astra_assistants/util.py +171 -0
- lfx/base/chains/__init__.py +0 -0
- lfx/base/chains/model.py +19 -0
- lfx/base/composio/__init__.py +0 -0
- lfx/base/composio/composio_base.py +1291 -0
- lfx/base/compressors/__init__.py +0 -0
- lfx/base/compressors/model.py +60 -0
- lfx/base/constants.py +46 -0
- lfx/base/curl/__init__.py +0 -0
- lfx/base/curl/parse.py +188 -0
- lfx/base/data/__init__.py +5 -0
- lfx/base/data/base_file.py +685 -0
- lfx/base/data/docling_utils.py +245 -0
- lfx/base/data/utils.py +198 -0
- lfx/base/document_transformers/__init__.py +0 -0
- lfx/base/document_transformers/model.py +43 -0
- lfx/base/embeddings/__init__.py +0 -0
- lfx/base/embeddings/aiml_embeddings.py +62 -0
- lfx/base/embeddings/model.py +26 -0
- lfx/base/flow_processing/__init__.py +0 -0
- lfx/base/flow_processing/utils.py +86 -0
- lfx/base/huggingface/__init__.py +0 -0
- lfx/base/huggingface/model_bridge.py +133 -0
- lfx/base/io/__init__.py +0 -0
- lfx/base/io/chat.py +20 -0
- lfx/base/io/text.py +22 -0
- lfx/base/langchain_utilities/__init__.py +0 -0
- lfx/base/langchain_utilities/model.py +35 -0
- lfx/base/langchain_utilities/spider_constants.py +1 -0
- lfx/base/langwatch/__init__.py +0 -0
- lfx/base/langwatch/utils.py +18 -0
- lfx/base/mcp/__init__.py +0 -0
- lfx/base/mcp/constants.py +2 -0
- lfx/base/mcp/util.py +1398 -0
- lfx/base/memory/__init__.py +0 -0
- lfx/base/memory/memory.py +49 -0
- lfx/base/memory/model.py +38 -0
- lfx/base/models/__init__.py +3 -0
- lfx/base/models/aiml_constants.py +51 -0
- lfx/base/models/anthropic_constants.py +47 -0
- lfx/base/models/aws_constants.py +151 -0
- lfx/base/models/chat_result.py +76 -0
- lfx/base/models/google_generative_ai_constants.py +70 -0
- lfx/base/models/groq_constants.py +134 -0
- lfx/base/models/model.py +375 -0
- lfx/base/models/model_input_constants.py +307 -0
- lfx/base/models/model_metadata.py +41 -0
- lfx/base/models/model_utils.py +8 -0
- lfx/base/models/novita_constants.py +35 -0
- lfx/base/models/ollama_constants.py +49 -0
- lfx/base/models/openai_constants.py +122 -0
- lfx/base/models/sambanova_constants.py +18 -0
- lfx/base/processing/__init__.py +0 -0
- lfx/base/prompts/__init__.py +0 -0
- lfx/base/prompts/api_utils.py +224 -0
- lfx/base/prompts/utils.py +61 -0
- lfx/base/textsplitters/__init__.py +0 -0
- lfx/base/textsplitters/model.py +28 -0
- lfx/base/tools/__init__.py +0 -0
- lfx/base/tools/base.py +26 -0
- lfx/base/tools/component_tool.py +325 -0
- lfx/base/tools/constants.py +49 -0
- lfx/base/tools/flow_tool.py +132 -0
- lfx/base/tools/run_flow.py +224 -0
- lfx/base/vectorstores/__init__.py +0 -0
- lfx/base/vectorstores/model.py +193 -0
- lfx/base/vectorstores/utils.py +22 -0
- lfx/base/vectorstores/vector_store_connection_decorator.py +52 -0
- lfx/cli/__init__.py +5 -0
- lfx/cli/commands.py +319 -0
- lfx/cli/common.py +650 -0
- lfx/cli/run.py +441 -0
- lfx/cli/script_loader.py +247 -0
- lfx/cli/serve_app.py +546 -0
- lfx/cli/validation.py +69 -0
- lfx/components/FAISS/__init__.py +34 -0
- lfx/components/FAISS/faiss.py +111 -0
- lfx/components/Notion/__init__.py +19 -0
- lfx/components/Notion/add_content_to_page.py +269 -0
- lfx/components/Notion/create_page.py +94 -0
- lfx/components/Notion/list_database_properties.py +68 -0
- lfx/components/Notion/list_pages.py +122 -0
- lfx/components/Notion/list_users.py +77 -0
- lfx/components/Notion/page_content_viewer.py +93 -0
- lfx/components/Notion/search.py +111 -0
- lfx/components/Notion/update_page_property.py +114 -0
- lfx/components/__init__.py +411 -0
- lfx/components/_importing.py +42 -0
- lfx/components/agentql/__init__.py +3 -0
- lfx/components/agentql/agentql_api.py +151 -0
- lfx/components/agents/__init__.py +34 -0
- lfx/components/agents/agent.py +558 -0
- lfx/components/agents/mcp_component.py +501 -0
- lfx/components/aiml/__init__.py +37 -0
- lfx/components/aiml/aiml.py +112 -0
- lfx/components/aiml/aiml_embeddings.py +37 -0
- lfx/components/amazon/__init__.py +36 -0
- lfx/components/amazon/amazon_bedrock_embedding.py +109 -0
- lfx/components/amazon/amazon_bedrock_model.py +124 -0
- lfx/components/amazon/s3_bucket_uploader.py +211 -0
- lfx/components/anthropic/__init__.py +34 -0
- lfx/components/anthropic/anthropic.py +187 -0
- lfx/components/apify/__init__.py +5 -0
- lfx/components/apify/apify_actor.py +325 -0
- lfx/components/arxiv/__init__.py +3 -0
- lfx/components/arxiv/arxiv.py +163 -0
- lfx/components/assemblyai/__init__.py +46 -0
- lfx/components/assemblyai/assemblyai_get_subtitles.py +83 -0
- lfx/components/assemblyai/assemblyai_lemur.py +183 -0
- lfx/components/assemblyai/assemblyai_list_transcripts.py +95 -0
- lfx/components/assemblyai/assemblyai_poll_transcript.py +72 -0
- lfx/components/assemblyai/assemblyai_start_transcript.py +188 -0
- lfx/components/azure/__init__.py +37 -0
- lfx/components/azure/azure_openai.py +95 -0
- lfx/components/azure/azure_openai_embeddings.py +83 -0
- lfx/components/baidu/__init__.py +32 -0
- lfx/components/baidu/baidu_qianfan_chat.py +113 -0
- lfx/components/bing/__init__.py +3 -0
- lfx/components/bing/bing_search_api.py +61 -0
- lfx/components/cassandra/__init__.py +40 -0
- lfx/components/cassandra/cassandra.py +264 -0
- lfx/components/cassandra/cassandra_chat.py +92 -0
- lfx/components/cassandra/cassandra_graph.py +238 -0
- lfx/components/chains/__init__.py +3 -0
- lfx/components/chroma/__init__.py +34 -0
- lfx/components/chroma/chroma.py +167 -0
- lfx/components/cleanlab/__init__.py +40 -0
- lfx/components/cleanlab/cleanlab_evaluator.py +155 -0
- lfx/components/cleanlab/cleanlab_rag_evaluator.py +254 -0
- lfx/components/cleanlab/cleanlab_remediator.py +131 -0
- lfx/components/clickhouse/__init__.py +34 -0
- lfx/components/clickhouse/clickhouse.py +135 -0
- lfx/components/cloudflare/__init__.py +32 -0
- lfx/components/cloudflare/cloudflare.py +81 -0
- lfx/components/cohere/__init__.py +40 -0
- lfx/components/cohere/cohere_embeddings.py +81 -0
- lfx/components/cohere/cohere_models.py +46 -0
- lfx/components/cohere/cohere_rerank.py +51 -0
- lfx/components/composio/__init__.py +74 -0
- lfx/components/composio/composio_api.py +268 -0
- lfx/components/composio/dropbox_compnent.py +11 -0
- lfx/components/composio/github_composio.py +11 -0
- lfx/components/composio/gmail_composio.py +38 -0
- lfx/components/composio/googlecalendar_composio.py +11 -0
- lfx/components/composio/googlemeet_composio.py +11 -0
- lfx/components/composio/googletasks_composio.py +8 -0
- lfx/components/composio/linear_composio.py +11 -0
- lfx/components/composio/outlook_composio.py +11 -0
- lfx/components/composio/reddit_composio.py +11 -0
- lfx/components/composio/slack_composio.py +582 -0
- lfx/components/composio/slackbot_composio.py +11 -0
- lfx/components/composio/supabase_composio.py +11 -0
- lfx/components/composio/todoist_composio.py +11 -0
- lfx/components/composio/youtube_composio.py +11 -0
- lfx/components/confluence/__init__.py +3 -0
- lfx/components/confluence/confluence.py +84 -0
- lfx/components/couchbase/__init__.py +34 -0
- lfx/components/couchbase/couchbase.py +102 -0
- lfx/components/crewai/__init__.py +49 -0
- lfx/components/crewai/crewai.py +107 -0
- lfx/components/crewai/hierarchical_crew.py +46 -0
- lfx/components/crewai/hierarchical_task.py +44 -0
- lfx/components/crewai/sequential_crew.py +52 -0
- lfx/components/crewai/sequential_task.py +73 -0
- lfx/components/crewai/sequential_task_agent.py +143 -0
- lfx/components/custom_component/__init__.py +34 -0
- lfx/components/custom_component/custom_component.py +31 -0
- lfx/components/data/__init__.py +64 -0
- lfx/components/data/api_request.py +544 -0
- lfx/components/data/csv_to_data.py +95 -0
- lfx/components/data/directory.py +113 -0
- lfx/components/data/file.py +577 -0
- lfx/components/data/json_to_data.py +98 -0
- lfx/components/data/news_search.py +164 -0
- lfx/components/data/rss.py +69 -0
- lfx/components/data/sql_executor.py +101 -0
- lfx/components/data/url.py +311 -0
- lfx/components/data/web_search.py +112 -0
- lfx/components/data/webhook.py +56 -0
- lfx/components/datastax/__init__.py +70 -0
- lfx/components/datastax/astra_assistant_manager.py +306 -0
- lfx/components/datastax/astra_db.py +75 -0
- lfx/components/datastax/astra_vectorize.py +124 -0
- lfx/components/datastax/astradb.py +1285 -0
- lfx/components/datastax/astradb_cql.py +314 -0
- lfx/components/datastax/astradb_graph.py +330 -0
- lfx/components/datastax/astradb_tool.py +414 -0
- lfx/components/datastax/astradb_vectorstore.py +1285 -0
- lfx/components/datastax/cassandra.py +92 -0
- lfx/components/datastax/create_assistant.py +58 -0
- lfx/components/datastax/create_thread.py +32 -0
- lfx/components/datastax/dotenv.py +35 -0
- lfx/components/datastax/get_assistant.py +37 -0
- lfx/components/datastax/getenvvar.py +30 -0
- lfx/components/datastax/graph_rag.py +141 -0
- lfx/components/datastax/hcd.py +314 -0
- lfx/components/datastax/list_assistants.py +25 -0
- lfx/components/datastax/run.py +89 -0
- lfx/components/deactivated/__init__.py +15 -0
- lfx/components/deactivated/amazon_kendra.py +66 -0
- lfx/components/deactivated/chat_litellm_model.py +158 -0
- lfx/components/deactivated/code_block_extractor.py +26 -0
- lfx/components/deactivated/documents_to_data.py +22 -0
- lfx/components/deactivated/embed.py +16 -0
- lfx/components/deactivated/extract_key_from_data.py +46 -0
- lfx/components/deactivated/json_document_builder.py +57 -0
- lfx/components/deactivated/list_flows.py +20 -0
- lfx/components/deactivated/mcp_sse.py +61 -0
- lfx/components/deactivated/mcp_stdio.py +62 -0
- lfx/components/deactivated/merge_data.py +93 -0
- lfx/components/deactivated/message.py +37 -0
- lfx/components/deactivated/metal.py +54 -0
- lfx/components/deactivated/multi_query.py +59 -0
- lfx/components/deactivated/retriever.py +43 -0
- lfx/components/deactivated/selective_passthrough.py +77 -0
- lfx/components/deactivated/should_run_next.py +40 -0
- lfx/components/deactivated/split_text.py +63 -0
- lfx/components/deactivated/store_message.py +24 -0
- lfx/components/deactivated/sub_flow.py +124 -0
- lfx/components/deactivated/vectara_self_query.py +76 -0
- lfx/components/deactivated/vector_store.py +24 -0
- lfx/components/deepseek/__init__.py +34 -0
- lfx/components/deepseek/deepseek.py +136 -0
- lfx/components/docling/__init__.py +43 -0
- lfx/components/docling/chunk_docling_document.py +186 -0
- lfx/components/docling/docling_inline.py +231 -0
- lfx/components/docling/docling_remote.py +193 -0
- lfx/components/docling/export_docling_document.py +117 -0
- lfx/components/documentloaders/__init__.py +3 -0
- lfx/components/duckduckgo/__init__.py +3 -0
- lfx/components/duckduckgo/duck_duck_go_search_run.py +92 -0
- lfx/components/elastic/__init__.py +37 -0
- lfx/components/elastic/elasticsearch.py +267 -0
- lfx/components/elastic/opensearch.py +243 -0
- lfx/components/embeddings/__init__.py +37 -0
- lfx/components/embeddings/similarity.py +76 -0
- lfx/components/embeddings/text_embedder.py +64 -0
- lfx/components/exa/__init__.py +3 -0
- lfx/components/exa/exa_search.py +68 -0
- lfx/components/firecrawl/__init__.py +43 -0
- lfx/components/firecrawl/firecrawl_crawl_api.py +88 -0
- lfx/components/firecrawl/firecrawl_extract_api.py +136 -0
- lfx/components/firecrawl/firecrawl_map_api.py +89 -0
- lfx/components/firecrawl/firecrawl_scrape_api.py +73 -0
- lfx/components/git/__init__.py +4 -0
- lfx/components/git/git.py +262 -0
- lfx/components/git/gitextractor.py +196 -0
- lfx/components/glean/__init__.py +3 -0
- lfx/components/glean/glean_search_api.py +173 -0
- lfx/components/google/__init__.py +17 -0
- lfx/components/google/gmail.py +192 -0
- lfx/components/google/google_bq_sql_executor.py +157 -0
- lfx/components/google/google_drive.py +92 -0
- lfx/components/google/google_drive_search.py +152 -0
- lfx/components/google/google_generative_ai.py +147 -0
- lfx/components/google/google_generative_ai_embeddings.py +141 -0
- lfx/components/google/google_oauth_token.py +89 -0
- lfx/components/google/google_search_api_core.py +68 -0
- lfx/components/google/google_serper_api_core.py +74 -0
- lfx/components/groq/__init__.py +34 -0
- lfx/components/groq/groq.py +136 -0
- lfx/components/helpers/__init__.py +52 -0
- lfx/components/helpers/calculator_core.py +89 -0
- lfx/components/helpers/create_list.py +40 -0
- lfx/components/helpers/current_date.py +42 -0
- lfx/components/helpers/id_generator.py +42 -0
- lfx/components/helpers/memory.py +251 -0
- lfx/components/helpers/output_parser.py +45 -0
- lfx/components/helpers/store_message.py +90 -0
- lfx/components/homeassistant/__init__.py +7 -0
- lfx/components/homeassistant/home_assistant_control.py +152 -0
- lfx/components/homeassistant/list_home_assistant_states.py +137 -0
- lfx/components/huggingface/__init__.py +37 -0
- lfx/components/huggingface/huggingface.py +197 -0
- lfx/components/huggingface/huggingface_inference_api.py +106 -0
- lfx/components/ibm/__init__.py +34 -0
- lfx/components/ibm/watsonx.py +203 -0
- lfx/components/ibm/watsonx_embeddings.py +135 -0
- lfx/components/icosacomputing/__init__.py +5 -0
- lfx/components/icosacomputing/combinatorial_reasoner.py +84 -0
- lfx/components/input_output/__init__.py +38 -0
- lfx/components/input_output/chat.py +120 -0
- lfx/components/input_output/chat_output.py +200 -0
- lfx/components/input_output/text.py +27 -0
- lfx/components/input_output/text_output.py +29 -0
- lfx/components/jigsawstack/__init__.py +23 -0
- lfx/components/jigsawstack/ai_scrape.py +126 -0
- lfx/components/jigsawstack/ai_web_search.py +136 -0
- lfx/components/jigsawstack/file_read.py +115 -0
- lfx/components/jigsawstack/file_upload.py +94 -0
- lfx/components/jigsawstack/image_generation.py +205 -0
- lfx/components/jigsawstack/nsfw.py +60 -0
- lfx/components/jigsawstack/object_detection.py +124 -0
- lfx/components/jigsawstack/sentiment.py +112 -0
- lfx/components/jigsawstack/text_to_sql.py +90 -0
- lfx/components/jigsawstack/text_translate.py +77 -0
- lfx/components/jigsawstack/vocr.py +107 -0
- lfx/components/langchain_utilities/__init__.py +109 -0
- lfx/components/langchain_utilities/character.py +53 -0
- lfx/components/langchain_utilities/conversation.py +59 -0
- lfx/components/langchain_utilities/csv_agent.py +107 -0
- lfx/components/langchain_utilities/fake_embeddings.py +26 -0
- lfx/components/langchain_utilities/html_link_extractor.py +35 -0
- lfx/components/langchain_utilities/json_agent.py +45 -0
- lfx/components/langchain_utilities/langchain_hub.py +126 -0
- lfx/components/langchain_utilities/language_recursive.py +49 -0
- lfx/components/langchain_utilities/language_semantic.py +138 -0
- lfx/components/langchain_utilities/llm_checker.py +39 -0
- lfx/components/langchain_utilities/llm_math.py +42 -0
- lfx/components/langchain_utilities/natural_language.py +61 -0
- lfx/components/langchain_utilities/openai_tools.py +53 -0
- lfx/components/langchain_utilities/openapi.py +48 -0
- lfx/components/langchain_utilities/recursive_character.py +60 -0
- lfx/components/langchain_utilities/retrieval_qa.py +83 -0
- lfx/components/langchain_utilities/runnable_executor.py +137 -0
- lfx/components/langchain_utilities/self_query.py +80 -0
- lfx/components/langchain_utilities/spider.py +142 -0
- lfx/components/langchain_utilities/sql.py +40 -0
- lfx/components/langchain_utilities/sql_database.py +35 -0
- lfx/components/langchain_utilities/sql_generator.py +78 -0
- lfx/components/langchain_utilities/tool_calling.py +59 -0
- lfx/components/langchain_utilities/vector_store_info.py +49 -0
- lfx/components/langchain_utilities/vector_store_router.py +33 -0
- lfx/components/langchain_utilities/xml_agent.py +71 -0
- lfx/components/langwatch/__init__.py +3 -0
- lfx/components/langwatch/langwatch.py +278 -0
- lfx/components/link_extractors/__init__.py +3 -0
- lfx/components/lmstudio/__init__.py +34 -0
- lfx/components/lmstudio/lmstudioembeddings.py +89 -0
- lfx/components/lmstudio/lmstudiomodel.py +129 -0
- lfx/components/logic/__init__.py +52 -0
- lfx/components/logic/conditional_router.py +171 -0
- lfx/components/logic/data_conditional_router.py +125 -0
- lfx/components/logic/flow_tool.py +110 -0
- lfx/components/logic/listen.py +29 -0
- lfx/components/logic/loop.py +125 -0
- lfx/components/logic/notify.py +88 -0
- lfx/components/logic/pass_message.py +35 -0
- lfx/components/logic/run_flow.py +71 -0
- lfx/components/logic/sub_flow.py +114 -0
- lfx/components/maritalk/__init__.py +32 -0
- lfx/components/maritalk/maritalk.py +52 -0
- lfx/components/mem0/__init__.py +3 -0
- lfx/components/mem0/mem0_chat_memory.py +136 -0
- lfx/components/milvus/__init__.py +34 -0
- lfx/components/milvus/milvus.py +115 -0
- lfx/components/mistral/__init__.py +37 -0
- lfx/components/mistral/mistral.py +114 -0
- lfx/components/mistral/mistral_embeddings.py +58 -0
- lfx/components/models/__init__.py +34 -0
- lfx/components/models/embedding_model.py +114 -0
- lfx/components/models/language_model.py +144 -0
- lfx/components/mongodb/__init__.py +34 -0
- lfx/components/mongodb/mongodb_atlas.py +213 -0
- lfx/components/needle/__init__.py +3 -0
- lfx/components/needle/needle.py +104 -0
- lfx/components/notdiamond/__init__.py +34 -0
- lfx/components/notdiamond/notdiamond.py +228 -0
- lfx/components/novita/__init__.py +32 -0
- lfx/components/novita/novita.py +130 -0
- lfx/components/nvidia/__init__.py +57 -0
- lfx/components/nvidia/nvidia.py +157 -0
- lfx/components/nvidia/nvidia_embedding.py +77 -0
- lfx/components/nvidia/nvidia_ingest.py +317 -0
- lfx/components/nvidia/nvidia_rerank.py +63 -0
- lfx/components/nvidia/system_assist.py +65 -0
- lfx/components/olivya/__init__.py +3 -0
- lfx/components/olivya/olivya.py +116 -0
- lfx/components/ollama/__init__.py +37 -0
- lfx/components/ollama/ollama.py +330 -0
- lfx/components/ollama/ollama_embeddings.py +106 -0
- lfx/components/openai/__init__.py +37 -0
- lfx/components/openai/openai.py +100 -0
- lfx/components/openai/openai_chat_model.py +176 -0
- lfx/components/openrouter/__init__.py +32 -0
- lfx/components/openrouter/openrouter.py +202 -0
- lfx/components/output_parsers/__init__.py +3 -0
- lfx/components/perplexity/__init__.py +34 -0
- lfx/components/perplexity/perplexity.py +75 -0
- lfx/components/pgvector/__init__.py +34 -0
- lfx/components/pgvector/pgvector.py +72 -0
- lfx/components/pinecone/__init__.py +34 -0
- lfx/components/pinecone/pinecone.py +134 -0
- lfx/components/processing/__init__.py +117 -0
- lfx/components/processing/alter_metadata.py +108 -0
- lfx/components/processing/batch_run.py +205 -0
- lfx/components/processing/combine_text.py +39 -0
- lfx/components/processing/converter.py +159 -0
- lfx/components/processing/create_data.py +110 -0
- lfx/components/processing/data_operations.py +438 -0
- lfx/components/processing/data_to_dataframe.py +70 -0
- lfx/components/processing/dataframe_operations.py +313 -0
- lfx/components/processing/extract_key.py +53 -0
- lfx/components/processing/filter_data.py +42 -0
- lfx/components/processing/filter_data_values.py +88 -0
- lfx/components/processing/json_cleaner.py +103 -0
- lfx/components/processing/lambda_filter.py +154 -0
- lfx/components/processing/llm_router.py +499 -0
- lfx/components/processing/merge_data.py +90 -0
- lfx/components/processing/message_to_data.py +36 -0
- lfx/components/processing/parse_data.py +70 -0
- lfx/components/processing/parse_dataframe.py +68 -0
- lfx/components/processing/parse_json_data.py +90 -0
- lfx/components/processing/parser.py +143 -0
- lfx/components/processing/prompt.py +67 -0
- lfx/components/processing/python_repl_core.py +98 -0
- lfx/components/processing/regex.py +82 -0
- lfx/components/processing/save_file.py +225 -0
- lfx/components/processing/select_data.py +48 -0
- lfx/components/processing/split_text.py +141 -0
- lfx/components/processing/structured_output.py +202 -0
- lfx/components/processing/update_data.py +160 -0
- lfx/components/prototypes/__init__.py +34 -0
- lfx/components/prototypes/python_function.py +73 -0
- lfx/components/qdrant/__init__.py +34 -0
- lfx/components/qdrant/qdrant.py +109 -0
- lfx/components/redis/__init__.py +37 -0
- lfx/components/redis/redis.py +89 -0
- lfx/components/redis/redis_chat.py +43 -0
- lfx/components/sambanova/__init__.py +32 -0
- lfx/components/sambanova/sambanova.py +84 -0
- lfx/components/scrapegraph/__init__.py +40 -0
- lfx/components/scrapegraph/scrapegraph_markdownify_api.py +64 -0
- lfx/components/scrapegraph/scrapegraph_search_api.py +64 -0
- lfx/components/scrapegraph/scrapegraph_smart_scraper_api.py +71 -0
- lfx/components/searchapi/__init__.py +34 -0
- lfx/components/searchapi/search.py +79 -0
- lfx/components/serpapi/__init__.py +3 -0
- lfx/components/serpapi/serp.py +115 -0
- lfx/components/supabase/__init__.py +34 -0
- lfx/components/supabase/supabase.py +76 -0
- lfx/components/tavily/__init__.py +4 -0
- lfx/components/tavily/tavily_extract.py +117 -0
- lfx/components/tavily/tavily_search.py +212 -0
- lfx/components/textsplitters/__init__.py +3 -0
- lfx/components/toolkits/__init__.py +3 -0
- lfx/components/tools/__init__.py +72 -0
- lfx/components/tools/calculator.py +108 -0
- lfx/components/tools/google_search_api.py +45 -0
- lfx/components/tools/google_serper_api.py +115 -0
- lfx/components/tools/python_code_structured_tool.py +327 -0
- lfx/components/tools/python_repl.py +97 -0
- lfx/components/tools/search_api.py +87 -0
- lfx/components/tools/searxng.py +145 -0
- lfx/components/tools/serp_api.py +119 -0
- lfx/components/tools/tavily_search_tool.py +344 -0
- lfx/components/tools/wikidata_api.py +102 -0
- lfx/components/tools/wikipedia_api.py +49 -0
- lfx/components/tools/yahoo_finance.py +129 -0
- lfx/components/twelvelabs/__init__.py +52 -0
- lfx/components/twelvelabs/convert_astra_results.py +84 -0
- lfx/components/twelvelabs/pegasus_index.py +311 -0
- lfx/components/twelvelabs/split_video.py +291 -0
- lfx/components/twelvelabs/text_embeddings.py +57 -0
- lfx/components/twelvelabs/twelvelabs_pegasus.py +408 -0
- lfx/components/twelvelabs/video_embeddings.py +100 -0
- lfx/components/twelvelabs/video_file.py +179 -0
- lfx/components/unstructured/__init__.py +3 -0
- lfx/components/unstructured/unstructured.py +121 -0
- lfx/components/upstash/__init__.py +34 -0
- lfx/components/upstash/upstash.py +124 -0
- lfx/components/vectara/__init__.py +37 -0
- lfx/components/vectara/vectara.py +97 -0
- lfx/components/vectara/vectara_rag.py +164 -0
- lfx/components/vectorstores/__init__.py +40 -0
- lfx/components/vectorstores/astradb.py +1285 -0
- lfx/components/vectorstores/astradb_graph.py +319 -0
- lfx/components/vectorstores/cassandra.py +264 -0
- lfx/components/vectorstores/cassandra_graph.py +238 -0
- lfx/components/vectorstores/chroma.py +167 -0
- lfx/components/vectorstores/clickhouse.py +135 -0
- lfx/components/vectorstores/couchbase.py +102 -0
- lfx/components/vectorstores/elasticsearch.py +267 -0
- lfx/components/vectorstores/faiss.py +111 -0
- lfx/components/vectorstores/graph_rag.py +141 -0
- lfx/components/vectorstores/hcd.py +314 -0
- lfx/components/vectorstores/local_db.py +261 -0
- lfx/components/vectorstores/milvus.py +115 -0
- lfx/components/vectorstores/mongodb_atlas.py +213 -0
- lfx/components/vectorstores/opensearch.py +243 -0
- lfx/components/vectorstores/pgvector.py +72 -0
- lfx/components/vectorstores/pinecone.py +134 -0
- lfx/components/vectorstores/qdrant.py +109 -0
- lfx/components/vectorstores/supabase.py +76 -0
- lfx/components/vectorstores/upstash.py +124 -0
- lfx/components/vectorstores/vectara.py +97 -0
- lfx/components/vectorstores/vectara_rag.py +164 -0
- lfx/components/vectorstores/weaviate.py +89 -0
- lfx/components/vertexai/__init__.py +37 -0
- lfx/components/vertexai/vertexai.py +71 -0
- lfx/components/vertexai/vertexai_embeddings.py +67 -0
- lfx/components/weaviate/__init__.py +34 -0
- lfx/components/weaviate/weaviate.py +89 -0
- lfx/components/wikipedia/__init__.py +4 -0
- lfx/components/wikipedia/wikidata.py +86 -0
- lfx/components/wikipedia/wikipedia.py +53 -0
- lfx/components/wolframalpha/__init__.py +3 -0
- lfx/components/wolframalpha/wolfram_alpha_api.py +54 -0
- lfx/components/xai/__init__.py +32 -0
- lfx/components/xai/xai.py +167 -0
- lfx/components/yahoosearch/__init__.py +3 -0
- lfx/components/yahoosearch/yahoo.py +137 -0
- lfx/components/youtube/__init__.py +52 -0
- lfx/components/youtube/channel.py +227 -0
- lfx/components/youtube/comments.py +231 -0
- lfx/components/youtube/playlist.py +33 -0
- lfx/components/youtube/search.py +120 -0
- lfx/components/youtube/trending.py +285 -0
- lfx/components/youtube/video_details.py +263 -0
- lfx/components/youtube/youtube_transcripts.py +118 -0
- lfx/components/zep/__init__.py +3 -0
- lfx/components/zep/zep.py +44 -0
- lfx/constants.py +6 -0
- lfx/custom/__init__.py +7 -0
- lfx/custom/attributes.py +86 -0
- lfx/custom/code_parser/__init__.py +3 -0
- lfx/custom/code_parser/code_parser.py +361 -0
- lfx/custom/custom_component/__init__.py +0 -0
- lfx/custom/custom_component/base_component.py +128 -0
- lfx/custom/custom_component/component.py +1808 -0
- lfx/custom/custom_component/component_with_cache.py +8 -0
- lfx/custom/custom_component/custom_component.py +588 -0
- lfx/custom/dependency_analyzer.py +165 -0
- lfx/custom/directory_reader/__init__.py +3 -0
- lfx/custom/directory_reader/directory_reader.py +359 -0
- lfx/custom/directory_reader/utils.py +171 -0
- lfx/custom/eval.py +12 -0
- lfx/custom/schema.py +32 -0
- lfx/custom/tree_visitor.py +21 -0
- lfx/custom/utils.py +877 -0
- lfx/custom/validate.py +488 -0
- lfx/events/__init__.py +1 -0
- lfx/events/event_manager.py +110 -0
- lfx/exceptions/__init__.py +0 -0
- lfx/exceptions/component.py +15 -0
- lfx/field_typing/__init__.py +91 -0
- lfx/field_typing/constants.py +215 -0
- lfx/field_typing/range_spec.py +35 -0
- lfx/graph/__init__.py +6 -0
- lfx/graph/edge/__init__.py +0 -0
- lfx/graph/edge/base.py +277 -0
- lfx/graph/edge/schema.py +119 -0
- lfx/graph/edge/utils.py +0 -0
- lfx/graph/graph/__init__.py +0 -0
- lfx/graph/graph/ascii.py +202 -0
- lfx/graph/graph/base.py +2238 -0
- lfx/graph/graph/constants.py +63 -0
- lfx/graph/graph/runnable_vertices_manager.py +133 -0
- lfx/graph/graph/schema.py +52 -0
- lfx/graph/graph/state_model.py +66 -0
- lfx/graph/graph/utils.py +1024 -0
- lfx/graph/schema.py +75 -0
- lfx/graph/state/__init__.py +0 -0
- lfx/graph/state/model.py +237 -0
- lfx/graph/utils.py +200 -0
- lfx/graph/vertex/__init__.py +0 -0
- lfx/graph/vertex/base.py +823 -0
- lfx/graph/vertex/constants.py +0 -0
- lfx/graph/vertex/exceptions.py +4 -0
- lfx/graph/vertex/param_handler.py +264 -0
- lfx/graph/vertex/schema.py +26 -0
- lfx/graph/vertex/utils.py +19 -0
- lfx/graph/vertex/vertex_types.py +489 -0
- lfx/helpers/__init__.py +1 -0
- lfx/helpers/base_model.py +71 -0
- lfx/helpers/custom.py +13 -0
- lfx/helpers/data.py +167 -0
- lfx/helpers/flow.py +194 -0
- lfx/inputs/__init__.py +68 -0
- lfx/inputs/constants.py +2 -0
- lfx/inputs/input_mixin.py +328 -0
- lfx/inputs/inputs.py +714 -0
- lfx/inputs/validators.py +19 -0
- lfx/interface/__init__.py +6 -0
- lfx/interface/components.py +489 -0
- lfx/interface/importing/__init__.py +5 -0
- lfx/interface/importing/utils.py +39 -0
- lfx/interface/initialize/__init__.py +3 -0
- lfx/interface/initialize/loading.py +224 -0
- lfx/interface/listing.py +26 -0
- lfx/interface/run.py +16 -0
- lfx/interface/utils.py +111 -0
- lfx/io/__init__.py +63 -0
- lfx/io/schema.py +289 -0
- lfx/load/__init__.py +8 -0
- lfx/load/load.py +256 -0
- lfx/load/utils.py +99 -0
- lfx/log/__init__.py +5 -0
- lfx/log/logger.py +385 -0
- lfx/memory/__init__.py +90 -0
- lfx/memory/stubs.py +283 -0
- lfx/processing/__init__.py +1 -0
- lfx/processing/process.py +238 -0
- lfx/processing/utils.py +25 -0
- lfx/py.typed +0 -0
- lfx/schema/__init__.py +66 -0
- lfx/schema/artifact.py +83 -0
- lfx/schema/content_block.py +62 -0
- lfx/schema/content_types.py +91 -0
- lfx/schema/data.py +308 -0
- lfx/schema/dataframe.py +210 -0
- lfx/schema/dotdict.py +74 -0
- lfx/schema/encoders.py +13 -0
- lfx/schema/graph.py +47 -0
- lfx/schema/image.py +131 -0
- lfx/schema/json_schema.py +141 -0
- lfx/schema/log.py +61 -0
- lfx/schema/message.py +473 -0
- lfx/schema/openai_responses_schemas.py +74 -0
- lfx/schema/properties.py +41 -0
- lfx/schema/schema.py +171 -0
- lfx/schema/serialize.py +13 -0
- lfx/schema/table.py +140 -0
- lfx/schema/validators.py +114 -0
- lfx/serialization/__init__.py +5 -0
- lfx/serialization/constants.py +2 -0
- lfx/serialization/serialization.py +314 -0
- lfx/services/__init__.py +23 -0
- lfx/services/base.py +28 -0
- lfx/services/cache/__init__.py +6 -0
- lfx/services/cache/base.py +183 -0
- lfx/services/cache/service.py +166 -0
- lfx/services/cache/utils.py +169 -0
- lfx/services/chat/__init__.py +1 -0
- lfx/services/chat/config.py +2 -0
- lfx/services/chat/schema.py +10 -0
- lfx/services/deps.py +129 -0
- lfx/services/factory.py +19 -0
- lfx/services/initialize.py +19 -0
- lfx/services/interfaces.py +103 -0
- lfx/services/manager.py +172 -0
- lfx/services/schema.py +20 -0
- lfx/services/session.py +82 -0
- lfx/services/settings/__init__.py +3 -0
- lfx/services/settings/auth.py +130 -0
- lfx/services/settings/base.py +539 -0
- lfx/services/settings/constants.py +31 -0
- lfx/services/settings/factory.py +23 -0
- lfx/services/settings/feature_flags.py +12 -0
- lfx/services/settings/service.py +35 -0
- lfx/services/settings/utils.py +40 -0
- lfx/services/shared_component_cache/__init__.py +1 -0
- lfx/services/shared_component_cache/factory.py +30 -0
- lfx/services/shared_component_cache/service.py +9 -0
- lfx/services/storage/__init__.py +5 -0
- lfx/services/storage/local.py +155 -0
- lfx/services/storage/service.py +54 -0
- lfx/services/tracing/__init__.py +1 -0
- lfx/services/tracing/service.py +21 -0
- lfx/settings.py +6 -0
- lfx/template/__init__.py +6 -0
- lfx/template/field/__init__.py +0 -0
- lfx/template/field/base.py +257 -0
- lfx/template/field/prompt.py +15 -0
- lfx/template/frontend_node/__init__.py +6 -0
- lfx/template/frontend_node/base.py +212 -0
- lfx/template/frontend_node/constants.py +65 -0
- lfx/template/frontend_node/custom_components.py +79 -0
- lfx/template/template/__init__.py +0 -0
- lfx/template/template/base.py +100 -0
- lfx/template/utils.py +217 -0
- lfx/type_extraction/__init__.py +19 -0
- lfx/type_extraction/type_extraction.py +75 -0
- lfx/type_extraction.py +80 -0
- lfx/utils/__init__.py +1 -0
- lfx/utils/async_helpers.py +42 -0
- lfx/utils/component_utils.py +154 -0
- lfx/utils/concurrency.py +60 -0
- lfx/utils/connection_string_parser.py +11 -0
- lfx/utils/constants.py +205 -0
- lfx/utils/data_structure.py +212 -0
- lfx/utils/exceptions.py +22 -0
- lfx/utils/helpers.py +28 -0
- lfx/utils/image.py +73 -0
- lfx/utils/lazy_load.py +15 -0
- lfx/utils/request_utils.py +18 -0
- lfx/utils/schemas.py +139 -0
- lfx/utils/util.py +481 -0
- lfx/utils/util_strings.py +56 -0
- lfx/utils/version.py +24 -0
- lfx_nightly-0.1.11.dev0.dist-info/METADATA +293 -0
- lfx_nightly-0.1.11.dev0.dist-info/RECORD +699 -0
- lfx_nightly-0.1.11.dev0.dist-info/WHEEL +4 -0
- lfx_nightly-0.1.11.dev0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,1291 @@
|
|
1
|
+
import copy
|
2
|
+
import re
|
3
|
+
from typing import Any
|
4
|
+
|
5
|
+
from composio import Composio
|
6
|
+
from composio_langchain import LangchainProvider
|
7
|
+
from langchain_core.tools import Tool
|
8
|
+
|
9
|
+
from lfx.custom.custom_component.component import Component
|
10
|
+
from lfx.inputs.inputs import AuthInput, FileInput, InputTypes, MessageTextInput, SecretStrInput, SortableListInput
|
11
|
+
from lfx.io import Output
|
12
|
+
from lfx.io.schema import flatten_schema, schema_to_langflow_inputs
|
13
|
+
from lfx.log.logger import logger
|
14
|
+
from lfx.schema.data import Data
|
15
|
+
from lfx.schema.dataframe import DataFrame
|
16
|
+
from lfx.schema.json_schema import create_input_schema_from_json_schema
|
17
|
+
from lfx.schema.message import Message
|
18
|
+
|
19
|
+
|
20
|
+
def _patch_graph_clean_null_input_types() -> None:
|
21
|
+
"""Monkey-patch Graph._create_vertex to clean legacy templates."""
|
22
|
+
try:
|
23
|
+
from lfx.graph.graph.base import Graph
|
24
|
+
|
25
|
+
if getattr(Graph, "_composio_patch_applied", False):
|
26
|
+
return
|
27
|
+
|
28
|
+
original_create_vertex = Graph._create_vertex
|
29
|
+
|
30
|
+
def _create_vertex_with_cleanup(self, frontend_data):
|
31
|
+
try:
|
32
|
+
node_id: str | None = frontend_data.get("id") if isinstance(frontend_data, dict) else None
|
33
|
+
if node_id and "Composio" in node_id:
|
34
|
+
template = frontend_data.get("data", {}).get("node", {}).get("template", {})
|
35
|
+
if isinstance(template, dict):
|
36
|
+
for field_cfg in template.values():
|
37
|
+
if isinstance(field_cfg, dict) and field_cfg.get("input_types") is None:
|
38
|
+
field_cfg["input_types"] = []
|
39
|
+
except (AttributeError, TypeError, KeyError) as e:
|
40
|
+
logger.debug(f"Composio template cleanup encountered error: {e}")
|
41
|
+
|
42
|
+
return original_create_vertex(self, frontend_data)
|
43
|
+
|
44
|
+
Graph._create_vertex = _create_vertex_with_cleanup # type: ignore[method-assign]
|
45
|
+
Graph._composio_patch_applied = True # type: ignore[attr-defined]
|
46
|
+
logger.debug("Applied Composio template cleanup patch to Graph._create_vertex")
|
47
|
+
|
48
|
+
except (AttributeError, TypeError) as e:
|
49
|
+
logger.debug(f"Failed to apply Composio Graph patch: {e}")
|
50
|
+
|
51
|
+
|
52
|
+
# Apply the patch at import time
|
53
|
+
_patch_graph_clean_null_input_types()
|
54
|
+
|
55
|
+
|
56
|
+
class ComposioBaseComponent(Component):
|
57
|
+
"""Base class for Composio components with common functionality."""
|
58
|
+
|
59
|
+
default_tools_limit: int = 5
|
60
|
+
|
61
|
+
_base_inputs = [
|
62
|
+
MessageTextInput(
|
63
|
+
name="entity_id",
|
64
|
+
display_name="Entity ID",
|
65
|
+
value="default",
|
66
|
+
advanced=True,
|
67
|
+
tool_mode=True,
|
68
|
+
),
|
69
|
+
SecretStrInput(
|
70
|
+
name="api_key",
|
71
|
+
display_name="Composio API Key",
|
72
|
+
required=True,
|
73
|
+
real_time_refresh=True,
|
74
|
+
value="COMPOSIO_API_KEY",
|
75
|
+
),
|
76
|
+
AuthInput(
|
77
|
+
name="auth_link",
|
78
|
+
value="",
|
79
|
+
auth_tooltip="Please insert a valid Composio API Key.",
|
80
|
+
show=False,
|
81
|
+
),
|
82
|
+
SortableListInput(
|
83
|
+
name="action_button",
|
84
|
+
display_name="Action",
|
85
|
+
placeholder="Select action",
|
86
|
+
options=[],
|
87
|
+
value="disabled",
|
88
|
+
helper_text="Please connect before selecting actions.",
|
89
|
+
helper_text_metadata={"variant": "destructive"},
|
90
|
+
show=True,
|
91
|
+
required=False,
|
92
|
+
real_time_refresh=True,
|
93
|
+
limit=1,
|
94
|
+
),
|
95
|
+
]
|
96
|
+
|
97
|
+
_name_sanitizer = re.compile(r"[^a-zA-Z0-9_-]")
|
98
|
+
|
99
|
+
# Class-level caches
|
100
|
+
_actions_cache: dict[str, dict[str, Any]] = {}
|
101
|
+
_action_schema_cache: dict[str, dict[str, Any]] = {}
|
102
|
+
|
103
|
+
outputs = [
|
104
|
+
Output(name="dataFrame", display_name="DataFrame", method="as_dataframe"),
|
105
|
+
]
|
106
|
+
|
107
|
+
inputs = list(_base_inputs)
|
108
|
+
|
109
|
+
def __init__(self, **kwargs):
|
110
|
+
"""Initialize instance variables to prevent shared state between components."""
|
111
|
+
super().__init__(**kwargs)
|
112
|
+
self._all_fields: set[str] = set()
|
113
|
+
self._bool_variables: set[str] = set()
|
114
|
+
self._actions_data: dict[str, dict[str, Any]] = {}
|
115
|
+
self._default_tools: set[str] = set()
|
116
|
+
self._display_to_key_map: dict[str, str] = {}
|
117
|
+
self._key_to_display_map: dict[str, str] = {}
|
118
|
+
self._sanitized_names: dict[str, str] = {}
|
119
|
+
self._action_schemas: dict[str, Any] = {}
|
120
|
+
|
121
|
+
def as_message(self) -> Message:
|
122
|
+
result = self.execute_action()
|
123
|
+
if result is None:
|
124
|
+
return Message(text="Action execution returned no result")
|
125
|
+
return Message(text=str(result))
|
126
|
+
|
127
|
+
def as_dataframe(self) -> DataFrame:
|
128
|
+
result = self.execute_action()
|
129
|
+
|
130
|
+
if isinstance(result, dict):
|
131
|
+
result = [result]
|
132
|
+
# Build DataFrame and avoid exposing a 'data' attribute via column access,
|
133
|
+
result_dataframe = DataFrame(result)
|
134
|
+
if hasattr(result_dataframe, "columns"):
|
135
|
+
try:
|
136
|
+
if "data" in result_dataframe.columns:
|
137
|
+
result_dataframe = result_dataframe.rename(columns={"data": "_data"})
|
138
|
+
except (AttributeError, TypeError, ValueError, KeyError) as e:
|
139
|
+
logger.debug(f"Failed to rename 'data' column: {e}")
|
140
|
+
return result_dataframe
|
141
|
+
|
142
|
+
def as_data(self) -> Data:
|
143
|
+
result = self.execute_action()
|
144
|
+
return Data(results=result)
|
145
|
+
|
146
|
+
def _build_action_maps(self):
|
147
|
+
"""Build lookup maps for action names."""
|
148
|
+
if not self._display_to_key_map or not self._key_to_display_map:
|
149
|
+
self._display_to_key_map = {data["display_name"]: key for key, data in self._actions_data.items()}
|
150
|
+
self._key_to_display_map = {key: data["display_name"] for key, data in self._actions_data.items()}
|
151
|
+
self._sanitized_names = {
|
152
|
+
action: self._name_sanitizer.sub("-", self.sanitize_action_name(action))
|
153
|
+
for action in self._actions_data
|
154
|
+
}
|
155
|
+
|
156
|
+
def sanitize_action_name(self, action_name: str) -> str:
|
157
|
+
"""Convert action name to display name using lookup."""
|
158
|
+
self._build_action_maps()
|
159
|
+
return self._key_to_display_map.get(action_name, action_name)
|
160
|
+
|
161
|
+
def desanitize_action_name(self, action_name: str) -> str:
|
162
|
+
"""Convert display name to action key using lookup."""
|
163
|
+
self._build_action_maps()
|
164
|
+
return self._display_to_key_map.get(action_name, action_name)
|
165
|
+
|
166
|
+
def _get_action_fields(self, action_key: str | None) -> set[str]:
|
167
|
+
"""Get fields for an action."""
|
168
|
+
if action_key is None:
|
169
|
+
return set()
|
170
|
+
return set(self._actions_data[action_key]["action_fields"]) if action_key in self._actions_data else set()
|
171
|
+
|
172
|
+
def _build_wrapper(self) -> Composio:
|
173
|
+
"""Build the Composio wrapper."""
|
174
|
+
try:
|
175
|
+
if not self.api_key:
|
176
|
+
msg = "Composio API Key is required"
|
177
|
+
raise ValueError(msg)
|
178
|
+
return Composio(api_key=self.api_key, provider=LangchainProvider())
|
179
|
+
|
180
|
+
except ValueError as e:
|
181
|
+
logger.error(f"Error building Composio wrapper: {e}")
|
182
|
+
msg = "Please provide a valid Composio API Key in the component settings"
|
183
|
+
raise ValueError(msg) from e
|
184
|
+
|
185
|
+
def show_hide_fields(self, build_config: dict, field_value: Any):
|
186
|
+
"""Optimized field visibility updates by only modifying show values."""
|
187
|
+
if not field_value:
|
188
|
+
for field in self._all_fields:
|
189
|
+
build_config[field]["show"] = False
|
190
|
+
if field in self._bool_variables:
|
191
|
+
build_config[field]["value"] = False
|
192
|
+
else:
|
193
|
+
build_config[field]["value"] = ""
|
194
|
+
return
|
195
|
+
|
196
|
+
action_key = None
|
197
|
+
if isinstance(field_value, list) and field_value:
|
198
|
+
action_key = self.desanitize_action_name(field_value[0]["name"])
|
199
|
+
else:
|
200
|
+
action_key = field_value
|
201
|
+
|
202
|
+
fields_to_show = self._get_action_fields(action_key)
|
203
|
+
|
204
|
+
for field in self._all_fields:
|
205
|
+
should_show = field in fields_to_show
|
206
|
+
if build_config[field]["show"] != should_show:
|
207
|
+
build_config[field]["show"] = should_show
|
208
|
+
if not should_show:
|
209
|
+
if field in self._bool_variables:
|
210
|
+
build_config[field]["value"] = False
|
211
|
+
else:
|
212
|
+
build_config[field]["value"] = ""
|
213
|
+
|
214
|
+
def _populate_actions_data(self):
|
215
|
+
"""Fetch the list of actions for the toolkit and build helper maps."""
|
216
|
+
if self._actions_data:
|
217
|
+
return
|
218
|
+
|
219
|
+
# Try to load from the class-level cache
|
220
|
+
toolkit_slug = self.app_name.lower()
|
221
|
+
if toolkit_slug in self.__class__._actions_cache:
|
222
|
+
# Deep-copy so that any mutation on this instance does not affect the
|
223
|
+
# cached master copy.
|
224
|
+
self._actions_data = copy.deepcopy(self.__class__._actions_cache[toolkit_slug])
|
225
|
+
self._action_schemas = copy.deepcopy(self.__class__._action_schema_cache.get(toolkit_slug, {}))
|
226
|
+
logger.debug(f"Loaded actions for {toolkit_slug} from in-process cache")
|
227
|
+
return
|
228
|
+
|
229
|
+
api_key = getattr(self, "api_key", None)
|
230
|
+
if not api_key:
|
231
|
+
logger.warning("API key is missing. Cannot populate actions data.")
|
232
|
+
return
|
233
|
+
|
234
|
+
try:
|
235
|
+
composio = self._build_wrapper()
|
236
|
+
toolkit_slug = self.app_name.lower()
|
237
|
+
|
238
|
+
raw_tools = composio.tools.get_raw_composio_tools(toolkits=[toolkit_slug], limit=999)
|
239
|
+
|
240
|
+
if not raw_tools:
|
241
|
+
msg = f"Toolkit '{toolkit_slug}' not found or has no available tools"
|
242
|
+
raise ValueError(msg)
|
243
|
+
|
244
|
+
for raw_tool in raw_tools:
|
245
|
+
try:
|
246
|
+
# Convert raw_tool to dict-like structure
|
247
|
+
tool_dict = raw_tool.__dict__ if hasattr(raw_tool, "__dict__") else raw_tool
|
248
|
+
|
249
|
+
if not tool_dict:
|
250
|
+
logger.warning(f"Tool is None or empty: {raw_tool}")
|
251
|
+
continue
|
252
|
+
|
253
|
+
action_key = tool_dict.get("slug")
|
254
|
+
if not action_key:
|
255
|
+
logger.warning(f"Action key (slug) is missing in tool: {tool_dict}")
|
256
|
+
continue
|
257
|
+
|
258
|
+
# Human-friendly display name
|
259
|
+
display_name = tool_dict.get("name") or tool_dict.get("display_name")
|
260
|
+
if not display_name:
|
261
|
+
# Better fallback: convert GMAIL_SEND_EMAIL to "Send Email"
|
262
|
+
# Remove app prefix and convert to title case
|
263
|
+
clean_name = action_key
|
264
|
+
clean_name = clean_name.removeprefix(f"{self.app_name.upper()}_")
|
265
|
+
# Convert underscores to spaces and title case
|
266
|
+
display_name = clean_name.replace("_", " ").title()
|
267
|
+
|
268
|
+
# Build list of parameter names and track bool fields
|
269
|
+
parameters_schema = tool_dict.get("input_parameters", {})
|
270
|
+
if parameters_schema is None:
|
271
|
+
logger.warning(f"Parameters schema is None for action key: {action_key}")
|
272
|
+
# Still add the action but with empty fields
|
273
|
+
self._action_schemas[action_key] = tool_dict
|
274
|
+
self._actions_data[action_key] = {
|
275
|
+
"display_name": display_name,
|
276
|
+
"action_fields": [],
|
277
|
+
"file_upload_fields": set(),
|
278
|
+
}
|
279
|
+
continue
|
280
|
+
|
281
|
+
try:
|
282
|
+
# Special handling for unusual schema structures
|
283
|
+
if not isinstance(parameters_schema, dict):
|
284
|
+
# Try to convert if it's a model object
|
285
|
+
if hasattr(parameters_schema, "model_dump"):
|
286
|
+
parameters_schema = parameters_schema.model_dump()
|
287
|
+
elif hasattr(parameters_schema, "__dict__"):
|
288
|
+
parameters_schema = parameters_schema.__dict__
|
289
|
+
else:
|
290
|
+
logger.warning(f"Cannot process parameters schema for {action_key}, skipping")
|
291
|
+
self._action_schemas[action_key] = tool_dict
|
292
|
+
self._actions_data[action_key] = {
|
293
|
+
"display_name": display_name,
|
294
|
+
"action_fields": [],
|
295
|
+
"file_upload_fields": set(),
|
296
|
+
}
|
297
|
+
continue
|
298
|
+
|
299
|
+
# Validate parameters_schema has required structure before flattening
|
300
|
+
if not parameters_schema.get("properties") and not parameters_schema.get("$defs"):
|
301
|
+
# Create a minimal valid schema to avoid errors
|
302
|
+
parameters_schema = {"type": "object", "properties": {}}
|
303
|
+
|
304
|
+
# Sanitize the schema before passing to flatten_schema
|
305
|
+
# Handle case where 'required' is explicitly None (causes "'NoneType' object is not iterable")
|
306
|
+
if parameters_schema.get("required") is None:
|
307
|
+
parameters_schema = parameters_schema.copy() # Don't modify the original
|
308
|
+
parameters_schema["required"] = []
|
309
|
+
|
310
|
+
try:
|
311
|
+
# Preserve original descriptions before flattening to restore if lost
|
312
|
+
original_descriptions = {}
|
313
|
+
original_props = parameters_schema.get("properties", {})
|
314
|
+
for prop_name, prop_schema in original_props.items():
|
315
|
+
if isinstance(prop_schema, dict) and "description" in prop_schema:
|
316
|
+
original_descriptions[prop_name] = prop_schema["description"]
|
317
|
+
|
318
|
+
flat_schema = flatten_schema(parameters_schema)
|
319
|
+
|
320
|
+
# Restore lost descriptions in flattened schema
|
321
|
+
if flat_schema and isinstance(flat_schema, dict) and "properties" in flat_schema:
|
322
|
+
flat_props = flat_schema["properties"]
|
323
|
+
for field_name, field_schema in flat_props.items():
|
324
|
+
# Check if this field lost its description during flattening
|
325
|
+
if isinstance(field_schema, dict) and "description" not in field_schema:
|
326
|
+
# Try to find the original description
|
327
|
+
# Handle array fields like bcc[0] -> bcc
|
328
|
+
base_field_name = field_name.replace("[0]", "")
|
329
|
+
if base_field_name in original_descriptions:
|
330
|
+
field_schema["description"] = original_descriptions[base_field_name]
|
331
|
+
elif field_name in original_descriptions:
|
332
|
+
field_schema["description"] = original_descriptions[field_name]
|
333
|
+
except (KeyError, TypeError, ValueError):
|
334
|
+
self._action_schemas[action_key] = tool_dict
|
335
|
+
self._actions_data[action_key] = {
|
336
|
+
"display_name": display_name,
|
337
|
+
"action_fields": [],
|
338
|
+
"file_upload_fields": set(),
|
339
|
+
}
|
340
|
+
continue
|
341
|
+
|
342
|
+
if flat_schema is None:
|
343
|
+
logger.warning(f"Flat schema is None for action key: {action_key}")
|
344
|
+
# Still add the action but with empty fields so the UI doesn't break
|
345
|
+
self._action_schemas[action_key] = tool_dict
|
346
|
+
self._actions_data[action_key] = {
|
347
|
+
"display_name": display_name,
|
348
|
+
"action_fields": [],
|
349
|
+
"file_upload_fields": set(),
|
350
|
+
}
|
351
|
+
continue
|
352
|
+
|
353
|
+
# Extract field names and detect file upload fields during parsing
|
354
|
+
raw_action_fields = list(flat_schema.get("properties", {}).keys())
|
355
|
+
action_fields = []
|
356
|
+
attachment_related_found = False
|
357
|
+
file_upload_fields = set()
|
358
|
+
|
359
|
+
# Check original schema properties for file_uploadable fields
|
360
|
+
original_props = parameters_schema.get("properties", {})
|
361
|
+
for field_name, field_schema in original_props.items():
|
362
|
+
if isinstance(field_schema, dict):
|
363
|
+
clean_field_name = field_name.replace("[0]", "")
|
364
|
+
# Check direct file_uploadable attribute
|
365
|
+
if field_schema.get("file_uploadable") is True:
|
366
|
+
file_upload_fields.add(clean_field_name)
|
367
|
+
|
368
|
+
# Check anyOf structures (like OUTLOOK_OUTLOOK_SEND_EMAIL)
|
369
|
+
if "anyOf" in field_schema:
|
370
|
+
for any_of_item in field_schema["anyOf"]:
|
371
|
+
if isinstance(any_of_item, dict) and any_of_item.get("file_uploadable") is True:
|
372
|
+
file_upload_fields.add(clean_field_name)
|
373
|
+
|
374
|
+
for field in raw_action_fields:
|
375
|
+
clean_field = field.replace("[0]", "")
|
376
|
+
# Check if this field is attachment-related
|
377
|
+
if clean_field.lower().startswith("attachment."):
|
378
|
+
attachment_related_found = True
|
379
|
+
continue # Skip individual attachment fields
|
380
|
+
|
381
|
+
# Handle conflicting field names - rename user_id to avoid conflicts with entity_id
|
382
|
+
if clean_field == "user_id":
|
383
|
+
clean_field = f"{self.app_name}_user_id"
|
384
|
+
elif clean_field == "status":
|
385
|
+
clean_field = f"{self.app_name}_status"
|
386
|
+
|
387
|
+
action_fields.append(clean_field)
|
388
|
+
|
389
|
+
# Add consolidated attachment field if we found attachment-related fields
|
390
|
+
if attachment_related_found:
|
391
|
+
action_fields.append("attachment")
|
392
|
+
file_upload_fields.add("attachment") # Attachment fields are also file upload fields
|
393
|
+
|
394
|
+
# Track boolean parameters so we can coerce them later
|
395
|
+
properties = flat_schema.get("properties", {})
|
396
|
+
if properties:
|
397
|
+
for p_name, p_schema in properties.items():
|
398
|
+
if isinstance(p_schema, dict) and p_schema.get("type") == "boolean":
|
399
|
+
# Use cleaned field name for boolean tracking
|
400
|
+
clean_field_name = p_name.replace("[0]", "")
|
401
|
+
self._bool_variables.add(clean_field_name)
|
402
|
+
|
403
|
+
self._action_schemas[action_key] = tool_dict
|
404
|
+
self._actions_data[action_key] = {
|
405
|
+
"display_name": display_name,
|
406
|
+
"action_fields": action_fields,
|
407
|
+
"file_upload_fields": file_upload_fields,
|
408
|
+
}
|
409
|
+
|
410
|
+
except (KeyError, TypeError, ValueError) as flatten_error:
|
411
|
+
logger.error(f"flatten_schema failed for {action_key}: {flatten_error}")
|
412
|
+
self._action_schemas[action_key] = tool_dict
|
413
|
+
self._actions_data[action_key] = {
|
414
|
+
"display_name": display_name,
|
415
|
+
"action_fields": [],
|
416
|
+
"file_upload_fields": set(),
|
417
|
+
}
|
418
|
+
continue
|
419
|
+
|
420
|
+
except ValueError as e:
|
421
|
+
logger.warning(f"Failed processing Composio tool for action {raw_tool}: {e}")
|
422
|
+
|
423
|
+
# Helper look-ups used elsewhere
|
424
|
+
self._all_fields = {f for d in self._actions_data.values() for f in d["action_fields"]}
|
425
|
+
self._build_action_maps()
|
426
|
+
|
427
|
+
# Cache actions for this toolkit so subsequent component instances
|
428
|
+
# can reuse them without hitting the Composio API again.
|
429
|
+
self.__class__._actions_cache[toolkit_slug] = copy.deepcopy(self._actions_data)
|
430
|
+
self.__class__._action_schema_cache[toolkit_slug] = copy.deepcopy(self._action_schemas)
|
431
|
+
|
432
|
+
except ValueError as e:
|
433
|
+
logger.debug(f"Could not populate Composio actions for {self.app_name}: {e}")
|
434
|
+
|
435
|
+
def _validate_schema_inputs(self, action_key: str) -> list[InputTypes]:
|
436
|
+
"""Convert the JSON schema for *action_key* into Langflow input objects."""
|
437
|
+
# Skip validation for default/placeholder values
|
438
|
+
if action_key in ("disabled", "placeholder", ""):
|
439
|
+
logger.debug(f"Skipping schema validation for placeholder value: {action_key}")
|
440
|
+
return []
|
441
|
+
|
442
|
+
schema_dict = self._action_schemas.get(action_key)
|
443
|
+
if not schema_dict:
|
444
|
+
logger.warning(f"No schema found for action key: {action_key}")
|
445
|
+
return []
|
446
|
+
|
447
|
+
try:
|
448
|
+
parameters_schema = schema_dict.get("input_parameters", {})
|
449
|
+
if parameters_schema is None:
|
450
|
+
logger.warning(f"Parameters schema is None for action key: {action_key}")
|
451
|
+
return []
|
452
|
+
|
453
|
+
# Check if parameters_schema has the expected structure
|
454
|
+
if not isinstance(parameters_schema, dict):
|
455
|
+
logger.warning(
|
456
|
+
f"Parameters schema is not a dict for action key: {action_key}, got: {type(parameters_schema)}"
|
457
|
+
)
|
458
|
+
return []
|
459
|
+
|
460
|
+
# Validate parameters_schema has required structure before flattening
|
461
|
+
if not parameters_schema.get("properties") and not parameters_schema.get("$defs"):
|
462
|
+
# Create a minimal valid schema to avoid errors
|
463
|
+
parameters_schema = {"type": "object", "properties": {}}
|
464
|
+
|
465
|
+
# Sanitize the schema before passing to flatten_schema
|
466
|
+
# Handle case where 'required' is explicitly None (causes "'NoneType' object is not iterable")
|
467
|
+
if parameters_schema.get("required") is None:
|
468
|
+
parameters_schema = parameters_schema.copy() # Don't modify the original
|
469
|
+
parameters_schema["required"] = []
|
470
|
+
|
471
|
+
try:
|
472
|
+
# Preserve original descriptions before flattening to restore if lost
|
473
|
+
original_descriptions = {}
|
474
|
+
original_props = parameters_schema.get("properties", {})
|
475
|
+
for prop_name, prop_schema in original_props.items():
|
476
|
+
if isinstance(prop_schema, dict) and "description" in prop_schema:
|
477
|
+
original_descriptions[prop_name] = prop_schema["description"]
|
478
|
+
|
479
|
+
flat_schema = flatten_schema(parameters_schema)
|
480
|
+
|
481
|
+
# Restore lost descriptions in flattened schema
|
482
|
+
if flat_schema and isinstance(flat_schema, dict) and "properties" in flat_schema:
|
483
|
+
flat_props = flat_schema["properties"]
|
484
|
+
for field_name, field_schema in flat_props.items():
|
485
|
+
# Check if this field lost its description during flattening
|
486
|
+
if isinstance(field_schema, dict) and "description" not in field_schema:
|
487
|
+
# Try to find the original description
|
488
|
+
# Handle array fields like bcc[0] -> bcc
|
489
|
+
base_field_name = field_name.replace("[0]", "")
|
490
|
+
if base_field_name in original_descriptions:
|
491
|
+
field_schema["description"] = original_descriptions[base_field_name]
|
492
|
+
elif field_name in original_descriptions:
|
493
|
+
field_schema["description"] = original_descriptions[field_name]
|
494
|
+
except (KeyError, TypeError, ValueError) as flatten_error:
|
495
|
+
logger.error(f"flatten_schema failed for {action_key}: {flatten_error}")
|
496
|
+
return []
|
497
|
+
|
498
|
+
if flat_schema is None:
|
499
|
+
logger.warning(f"Flat schema is None for action key: {action_key}")
|
500
|
+
return []
|
501
|
+
|
502
|
+
# Additional check for flat_schema structure
|
503
|
+
if not isinstance(flat_schema, dict):
|
504
|
+
logger.warning(f"Flat schema is not a dict for action key: {action_key}, got: {type(flat_schema)}")
|
505
|
+
return []
|
506
|
+
|
507
|
+
# Ensure flat_schema has the expected structure for create_input_schema_from_json_schema
|
508
|
+
if flat_schema.get("type") != "object":
|
509
|
+
logger.warning(f"Flat schema for {action_key} is not of type 'object', got: {flat_schema.get('type')}")
|
510
|
+
# Fix the schema type if it's missing
|
511
|
+
flat_schema["type"] = "object"
|
512
|
+
|
513
|
+
if "properties" not in flat_schema:
|
514
|
+
flat_schema["properties"] = {}
|
515
|
+
|
516
|
+
# Clean up field names - remove [0] suffixes from array fields
|
517
|
+
cleaned_properties = {}
|
518
|
+
attachment_related_fields = set() # Track fields that are attachment-related
|
519
|
+
|
520
|
+
for field_name, field_schema in flat_schema.get("properties", {}).items():
|
521
|
+
# Remove [0] suffix from field names (e.g., "bcc[0]" -> "bcc", "cc[0]" -> "cc")
|
522
|
+
clean_field_name = field_name.replace("[0]", "")
|
523
|
+
|
524
|
+
# Check if this field is attachment-related (contains "attachment." prefix)
|
525
|
+
if clean_field_name.lower().startswith("attachment."):
|
526
|
+
attachment_related_fields.add(clean_field_name)
|
527
|
+
# Don't add individual attachment sub-fields to the schema
|
528
|
+
continue
|
529
|
+
|
530
|
+
# Handle conflicting field names - rename user_id to avoid conflicts with entity_id
|
531
|
+
if clean_field_name == "user_id":
|
532
|
+
clean_field_name = f"{self.app_name}_user_id"
|
533
|
+
# Update
|
534
|
+
field_schema_copy = field_schema.copy()
|
535
|
+
field_schema_copy["description"] = (
|
536
|
+
f"User ID for {self.app_name.title()}: " + field_schema["description"]
|
537
|
+
)
|
538
|
+
elif clean_field_name == "status":
|
539
|
+
clean_field_name = f"{self.app_name}_status"
|
540
|
+
# Update
|
541
|
+
field_schema_copy = field_schema.copy()
|
542
|
+
field_schema_copy["description"] = (
|
543
|
+
f"Status for {self.app_name.title()}: " + field_schema["description"]
|
544
|
+
)
|
545
|
+
else:
|
546
|
+
# Use the original field schema for all other fields
|
547
|
+
field_schema_copy = field_schema
|
548
|
+
|
549
|
+
# Preserve the full schema information, not just the type
|
550
|
+
cleaned_properties[clean_field_name] = field_schema_copy
|
551
|
+
|
552
|
+
# If we found attachment-related fields, add a single "attachment" field
|
553
|
+
if attachment_related_fields:
|
554
|
+
# Create a generic attachment field schema
|
555
|
+
attachment_schema = {
|
556
|
+
"type": "string",
|
557
|
+
"description": "File attachment for the email",
|
558
|
+
"title": "Attachment",
|
559
|
+
}
|
560
|
+
cleaned_properties["attachment"] = attachment_schema
|
561
|
+
|
562
|
+
# Update the flat schema with cleaned field names
|
563
|
+
flat_schema["properties"] = cleaned_properties
|
564
|
+
|
565
|
+
# Also update required fields to match cleaned names
|
566
|
+
if flat_schema.get("required"):
|
567
|
+
cleaned_required = [field.replace("[0]", "") for field in flat_schema["required"]]
|
568
|
+
flat_schema["required"] = cleaned_required
|
569
|
+
|
570
|
+
input_schema = create_input_schema_from_json_schema(flat_schema)
|
571
|
+
if input_schema is None:
|
572
|
+
logger.warning(f"Input schema is None for action key: {action_key}")
|
573
|
+
return []
|
574
|
+
|
575
|
+
# Additional safety check before calling schema_to_langflow_inputs
|
576
|
+
if not hasattr(input_schema, "model_fields"):
|
577
|
+
logger.warning(f"Input schema for {action_key} does not have model_fields attribute")
|
578
|
+
return []
|
579
|
+
|
580
|
+
if input_schema.model_fields is None:
|
581
|
+
logger.warning(f"Input schema model_fields is None for {action_key}")
|
582
|
+
return []
|
583
|
+
|
584
|
+
result = schema_to_langflow_inputs(input_schema)
|
585
|
+
|
586
|
+
# Process inputs to handle attachment fields and set advanced status
|
587
|
+
if result:
|
588
|
+
processed_inputs = []
|
589
|
+
required_fields_set = set(flat_schema.get("required", []))
|
590
|
+
|
591
|
+
# Get file upload fields from stored action data
|
592
|
+
file_upload_fields = self._actions_data.get(action_key, {}).get("file_upload_fields", set())
|
593
|
+
if attachment_related_fields: # If we consolidated attachment fields
|
594
|
+
file_upload_fields = file_upload_fields | {"attachment"}
|
595
|
+
|
596
|
+
for inp in result:
|
597
|
+
if hasattr(inp, "name") and inp.name is not None:
|
598
|
+
# Check if this specific field is a file upload field
|
599
|
+
if inp.name.lower() in file_upload_fields or inp.name.lower() == "attachment":
|
600
|
+
# Replace with FileInput for file upload fields
|
601
|
+
file_input = FileInput(
|
602
|
+
name=inp.name,
|
603
|
+
display_name=getattr(inp, "display_name", inp.name.replace("_", " ").title()),
|
604
|
+
required=inp.name in required_fields_set,
|
605
|
+
advanced=inp.name not in required_fields_set,
|
606
|
+
info=getattr(inp, "info", "Upload file for this field"),
|
607
|
+
show=True,
|
608
|
+
file_types=[
|
609
|
+
"csv",
|
610
|
+
"txt",
|
611
|
+
"doc",
|
612
|
+
"docx",
|
613
|
+
"xls",
|
614
|
+
"xlsx",
|
615
|
+
"pdf",
|
616
|
+
"png",
|
617
|
+
"jpg",
|
618
|
+
"jpeg",
|
619
|
+
"gif",
|
620
|
+
"zip",
|
621
|
+
"rar",
|
622
|
+
"ppt",
|
623
|
+
"pptx",
|
624
|
+
],
|
625
|
+
)
|
626
|
+
processed_inputs.append(file_input)
|
627
|
+
else:
|
628
|
+
# Ensure proper display_name and info are set for regular fields
|
629
|
+
if not hasattr(inp, "display_name") or not inp.display_name:
|
630
|
+
inp.display_name = inp.name.replace("_", " ").title()
|
631
|
+
|
632
|
+
# Preserve description from schema if available
|
633
|
+
field_schema = flat_schema.get("properties", {}).get(inp.name, {})
|
634
|
+
schema_description = field_schema.get("description")
|
635
|
+
current_info = getattr(inp, "info", None)
|
636
|
+
|
637
|
+
# Use schema description if available, otherwise keep current info or create from name
|
638
|
+
if schema_description:
|
639
|
+
inp.info = schema_description
|
640
|
+
elif not current_info:
|
641
|
+
# Fallback: create a basic description from the field name if no description exists
|
642
|
+
inp.info = f"{inp.name.replace('_', ' ').title()} field"
|
643
|
+
|
644
|
+
# Set advanced status for non-file-upload fields
|
645
|
+
if inp.name not in required_fields_set:
|
646
|
+
inp.advanced = True
|
647
|
+
|
648
|
+
# Skip entity_id being mapped to user_id parameter
|
649
|
+
if inp.name == "user_id" and getattr(self, "entity_id", None) == getattr(
|
650
|
+
inp, "value", None
|
651
|
+
):
|
652
|
+
continue
|
653
|
+
|
654
|
+
processed_inputs.append(inp)
|
655
|
+
else:
|
656
|
+
processed_inputs.append(inp)
|
657
|
+
|
658
|
+
return processed_inputs
|
659
|
+
return result # noqa: TRY300
|
660
|
+
except ValueError as e:
|
661
|
+
logger.warning(f"Error generating inputs for {action_key}: {e}")
|
662
|
+
return []
|
663
|
+
|
664
|
+
def _get_inputs_for_all_actions(self) -> dict[str, list[InputTypes]]:
|
665
|
+
"""Return a mapping action_key → list[InputTypes] for every action."""
|
666
|
+
result: dict[str, list[InputTypes]] = {}
|
667
|
+
for key in self._actions_data:
|
668
|
+
result[key] = self._validate_schema_inputs(key)
|
669
|
+
return result
|
670
|
+
|
671
|
+
def _remove_inputs_from_build_config(self, build_config: dict, keep_for_action: str) -> None:
|
672
|
+
"""Remove parameter UI fields that belong to other actions."""
|
673
|
+
protected_keys = {"code", "entity_id", "api_key", "auth_link", "action_button", "tool_mode"}
|
674
|
+
|
675
|
+
for action_key, lf_inputs in self._get_inputs_for_all_actions().items():
|
676
|
+
if action_key == keep_for_action:
|
677
|
+
continue
|
678
|
+
for inp in lf_inputs:
|
679
|
+
if inp.name is not None and inp.name not in protected_keys:
|
680
|
+
build_config.pop(inp.name, None)
|
681
|
+
|
682
|
+
def _update_action_config(self, build_config: dict, selected_value: Any) -> None:
|
683
|
+
"""Add or update parameter input fields for the chosen action."""
|
684
|
+
if not selected_value:
|
685
|
+
return
|
686
|
+
|
687
|
+
# The UI passes either a list with dict [{name: display_name}] OR the raw key
|
688
|
+
if isinstance(selected_value, list) and selected_value:
|
689
|
+
display_name = selected_value[0]["name"]
|
690
|
+
else:
|
691
|
+
display_name = selected_value
|
692
|
+
|
693
|
+
action_key = self.desanitize_action_name(display_name)
|
694
|
+
|
695
|
+
# Skip validation for default/placeholder values
|
696
|
+
if action_key in ("disabled", "placeholder", ""):
|
697
|
+
logger.debug(f"Skipping action config update for placeholder value: {action_key}")
|
698
|
+
return
|
699
|
+
|
700
|
+
lf_inputs = self._validate_schema_inputs(action_key)
|
701
|
+
|
702
|
+
# First remove inputs belonging to other actions
|
703
|
+
self._remove_inputs_from_build_config(build_config, action_key)
|
704
|
+
|
705
|
+
# Add / update the inputs for this action
|
706
|
+
for inp in lf_inputs:
|
707
|
+
if inp.name is not None:
|
708
|
+
inp_dict = inp.to_dict() if hasattr(inp, "to_dict") else inp.__dict__.copy()
|
709
|
+
|
710
|
+
# Ensure input_types is always a list
|
711
|
+
if not isinstance(inp_dict.get("input_types"), list):
|
712
|
+
inp_dict["input_types"] = []
|
713
|
+
|
714
|
+
inp_dict.setdefault("show", True) # visible once action selected
|
715
|
+
# Preserve previously entered value if user already filled something
|
716
|
+
if inp.name in build_config:
|
717
|
+
existing_val = build_config[inp.name].get("value")
|
718
|
+
inp_dict.setdefault("value", existing_val)
|
719
|
+
build_config[inp.name] = inp_dict
|
720
|
+
|
721
|
+
# Ensure _all_fields includes new ones
|
722
|
+
self._all_fields.update({i.name for i in lf_inputs if i.name is not None})
|
723
|
+
|
724
|
+
def _is_tool_mode_enabled(self) -> bool:
|
725
|
+
"""Check if tool_mode is currently enabled."""
|
726
|
+
return getattr(self, "tool_mode", False)
|
727
|
+
|
728
|
+
def _set_action_visibility(self, build_config: dict, *, force_show: bool | None = None) -> None:
|
729
|
+
"""Set action field visibility based on tool_mode state or forced value."""
|
730
|
+
if force_show is not None:
|
731
|
+
build_config["action_button"]["show"] = force_show
|
732
|
+
else:
|
733
|
+
# When tool_mode is enabled, hide action field
|
734
|
+
build_config["action_button"]["show"] = not self._is_tool_mode_enabled()
|
735
|
+
|
736
|
+
def create_new_auth_config(self, app_name: str) -> str:
|
737
|
+
"""Create a new auth config for the given app name."""
|
738
|
+
composio = self._build_wrapper()
|
739
|
+
auth_config = composio.auth_configs.create(toolkit=app_name, options={"type": "use_composio_managed_auth"})
|
740
|
+
return auth_config.id
|
741
|
+
|
742
|
+
def _initiate_connection(self, app_name: str) -> tuple[str, str]:
|
743
|
+
"""Initiate OAuth connection and return (redirect_url, connection_id)."""
|
744
|
+
try:
|
745
|
+
composio = self._build_wrapper()
|
746
|
+
|
747
|
+
auth_configs = composio.auth_configs.list(toolkit_slug=app_name)
|
748
|
+
if len(auth_configs.items) == 0:
|
749
|
+
auth_config_id = self.create_new_auth_config(app_name)
|
750
|
+
else:
|
751
|
+
auth_config_id = None
|
752
|
+
for auth_config in auth_configs.items:
|
753
|
+
if auth_config.auth_scheme == "OAUTH2":
|
754
|
+
auth_config_id = auth_config.id
|
755
|
+
|
756
|
+
auth_config_id = auth_configs.items[0].id
|
757
|
+
|
758
|
+
connection_request = composio.connected_accounts.initiate(
|
759
|
+
user_id=self.entity_id, auth_config_id=auth_config_id
|
760
|
+
)
|
761
|
+
|
762
|
+
redirect_url = getattr(connection_request, "redirect_url", None)
|
763
|
+
connection_id = getattr(connection_request, "id", None)
|
764
|
+
|
765
|
+
if not redirect_url or not redirect_url.startswith(("http://", "https://")):
|
766
|
+
msg = "Invalid redirect URL received from Composio"
|
767
|
+
raise ValueError(msg)
|
768
|
+
|
769
|
+
if not connection_id:
|
770
|
+
msg = "No connection ID received from Composio"
|
771
|
+
raise ValueError(msg)
|
772
|
+
|
773
|
+
logger.info(f"OAuth connection initiated for {app_name}: {redirect_url} (ID: {connection_id})")
|
774
|
+
return redirect_url, connection_id # noqa: TRY300
|
775
|
+
|
776
|
+
except Exception as e:
|
777
|
+
logger.error(f"Error initiating connection for {app_name}: {e}")
|
778
|
+
msg = f"Failed to initiate OAuth connection: {e}"
|
779
|
+
raise ValueError(msg) from e
|
780
|
+
|
781
|
+
def _check_connection_status_by_id(self, connection_id: str) -> str | None:
|
782
|
+
"""Check status of a specific connection by ID. Returns status or None if not found."""
|
783
|
+
try:
|
784
|
+
composio = self._build_wrapper()
|
785
|
+
connection = composio.connected_accounts.get(nanoid=connection_id)
|
786
|
+
status = getattr(connection, "status", None)
|
787
|
+
logger.info(f"Connection {connection_id} status: {status}")
|
788
|
+
except (ValueError, ConnectionError) as e:
|
789
|
+
logger.error(f"Error checking connection {connection_id}: {e}")
|
790
|
+
return None
|
791
|
+
else:
|
792
|
+
return status
|
793
|
+
|
794
|
+
def _find_active_connection_for_app(self, app_name: str) -> tuple[str, str] | None:
|
795
|
+
"""Find any ACTIVE connection for this app/user. Returns (connection_id, status) or None."""
|
796
|
+
try:
|
797
|
+
composio = self._build_wrapper()
|
798
|
+
connection_list = composio.connected_accounts.list(
|
799
|
+
user_ids=[self.entity_id], toolkit_slugs=[app_name.lower()]
|
800
|
+
)
|
801
|
+
|
802
|
+
if connection_list and hasattr(connection_list, "items") and connection_list.items:
|
803
|
+
for connection in connection_list.items:
|
804
|
+
connection_id = getattr(connection, "id", None)
|
805
|
+
connection_status = getattr(connection, "status", None)
|
806
|
+
if connection_status == "ACTIVE" and connection_id:
|
807
|
+
logger.info(f"Found existing ACTIVE connection for {app_name}: {connection_id}")
|
808
|
+
return connection_id, connection_status
|
809
|
+
|
810
|
+
except (ValueError, ConnectionError) as e:
|
811
|
+
logger.error(f"Error finding active connection for {app_name}: {e}")
|
812
|
+
return None
|
813
|
+
else:
|
814
|
+
return None
|
815
|
+
|
816
|
+
def _disconnect_specific_connection(self, connection_id: str) -> None:
|
817
|
+
"""Disconnect a specific Composio connection by ID."""
|
818
|
+
try:
|
819
|
+
composio = self._build_wrapper()
|
820
|
+
composio.connected_accounts.delete(nanoid=connection_id)
|
821
|
+
logger.info(f"✅ Disconnected specific connection: {connection_id}")
|
822
|
+
|
823
|
+
except Exception as e:
|
824
|
+
logger.error(f"Error disconnecting connection {connection_id}: {e}")
|
825
|
+
msg = f"Failed to disconnect connection {connection_id}: {e}"
|
826
|
+
raise ValueError(msg) from e
|
827
|
+
|
828
|
+
def update_build_config(self, build_config: dict, field_value: Any, field_name: str | None = None) -> dict:
|
829
|
+
"""Update build config for auth and action selection."""
|
830
|
+
# Clean any legacy None values that may still be present
|
831
|
+
for _fconfig in build_config.values():
|
832
|
+
if isinstance(_fconfig, dict) and _fconfig.get("input_types") is None:
|
833
|
+
_fconfig["input_types"] = []
|
834
|
+
|
835
|
+
# BULLETPROOF tool_mode checking - check all possible places where tool_mode could be stored
|
836
|
+
instance_tool_mode = getattr(self, "tool_mode", False) if hasattr(self, "tool_mode") else False
|
837
|
+
|
838
|
+
# Check build_config for tool_mode in multiple possible structures
|
839
|
+
build_config_tool_mode = False
|
840
|
+
if "tool_mode" in build_config:
|
841
|
+
tool_mode_config = build_config["tool_mode"]
|
842
|
+
if isinstance(tool_mode_config, dict):
|
843
|
+
build_config_tool_mode = tool_mode_config.get("value", False)
|
844
|
+
else:
|
845
|
+
build_config_tool_mode = bool(tool_mode_config)
|
846
|
+
|
847
|
+
# If this is a tool_mode change, update BOTH instance variable AND build_config
|
848
|
+
if field_name == "tool_mode":
|
849
|
+
self.tool_mode = field_value
|
850
|
+
instance_tool_mode = field_value
|
851
|
+
# CRITICAL: Store tool_mode state in build_config so it persists
|
852
|
+
if "tool_mode" not in build_config:
|
853
|
+
build_config["tool_mode"] = {}
|
854
|
+
if isinstance(build_config["tool_mode"], dict):
|
855
|
+
build_config["tool_mode"]["value"] = field_value
|
856
|
+
build_config_tool_mode = field_value
|
857
|
+
|
858
|
+
# Current tool_mode is True if ANY source indicates it's enabled
|
859
|
+
current_tool_mode = instance_tool_mode or build_config_tool_mode or (field_name == "tool_mode" and field_value)
|
860
|
+
|
861
|
+
# CRITICAL: Ensure dynamic action metadata is available whenever we have an API key
|
862
|
+
# This must happen BEFORE any early returns to ensure tools are always loaded
|
863
|
+
api_key_available = hasattr(self, "api_key") and self.api_key
|
864
|
+
|
865
|
+
# Check if we need to populate actions - but also check cache availability
|
866
|
+
actions_available = bool(self._actions_data)
|
867
|
+
toolkit_slug = getattr(self, "app_name", "").lower()
|
868
|
+
cached_actions_available = toolkit_slug in self.__class__._actions_cache
|
869
|
+
|
870
|
+
should_populate = False
|
871
|
+
|
872
|
+
if (field_name == "api_key" and field_value) or (
|
873
|
+
api_key_available and not actions_available and not cached_actions_available
|
874
|
+
):
|
875
|
+
should_populate = True
|
876
|
+
elif api_key_available and not actions_available and cached_actions_available:
|
877
|
+
self._populate_actions_data()
|
878
|
+
|
879
|
+
if should_populate:
|
880
|
+
logger.info(f"Populating actions data for {getattr(self, 'app_name', 'unknown')}...")
|
881
|
+
self._populate_actions_data()
|
882
|
+
logger.info(f"Actions populated: {len(self._actions_data)} actions found")
|
883
|
+
|
884
|
+
# CRITICAL: Set action options if we have actions (either from fresh population or cache)
|
885
|
+
if self._actions_data:
|
886
|
+
self._build_action_maps()
|
887
|
+
build_config["action_button"]["options"] = [
|
888
|
+
{"name": self.sanitize_action_name(action), "metadata": action} for action in self._actions_data
|
889
|
+
]
|
890
|
+
logger.info(f"Action options set in build_config: {len(build_config['action_button']['options'])} options")
|
891
|
+
else:
|
892
|
+
build_config["action_button"]["options"] = []
|
893
|
+
logger.warning("No actions found, setting empty options")
|
894
|
+
|
895
|
+
# clear stored connection_id when api_key is changed
|
896
|
+
if field_name == "api_key" and field_value:
|
897
|
+
stored_connection_before = build_config.get("auth_link", {}).get("connection_id")
|
898
|
+
if "auth_link" in build_config and "connection_id" in build_config["auth_link"]:
|
899
|
+
build_config["auth_link"].pop("connection_id", None)
|
900
|
+
build_config["auth_link"]["value"] = "connect"
|
901
|
+
build_config["auth_link"]["auth_tooltip"] = "Connect"
|
902
|
+
logger.info(f"Cleared stored connection_id '{stored_connection_before}' due to API key change")
|
903
|
+
else:
|
904
|
+
logger.info("DEBUG: EARLY No stored connection_id to clear on API key change")
|
905
|
+
|
906
|
+
# Handle disconnect operations when tool mode is enabled
|
907
|
+
if field_name == "auth_link" and field_value == "disconnect":
|
908
|
+
try:
|
909
|
+
# Get the specific connection ID that's currently being used
|
910
|
+
stored_connection_id = build_config.get("auth_link", {}).get("connection_id")
|
911
|
+
if stored_connection_id:
|
912
|
+
self._disconnect_specific_connection(stored_connection_id)
|
913
|
+
else:
|
914
|
+
# No connection ID stored - nothing to disconnect
|
915
|
+
logger.warning("No connection ID found to disconnect")
|
916
|
+
build_config["auth_link"]["value"] = "connect"
|
917
|
+
build_config["auth_link"]["auth_tooltip"] = "Connect"
|
918
|
+
return build_config
|
919
|
+
except (ValueError, ConnectionError) as e:
|
920
|
+
logger.error(f"Error disconnecting: {e}")
|
921
|
+
build_config["auth_link"]["value"] = "error"
|
922
|
+
build_config["auth_link"]["auth_tooltip"] = f"Disconnect failed: {e!s}"
|
923
|
+
return build_config
|
924
|
+
else:
|
925
|
+
build_config["auth_link"]["value"] = "connect"
|
926
|
+
build_config["auth_link"]["auth_tooltip"] = "Connect"
|
927
|
+
build_config["auth_link"].pop("connection_id", None) # Clear stored connection ID
|
928
|
+
build_config["action_button"]["helper_text"] = "Please connect before selecting actions."
|
929
|
+
build_config["action_button"]["helper_text_metadata"] = {"variant": "destructive"}
|
930
|
+
return build_config
|
931
|
+
|
932
|
+
# Handle connection initiation when tool mode is enabled
|
933
|
+
if field_name == "auth_link" and isinstance(field_value, dict):
|
934
|
+
try:
|
935
|
+
toolkit_slug = self.app_name.lower()
|
936
|
+
|
937
|
+
# First check if we already have an ACTIVE connection
|
938
|
+
existing_active = self._find_active_connection_for_app(self.app_name)
|
939
|
+
if existing_active:
|
940
|
+
connection_id, _ = existing_active
|
941
|
+
build_config["auth_link"]["value"] = "validated"
|
942
|
+
build_config["auth_link"]["auth_tooltip"] = "Disconnect"
|
943
|
+
build_config["auth_link"]["connection_id"] = connection_id
|
944
|
+
build_config["action_button"]["helper_text"] = ""
|
945
|
+
build_config["action_button"]["helper_text_metadata"] = {}
|
946
|
+
logger.info(f"Using existing ACTIVE connection {connection_id} for {toolkit_slug}")
|
947
|
+
return build_config
|
948
|
+
|
949
|
+
# Check if we have a stored connection ID with INITIATED status
|
950
|
+
stored_connection_id = build_config.get("auth_link", {}).get("connection_id")
|
951
|
+
if stored_connection_id:
|
952
|
+
# Check status of existing connection
|
953
|
+
status = self._check_connection_status_by_id(stored_connection_id)
|
954
|
+
if status == "INITIATED":
|
955
|
+
# Get redirect URL from stored connection
|
956
|
+
try:
|
957
|
+
composio = self._build_wrapper()
|
958
|
+
connection = composio.connected_accounts.get(nanoid=stored_connection_id)
|
959
|
+
state = getattr(connection, "state", None)
|
960
|
+
if state and hasattr(state, "val"):
|
961
|
+
redirect_url = getattr(state.val, "redirect_url", None)
|
962
|
+
if redirect_url:
|
963
|
+
build_config["auth_link"]["value"] = redirect_url
|
964
|
+
logger.info(f"Reusing existing OAuth URL for {toolkit_slug}: {redirect_url}")
|
965
|
+
return build_config
|
966
|
+
except (AttributeError, ValueError, ConnectionError) as e:
|
967
|
+
logger.debug(f"Could not retrieve connection {stored_connection_id}: {e}")
|
968
|
+
# Continue to create new connection below
|
969
|
+
|
970
|
+
# Create new OAuth connection ONLY if we truly have no usable connection yet
|
971
|
+
if existing_active is None and not (stored_connection_id and status in ("ACTIVE", "INITIATED")):
|
972
|
+
try:
|
973
|
+
redirect_url, connection_id = self._initiate_connection(toolkit_slug)
|
974
|
+
build_config["auth_link"]["value"] = redirect_url
|
975
|
+
build_config["auth_link"]["connection_id"] = connection_id # Store connection ID
|
976
|
+
logger.info(f"New OAuth URL created for {toolkit_slug}: {redirect_url}")
|
977
|
+
except (ValueError, ConnectionError) as e:
|
978
|
+
logger.error(f"Error creating OAuth connection: {e}")
|
979
|
+
build_config["auth_link"]["value"] = "connect"
|
980
|
+
build_config["auth_link"]["auth_tooltip"] = f"Error: {e!s}"
|
981
|
+
else:
|
982
|
+
return build_config
|
983
|
+
else:
|
984
|
+
# We already have a usable connection; no new OAuth request
|
985
|
+
build_config["auth_link"]["auth_tooltip"] = "Disconnect"
|
986
|
+
|
987
|
+
except (ValueError, ConnectionError) as e:
|
988
|
+
logger.error(f"Error in connection initiation: {e}")
|
989
|
+
build_config["auth_link"]["value"] = "connect"
|
990
|
+
build_config["auth_link"]["auth_tooltip"] = f"Error: {e!s}"
|
991
|
+
build_config["action_button"]["helper_text"] = "Please connect before selecting actions."
|
992
|
+
build_config["action_button"]["helper_text_metadata"] = {"variant": "destructive"}
|
993
|
+
return build_config
|
994
|
+
|
995
|
+
# Check for ACTIVE connections and update status accordingly (tool mode)
|
996
|
+
if hasattr(self, "api_key") and self.api_key:
|
997
|
+
stored_connection_id = build_config.get("auth_link", {}).get("connection_id")
|
998
|
+
active_connection_id = None
|
999
|
+
|
1000
|
+
# First try to check stored connection ID
|
1001
|
+
if stored_connection_id:
|
1002
|
+
status = self._check_connection_status_by_id(stored_connection_id)
|
1003
|
+
if status == "ACTIVE":
|
1004
|
+
active_connection_id = stored_connection_id
|
1005
|
+
|
1006
|
+
# If no stored connection or stored connection is not ACTIVE, find any ACTIVE connection
|
1007
|
+
if not active_connection_id:
|
1008
|
+
active_connection = self._find_active_connection_for_app(self.app_name)
|
1009
|
+
if active_connection:
|
1010
|
+
active_connection_id, _ = active_connection
|
1011
|
+
# Store the found active connection ID for future use
|
1012
|
+
if "auth_link" not in build_config:
|
1013
|
+
build_config["auth_link"] = {}
|
1014
|
+
build_config["auth_link"]["connection_id"] = active_connection_id
|
1015
|
+
|
1016
|
+
if active_connection_id:
|
1017
|
+
# Show validated connection status
|
1018
|
+
build_config["auth_link"]["value"] = "validated"
|
1019
|
+
build_config["auth_link"]["auth_tooltip"] = "Disconnect"
|
1020
|
+
build_config["action_button"]["helper_text"] = ""
|
1021
|
+
build_config["action_button"]["helper_text_metadata"] = {}
|
1022
|
+
else:
|
1023
|
+
build_config["auth_link"]["value"] = "connect"
|
1024
|
+
build_config["auth_link"]["auth_tooltip"] = "Connect"
|
1025
|
+
build_config["action_button"]["helper_text"] = "Please connect before selecting actions."
|
1026
|
+
build_config["action_button"]["helper_text_metadata"] = {"variant": "destructive"}
|
1027
|
+
|
1028
|
+
# CRITICAL: If tool_mode is enabled from ANY source, immediately hide action field and return
|
1029
|
+
if current_tool_mode:
|
1030
|
+
build_config["action_button"]["show"] = False
|
1031
|
+
|
1032
|
+
# CRITICAL: Hide ALL action parameter fields when tool mode is enabled
|
1033
|
+
for field in self._all_fields:
|
1034
|
+
if field in build_config:
|
1035
|
+
build_config[field]["show"] = False
|
1036
|
+
|
1037
|
+
# Also hide any other action-related fields that might be in build_config
|
1038
|
+
for field_name_in_config in build_config: # noqa: PLC0206
|
1039
|
+
# Skip base fields like api_key, tool_mode, action, etc.
|
1040
|
+
if (
|
1041
|
+
field_name_in_config not in ["api_key", "tool_mode", "action_button", "auth_link", "entity_id"]
|
1042
|
+
and isinstance(build_config[field_name_in_config], dict)
|
1043
|
+
and "show" in build_config[field_name_in_config]
|
1044
|
+
):
|
1045
|
+
build_config[field_name_in_config]["show"] = False
|
1046
|
+
|
1047
|
+
# ENSURE tool_mode state is preserved in build_config for future calls
|
1048
|
+
if "tool_mode" not in build_config:
|
1049
|
+
build_config["tool_mode"] = {"value": True}
|
1050
|
+
elif isinstance(build_config["tool_mode"], dict):
|
1051
|
+
build_config["tool_mode"]["value"] = True
|
1052
|
+
# Don't proceed with any other logic that might override this
|
1053
|
+
return build_config
|
1054
|
+
|
1055
|
+
if field_name == "tool_mode":
|
1056
|
+
if field_value is True:
|
1057
|
+
build_config["action_button"]["show"] = False # Hide action field when tool mode is enabled
|
1058
|
+
for field in self._all_fields:
|
1059
|
+
build_config[field]["show"] = False # Update show status for all fields based on tool mode
|
1060
|
+
elif field_value is False:
|
1061
|
+
build_config["action_button"]["show"] = True # Show action field when tool mode is disabled
|
1062
|
+
for field in self._all_fields:
|
1063
|
+
build_config[field]["show"] = True # Update show status for all fields based on tool mode
|
1064
|
+
return build_config
|
1065
|
+
|
1066
|
+
if field_name == "action_button":
|
1067
|
+
self._update_action_config(build_config, field_value)
|
1068
|
+
# Keep the existing show/hide behaviour
|
1069
|
+
self.show_hide_fields(build_config, field_value)
|
1070
|
+
return build_config
|
1071
|
+
|
1072
|
+
# Handle API key removal
|
1073
|
+
if field_name == "api_key" and len(field_value) == 0:
|
1074
|
+
build_config["auth_link"]["value"] = ""
|
1075
|
+
build_config["auth_link"]["auth_tooltip"] = "Please provide a valid Composio API Key."
|
1076
|
+
build_config["action_button"]["options"] = []
|
1077
|
+
build_config["action_button"]["helper_text"] = "Please connect before selecting actions."
|
1078
|
+
build_config["action_button"]["helper_text_metadata"] = {"variant": "destructive"}
|
1079
|
+
build_config["auth_link"].pop("connection_id", None)
|
1080
|
+
return build_config
|
1081
|
+
|
1082
|
+
# Only proceed with connection logic if we have an API key
|
1083
|
+
if not hasattr(self, "api_key") or not self.api_key:
|
1084
|
+
return build_config
|
1085
|
+
|
1086
|
+
# CRITICAL: If tool_mode is enabled (check both instance and build_config), skip all connection logic
|
1087
|
+
if current_tool_mode:
|
1088
|
+
build_config["action_button"]["show"] = False
|
1089
|
+
return build_config
|
1090
|
+
|
1091
|
+
# Update action options only if tool_mode is disabled
|
1092
|
+
self._build_action_maps()
|
1093
|
+
# Only set options if they haven't been set already during action population
|
1094
|
+
if "options" not in build_config.get("action_button", {}) or not build_config["action_button"]["options"]:
|
1095
|
+
build_config["action_button"]["options"] = [
|
1096
|
+
{"name": self.sanitize_action_name(action), "metadata": action} for action in self._actions_data
|
1097
|
+
]
|
1098
|
+
logger.debug("Setting action options from main logic path")
|
1099
|
+
else:
|
1100
|
+
logger.debug("Action options already set, skipping duplicate setting")
|
1101
|
+
# Only set show=True if tool_mode is not enabled
|
1102
|
+
if not current_tool_mode:
|
1103
|
+
build_config["action_button"]["show"] = True
|
1104
|
+
|
1105
|
+
stored_connection_id = build_config.get("auth_link", {}).get("connection_id")
|
1106
|
+
active_connection_id = None
|
1107
|
+
|
1108
|
+
if stored_connection_id:
|
1109
|
+
status = self._check_connection_status_by_id(stored_connection_id)
|
1110
|
+
if status == "ACTIVE":
|
1111
|
+
active_connection_id = stored_connection_id
|
1112
|
+
|
1113
|
+
if not active_connection_id:
|
1114
|
+
active_connection = self._find_active_connection_for_app(self.app_name)
|
1115
|
+
if active_connection:
|
1116
|
+
active_connection_id, _ = active_connection
|
1117
|
+
if "auth_link" not in build_config:
|
1118
|
+
build_config["auth_link"] = {}
|
1119
|
+
build_config["auth_link"]["connection_id"] = active_connection_id
|
1120
|
+
|
1121
|
+
if active_connection_id:
|
1122
|
+
build_config["auth_link"]["value"] = "validated"
|
1123
|
+
build_config["auth_link"]["auth_tooltip"] = "Disconnect"
|
1124
|
+
build_config["action_button"]["helper_text"] = ""
|
1125
|
+
build_config["action_button"]["helper_text_metadata"] = {}
|
1126
|
+
elif stored_connection_id:
|
1127
|
+
status = self._check_connection_status_by_id(stored_connection_id)
|
1128
|
+
if status == "INITIATED":
|
1129
|
+
current_value = build_config.get("auth_link", {}).get("value")
|
1130
|
+
if not current_value or current_value == "connect":
|
1131
|
+
build_config["auth_link"]["value"] = "connect"
|
1132
|
+
build_config["auth_link"]["auth_tooltip"] = "Connect"
|
1133
|
+
build_config["action_button"]["helper_text"] = "Please connect before selecting actions."
|
1134
|
+
build_config["action_button"]["helper_text_metadata"] = {"variant": "destructive"}
|
1135
|
+
else:
|
1136
|
+
# Connection not found or other status
|
1137
|
+
build_config["auth_link"]["value"] = "connect"
|
1138
|
+
build_config["auth_link"]["auth_tooltip"] = "Connect"
|
1139
|
+
build_config["action_button"]["helper_text"] = "Please connect before selecting actions."
|
1140
|
+
build_config["action_button"]["helper_text_metadata"] = {"variant": "destructive"}
|
1141
|
+
else:
|
1142
|
+
build_config["auth_link"]["value"] = "connect"
|
1143
|
+
build_config["auth_link"]["auth_tooltip"] = "Connect"
|
1144
|
+
build_config["action_button"]["helper_text"] = "Please connect before selecting actions."
|
1145
|
+
build_config["action_button"]["helper_text_metadata"] = {"variant": "destructive"}
|
1146
|
+
|
1147
|
+
if self._is_tool_mode_enabled():
|
1148
|
+
build_config["action_button"]["show"] = False
|
1149
|
+
|
1150
|
+
return build_config
|
1151
|
+
|
1152
|
+
def configure_tools(self, composio: Composio, limit: int | None = None) -> list[Tool]:
|
1153
|
+
if limit is None:
|
1154
|
+
limit = 999
|
1155
|
+
|
1156
|
+
tools = composio.tools.get(user_id=self.entity_id, toolkits=[self.app_name.lower()], limit=limit)
|
1157
|
+
configured_tools = []
|
1158
|
+
for tool in tools:
|
1159
|
+
# Set the sanitized name
|
1160
|
+
display_name = self._actions_data.get(tool.name, {}).get(
|
1161
|
+
"display_name", self._sanitized_names.get(tool.name, self._name_sanitizer.sub("-", tool.name))
|
1162
|
+
)
|
1163
|
+
# Set the tags
|
1164
|
+
tool.tags = [tool.name]
|
1165
|
+
tool.metadata = {"display_name": display_name, "display_description": tool.description, "readonly": True}
|
1166
|
+
configured_tools.append(tool)
|
1167
|
+
return configured_tools
|
1168
|
+
|
1169
|
+
async def _get_tools(self) -> list[Tool]:
|
1170
|
+
"""Get tools with cached results and optimized name sanitization."""
|
1171
|
+
composio = self._build_wrapper()
|
1172
|
+
self.set_default_tools()
|
1173
|
+
return self.configure_tools(composio)
|
1174
|
+
|
1175
|
+
@property
|
1176
|
+
def enabled_tools(self):
|
1177
|
+
"""Return tag names for actions of this app that should be exposed to the agent.
|
1178
|
+
|
1179
|
+
If default tools are set via set_default_tools(), returns those.
|
1180
|
+
Otherwise, returns only the first few tools (limited by default_tools_limit)
|
1181
|
+
to prevent overwhelming the agent. Subclasses can override this behavior.
|
1182
|
+
|
1183
|
+
"""
|
1184
|
+
if not self._actions_data:
|
1185
|
+
self._populate_actions_data()
|
1186
|
+
|
1187
|
+
if hasattr(self, "_default_tools") and self._default_tools:
|
1188
|
+
return list(self._default_tools)
|
1189
|
+
|
1190
|
+
all_tools = list(self._actions_data.keys())
|
1191
|
+
limit = getattr(self, "default_tools_limit", 5)
|
1192
|
+
return all_tools[:limit]
|
1193
|
+
|
1194
|
+
def execute_action(self):
|
1195
|
+
"""Execute the selected Composio tool."""
|
1196
|
+
composio = self._build_wrapper()
|
1197
|
+
self._populate_actions_data()
|
1198
|
+
self._build_action_maps()
|
1199
|
+
|
1200
|
+
display_name = (
|
1201
|
+
self.action_button[0]["name"]
|
1202
|
+
if isinstance(getattr(self, "action_button", None), list) and self.action_button
|
1203
|
+
else self.action_button
|
1204
|
+
)
|
1205
|
+
action_key = self._display_to_key_map.get(display_name)
|
1206
|
+
|
1207
|
+
if not action_key:
|
1208
|
+
msg = f"Invalid action: {display_name}"
|
1209
|
+
raise ValueError(msg)
|
1210
|
+
|
1211
|
+
try:
|
1212
|
+
arguments: dict[str, Any] = {}
|
1213
|
+
param_fields = self._actions_data.get(action_key, {}).get("action_fields", [])
|
1214
|
+
|
1215
|
+
schema_dict = self._action_schemas.get(action_key, {})
|
1216
|
+
parameters_schema = schema_dict.get("input_parameters", {})
|
1217
|
+
schema_properties = parameters_schema.get("properties", {}) if parameters_schema else {}
|
1218
|
+
# Handle case where 'required' field is None (causes "'NoneType' object is not iterable")
|
1219
|
+
required_list = parameters_schema.get("required", []) if parameters_schema else []
|
1220
|
+
required_fields = set(required_list) if required_list is not None else set()
|
1221
|
+
|
1222
|
+
for field in param_fields:
|
1223
|
+
if not hasattr(self, field):
|
1224
|
+
continue
|
1225
|
+
value = getattr(self, field)
|
1226
|
+
|
1227
|
+
# Skip None, empty strings, and empty lists
|
1228
|
+
if value is None or value == "" or (isinstance(value, list) and len(value) == 0):
|
1229
|
+
continue
|
1230
|
+
|
1231
|
+
# For optional fields, be more strict about including them
|
1232
|
+
# Only include if the user has explicitly provided a meaningful value
|
1233
|
+
if field not in required_fields:
|
1234
|
+
# Get the default value from the schema
|
1235
|
+
field_schema = schema_properties.get(field, {})
|
1236
|
+
schema_default = field_schema.get("default")
|
1237
|
+
|
1238
|
+
# Skip if the current value matches the schema default
|
1239
|
+
if value == schema_default:
|
1240
|
+
continue
|
1241
|
+
|
1242
|
+
# Convert comma-separated to list for array parameters (heuristic)
|
1243
|
+
prop_schema = schema_properties.get(field, {})
|
1244
|
+
if prop_schema.get("type") == "array" and isinstance(value, str):
|
1245
|
+
value = [item.strip() for item in value.split(",")]
|
1246
|
+
|
1247
|
+
if field in self._bool_variables:
|
1248
|
+
value = bool(value)
|
1249
|
+
|
1250
|
+
# Handle renamed fields - map back to original names for API execution
|
1251
|
+
final_field_name = field
|
1252
|
+
if field.endswith("_user_id") and field.startswith(self.app_name):
|
1253
|
+
final_field_name = "user_id"
|
1254
|
+
elif field.endswith("_status") and field.startswith(self.app_name):
|
1255
|
+
final_field_name = "status"
|
1256
|
+
|
1257
|
+
arguments[final_field_name] = value
|
1258
|
+
|
1259
|
+
# Execute using new SDK
|
1260
|
+
result = composio.tools.execute(
|
1261
|
+
slug=action_key,
|
1262
|
+
arguments=arguments,
|
1263
|
+
user_id=self.entity_id,
|
1264
|
+
)
|
1265
|
+
|
1266
|
+
if isinstance(result, dict) and "successful" in result:
|
1267
|
+
if result["successful"]:
|
1268
|
+
raw_data = result.get("data", result)
|
1269
|
+
return self._apply_post_processor(action_key, raw_data)
|
1270
|
+
error_msg = result.get("error", "Tool execution failed")
|
1271
|
+
raise ValueError(error_msg)
|
1272
|
+
|
1273
|
+
except ValueError as e:
|
1274
|
+
logger.error(f"Failed to execute {action_key}: {e}")
|
1275
|
+
raise
|
1276
|
+
|
1277
|
+
def _apply_post_processor(self, action_key: str, raw_data: Any) -> Any:
|
1278
|
+
"""Apply post-processor for the given action if defined."""
|
1279
|
+
if hasattr(self, "post_processors") and isinstance(self.post_processors, dict):
|
1280
|
+
processor_func = self.post_processors.get(action_key)
|
1281
|
+
if processor_func and callable(processor_func):
|
1282
|
+
try:
|
1283
|
+
return processor_func(raw_data)
|
1284
|
+
except (TypeError, ValueError, KeyError) as e:
|
1285
|
+
logger.error(f"Error in post-processor for {action_key}: {e} (Exception type: {type(e).__name__})")
|
1286
|
+
return raw_data
|
1287
|
+
|
1288
|
+
return raw_data
|
1289
|
+
|
1290
|
+
def set_default_tools(self):
|
1291
|
+
"""Set the default tools."""
|