lfx-nightly 0.1.13.dev0__py3-none-any.whl → 0.2.0.dev26__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lfx/_assets/component_index.json +1 -1
- lfx/base/agents/agent.py +121 -29
- lfx/base/agents/altk_base_agent.py +380 -0
- lfx/base/agents/altk_tool_wrappers.py +565 -0
- lfx/base/agents/events.py +103 -35
- lfx/base/agents/utils.py +15 -2
- lfx/base/composio/composio_base.py +183 -233
- lfx/base/data/base_file.py +88 -21
- lfx/base/data/storage_utils.py +192 -0
- lfx/base/data/utils.py +178 -14
- lfx/base/datastax/__init__.py +5 -0
- lfx/{components/vectorstores/astradb.py → base/datastax/astradb_base.py} +84 -473
- lfx/base/embeddings/embeddings_class.py +113 -0
- lfx/base/io/chat.py +5 -4
- lfx/base/mcp/util.py +101 -15
- lfx/base/models/groq_constants.py +74 -58
- lfx/base/models/groq_model_discovery.py +265 -0
- lfx/base/models/model.py +1 -1
- lfx/base/models/model_input_constants.py +74 -7
- lfx/base/models/model_utils.py +100 -0
- lfx/base/models/ollama_constants.py +3 -0
- lfx/base/models/openai_constants.py +7 -0
- lfx/base/models/watsonx_constants.py +36 -0
- lfx/base/tools/run_flow.py +601 -129
- lfx/cli/commands.py +7 -4
- lfx/cli/common.py +2 -2
- lfx/cli/run.py +1 -1
- lfx/cli/script_loader.py +53 -11
- lfx/components/Notion/create_page.py +1 -1
- lfx/components/Notion/list_database_properties.py +1 -1
- lfx/components/Notion/list_pages.py +1 -1
- lfx/components/Notion/list_users.py +1 -1
- lfx/components/Notion/page_content_viewer.py +1 -1
- lfx/components/Notion/search.py +1 -1
- lfx/components/Notion/update_page_property.py +1 -1
- lfx/components/__init__.py +19 -5
- lfx/components/altk/__init__.py +34 -0
- lfx/components/altk/altk_agent.py +193 -0
- lfx/components/amazon/amazon_bedrock_converse.py +1 -1
- lfx/components/apify/apify_actor.py +4 -4
- lfx/components/composio/__init__.py +70 -18
- lfx/components/composio/apollo_composio.py +11 -0
- lfx/components/composio/bitbucket_composio.py +11 -0
- lfx/components/composio/canva_composio.py +11 -0
- lfx/components/composio/coda_composio.py +11 -0
- lfx/components/composio/composio_api.py +10 -0
- lfx/components/composio/discord_composio.py +1 -1
- lfx/components/composio/elevenlabs_composio.py +11 -0
- lfx/components/composio/exa_composio.py +11 -0
- lfx/components/composio/firecrawl_composio.py +11 -0
- lfx/components/composio/fireflies_composio.py +11 -0
- lfx/components/composio/gmail_composio.py +1 -1
- lfx/components/composio/googlebigquery_composio.py +11 -0
- lfx/components/composio/googlecalendar_composio.py +1 -1
- lfx/components/composio/googledocs_composio.py +1 -1
- lfx/components/composio/googlemeet_composio.py +1 -1
- lfx/components/composio/googlesheets_composio.py +1 -1
- lfx/components/composio/googletasks_composio.py +1 -1
- lfx/components/composio/heygen_composio.py +11 -0
- lfx/components/composio/mem0_composio.py +11 -0
- lfx/components/composio/peopledatalabs_composio.py +11 -0
- lfx/components/composio/perplexityai_composio.py +11 -0
- lfx/components/composio/serpapi_composio.py +11 -0
- lfx/components/composio/slack_composio.py +3 -574
- lfx/components/composio/slackbot_composio.py +1 -1
- lfx/components/composio/snowflake_composio.py +11 -0
- lfx/components/composio/tavily_composio.py +11 -0
- lfx/components/composio/youtube_composio.py +2 -2
- lfx/components/{agents → cuga}/__init__.py +5 -7
- lfx/components/cuga/cuga_agent.py +730 -0
- lfx/components/data/__init__.py +78 -28
- lfx/components/data_source/__init__.py +58 -0
- lfx/components/{data → data_source}/api_request.py +26 -3
- lfx/components/{data → data_source}/csv_to_data.py +15 -10
- lfx/components/{data → data_source}/json_to_data.py +15 -8
- lfx/components/{data → data_source}/news_search.py +1 -1
- lfx/components/{data → data_source}/rss.py +1 -1
- lfx/components/{data → data_source}/sql_executor.py +1 -1
- lfx/components/{data → data_source}/url.py +1 -1
- lfx/components/{data → data_source}/web_search.py +1 -1
- lfx/components/datastax/__init__.py +12 -6
- lfx/components/datastax/{astra_assistant_manager.py → astradb_assistant_manager.py} +1 -0
- lfx/components/datastax/astradb_chatmemory.py +40 -0
- lfx/components/datastax/astradb_cql.py +6 -32
- lfx/components/datastax/astradb_graph.py +10 -124
- lfx/components/datastax/astradb_tool.py +13 -53
- lfx/components/datastax/astradb_vectorstore.py +134 -977
- lfx/components/datastax/create_assistant.py +1 -0
- lfx/components/datastax/create_thread.py +1 -0
- lfx/components/datastax/dotenv.py +1 -0
- lfx/components/datastax/get_assistant.py +1 -0
- lfx/components/datastax/getenvvar.py +1 -0
- lfx/components/datastax/graph_rag.py +1 -1
- lfx/components/datastax/hcd.py +1 -1
- lfx/components/datastax/list_assistants.py +1 -0
- lfx/components/datastax/run.py +1 -0
- lfx/components/deactivated/json_document_builder.py +1 -1
- lfx/components/elastic/elasticsearch.py +1 -1
- lfx/components/elastic/opensearch_multimodal.py +1575 -0
- lfx/components/files_and_knowledge/__init__.py +47 -0
- lfx/components/{data → files_and_knowledge}/directory.py +1 -1
- lfx/components/{data → files_and_knowledge}/file.py +246 -18
- lfx/components/{knowledge_bases → files_and_knowledge}/ingestion.py +17 -9
- lfx/components/{knowledge_bases → files_and_knowledge}/retrieval.py +18 -10
- lfx/components/{data → files_and_knowledge}/save_file.py +142 -22
- lfx/components/flow_controls/__init__.py +58 -0
- lfx/components/{logic → flow_controls}/conditional_router.py +1 -1
- lfx/components/{logic → flow_controls}/loop.py +47 -9
- lfx/components/flow_controls/run_flow.py +108 -0
- lfx/components/glean/glean_search_api.py +1 -1
- lfx/components/groq/groq.py +35 -28
- lfx/components/helpers/__init__.py +102 -0
- lfx/components/ibm/watsonx.py +25 -21
- lfx/components/input_output/__init__.py +3 -1
- lfx/components/input_output/chat.py +12 -3
- lfx/components/input_output/chat_output.py +12 -4
- lfx/components/input_output/text.py +1 -1
- lfx/components/input_output/text_output.py +1 -1
- lfx/components/{data → input_output}/webhook.py +1 -1
- lfx/components/knowledge_bases/__init__.py +59 -4
- lfx/components/langchain_utilities/character.py +1 -1
- lfx/components/langchain_utilities/csv_agent.py +84 -16
- lfx/components/langchain_utilities/json_agent.py +67 -12
- lfx/components/langchain_utilities/language_recursive.py +1 -1
- lfx/components/llm_operations/__init__.py +46 -0
- lfx/components/{processing → llm_operations}/batch_run.py +1 -1
- lfx/components/{processing → llm_operations}/lambda_filter.py +1 -1
- lfx/components/{logic → llm_operations}/llm_conditional_router.py +1 -1
- lfx/components/{processing/llm_router.py → llm_operations/llm_selector.py} +3 -3
- lfx/components/{processing → llm_operations}/structured_output.py +56 -18
- lfx/components/logic/__init__.py +126 -0
- lfx/components/mem0/mem0_chat_memory.py +11 -0
- lfx/components/mistral/mistral_embeddings.py +1 -1
- lfx/components/models/__init__.py +64 -9
- lfx/components/models_and_agents/__init__.py +49 -0
- lfx/components/{agents → models_and_agents}/agent.py +49 -6
- lfx/components/models_and_agents/embedding_model.py +423 -0
- lfx/components/models_and_agents/language_model.py +398 -0
- lfx/components/{agents → models_and_agents}/mcp_component.py +84 -45
- lfx/components/{helpers → models_and_agents}/memory.py +1 -1
- lfx/components/nvidia/system_assist.py +1 -1
- lfx/components/olivya/olivya.py +1 -1
- lfx/components/ollama/ollama.py +235 -14
- lfx/components/openrouter/openrouter.py +49 -147
- lfx/components/processing/__init__.py +9 -57
- lfx/components/processing/converter.py +1 -1
- lfx/components/processing/dataframe_operations.py +1 -1
- lfx/components/processing/parse_json_data.py +2 -2
- lfx/components/processing/parser.py +7 -2
- lfx/components/processing/split_text.py +1 -1
- lfx/components/qdrant/qdrant.py +1 -1
- lfx/components/redis/redis.py +1 -1
- lfx/components/twelvelabs/split_video.py +10 -0
- lfx/components/twelvelabs/video_file.py +12 -0
- lfx/components/utilities/__init__.py +43 -0
- lfx/components/{helpers → utilities}/calculator_core.py +1 -1
- lfx/components/{helpers → utilities}/current_date.py +1 -1
- lfx/components/{processing → utilities}/python_repl_core.py +1 -1
- lfx/components/vectorstores/__init__.py +0 -6
- lfx/components/vectorstores/local_db.py +9 -0
- lfx/components/youtube/youtube_transcripts.py +118 -30
- lfx/custom/custom_component/component.py +60 -3
- lfx/custom/custom_component/custom_component.py +68 -6
- lfx/field_typing/constants.py +1 -0
- lfx/graph/edge/base.py +45 -22
- lfx/graph/graph/base.py +5 -2
- lfx/graph/graph/schema.py +3 -2
- lfx/graph/state/model.py +15 -2
- lfx/graph/utils.py +6 -0
- lfx/graph/vertex/base.py +4 -1
- lfx/graph/vertex/param_handler.py +10 -7
- lfx/graph/vertex/vertex_types.py +1 -1
- lfx/helpers/__init__.py +12 -0
- lfx/helpers/flow.py +117 -0
- lfx/inputs/input_mixin.py +24 -1
- lfx/inputs/inputs.py +13 -1
- lfx/interface/components.py +161 -83
- lfx/io/schema.py +6 -0
- lfx/log/logger.py +5 -3
- lfx/schema/schema.py +5 -0
- lfx/services/database/__init__.py +5 -0
- lfx/services/database/service.py +25 -0
- lfx/services/deps.py +87 -22
- lfx/services/manager.py +19 -6
- lfx/services/mcp_composer/service.py +998 -157
- lfx/services/session.py +5 -0
- lfx/services/settings/base.py +51 -7
- lfx/services/settings/constants.py +8 -0
- lfx/services/storage/local.py +76 -46
- lfx/services/storage/service.py +152 -29
- lfx/template/field/base.py +3 -0
- lfx/utils/ssrf_protection.py +384 -0
- lfx/utils/validate_cloud.py +26 -0
- {lfx_nightly-0.1.13.dev0.dist-info → lfx_nightly-0.2.0.dev26.dist-info}/METADATA +38 -22
- {lfx_nightly-0.1.13.dev0.dist-info → lfx_nightly-0.2.0.dev26.dist-info}/RECORD +210 -196
- {lfx_nightly-0.1.13.dev0.dist-info → lfx_nightly-0.2.0.dev26.dist-info}/WHEEL +1 -1
- lfx/components/agents/cuga_agent.py +0 -1013
- lfx/components/datastax/astra_db.py +0 -77
- lfx/components/datastax/cassandra.py +0 -92
- lfx/components/logic/run_flow.py +0 -71
- lfx/components/models/embedding_model.py +0 -114
- lfx/components/models/language_model.py +0 -144
- lfx/components/vectorstores/astradb_graph.py +0 -326
- lfx/components/vectorstores/cassandra.py +0 -264
- lfx/components/vectorstores/cassandra_graph.py +0 -238
- lfx/components/vectorstores/chroma.py +0 -167
- lfx/components/vectorstores/clickhouse.py +0 -135
- lfx/components/vectorstores/couchbase.py +0 -102
- lfx/components/vectorstores/elasticsearch.py +0 -267
- lfx/components/vectorstores/faiss.py +0 -111
- lfx/components/vectorstores/graph_rag.py +0 -141
- lfx/components/vectorstores/hcd.py +0 -314
- lfx/components/vectorstores/milvus.py +0 -115
- lfx/components/vectorstores/mongodb_atlas.py +0 -213
- lfx/components/vectorstores/opensearch.py +0 -243
- lfx/components/vectorstores/pgvector.py +0 -72
- lfx/components/vectorstores/pinecone.py +0 -134
- lfx/components/vectorstores/qdrant.py +0 -109
- lfx/components/vectorstores/supabase.py +0 -76
- lfx/components/vectorstores/upstash.py +0 -124
- lfx/components/vectorstores/vectara.py +0 -97
- lfx/components/vectorstores/vectara_rag.py +0 -164
- lfx/components/vectorstores/weaviate.py +0 -89
- /lfx/components/{data → data_source}/mock_data.py +0 -0
- /lfx/components/datastax/{astra_vectorize.py → astradb_vectorize.py} +0 -0
- /lfx/components/{logic → flow_controls}/data_conditional_router.py +0 -0
- /lfx/components/{logic → flow_controls}/flow_tool.py +0 -0
- /lfx/components/{logic → flow_controls}/listen.py +0 -0
- /lfx/components/{logic → flow_controls}/notify.py +0 -0
- /lfx/components/{logic → flow_controls}/pass_message.py +0 -0
- /lfx/components/{logic → flow_controls}/sub_flow.py +0 -0
- /lfx/components/{processing → models_and_agents}/prompt.py +0 -0
- /lfx/components/{helpers → processing}/create_list.py +0 -0
- /lfx/components/{helpers → processing}/output_parser.py +0 -0
- /lfx/components/{helpers → processing}/store_message.py +0 -0
- /lfx/components/{helpers → utilities}/id_generator.py +0 -0
- {lfx_nightly-0.1.13.dev0.dist-info → lfx_nightly-0.2.0.dev26.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""Dynamic Groq model discovery and tool calling detection.
|
|
2
|
+
|
|
3
|
+
This module fetches available models directly from the Groq API
|
|
4
|
+
and tests their tool calling capabilities programmatically,
|
|
5
|
+
eliminating the need for manual metadata updates.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from datetime import datetime, timedelta, timezone
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import requests
|
|
14
|
+
|
|
15
|
+
from lfx.log.logger import logger
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GroqModelDiscovery:
|
|
19
|
+
"""Discovers and caches Groq model capabilities dynamically."""
|
|
20
|
+
|
|
21
|
+
# Cache file location - use local cache directory within models
|
|
22
|
+
CACHE_FILE = Path(__file__).parent / ".cache" / "groq_models_cache.json"
|
|
23
|
+
CACHE_DURATION = timedelta(hours=24) # Refresh cache every 24 hours
|
|
24
|
+
|
|
25
|
+
# Models to skip from LLM list (audio, TTS, guards)
|
|
26
|
+
SKIP_PATTERNS = ["whisper", "tts", "guard", "safeguard", "prompt-guard", "saba"]
|
|
27
|
+
|
|
28
|
+
def __init__(self, api_key: str | None = None, base_url: str = "https://api.groq.com"):
|
|
29
|
+
"""Initialize discovery with optional API key for testing.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
api_key: Groq API key. If None, only cached data will be used.
|
|
33
|
+
base_url: Groq API base URL
|
|
34
|
+
"""
|
|
35
|
+
self.api_key = api_key
|
|
36
|
+
self.base_url = base_url
|
|
37
|
+
|
|
38
|
+
def get_models(self, *, force_refresh: bool = False) -> dict[str, dict[str, Any]]:
|
|
39
|
+
"""Get available models with their capabilities.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
force_refresh: If True, bypass cache and fetch fresh data
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Dictionary mapping model IDs to their metadata:
|
|
46
|
+
{
|
|
47
|
+
"model-id": {
|
|
48
|
+
"name": "model-id",
|
|
49
|
+
"provider": "Provider Name",
|
|
50
|
+
"tool_calling": True/False,
|
|
51
|
+
"preview": True/False,
|
|
52
|
+
"not_supported": True/False, # for non-LLM models
|
|
53
|
+
"last_tested": "2025-01-06T10:30:00"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
"""
|
|
57
|
+
# Try to load from cache first
|
|
58
|
+
if not force_refresh:
|
|
59
|
+
cached = self._load_cache()
|
|
60
|
+
if cached:
|
|
61
|
+
logger.info("Using cached Groq model metadata")
|
|
62
|
+
return cached
|
|
63
|
+
|
|
64
|
+
# Fetch fresh data from API
|
|
65
|
+
if not self.api_key:
|
|
66
|
+
logger.warning("No API key provided, using minimal fallback list")
|
|
67
|
+
return self._get_fallback_models()
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
models_metadata = {}
|
|
71
|
+
|
|
72
|
+
# Step 1: Get list of available models
|
|
73
|
+
available_models = self._fetch_available_models()
|
|
74
|
+
logger.info(f"Found {len(available_models)} models from Groq API")
|
|
75
|
+
|
|
76
|
+
# Step 2: Categorize models
|
|
77
|
+
llm_models = []
|
|
78
|
+
non_llm_models = []
|
|
79
|
+
|
|
80
|
+
for model_id in available_models:
|
|
81
|
+
if any(pattern in model_id.lower() for pattern in self.SKIP_PATTERNS):
|
|
82
|
+
non_llm_models.append(model_id)
|
|
83
|
+
else:
|
|
84
|
+
llm_models.append(model_id)
|
|
85
|
+
|
|
86
|
+
# Step 3: Test LLM models for tool calling
|
|
87
|
+
logger.info(f"Testing {len(llm_models)} LLM models for tool calling support...")
|
|
88
|
+
for model_id in llm_models:
|
|
89
|
+
supports_tools = self._test_tool_calling(model_id)
|
|
90
|
+
models_metadata[model_id] = {
|
|
91
|
+
"name": model_id,
|
|
92
|
+
"provider": self._get_provider_name(model_id),
|
|
93
|
+
"tool_calling": supports_tools,
|
|
94
|
+
"preview": "preview" in model_id.lower() or "/" in model_id,
|
|
95
|
+
"last_tested": datetime.now(timezone.utc).isoformat(),
|
|
96
|
+
}
|
|
97
|
+
logger.debug(f"{model_id}: tool_calling={supports_tools}")
|
|
98
|
+
|
|
99
|
+
# Step 4: Add non-LLM models as unsupported
|
|
100
|
+
for model_id in non_llm_models:
|
|
101
|
+
models_metadata[model_id] = {
|
|
102
|
+
"name": model_id,
|
|
103
|
+
"provider": self._get_provider_name(model_id),
|
|
104
|
+
"not_supported": True,
|
|
105
|
+
"last_tested": datetime.now(timezone.utc).isoformat(),
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Save to cache
|
|
109
|
+
self._save_cache(models_metadata)
|
|
110
|
+
|
|
111
|
+
except (requests.RequestException, KeyError, ValueError, ImportError) as e:
|
|
112
|
+
logger.exception(f"Error discovering models: {e}")
|
|
113
|
+
return self._get_fallback_models()
|
|
114
|
+
else:
|
|
115
|
+
return models_metadata
|
|
116
|
+
|
|
117
|
+
def _fetch_available_models(self) -> list[str]:
|
|
118
|
+
"""Fetch list of available models from Groq API."""
|
|
119
|
+
url = f"{self.base_url}/openai/v1/models"
|
|
120
|
+
headers = {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"}
|
|
121
|
+
|
|
122
|
+
response = requests.get(url, headers=headers, timeout=10)
|
|
123
|
+
response.raise_for_status()
|
|
124
|
+
|
|
125
|
+
model_list = response.json()
|
|
126
|
+
# Use direct access to raise KeyError if 'data' is missing
|
|
127
|
+
return [model["id"] for model in model_list["data"]]
|
|
128
|
+
|
|
129
|
+
def _test_tool_calling(self, model_id: str) -> bool:
|
|
130
|
+
"""Test if a model supports tool calling.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
model_id: The model ID to test
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
True if model supports tool calling, False otherwise
|
|
137
|
+
"""
|
|
138
|
+
try:
|
|
139
|
+
import groq
|
|
140
|
+
|
|
141
|
+
client = groq.Groq(api_key=self.api_key)
|
|
142
|
+
|
|
143
|
+
# Simple tool definition
|
|
144
|
+
tools = [
|
|
145
|
+
{
|
|
146
|
+
"type": "function",
|
|
147
|
+
"function": {
|
|
148
|
+
"name": "test_tool",
|
|
149
|
+
"description": "A test tool",
|
|
150
|
+
"parameters": {
|
|
151
|
+
"type": "object",
|
|
152
|
+
"properties": {"x": {"type": "string"}},
|
|
153
|
+
"required": ["x"],
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
}
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
messages = [{"role": "user", "content": "test"}]
|
|
160
|
+
|
|
161
|
+
# Try to make a request with tools
|
|
162
|
+
client.chat.completions.create(
|
|
163
|
+
model=model_id, messages=messages, tools=tools, tool_choice="auto", max_tokens=10
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
except (ImportError, AttributeError, TypeError, ValueError, RuntimeError, KeyError) as e:
|
|
167
|
+
error_msg = str(e).lower()
|
|
168
|
+
# If error mentions tool calling, model doesn't support it
|
|
169
|
+
if "tool" in error_msg:
|
|
170
|
+
return False
|
|
171
|
+
# Other errors might be rate limits, etc - be conservative
|
|
172
|
+
logger.warning(f"Error testing {model_id}: {e}")
|
|
173
|
+
return False
|
|
174
|
+
else:
|
|
175
|
+
return True
|
|
176
|
+
|
|
177
|
+
def _get_provider_name(self, model_id: str) -> str:
|
|
178
|
+
"""Extract provider name from model ID."""
|
|
179
|
+
if "/" in model_id:
|
|
180
|
+
provider_map = {
|
|
181
|
+
"meta-llama": "Meta",
|
|
182
|
+
"openai": "OpenAI",
|
|
183
|
+
"groq": "Groq",
|
|
184
|
+
"moonshotai": "Moonshot AI",
|
|
185
|
+
"qwen": "Alibaba Cloud",
|
|
186
|
+
}
|
|
187
|
+
prefix = model_id.split("/")[0]
|
|
188
|
+
return provider_map.get(prefix, prefix.title())
|
|
189
|
+
|
|
190
|
+
# Common patterns
|
|
191
|
+
if model_id.startswith("llama"):
|
|
192
|
+
return "Meta"
|
|
193
|
+
if model_id.startswith("qwen"):
|
|
194
|
+
return "Alibaba Cloud"
|
|
195
|
+
if model_id.startswith("allam"):
|
|
196
|
+
return "SDAIA"
|
|
197
|
+
|
|
198
|
+
return "Groq"
|
|
199
|
+
|
|
200
|
+
def _load_cache(self) -> dict[str, dict] | None:
|
|
201
|
+
"""Load cached model metadata if it exists and is fresh."""
|
|
202
|
+
if not self.CACHE_FILE.exists():
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
with self.CACHE_FILE.open() as f:
|
|
207
|
+
cache_data = json.load(f)
|
|
208
|
+
|
|
209
|
+
# Check cache age
|
|
210
|
+
cache_time = datetime.fromisoformat(cache_data["cached_at"])
|
|
211
|
+
if datetime.now(timezone.utc) - cache_time > self.CACHE_DURATION:
|
|
212
|
+
logger.info("Cache expired, will fetch fresh data")
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
return cache_data["models"]
|
|
216
|
+
|
|
217
|
+
except (json.JSONDecodeError, KeyError, ValueError) as e:
|
|
218
|
+
logger.warning(f"Invalid cache file: {e}")
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
def _save_cache(self, models_metadata: dict[str, dict]) -> None:
|
|
222
|
+
"""Save model metadata to cache."""
|
|
223
|
+
try:
|
|
224
|
+
cache_data = {"cached_at": datetime.now(timezone.utc).isoformat(), "models": models_metadata}
|
|
225
|
+
|
|
226
|
+
self.CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
227
|
+
with self.CACHE_FILE.open("w") as f:
|
|
228
|
+
json.dump(cache_data, f, indent=2)
|
|
229
|
+
|
|
230
|
+
logger.info(f"Cached {len(models_metadata)} models to {self.CACHE_FILE}")
|
|
231
|
+
|
|
232
|
+
except (OSError, TypeError, ValueError) as e:
|
|
233
|
+
logger.warning(f"Failed to save cache: {e}")
|
|
234
|
+
|
|
235
|
+
def _get_fallback_models(self) -> dict[str, dict]:
|
|
236
|
+
"""Return minimal fallback list when API is unavailable."""
|
|
237
|
+
return {
|
|
238
|
+
"llama-3.1-8b-instant": {
|
|
239
|
+
"name": "llama-3.1-8b-instant",
|
|
240
|
+
"provider": "Meta",
|
|
241
|
+
"tool_calling": True,
|
|
242
|
+
"preview": False,
|
|
243
|
+
},
|
|
244
|
+
"llama-3.3-70b-versatile": {
|
|
245
|
+
"name": "llama-3.3-70b-versatile",
|
|
246
|
+
"provider": "Meta",
|
|
247
|
+
"tool_calling": True,
|
|
248
|
+
"preview": False,
|
|
249
|
+
},
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# Convenience function for use in other modules
|
|
254
|
+
def get_groq_models(api_key: str | None = None, *, force_refresh: bool = False) -> dict[str, dict]:
|
|
255
|
+
"""Get Groq models with their capabilities.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
api_key: Optional API key for testing. If None, uses cached data.
|
|
259
|
+
force_refresh: If True, bypass cache and fetch fresh data.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Dictionary of model metadata
|
|
263
|
+
"""
|
|
264
|
+
discovery = GroqModelDiscovery(api_key=api_key)
|
|
265
|
+
return discovery.get_models(force_refresh=force_refresh)
|
lfx/base/models/model.py
CHANGED
|
@@ -296,7 +296,7 @@ class LCModelComponent(Component):
|
|
|
296
296
|
)
|
|
297
297
|
model_message.properties.source = self._build_source(self._id, self.display_name, self)
|
|
298
298
|
lf_message = await self.send_message(model_message)
|
|
299
|
-
result = lf_message.text
|
|
299
|
+
result = lf_message.text or ""
|
|
300
300
|
else:
|
|
301
301
|
message = await runnable.ainvoke(inputs)
|
|
302
302
|
result = message.content if hasattr(message, "content") else message
|
|
@@ -14,14 +14,18 @@ class ModelProvidersDict(TypedDict):
|
|
|
14
14
|
is_active: bool
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
def get_filtered_inputs(component_class):
|
|
17
|
+
def get_filtered_inputs(component_class, provider_name: str | None = None):
|
|
18
18
|
base_input_names = {field.name for field in LCModelComponent.get_base_inputs()}
|
|
19
19
|
component_instance = component_class()
|
|
20
20
|
|
|
21
|
-
return [
|
|
21
|
+
return [
|
|
22
|
+
process_inputs(input_, provider_name)
|
|
23
|
+
for input_ in component_instance.inputs
|
|
24
|
+
if input_.name not in base_input_names
|
|
25
|
+
]
|
|
22
26
|
|
|
23
27
|
|
|
24
|
-
def process_inputs(component_data: Input):
|
|
28
|
+
def process_inputs(component_data: Input, provider_name: str | None = None):
|
|
25
29
|
"""Processes and modifies an input configuration based on its type or name.
|
|
26
30
|
|
|
27
31
|
Adjusts properties such as value, advanced status, real-time refresh, and additional information for specific
|
|
@@ -29,6 +33,7 @@ def process_inputs(component_data: Input):
|
|
|
29
33
|
|
|
30
34
|
Args:
|
|
31
35
|
component_data: The input configuration to process.
|
|
36
|
+
provider_name: The name of the provider to process the inputs for.
|
|
32
37
|
|
|
33
38
|
Returns:
|
|
34
39
|
The modified input configuration.
|
|
@@ -43,9 +48,11 @@ def process_inputs(component_data: Input):
|
|
|
43
48
|
component_data.advanced = True
|
|
44
49
|
component_data.value = True
|
|
45
50
|
elif component_data.name in {"temperature", "base_url"}:
|
|
46
|
-
|
|
51
|
+
if provider_name not in ["IBM watsonx.ai", "Ollama"]:
|
|
52
|
+
component_data = set_advanced_true(component_data)
|
|
47
53
|
elif component_data.name == "model_name":
|
|
48
|
-
|
|
54
|
+
if provider_name not in ["IBM watsonx.ai"]:
|
|
55
|
+
component_data = set_real_time_refresh_false(component_data)
|
|
49
56
|
component_data = add_combobox_true(component_data)
|
|
50
57
|
component_data = add_info(
|
|
51
58
|
component_data,
|
|
@@ -79,6 +86,28 @@ def create_input_fields_dict(inputs: list[Input], prefix: str) -> dict[str, Inpu
|
|
|
79
86
|
return {f"{prefix}{input_.name}": input_.to_dict() for input_ in inputs}
|
|
80
87
|
|
|
81
88
|
|
|
89
|
+
def _get_ollama_inputs_and_fields():
|
|
90
|
+
try:
|
|
91
|
+
from lfx.components.ollama.ollama import ChatOllamaComponent
|
|
92
|
+
|
|
93
|
+
ollama_inputs = get_filtered_inputs(ChatOllamaComponent, provider_name="Ollama")
|
|
94
|
+
except ImportError as e:
|
|
95
|
+
msg = "Ollama is not installed. Please install it with `pip install langchain-ollama`."
|
|
96
|
+
raise ImportError(msg) from e
|
|
97
|
+
return ollama_inputs, create_input_fields_dict(ollama_inputs, "")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _get_watsonx_inputs_and_fields():
|
|
101
|
+
try:
|
|
102
|
+
from lfx.components.ibm.watsonx import WatsonxAIComponent
|
|
103
|
+
|
|
104
|
+
watsonx_inputs = get_filtered_inputs(WatsonxAIComponent, provider_name="IBM watsonx.ai")
|
|
105
|
+
except ImportError as e:
|
|
106
|
+
msg = "IBM watsonx.ai is not installed. Please install it with `pip install langchain-ibm-watsonx`."
|
|
107
|
+
raise ImportError(msg) from e
|
|
108
|
+
return watsonx_inputs, create_input_fields_dict(watsonx_inputs, "")
|
|
109
|
+
|
|
110
|
+
|
|
82
111
|
def _get_google_generative_ai_inputs_and_fields():
|
|
83
112
|
try:
|
|
84
113
|
from lfx.components.google.google_generative_ai import GoogleGenerativeAIComponent
|
|
@@ -293,6 +322,36 @@ try:
|
|
|
293
322
|
except ImportError:
|
|
294
323
|
pass
|
|
295
324
|
|
|
325
|
+
try:
|
|
326
|
+
from lfx.components.ibm.watsonx import WatsonxAIComponent
|
|
327
|
+
|
|
328
|
+
watsonx_inputs, watsonx_fields = _get_watsonx_inputs_and_fields()
|
|
329
|
+
MODEL_PROVIDERS_DICT["IBM watsonx.ai"] = {
|
|
330
|
+
"fields": watsonx_fields,
|
|
331
|
+
"inputs": watsonx_inputs,
|
|
332
|
+
"prefix": "",
|
|
333
|
+
"component_class": WatsonxAIComponent(),
|
|
334
|
+
"icon": WatsonxAIComponent.icon,
|
|
335
|
+
"is_active": True,
|
|
336
|
+
}
|
|
337
|
+
except ImportError:
|
|
338
|
+
pass
|
|
339
|
+
|
|
340
|
+
try:
|
|
341
|
+
from lfx.components.ollama.ollama import ChatOllamaComponent
|
|
342
|
+
|
|
343
|
+
ollama_inputs, ollama_fields = _get_ollama_inputs_and_fields()
|
|
344
|
+
MODEL_PROVIDERS_DICT["Ollama"] = {
|
|
345
|
+
"fields": ollama_fields,
|
|
346
|
+
"inputs": ollama_inputs,
|
|
347
|
+
"prefix": "",
|
|
348
|
+
"component_class": ChatOllamaComponent(),
|
|
349
|
+
"icon": ChatOllamaComponent.icon,
|
|
350
|
+
"is_active": True,
|
|
351
|
+
}
|
|
352
|
+
except ImportError:
|
|
353
|
+
pass
|
|
354
|
+
|
|
296
355
|
# Expose only active providers ----------------------------------------------
|
|
297
356
|
ACTIVE_MODEL_PROVIDERS_DICT: dict[str, ModelProvidersDict] = {
|
|
298
357
|
name: prov for name, prov in MODEL_PROVIDERS_DICT.items() if prov.get("is_active", True)
|
|
@@ -302,10 +361,18 @@ MODEL_PROVIDERS: list[str] = list(ACTIVE_MODEL_PROVIDERS_DICT.keys())
|
|
|
302
361
|
|
|
303
362
|
ALL_PROVIDER_FIELDS: list[str] = [field for prov in ACTIVE_MODEL_PROVIDERS_DICT.values() for field in prov["fields"]]
|
|
304
363
|
|
|
305
|
-
MODEL_DYNAMIC_UPDATE_FIELDS = [
|
|
364
|
+
MODEL_DYNAMIC_UPDATE_FIELDS = [
|
|
365
|
+
"api_key",
|
|
366
|
+
"model",
|
|
367
|
+
"tool_model_enabled",
|
|
368
|
+
"base_url",
|
|
369
|
+
"model_name",
|
|
370
|
+
"watsonx_endpoint",
|
|
371
|
+
"url",
|
|
372
|
+
]
|
|
306
373
|
|
|
307
374
|
MODELS_METADATA = {name: {"icon": prov["icon"]} for name, prov in ACTIVE_MODEL_PROVIDERS_DICT.items()}
|
|
308
375
|
|
|
309
|
-
MODEL_PROVIDERS_LIST = ["Anthropic", "Google Generative AI", "OpenAI"]
|
|
376
|
+
MODEL_PROVIDERS_LIST = ["Anthropic", "Google Generative AI", "OpenAI", "IBM watsonx.ai", "Ollama"]
|
|
310
377
|
|
|
311
378
|
MODEL_OPTIONS_METADATA = [MODELS_METADATA[key] for key in MODEL_PROVIDERS_LIST if key in MODELS_METADATA]
|
lfx/base/models/model_utils.py
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from urllib.parse import urljoin
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from lfx.log.logger import logger
|
|
7
|
+
from lfx.utils.util import transform_localhost_url
|
|
8
|
+
|
|
9
|
+
HTTP_STATUS_OK = 200
|
|
10
|
+
|
|
11
|
+
|
|
1
12
|
def get_model_name(llm, display_name: str | None = "Custom"):
|
|
2
13
|
attributes_to_check = ["model_name", "model", "model_id", "deployment_name"]
|
|
3
14
|
|
|
@@ -6,3 +17,92 @@ def get_model_name(llm, display_name: str | None = "Custom"):
|
|
|
6
17
|
|
|
7
18
|
# If no matching attribute is found, return the class name as a fallback
|
|
8
19
|
return model_name if model_name is not None else display_name
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def is_valid_ollama_url(url: str) -> bool:
|
|
23
|
+
"""Check if the provided URL is a valid Ollama API endpoint."""
|
|
24
|
+
try:
|
|
25
|
+
url = transform_localhost_url(url)
|
|
26
|
+
if not url:
|
|
27
|
+
return False
|
|
28
|
+
# Strip /v1 suffix if present, as Ollama API endpoints are at root level
|
|
29
|
+
url = url.rstrip("/").removesuffix("/v1")
|
|
30
|
+
if not url.endswith("/"):
|
|
31
|
+
url = url + "/"
|
|
32
|
+
async with httpx.AsyncClient() as client:
|
|
33
|
+
return (await client.get(url=urljoin(url, "api/tags"))).status_code == HTTP_STATUS_OK
|
|
34
|
+
except httpx.RequestError:
|
|
35
|
+
logger.debug(f"Invalid Ollama URL: {url}")
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async def get_ollama_models(
|
|
40
|
+
base_url_value: str, desired_capability: str, json_models_key: str, json_name_key: str, json_capabilities_key: str
|
|
41
|
+
) -> list[str]:
|
|
42
|
+
"""Fetch available completion models from the Ollama API.
|
|
43
|
+
|
|
44
|
+
Filters out embedding models and only returns models with completion capability.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
base_url_value (str): The base URL of the Ollama API.
|
|
48
|
+
desired_capability (str): The desired capability of the model.
|
|
49
|
+
json_models_key (str): The key in the JSON response that contains the models.
|
|
50
|
+
json_name_key (str): The key in the JSON response that contains the model names.
|
|
51
|
+
json_capabilities_key (str): The key in the JSON response that contains the model capabilities.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
list[str]: A sorted list of model names that support completion.
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
ValueError: If there is an issue with the API request or response.
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
# Strip /v1 suffix if present, as Ollama API endpoints are at root level
|
|
61
|
+
base_url = base_url_value.rstrip("/").removesuffix("/v1")
|
|
62
|
+
if not base_url.endswith("/"):
|
|
63
|
+
base_url = base_url + "/"
|
|
64
|
+
base_url = transform_localhost_url(base_url)
|
|
65
|
+
|
|
66
|
+
# Ollama REST API to return models
|
|
67
|
+
tags_url = urljoin(base_url, "api/tags")
|
|
68
|
+
|
|
69
|
+
# Ollama REST API to return model capabilities
|
|
70
|
+
show_url = urljoin(base_url, "api/show")
|
|
71
|
+
tags_response = None
|
|
72
|
+
|
|
73
|
+
async with httpx.AsyncClient() as client:
|
|
74
|
+
# Fetch available models
|
|
75
|
+
tags_response = await client.get(url=tags_url)
|
|
76
|
+
tags_response.raise_for_status()
|
|
77
|
+
models = tags_response.json()
|
|
78
|
+
if asyncio.iscoroutine(models):
|
|
79
|
+
models = await models
|
|
80
|
+
await logger.adebug(f"Available models: {models}")
|
|
81
|
+
|
|
82
|
+
# Filter models that are NOT embedding models
|
|
83
|
+
model_ids = []
|
|
84
|
+
for model in models.get(json_models_key, []):
|
|
85
|
+
model_name = model.get(json_name_key)
|
|
86
|
+
if not model_name:
|
|
87
|
+
continue
|
|
88
|
+
await logger.adebug(f"Checking model: {model_name}")
|
|
89
|
+
|
|
90
|
+
payload = {"model": model_name}
|
|
91
|
+
show_response = await client.post(url=show_url, json=payload)
|
|
92
|
+
show_response.raise_for_status()
|
|
93
|
+
json_data = show_response.json()
|
|
94
|
+
if asyncio.iscoroutine(json_data):
|
|
95
|
+
json_data = await json_data
|
|
96
|
+
|
|
97
|
+
capabilities = json_data.get(json_capabilities_key, [])
|
|
98
|
+
await logger.adebug(f"Model: {model_name}, Capabilities: {capabilities}")
|
|
99
|
+
|
|
100
|
+
if desired_capability in capabilities:
|
|
101
|
+
model_ids.append(model_name)
|
|
102
|
+
|
|
103
|
+
return sorted(model_ids)
|
|
104
|
+
|
|
105
|
+
except (httpx.RequestError, ValueError) as e:
|
|
106
|
+
msg = "Could not get model names from Ollama."
|
|
107
|
+
await logger.aexception(msg)
|
|
108
|
+
raise ValueError(msg) from e
|
|
@@ -3,6 +3,13 @@ from .model_metadata import create_model_metadata
|
|
|
3
3
|
# Unified model metadata - single source of truth
|
|
4
4
|
OPENAI_MODELS_DETAILED = [
|
|
5
5
|
# GPT-5 Series
|
|
6
|
+
create_model_metadata(
|
|
7
|
+
provider="OpenAI",
|
|
8
|
+
name="gpt-5.1",
|
|
9
|
+
icon="OpenAI",
|
|
10
|
+
tool_calling=True,
|
|
11
|
+
reasoning=True,
|
|
12
|
+
),
|
|
6
13
|
create_model_metadata(
|
|
7
14
|
provider="OpenAI",
|
|
8
15
|
name="gpt-5",
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from .model_metadata import create_model_metadata
|
|
2
|
+
|
|
3
|
+
WATSONX_DEFAULT_EMBEDDING_MODELS = [
|
|
4
|
+
create_model_metadata(
|
|
5
|
+
provider="IBM Watsonx",
|
|
6
|
+
name="sentence-transformers/all-minilm-l12-v2",
|
|
7
|
+
icon="WatsonxAI",
|
|
8
|
+
),
|
|
9
|
+
create_model_metadata(
|
|
10
|
+
provider="IBM Watsonx",
|
|
11
|
+
name="ibm/slate-125m-english-rtrvr-v2",
|
|
12
|
+
icon="WatsonxAI",
|
|
13
|
+
),
|
|
14
|
+
create_model_metadata(
|
|
15
|
+
provider="IBM Watsonx",
|
|
16
|
+
name="ibm/slate-30m-english-rtrvr-v2",
|
|
17
|
+
icon="WatsonxAI",
|
|
18
|
+
),
|
|
19
|
+
create_model_metadata(
|
|
20
|
+
provider="IBM Watsonx",
|
|
21
|
+
name="intfloat/multilingual-e5-large",
|
|
22
|
+
icon="WatsonxAI",
|
|
23
|
+
),
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
WATSONX_EMBEDDING_MODEL_NAMES = [metadata["name"] for metadata in WATSONX_DEFAULT_EMBEDDING_MODELS]
|
|
28
|
+
|
|
29
|
+
IBM_WATSONX_URLS = [
|
|
30
|
+
"https://us-south.ml.cloud.ibm.com",
|
|
31
|
+
"https://eu-de.ml.cloud.ibm.com",
|
|
32
|
+
"https://eu-gb.ml.cloud.ibm.com",
|
|
33
|
+
"https://au-syd.ml.cloud.ibm.com",
|
|
34
|
+
"https://jp-tok.ml.cloud.ibm.com",
|
|
35
|
+
"https://ca-tor.ml.cloud.ibm.com",
|
|
36
|
+
]
|