lfx-nightly 0.2.0.dev41__py3-none-any.whl → 0.3.0.dev3__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/__main__.py +137 -6
- lfx/_assets/component_index.json +1 -1
- lfx/base/agents/agent.py +10 -6
- lfx/base/agents/altk_base_agent.py +5 -3
- lfx/base/agents/altk_tool_wrappers.py +1 -1
- lfx/base/agents/events.py +1 -1
- lfx/base/agents/utils.py +4 -0
- lfx/base/composio/composio_base.py +78 -41
- lfx/base/data/cloud_storage_utils.py +156 -0
- lfx/base/data/docling_utils.py +130 -55
- lfx/base/datastax/astradb_base.py +75 -64
- lfx/base/embeddings/embeddings_class.py +113 -0
- lfx/base/models/__init__.py +11 -1
- lfx/base/models/google_generative_ai_constants.py +33 -9
- lfx/base/models/model_metadata.py +6 -0
- lfx/base/models/ollama_constants.py +196 -30
- lfx/base/models/openai_constants.py +37 -10
- lfx/base/models/unified_models.py +1123 -0
- lfx/base/models/watsonx_constants.py +43 -4
- lfx/base/prompts/api_utils.py +40 -5
- lfx/base/tools/component_tool.py +2 -9
- lfx/cli/__init__.py +10 -2
- lfx/cli/commands.py +3 -0
- lfx/cli/run.py +65 -409
- lfx/cli/script_loader.py +18 -7
- lfx/cli/validation.py +6 -3
- lfx/components/__init__.py +0 -3
- lfx/components/composio/github_composio.py +1 -1
- lfx/components/cuga/cuga_agent.py +39 -27
- lfx/components/data_source/api_request.py +4 -2
- lfx/components/datastax/astradb_assistant_manager.py +4 -2
- lfx/components/docling/__init__.py +45 -11
- lfx/components/docling/docling_inline.py +39 -49
- lfx/components/docling/docling_remote.py +1 -0
- lfx/components/elastic/opensearch_multimodal.py +1733 -0
- lfx/components/files_and_knowledge/file.py +384 -36
- lfx/components/files_and_knowledge/ingestion.py +8 -0
- lfx/components/files_and_knowledge/retrieval.py +10 -0
- lfx/components/files_and_knowledge/save_file.py +91 -88
- lfx/components/langchain_utilities/ibm_granite_handler.py +211 -0
- lfx/components/langchain_utilities/tool_calling.py +37 -6
- lfx/components/llm_operations/batch_run.py +64 -18
- lfx/components/llm_operations/lambda_filter.py +213 -101
- lfx/components/llm_operations/llm_conditional_router.py +39 -7
- lfx/components/llm_operations/structured_output.py +38 -12
- lfx/components/models/__init__.py +16 -74
- lfx/components/models_and_agents/agent.py +51 -203
- lfx/components/models_and_agents/embedding_model.py +171 -255
- lfx/components/models_and_agents/language_model.py +54 -318
- lfx/components/models_and_agents/mcp_component.py +96 -10
- lfx/components/models_and_agents/prompt.py +105 -18
- lfx/components/ollama/ollama_embeddings.py +111 -29
- lfx/components/openai/openai_chat_model.py +1 -1
- lfx/components/processing/text_operations.py +580 -0
- lfx/components/vllm/__init__.py +37 -0
- lfx/components/vllm/vllm.py +141 -0
- lfx/components/vllm/vllm_embeddings.py +110 -0
- lfx/custom/custom_component/component.py +65 -10
- lfx/custom/custom_component/custom_component.py +8 -6
- lfx/events/observability/__init__.py +0 -0
- lfx/events/observability/lifecycle_events.py +111 -0
- lfx/field_typing/__init__.py +57 -58
- lfx/graph/graph/base.py +40 -1
- lfx/graph/utils.py +109 -30
- lfx/graph/vertex/base.py +75 -23
- lfx/graph/vertex/vertex_types.py +0 -5
- lfx/inputs/__init__.py +2 -0
- lfx/inputs/input_mixin.py +55 -0
- lfx/inputs/inputs.py +120 -0
- lfx/interface/components.py +24 -7
- lfx/interface/initialize/loading.py +42 -12
- lfx/io/__init__.py +2 -0
- lfx/run/__init__.py +5 -0
- lfx/run/base.py +464 -0
- lfx/schema/__init__.py +50 -0
- lfx/schema/data.py +1 -1
- lfx/schema/image.py +26 -7
- lfx/schema/message.py +104 -11
- lfx/schema/workflow.py +171 -0
- lfx/services/deps.py +12 -0
- lfx/services/interfaces.py +43 -1
- lfx/services/mcp_composer/service.py +7 -1
- lfx/services/schema.py +1 -0
- lfx/services/settings/auth.py +95 -4
- lfx/services/settings/base.py +11 -1
- lfx/services/settings/constants.py +2 -0
- lfx/services/settings/utils.py +82 -0
- lfx/services/storage/local.py +13 -8
- lfx/services/transaction/__init__.py +5 -0
- lfx/services/transaction/service.py +35 -0
- lfx/tests/unit/components/__init__.py +0 -0
- lfx/utils/constants.py +2 -0
- lfx/utils/mustache_security.py +79 -0
- lfx/utils/validate_cloud.py +81 -3
- {lfx_nightly-0.2.0.dev41.dist-info → lfx_nightly-0.3.0.dev3.dist-info}/METADATA +7 -2
- {lfx_nightly-0.2.0.dev41.dist-info → lfx_nightly-0.3.0.dev3.dist-info}/RECORD +98 -80
- {lfx_nightly-0.2.0.dev41.dist-info → lfx_nightly-0.3.0.dev3.dist-info}/WHEEL +0 -0
- {lfx_nightly-0.2.0.dev41.dist-info → lfx_nightly-0.3.0.dev3.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,1123 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
|
|
10
|
+
import contextlib
|
|
11
|
+
|
|
12
|
+
from lfx.base.models.anthropic_constants import ANTHROPIC_MODELS_DETAILED
|
|
13
|
+
from lfx.base.models.google_generative_ai_constants import (
|
|
14
|
+
GOOGLE_GENERATIVE_AI_MODELS_DETAILED,
|
|
15
|
+
)
|
|
16
|
+
from lfx.base.models.ollama_constants import OLLAMA_EMBEDDING_MODELS_DETAILED, OLLAMA_MODELS_DETAILED
|
|
17
|
+
from lfx.base.models.openai_constants import OPENAI_EMBEDDING_MODELS_DETAILED, OPENAI_MODELS_DETAILED
|
|
18
|
+
from lfx.base.models.watsonx_constants import WATSONX_MODELS_DETAILED
|
|
19
|
+
from lfx.log.logger import logger
|
|
20
|
+
from lfx.services.deps import get_variable_service, session_scope
|
|
21
|
+
from lfx.utils.async_helpers import run_until_complete
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@lru_cache(maxsize=1)
|
|
25
|
+
def get_model_classes():
|
|
26
|
+
"""Lazy load model classes to avoid importing optional dependencies at module level."""
|
|
27
|
+
from langchain_anthropic import ChatAnthropic
|
|
28
|
+
from langchain_ibm import ChatWatsonx
|
|
29
|
+
from langchain_ollama import ChatOllama
|
|
30
|
+
from langchain_openai import ChatOpenAI
|
|
31
|
+
|
|
32
|
+
from lfx.base.models.google_generative_ai_model import ChatGoogleGenerativeAIFixed
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
"ChatOpenAI": ChatOpenAI,
|
|
36
|
+
"ChatAnthropic": ChatAnthropic,
|
|
37
|
+
"ChatGoogleGenerativeAIFixed": ChatGoogleGenerativeAIFixed,
|
|
38
|
+
"ChatOllama": ChatOllama,
|
|
39
|
+
"ChatWatsonx": ChatWatsonx,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@lru_cache(maxsize=1)
|
|
44
|
+
def get_embedding_classes():
|
|
45
|
+
"""Lazy load embedding classes to avoid importing optional dependencies at module level."""
|
|
46
|
+
from langchain_google_genai import GoogleGenerativeAIEmbeddings
|
|
47
|
+
from langchain_ibm import WatsonxEmbeddings
|
|
48
|
+
from langchain_ollama import OllamaEmbeddings
|
|
49
|
+
from langchain_openai import OpenAIEmbeddings
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
"GoogleGenerativeAIEmbeddings": GoogleGenerativeAIEmbeddings,
|
|
53
|
+
"OpenAIEmbeddings": OpenAIEmbeddings,
|
|
54
|
+
"OllamaEmbeddings": OllamaEmbeddings,
|
|
55
|
+
"WatsonxEmbeddings": WatsonxEmbeddings,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@lru_cache(maxsize=1)
|
|
60
|
+
def get_model_provider_metadata():
|
|
61
|
+
return {
|
|
62
|
+
"OpenAI": {
|
|
63
|
+
"icon": "OpenAI",
|
|
64
|
+
"variable_name": "OPENAI_API_KEY",
|
|
65
|
+
},
|
|
66
|
+
"Anthropic": {
|
|
67
|
+
"icon": "Anthropic",
|
|
68
|
+
"variable_name": "ANTHROPIC_API_KEY",
|
|
69
|
+
},
|
|
70
|
+
"Google Generative AI": {
|
|
71
|
+
"icon": "GoogleGenerativeAI",
|
|
72
|
+
"variable_name": "GOOGLE_API_KEY",
|
|
73
|
+
},
|
|
74
|
+
"Ollama": {
|
|
75
|
+
"icon": "Ollama",
|
|
76
|
+
"variable_name": "OLLAMA_BASE_URL", # Ollama is local but can have custom URL
|
|
77
|
+
},
|
|
78
|
+
"IBM WatsonX": {
|
|
79
|
+
"icon": "WatsonxAI",
|
|
80
|
+
"variable_name": "WATSONX_APIKEY",
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
model_provider_metadata = get_model_provider_metadata()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@lru_cache(maxsize=1)
|
|
89
|
+
def get_models_detailed():
|
|
90
|
+
return [
|
|
91
|
+
ANTHROPIC_MODELS_DETAILED,
|
|
92
|
+
OPENAI_MODELS_DETAILED,
|
|
93
|
+
OPENAI_EMBEDDING_MODELS_DETAILED,
|
|
94
|
+
GOOGLE_GENERATIVE_AI_MODELS_DETAILED,
|
|
95
|
+
OLLAMA_MODELS_DETAILED,
|
|
96
|
+
OLLAMA_EMBEDDING_MODELS_DETAILED,
|
|
97
|
+
WATSONX_MODELS_DETAILED,
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
MODELS_DETAILED = get_models_detailed()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@lru_cache(maxsize=1)
|
|
105
|
+
def get_model_provider_variable_mapping() -> dict[str, str]:
|
|
106
|
+
return {provider: meta["variable_name"] for provider, meta in model_provider_metadata.items()}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def get_model_providers() -> list[str]:
|
|
110
|
+
"""Return a sorted list of unique provider names."""
|
|
111
|
+
return sorted({md.get("provider", "Unknown") for group in MODELS_DETAILED for md in group})
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_unified_models_detailed(
|
|
115
|
+
providers: list[str] | None = None,
|
|
116
|
+
model_name: str | None = None,
|
|
117
|
+
model_type: str | None = None,
|
|
118
|
+
*,
|
|
119
|
+
include_unsupported: bool | None = None,
|
|
120
|
+
include_deprecated: bool | None = None,
|
|
121
|
+
only_defaults: bool = False,
|
|
122
|
+
**metadata_filters,
|
|
123
|
+
):
|
|
124
|
+
"""Return a list of providers and their models, optionally filtered.
|
|
125
|
+
|
|
126
|
+
Parameters
|
|
127
|
+
----------
|
|
128
|
+
providers : list[str] | None
|
|
129
|
+
If given, only models from these providers are returned.
|
|
130
|
+
model_name : str | None
|
|
131
|
+
If given, only the model with this exact name is returned.
|
|
132
|
+
model_type : str | None
|
|
133
|
+
Optional. Restrict to models whose metadata "model_type" matches this value.
|
|
134
|
+
include_unsupported : bool
|
|
135
|
+
When False (default) models whose metadata contains ``not_supported=True``
|
|
136
|
+
are filtered out.
|
|
137
|
+
include_deprecated : bool
|
|
138
|
+
When False (default) models whose metadata contains ``deprecated=True``
|
|
139
|
+
are filtered out.
|
|
140
|
+
only_defaults : bool
|
|
141
|
+
When True, only models marked as default are returned.
|
|
142
|
+
The first 5 models from each provider (in list order) are automatically
|
|
143
|
+
marked as default. Defaults to False to maintain backward compatibility.
|
|
144
|
+
**metadata_filters
|
|
145
|
+
Arbitrary key/value pairs to match against the model's metadata.
|
|
146
|
+
Example: ``get_unified_models_detailed(size="4k", context_window=8192)``
|
|
147
|
+
|
|
148
|
+
Notes:
|
|
149
|
+
• Filtering is exact-match on the metadata values.
|
|
150
|
+
• If you *do* want to see unsupported models set ``include_unsupported=True``.
|
|
151
|
+
• If you *do* want to see deprecated models set ``include_deprecated=True``.
|
|
152
|
+
"""
|
|
153
|
+
if include_unsupported is None:
|
|
154
|
+
include_unsupported = False
|
|
155
|
+
if include_deprecated is None:
|
|
156
|
+
include_deprecated = False
|
|
157
|
+
|
|
158
|
+
# Gather all models from imported *_MODELS_DETAILED lists
|
|
159
|
+
all_models: list[dict] = []
|
|
160
|
+
for models_detailed in MODELS_DETAILED:
|
|
161
|
+
all_models.extend(models_detailed)
|
|
162
|
+
|
|
163
|
+
# Apply filters
|
|
164
|
+
filtered_models: list[dict] = []
|
|
165
|
+
for md in all_models:
|
|
166
|
+
# Skip models flagged as not_supported unless explicitly included
|
|
167
|
+
if (not include_unsupported) and md.get("not_supported", False):
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
# Skip models flagged as deprecated unless explicitly included
|
|
171
|
+
if (not include_deprecated) and md.get("deprecated", False):
|
|
172
|
+
continue
|
|
173
|
+
|
|
174
|
+
if providers and md.get("provider") not in providers:
|
|
175
|
+
continue
|
|
176
|
+
if model_name and md.get("name") != model_name:
|
|
177
|
+
continue
|
|
178
|
+
if model_type and md.get("model_type") != model_type:
|
|
179
|
+
continue
|
|
180
|
+
# Match arbitrary metadata key/value pairs
|
|
181
|
+
if any(md.get(k) != v for k, v in metadata_filters.items()):
|
|
182
|
+
continue
|
|
183
|
+
|
|
184
|
+
filtered_models.append(md)
|
|
185
|
+
|
|
186
|
+
# Group by provider
|
|
187
|
+
provider_map: dict[str, list[dict]] = {}
|
|
188
|
+
for metadata in filtered_models:
|
|
189
|
+
prov = metadata.get("provider", "Unknown")
|
|
190
|
+
provider_map.setdefault(prov, []).append(
|
|
191
|
+
{
|
|
192
|
+
"model_name": metadata.get("name"),
|
|
193
|
+
"metadata": {k: v for k, v in metadata.items() if k not in ("provider", "name")},
|
|
194
|
+
}
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Mark the first 5 models in each provider as default (based on list order)
|
|
198
|
+
# and optionally filter to only defaults
|
|
199
|
+
default_model_count = 5 # Number of default models per provider
|
|
200
|
+
|
|
201
|
+
for prov, models in provider_map.items():
|
|
202
|
+
for i, model in enumerate(models):
|
|
203
|
+
if i < default_model_count:
|
|
204
|
+
model["metadata"]["default"] = True
|
|
205
|
+
else:
|
|
206
|
+
model["metadata"]["default"] = False
|
|
207
|
+
|
|
208
|
+
# If only_defaults is True, filter to only default models
|
|
209
|
+
if only_defaults:
|
|
210
|
+
provider_map[prov] = [m for m in models if m["metadata"].get("default", False)]
|
|
211
|
+
|
|
212
|
+
# Format as requested
|
|
213
|
+
return [
|
|
214
|
+
{
|
|
215
|
+
"provider": prov,
|
|
216
|
+
"models": models,
|
|
217
|
+
"num_models": len(models),
|
|
218
|
+
**model_provider_metadata.get(prov, {}),
|
|
219
|
+
}
|
|
220
|
+
for prov, models in provider_map.items()
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def get_api_key_for_provider(user_id: UUID | str | None, provider: str, api_key: str | None = None) -> str | None:
|
|
225
|
+
"""Get API key from self.api_key or global variables.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
user_id: The user ID to look up global variables for
|
|
229
|
+
provider: The provider name (e.g., "OpenAI", "Anthropic")
|
|
230
|
+
api_key: An optional API key provided directly
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
The API key if found, None otherwise
|
|
234
|
+
"""
|
|
235
|
+
# First check if user provided an API key directly
|
|
236
|
+
if api_key:
|
|
237
|
+
return api_key
|
|
238
|
+
|
|
239
|
+
# If no user_id or user_id is the string "None", we can't look up global variables
|
|
240
|
+
if user_id is None or (isinstance(user_id, str) and user_id == "None"):
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
# Map provider to global variable name
|
|
244
|
+
provider_variable_map = {
|
|
245
|
+
"OpenAI": "OPENAI_API_KEY",
|
|
246
|
+
"Anthropic": "ANTHROPIC_API_KEY",
|
|
247
|
+
"Google Generative AI": "GOOGLE_API_KEY",
|
|
248
|
+
"IBM WatsonX": "WATSONX_APIKEY",
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
variable_name = provider_variable_map.get(provider)
|
|
252
|
+
if not variable_name:
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
# Try to get from global variables
|
|
256
|
+
async def _get_variable():
|
|
257
|
+
async with session_scope() as session:
|
|
258
|
+
variable_service = get_variable_service()
|
|
259
|
+
if variable_service is None:
|
|
260
|
+
return None
|
|
261
|
+
return await variable_service.get_variable(
|
|
262
|
+
user_id=UUID(user_id) if isinstance(user_id, str) else user_id,
|
|
263
|
+
name=variable_name,
|
|
264
|
+
field="",
|
|
265
|
+
session=session,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
return run_until_complete(_get_variable())
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def validate_model_provider_key(variable_name: str, api_key: str) -> None:
|
|
272
|
+
"""Validate a model provider API key by making a minimal test call.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
variable_name: The variable name (e.g., OPENAI_API_KEY)
|
|
276
|
+
api_key: The API key to validate
|
|
277
|
+
|
|
278
|
+
Raises:
|
|
279
|
+
HTTPException: If the API key is invalid
|
|
280
|
+
"""
|
|
281
|
+
# Map variable names to providers
|
|
282
|
+
provider_map = {
|
|
283
|
+
"OPENAI_API_KEY": "OpenAI",
|
|
284
|
+
"ANTHROPIC_API_KEY": "Anthropic",
|
|
285
|
+
"GOOGLE_API_KEY": "Google Generative AI",
|
|
286
|
+
"WATSONX_APIKEY": "IBM WatsonX",
|
|
287
|
+
"OLLAMA_BASE_URL": "Ollama",
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
provider = provider_map.get(variable_name)
|
|
291
|
+
if not provider:
|
|
292
|
+
return # Not a model provider key we validate
|
|
293
|
+
|
|
294
|
+
# Get the first available model for this provider
|
|
295
|
+
try:
|
|
296
|
+
models = get_unified_models_detailed(providers=[provider])
|
|
297
|
+
if not models or not models[0].get("models"):
|
|
298
|
+
return # No models available, skip validation
|
|
299
|
+
|
|
300
|
+
first_model = models[0]["models"][0]["model_name"]
|
|
301
|
+
except Exception: # noqa: BLE001
|
|
302
|
+
return # Can't get models, skip validation
|
|
303
|
+
|
|
304
|
+
# Test the API key based on provider
|
|
305
|
+
try:
|
|
306
|
+
if provider == "OpenAI":
|
|
307
|
+
from langchain_openai import ChatOpenAI
|
|
308
|
+
|
|
309
|
+
llm = ChatOpenAI(api_key=api_key, model_name=first_model, max_tokens=1)
|
|
310
|
+
llm.invoke("test")
|
|
311
|
+
elif provider == "Anthropic":
|
|
312
|
+
from langchain_anthropic import ChatAnthropic
|
|
313
|
+
|
|
314
|
+
llm = ChatAnthropic(anthropic_api_key=api_key, model=first_model, max_tokens=1)
|
|
315
|
+
llm.invoke("test")
|
|
316
|
+
elif provider == "Google Generative AI":
|
|
317
|
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
|
318
|
+
|
|
319
|
+
llm = ChatGoogleGenerativeAI(google_api_key=api_key, model=first_model, max_tokens=1)
|
|
320
|
+
llm.invoke("test")
|
|
321
|
+
elif provider == "IBM WatsonX":
|
|
322
|
+
# WatsonX validation would require additional parameters
|
|
323
|
+
# Skip for now as it needs project_id, url, etc.
|
|
324
|
+
return
|
|
325
|
+
elif provider == "Ollama":
|
|
326
|
+
# Ollama is local, just verify the URL is accessible
|
|
327
|
+
import requests
|
|
328
|
+
|
|
329
|
+
response = requests.get(f"{api_key}/api/tags", timeout=5)
|
|
330
|
+
if response.status_code != requests.codes.ok:
|
|
331
|
+
msg = "Invalid Ollama base URL"
|
|
332
|
+
raise ValueError(msg)
|
|
333
|
+
except ValueError:
|
|
334
|
+
# Re-raise ValueError (validation failed)
|
|
335
|
+
raise
|
|
336
|
+
except Exception as e:
|
|
337
|
+
error_msg = str(e)
|
|
338
|
+
if "401" in error_msg or "authentication" in error_msg.lower() or "api key" in error_msg.lower():
|
|
339
|
+
msg = f"Invalid API key for {provider}"
|
|
340
|
+
raise ValueError(msg) from e
|
|
341
|
+
# For other errors, we'll allow the key to be saved (might be network issues, etc.)
|
|
342
|
+
return
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def get_language_model_options(
|
|
346
|
+
user_id: UUID | str | None = None, *, tool_calling: bool | None = None
|
|
347
|
+
) -> list[dict[str, Any]]:
|
|
348
|
+
"""Return a list of available language model providers with their configuration.
|
|
349
|
+
|
|
350
|
+
This function uses get_unified_models_detailed() which respects the enabled/disabled
|
|
351
|
+
status from the settings page and automatically filters out deprecated/unsupported models.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
user_id: Optional user ID to filter by user-specific enabled/disabled models
|
|
355
|
+
tool_calling: If True, only return models that support tool calling.
|
|
356
|
+
If False, only return models that don't support tool calling.
|
|
357
|
+
If None (default), return all models regardless of tool calling support.
|
|
358
|
+
"""
|
|
359
|
+
# Get all LLM models (excluding embeddings, deprecated, and unsupported by default)
|
|
360
|
+
# Apply tool_calling filter if specified
|
|
361
|
+
if tool_calling is not None:
|
|
362
|
+
all_models = get_unified_models_detailed(
|
|
363
|
+
model_type="llm",
|
|
364
|
+
include_deprecated=False,
|
|
365
|
+
include_unsupported=False,
|
|
366
|
+
tool_calling=tool_calling,
|
|
367
|
+
)
|
|
368
|
+
else:
|
|
369
|
+
all_models = get_unified_models_detailed(
|
|
370
|
+
model_type="llm",
|
|
371
|
+
include_deprecated=False,
|
|
372
|
+
include_unsupported=False,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
# Get disabled and explicitly enabled models for this user if user_id is provided
|
|
376
|
+
disabled_models = set()
|
|
377
|
+
explicitly_enabled_models = set()
|
|
378
|
+
if user_id:
|
|
379
|
+
try:
|
|
380
|
+
|
|
381
|
+
async def _get_model_status():
|
|
382
|
+
async with session_scope() as session:
|
|
383
|
+
variable_service = get_variable_service()
|
|
384
|
+
if variable_service is None:
|
|
385
|
+
return set(), set()
|
|
386
|
+
from langflow.services.variable.service import DatabaseVariableService
|
|
387
|
+
|
|
388
|
+
if not isinstance(variable_service, DatabaseVariableService):
|
|
389
|
+
return set(), set()
|
|
390
|
+
all_vars = await variable_service.get_all(
|
|
391
|
+
user_id=UUID(user_id) if isinstance(user_id, str) else user_id,
|
|
392
|
+
session=session,
|
|
393
|
+
)
|
|
394
|
+
disabled = set()
|
|
395
|
+
enabled = set()
|
|
396
|
+
import json
|
|
397
|
+
|
|
398
|
+
for var in all_vars:
|
|
399
|
+
if var.name == "__disabled_models__" and var.value:
|
|
400
|
+
with contextlib.suppress(json.JSONDecodeError, TypeError):
|
|
401
|
+
disabled = set(json.loads(var.value))
|
|
402
|
+
elif var.name == "__enabled_models__" and var.value:
|
|
403
|
+
with contextlib.suppress(json.JSONDecodeError, TypeError):
|
|
404
|
+
enabled = set(json.loads(var.value))
|
|
405
|
+
return disabled, enabled
|
|
406
|
+
|
|
407
|
+
disabled_models, explicitly_enabled_models = run_until_complete(_get_model_status())
|
|
408
|
+
except Exception: # noqa: BLE001, S110
|
|
409
|
+
# If we can't get model status, continue without filtering
|
|
410
|
+
pass
|
|
411
|
+
|
|
412
|
+
# Get enabled providers (those with credentials configured)
|
|
413
|
+
enabled_providers = set()
|
|
414
|
+
if user_id:
|
|
415
|
+
try:
|
|
416
|
+
|
|
417
|
+
async def _get_enabled_providers():
|
|
418
|
+
async with session_scope() as session:
|
|
419
|
+
variable_service = get_variable_service()
|
|
420
|
+
if variable_service is None:
|
|
421
|
+
return set()
|
|
422
|
+
from langflow.services.variable.constants import CREDENTIAL_TYPE
|
|
423
|
+
from langflow.services.variable.service import DatabaseVariableService
|
|
424
|
+
|
|
425
|
+
if not isinstance(variable_service, DatabaseVariableService):
|
|
426
|
+
return set()
|
|
427
|
+
all_vars = await variable_service.get_all(
|
|
428
|
+
user_id=UUID(user_id) if isinstance(user_id, str) else user_id,
|
|
429
|
+
session=session,
|
|
430
|
+
)
|
|
431
|
+
credential_names = {var.name for var in all_vars if var.type == CREDENTIAL_TYPE}
|
|
432
|
+
provider_variable_map = get_model_provider_variable_mapping()
|
|
433
|
+
return {
|
|
434
|
+
provider for provider, var_name in provider_variable_map.items() if var_name in credential_names
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
enabled_providers = run_until_complete(_get_enabled_providers())
|
|
438
|
+
except Exception: # noqa: BLE001, S110
|
|
439
|
+
# If we can't get enabled providers, show all
|
|
440
|
+
pass
|
|
441
|
+
|
|
442
|
+
options = []
|
|
443
|
+
model_class_mapping = {
|
|
444
|
+
"OpenAI": "ChatOpenAI",
|
|
445
|
+
"Anthropic": "ChatAnthropic",
|
|
446
|
+
"Google Generative AI": "ChatGoogleGenerativeAIFixed",
|
|
447
|
+
"Ollama": "ChatOllama",
|
|
448
|
+
"IBM WatsonX": "ChatWatsonx",
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
api_key_param_mapping = {
|
|
452
|
+
"OpenAI": "api_key",
|
|
453
|
+
"Anthropic": "api_key",
|
|
454
|
+
"Google Generative AI": "google_api_key",
|
|
455
|
+
"Ollama": "base_url",
|
|
456
|
+
"IBM WatsonX": "apikey",
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
# Track which providers have models
|
|
460
|
+
providers_with_models = set()
|
|
461
|
+
|
|
462
|
+
for provider_data in all_models:
|
|
463
|
+
provider = provider_data.get("provider")
|
|
464
|
+
models = provider_data.get("models", [])
|
|
465
|
+
icon = provider_data.get("icon", "Bot")
|
|
466
|
+
|
|
467
|
+
# Check if provider is enabled
|
|
468
|
+
is_provider_enabled = not user_id or not enabled_providers or provider in enabled_providers
|
|
469
|
+
|
|
470
|
+
# Track this provider
|
|
471
|
+
if is_provider_enabled:
|
|
472
|
+
providers_with_models.add(provider)
|
|
473
|
+
|
|
474
|
+
# Skip provider if user_id is provided and provider is not enabled
|
|
475
|
+
if user_id and enabled_providers and provider not in enabled_providers:
|
|
476
|
+
continue
|
|
477
|
+
|
|
478
|
+
for model_data in models:
|
|
479
|
+
model_name = model_data.get("model_name")
|
|
480
|
+
metadata = model_data.get("metadata", {})
|
|
481
|
+
is_default = metadata.get("default", False)
|
|
482
|
+
|
|
483
|
+
# Determine if model should be shown:
|
|
484
|
+
# - If not default and not explicitly enabled, skip it
|
|
485
|
+
# - If in disabled list, skip it
|
|
486
|
+
# - Otherwise, show it
|
|
487
|
+
if not is_default and model_name not in explicitly_enabled_models:
|
|
488
|
+
continue
|
|
489
|
+
if model_name in disabled_models:
|
|
490
|
+
continue
|
|
491
|
+
|
|
492
|
+
# Build the option dict
|
|
493
|
+
option = {
|
|
494
|
+
"name": model_name,
|
|
495
|
+
"icon": icon,
|
|
496
|
+
"category": provider,
|
|
497
|
+
"provider": provider,
|
|
498
|
+
"metadata": {
|
|
499
|
+
"context_length": 128000, # Default, can be overridden
|
|
500
|
+
"model_class": model_class_mapping.get(provider, "ChatOpenAI"),
|
|
501
|
+
"model_name_param": "model",
|
|
502
|
+
"api_key_param": api_key_param_mapping.get(provider, "api_key"),
|
|
503
|
+
},
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
# Add reasoning models list for OpenAI
|
|
507
|
+
if provider == "OpenAI" and metadata.get("reasoning"):
|
|
508
|
+
if "reasoning_models" not in option["metadata"]:
|
|
509
|
+
option["metadata"]["reasoning_models"] = []
|
|
510
|
+
option["metadata"]["reasoning_models"].append(model_name)
|
|
511
|
+
|
|
512
|
+
# Add base_url_param for Ollama
|
|
513
|
+
if provider == "Ollama":
|
|
514
|
+
option["metadata"]["base_url_param"] = "base_url"
|
|
515
|
+
|
|
516
|
+
# Add extra params for WatsonX
|
|
517
|
+
if provider == "IBM WatsonX":
|
|
518
|
+
option["metadata"]["model_name_param"] = "model_id"
|
|
519
|
+
option["metadata"]["url_param"] = "url"
|
|
520
|
+
option["metadata"]["project_id_param"] = "project_id"
|
|
521
|
+
|
|
522
|
+
options.append(option)
|
|
523
|
+
|
|
524
|
+
# Add disabled providers (providers that exist in metadata but have no enabled models)
|
|
525
|
+
if user_id:
|
|
526
|
+
for provider, metadata in model_provider_metadata.items():
|
|
527
|
+
if provider not in providers_with_models:
|
|
528
|
+
# This provider has no enabled models, add it as a disabled provider entry
|
|
529
|
+
options.append(
|
|
530
|
+
{
|
|
531
|
+
"name": f"__enable_provider_{provider}__",
|
|
532
|
+
"icon": metadata.get("icon", "Bot"),
|
|
533
|
+
"category": provider,
|
|
534
|
+
"provider": provider,
|
|
535
|
+
"metadata": {
|
|
536
|
+
"is_disabled_provider": True,
|
|
537
|
+
"variable_name": metadata.get("variable_name"),
|
|
538
|
+
},
|
|
539
|
+
}
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
return options
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def get_embedding_model_options(user_id: UUID | str | None = None) -> list[dict[str, Any]]:
|
|
546
|
+
"""Return a list of available embedding model providers with their configuration.
|
|
547
|
+
|
|
548
|
+
This function uses get_unified_models_detailed() which respects the enabled/disabled
|
|
549
|
+
status from the settings page and automatically filters out deprecated/unsupported models.
|
|
550
|
+
|
|
551
|
+
Args:
|
|
552
|
+
user_id: Optional user ID to filter by user-specific enabled/disabled models
|
|
553
|
+
"""
|
|
554
|
+
# Get all embedding models (excluding deprecated and unsupported by default)
|
|
555
|
+
all_models = get_unified_models_detailed(
|
|
556
|
+
model_type="embeddings",
|
|
557
|
+
include_deprecated=False,
|
|
558
|
+
include_unsupported=False,
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
# Get disabled and explicitly enabled models for this user if user_id is provided
|
|
562
|
+
disabled_models = set()
|
|
563
|
+
explicitly_enabled_models = set()
|
|
564
|
+
if user_id:
|
|
565
|
+
try:
|
|
566
|
+
|
|
567
|
+
async def _get_model_status():
|
|
568
|
+
async with session_scope() as session:
|
|
569
|
+
variable_service = get_variable_service()
|
|
570
|
+
if variable_service is None:
|
|
571
|
+
return set(), set()
|
|
572
|
+
from langflow.services.variable.service import DatabaseVariableService
|
|
573
|
+
|
|
574
|
+
if not isinstance(variable_service, DatabaseVariableService):
|
|
575
|
+
return set(), set()
|
|
576
|
+
all_vars = await variable_service.get_all(
|
|
577
|
+
user_id=UUID(user_id) if isinstance(user_id, str) else user_id,
|
|
578
|
+
session=session,
|
|
579
|
+
)
|
|
580
|
+
disabled = set()
|
|
581
|
+
enabled = set()
|
|
582
|
+
import json
|
|
583
|
+
|
|
584
|
+
for var in all_vars:
|
|
585
|
+
if var.name == "__disabled_models__" and var.value:
|
|
586
|
+
with contextlib.suppress(json.JSONDecodeError, TypeError):
|
|
587
|
+
disabled = set(json.loads(var.value))
|
|
588
|
+
elif var.name == "__enabled_models__" and var.value:
|
|
589
|
+
with contextlib.suppress(json.JSONDecodeError, TypeError):
|
|
590
|
+
enabled = set(json.loads(var.value))
|
|
591
|
+
return disabled, enabled
|
|
592
|
+
|
|
593
|
+
disabled_models, explicitly_enabled_models = run_until_complete(_get_model_status())
|
|
594
|
+
except Exception: # noqa: BLE001, S110
|
|
595
|
+
# If we can't get model status, continue without filtering
|
|
596
|
+
pass
|
|
597
|
+
|
|
598
|
+
# Get enabled providers (those with credentials configured)
|
|
599
|
+
enabled_providers = set()
|
|
600
|
+
if user_id:
|
|
601
|
+
try:
|
|
602
|
+
|
|
603
|
+
async def _get_enabled_providers():
|
|
604
|
+
async with session_scope() as session:
|
|
605
|
+
variable_service = get_variable_service()
|
|
606
|
+
if variable_service is None:
|
|
607
|
+
return set()
|
|
608
|
+
from langflow.services.variable.constants import CREDENTIAL_TYPE
|
|
609
|
+
from langflow.services.variable.service import DatabaseVariableService
|
|
610
|
+
|
|
611
|
+
if not isinstance(variable_service, DatabaseVariableService):
|
|
612
|
+
return set()
|
|
613
|
+
all_vars = await variable_service.get_all(
|
|
614
|
+
user_id=UUID(user_id) if isinstance(user_id, str) else user_id,
|
|
615
|
+
session=session,
|
|
616
|
+
)
|
|
617
|
+
credential_names = {var.name for var in all_vars if var.type == CREDENTIAL_TYPE}
|
|
618
|
+
provider_variable_map = get_model_provider_variable_mapping()
|
|
619
|
+
return {
|
|
620
|
+
provider for provider, var_name in provider_variable_map.items() if var_name in credential_names
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
enabled_providers = run_until_complete(_get_enabled_providers())
|
|
624
|
+
except Exception: # noqa: BLE001, S110
|
|
625
|
+
# If we can't get enabled providers, show all
|
|
626
|
+
pass
|
|
627
|
+
|
|
628
|
+
options = []
|
|
629
|
+
embedding_class_mapping = {
|
|
630
|
+
"OpenAI": "OpenAIEmbeddings",
|
|
631
|
+
"Google Generative AI": "GoogleGenerativeAIEmbeddings",
|
|
632
|
+
"Ollama": "OllamaEmbeddings",
|
|
633
|
+
"IBM WatsonX": "WatsonxEmbeddings",
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
# Provider-specific param mappings
|
|
637
|
+
param_mappings = {
|
|
638
|
+
"OpenAI": {
|
|
639
|
+
"model": "model",
|
|
640
|
+
"api_key": "api_key",
|
|
641
|
+
"api_base": "base_url",
|
|
642
|
+
"dimensions": "dimensions",
|
|
643
|
+
"chunk_size": "chunk_size",
|
|
644
|
+
"request_timeout": "timeout",
|
|
645
|
+
"max_retries": "max_retries",
|
|
646
|
+
"show_progress_bar": "show_progress_bar",
|
|
647
|
+
"model_kwargs": "model_kwargs",
|
|
648
|
+
},
|
|
649
|
+
"Google Generative AI": {
|
|
650
|
+
"model": "model",
|
|
651
|
+
"api_key": "google_api_key",
|
|
652
|
+
"request_timeout": "request_options",
|
|
653
|
+
"model_kwargs": "client_options",
|
|
654
|
+
},
|
|
655
|
+
"Ollama": {
|
|
656
|
+
"model": "model",
|
|
657
|
+
"base_url": "base_url",
|
|
658
|
+
"num_ctx": "num_ctx",
|
|
659
|
+
"request_timeout": "request_timeout",
|
|
660
|
+
"model_kwargs": "model_kwargs",
|
|
661
|
+
},
|
|
662
|
+
"IBM WatsonX": {
|
|
663
|
+
"model_id": "model_id",
|
|
664
|
+
"url": "url",
|
|
665
|
+
"api_key": "apikey",
|
|
666
|
+
"project_id": "project_id",
|
|
667
|
+
"space_id": "space_id",
|
|
668
|
+
"request_timeout": "request_timeout",
|
|
669
|
+
},
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
# Track which providers have models
|
|
673
|
+
providers_with_models = set()
|
|
674
|
+
|
|
675
|
+
for provider_data in all_models:
|
|
676
|
+
provider = provider_data.get("provider")
|
|
677
|
+
models = provider_data.get("models", [])
|
|
678
|
+
icon = provider_data.get("icon", "Bot")
|
|
679
|
+
|
|
680
|
+
# Check if provider is enabled
|
|
681
|
+
is_provider_enabled = not user_id or not enabled_providers or provider in enabled_providers
|
|
682
|
+
|
|
683
|
+
# Track this provider
|
|
684
|
+
if is_provider_enabled:
|
|
685
|
+
providers_with_models.add(provider)
|
|
686
|
+
|
|
687
|
+
# Skip provider if user_id is provided and provider is not enabled
|
|
688
|
+
if user_id and enabled_providers and provider not in enabled_providers:
|
|
689
|
+
continue
|
|
690
|
+
|
|
691
|
+
for model_data in models:
|
|
692
|
+
model_name = model_data.get("model_name")
|
|
693
|
+
metadata = model_data.get("metadata", {})
|
|
694
|
+
is_default = metadata.get("default", False)
|
|
695
|
+
|
|
696
|
+
# Determine if model should be shown:
|
|
697
|
+
# - If not default and not explicitly enabled, skip it
|
|
698
|
+
# - If in disabled list, skip it
|
|
699
|
+
# - Otherwise, show it
|
|
700
|
+
if not is_default and model_name not in explicitly_enabled_models:
|
|
701
|
+
continue
|
|
702
|
+
if model_name in disabled_models:
|
|
703
|
+
continue
|
|
704
|
+
|
|
705
|
+
# Build the option dict
|
|
706
|
+
option = {
|
|
707
|
+
"name": model_name,
|
|
708
|
+
"icon": icon,
|
|
709
|
+
"category": provider,
|
|
710
|
+
"provider": provider,
|
|
711
|
+
"metadata": {
|
|
712
|
+
"embedding_class": embedding_class_mapping.get(provider, "OpenAIEmbeddings"),
|
|
713
|
+
"param_mapping": param_mappings.get(provider, param_mappings["OpenAI"]),
|
|
714
|
+
"model_type": "embeddings", # Mark as embedding model
|
|
715
|
+
},
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
options.append(option)
|
|
719
|
+
|
|
720
|
+
# Add disabled providers (providers that exist in metadata but have no enabled models)
|
|
721
|
+
if user_id:
|
|
722
|
+
for provider, metadata in model_provider_metadata.items():
|
|
723
|
+
if provider not in providers_with_models and provider in embedding_class_mapping:
|
|
724
|
+
# This provider has no enabled models and supports embeddings, add it as a disabled provider entry
|
|
725
|
+
options.append(
|
|
726
|
+
{
|
|
727
|
+
"name": f"__enable_provider_{provider}__",
|
|
728
|
+
"icon": metadata.get("icon", "Bot"),
|
|
729
|
+
"category": provider,
|
|
730
|
+
"provider": provider,
|
|
731
|
+
"metadata": {
|
|
732
|
+
"is_disabled_provider": True,
|
|
733
|
+
"variable_name": metadata.get("variable_name"),
|
|
734
|
+
},
|
|
735
|
+
}
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
return options
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def normalize_model_names_to_dicts(model_names: list[str] | str) -> list[dict[str, Any]]:
|
|
742
|
+
"""Convert simple model name(s) to list of dicts format.
|
|
743
|
+
|
|
744
|
+
Args:
|
|
745
|
+
model_names: A string or list of strings representing model names
|
|
746
|
+
|
|
747
|
+
Returns:
|
|
748
|
+
A list of dicts with full model metadata including runtime info
|
|
749
|
+
|
|
750
|
+
Examples:
|
|
751
|
+
>>> normalize_model_names_to_dicts('gpt-4o')
|
|
752
|
+
[{'name': 'gpt-4o', 'provider': 'OpenAI', 'metadata': {'model_class': 'ChatOpenAI', ...}}]
|
|
753
|
+
|
|
754
|
+
>>> normalize_model_names_to_dicts(['gpt-4o', 'claude-3'])
|
|
755
|
+
[{'name': 'gpt-4o', ...}, {'name': 'claude-3', ...}]
|
|
756
|
+
"""
|
|
757
|
+
# Convert single string to list
|
|
758
|
+
if isinstance(model_names, str):
|
|
759
|
+
model_names = [model_names]
|
|
760
|
+
|
|
761
|
+
# Get all available models to look up metadata
|
|
762
|
+
try:
|
|
763
|
+
all_models = get_unified_models_detailed()
|
|
764
|
+
except Exception: # noqa: BLE001
|
|
765
|
+
# If we can't get models, just create basic dicts
|
|
766
|
+
return [{"name": name} for name in model_names]
|
|
767
|
+
|
|
768
|
+
# Model class mapping for runtime metadata
|
|
769
|
+
model_class_mapping = {
|
|
770
|
+
"OpenAI": "ChatOpenAI",
|
|
771
|
+
"Anthropic": "ChatAnthropic",
|
|
772
|
+
"Google Generative AI": "ChatGoogleGenerativeAIFixed",
|
|
773
|
+
"Ollama": "ChatOllama",
|
|
774
|
+
"IBM WatsonX": "ChatWatsonx",
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
api_key_param_mapping = {
|
|
778
|
+
"OpenAI": "api_key",
|
|
779
|
+
"Anthropic": "api_key",
|
|
780
|
+
"Google Generative AI": "google_api_key",
|
|
781
|
+
"Ollama": "base_url",
|
|
782
|
+
"IBM WatsonX": "apikey",
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
# Build a lookup map of model_name -> full model data with runtime metadata
|
|
786
|
+
model_lookup = {}
|
|
787
|
+
for provider_data in all_models:
|
|
788
|
+
provider = provider_data.get("provider")
|
|
789
|
+
icon = provider_data.get("icon", "Bot")
|
|
790
|
+
for model_data in provider_data.get("models", []):
|
|
791
|
+
model_name = model_data.get("model_name")
|
|
792
|
+
base_metadata = model_data.get("metadata", {})
|
|
793
|
+
|
|
794
|
+
# Build runtime metadata similar to get_language_model_options
|
|
795
|
+
runtime_metadata = {
|
|
796
|
+
"context_length": 128000, # Default
|
|
797
|
+
"model_class": model_class_mapping.get(provider, "ChatOpenAI"),
|
|
798
|
+
"model_name_param": "model",
|
|
799
|
+
"api_key_param": api_key_param_mapping.get(provider, "api_key"),
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
# Add reasoning models list for OpenAI
|
|
803
|
+
if provider == "OpenAI" and base_metadata.get("reasoning"):
|
|
804
|
+
runtime_metadata["reasoning_models"] = [model_name]
|
|
805
|
+
|
|
806
|
+
# Add base_url_param for Ollama
|
|
807
|
+
if provider == "Ollama":
|
|
808
|
+
runtime_metadata["base_url_param"] = "base_url"
|
|
809
|
+
|
|
810
|
+
# Add extra params for WatsonX
|
|
811
|
+
if provider == "IBM WatsonX":
|
|
812
|
+
runtime_metadata["model_name_param"] = "model_id"
|
|
813
|
+
runtime_metadata["url_param"] = "url"
|
|
814
|
+
runtime_metadata["project_id_param"] = "project_id"
|
|
815
|
+
|
|
816
|
+
# Merge base metadata with runtime metadata
|
|
817
|
+
full_metadata = {**base_metadata, **runtime_metadata}
|
|
818
|
+
|
|
819
|
+
model_lookup[model_name] = {
|
|
820
|
+
"name": model_name,
|
|
821
|
+
"icon": icon,
|
|
822
|
+
"category": provider,
|
|
823
|
+
"provider": provider,
|
|
824
|
+
"metadata": full_metadata,
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
# Convert string list to dict list
|
|
828
|
+
result = []
|
|
829
|
+
for name in model_names:
|
|
830
|
+
if name in model_lookup:
|
|
831
|
+
result.append(model_lookup[name])
|
|
832
|
+
else:
|
|
833
|
+
# Model not found in registry, create basic entry with minimal required metadata
|
|
834
|
+
result.append(
|
|
835
|
+
{
|
|
836
|
+
"name": name,
|
|
837
|
+
"provider": "Unknown",
|
|
838
|
+
"metadata": {
|
|
839
|
+
"model_class": "ChatOpenAI", # Default fallback
|
|
840
|
+
"model_name_param": "model",
|
|
841
|
+
"api_key_param": "api_key",
|
|
842
|
+
},
|
|
843
|
+
}
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
return result
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def get_llm(
|
|
850
|
+
model,
|
|
851
|
+
user_id: UUID | str | None,
|
|
852
|
+
api_key=None,
|
|
853
|
+
temperature=None,
|
|
854
|
+
*,
|
|
855
|
+
stream=False,
|
|
856
|
+
watsonx_url=None,
|
|
857
|
+
watsonx_project_id=None,
|
|
858
|
+
ollama_base_url=None,
|
|
859
|
+
) -> Any:
|
|
860
|
+
# Check if model is already a BaseLanguageModel instance (from a connection)
|
|
861
|
+
try:
|
|
862
|
+
from langchain_core.language_models import BaseLanguageModel
|
|
863
|
+
|
|
864
|
+
if isinstance(model, BaseLanguageModel):
|
|
865
|
+
# Model is already instantiated, return it directly
|
|
866
|
+
return model
|
|
867
|
+
except ImportError:
|
|
868
|
+
pass
|
|
869
|
+
|
|
870
|
+
# Safely extract model configuration
|
|
871
|
+
if not model or not isinstance(model, list) or len(model) == 0:
|
|
872
|
+
msg = "A model selection is required"
|
|
873
|
+
raise ValueError(msg)
|
|
874
|
+
|
|
875
|
+
# Extract the first model (only one expected)
|
|
876
|
+
model = model[0]
|
|
877
|
+
|
|
878
|
+
# Extract model configuration from metadata
|
|
879
|
+
model_name = model.get("name")
|
|
880
|
+
provider = model.get("provider")
|
|
881
|
+
metadata = model.get("metadata", {})
|
|
882
|
+
|
|
883
|
+
# Get model class and parameter names from metadata
|
|
884
|
+
api_key_param = metadata.get("api_key_param", "api_key")
|
|
885
|
+
|
|
886
|
+
# Get API key from user input or global variables
|
|
887
|
+
api_key = get_api_key_for_provider(user_id, provider, api_key)
|
|
888
|
+
|
|
889
|
+
# Validate API key (Ollama doesn't require one)
|
|
890
|
+
if not api_key and provider != "Ollama":
|
|
891
|
+
# Get the correct variable name from the provider variable mapping
|
|
892
|
+
provider_variable_map = get_model_provider_variable_mapping()
|
|
893
|
+
variable_name = provider_variable_map.get(provider, f"{provider.upper().replace(' ', '_')}_API_KEY")
|
|
894
|
+
msg = (
|
|
895
|
+
f"{provider} API key is required when using {provider} provider. "
|
|
896
|
+
f"Please provide it in the component or configure it globally as {variable_name}."
|
|
897
|
+
)
|
|
898
|
+
raise ValueError(msg)
|
|
899
|
+
|
|
900
|
+
# Get model class from metadata
|
|
901
|
+
model_class = get_model_classes().get(metadata.get("model_class"))
|
|
902
|
+
if model_class is None:
|
|
903
|
+
msg = f"No model class defined for {model_name}"
|
|
904
|
+
raise ValueError(msg)
|
|
905
|
+
model_name_param = metadata.get("model_name_param", "model")
|
|
906
|
+
|
|
907
|
+
# Check if this is a reasoning model that doesn't support temperature
|
|
908
|
+
reasoning_models = metadata.get("reasoning_models", [])
|
|
909
|
+
if model_name in reasoning_models:
|
|
910
|
+
temperature = None
|
|
911
|
+
|
|
912
|
+
# Build kwargs dynamically
|
|
913
|
+
kwargs = {
|
|
914
|
+
model_name_param: model_name,
|
|
915
|
+
"streaming": stream,
|
|
916
|
+
api_key_param: api_key,
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
if temperature is not None:
|
|
920
|
+
kwargs["temperature"] = temperature
|
|
921
|
+
|
|
922
|
+
# Add provider-specific parameters
|
|
923
|
+
if provider == "IBM WatsonX":
|
|
924
|
+
# For watsonx, url and project_id are required parameters
|
|
925
|
+
# Only add them if both are provided by the component
|
|
926
|
+
# If neither are provided, let ChatWatsonx handle it with its native error
|
|
927
|
+
# This allows components without WatsonX-specific fields to fail gracefully
|
|
928
|
+
|
|
929
|
+
url_param = metadata.get("url_param", "url")
|
|
930
|
+
project_id_param = metadata.get("project_id_param", "project_id")
|
|
931
|
+
|
|
932
|
+
has_url = watsonx_url is not None
|
|
933
|
+
has_project_id = watsonx_project_id is not None
|
|
934
|
+
|
|
935
|
+
if has_url and has_project_id:
|
|
936
|
+
# Both provided - add them to kwargs
|
|
937
|
+
kwargs[url_param] = watsonx_url
|
|
938
|
+
kwargs[project_id_param] = watsonx_project_id
|
|
939
|
+
elif has_url or has_project_id:
|
|
940
|
+
# Only one provided - this is a misconfiguration in the component
|
|
941
|
+
missing = "project ID" if has_url else "URL"
|
|
942
|
+
provided = "URL" if has_url else "project ID"
|
|
943
|
+
msg = (
|
|
944
|
+
f"IBM WatsonX requires both a URL and project ID. "
|
|
945
|
+
f"You provided a watsonx {provided} but no {missing}. "
|
|
946
|
+
f"Please add a 'watsonx {missing.title()}' field to your component or use the Language Model component "
|
|
947
|
+
f"which fully supports IBM WatsonX configuration."
|
|
948
|
+
)
|
|
949
|
+
raise ValueError(msg)
|
|
950
|
+
# else: neither provided - let ChatWatsonx handle it (will fail with its own error)
|
|
951
|
+
elif provider == "Ollama" and ollama_base_url:
|
|
952
|
+
# For Ollama, handle custom base_url
|
|
953
|
+
base_url_param = metadata.get("base_url_param", "base_url")
|
|
954
|
+
kwargs[base_url_param] = ollama_base_url
|
|
955
|
+
|
|
956
|
+
try:
|
|
957
|
+
return model_class(**kwargs)
|
|
958
|
+
except Exception as e:
|
|
959
|
+
# If instantiation fails and it's WatsonX, provide additional context
|
|
960
|
+
if provider == "IBM WatsonX" and ("url" in str(e).lower() or "project" in str(e).lower()):
|
|
961
|
+
msg = (
|
|
962
|
+
f"Failed to initialize IBM WatsonX model: {e}\n\n"
|
|
963
|
+
"IBM WatsonX requires additional configuration parameters (API endpoint URL and project ID). "
|
|
964
|
+
"This component may not support these parameters. "
|
|
965
|
+
"Consider using the 'Language Model' component instead, which fully supports IBM WatsonX."
|
|
966
|
+
)
|
|
967
|
+
raise ValueError(msg) from e
|
|
968
|
+
# Re-raise the original exception for other cases
|
|
969
|
+
raise
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
def update_model_options_in_build_config(
|
|
973
|
+
component: Any,
|
|
974
|
+
build_config: dict,
|
|
975
|
+
cache_key_prefix: str,
|
|
976
|
+
get_options_func: Callable,
|
|
977
|
+
field_name: str | None = None,
|
|
978
|
+
field_value: Any = None,
|
|
979
|
+
) -> dict:
|
|
980
|
+
"""Helper function to update build config with cached model options.
|
|
981
|
+
|
|
982
|
+
Uses instance-level caching to avoid expensive database calls on every field change.
|
|
983
|
+
Cache is refreshed when:
|
|
984
|
+
- api_key changes (may enable/disable providers)
|
|
985
|
+
- Initial load (field_name is None)
|
|
986
|
+
- Cache is empty or expired
|
|
987
|
+
- Model field is being refreshed (field_name == "model")
|
|
988
|
+
|
|
989
|
+
Args:
|
|
990
|
+
component: Component instance with cache, user_id, and log attributes
|
|
991
|
+
build_config: The build configuration dict to update
|
|
992
|
+
cache_key_prefix: Prefix for the cache key (e.g., "language_model_options" or "embedding_model_options")
|
|
993
|
+
get_options_func: Function to call to get model options (e.g., get_language_model_options)
|
|
994
|
+
field_name: The name of the field being changed, if any
|
|
995
|
+
field_value: The current value of the field being changed, if any
|
|
996
|
+
|
|
997
|
+
Returns:
|
|
998
|
+
Updated build_config dict with model options and providers set
|
|
999
|
+
"""
|
|
1000
|
+
import time
|
|
1001
|
+
|
|
1002
|
+
# Cache key based on user_id
|
|
1003
|
+
cache_key = f"{cache_key_prefix}_{component.user_id}"
|
|
1004
|
+
cache_timestamp_key = f"{cache_key}_timestamp"
|
|
1005
|
+
cache_ttl = 30 # 30 seconds TTL to catch global variable changes faster
|
|
1006
|
+
|
|
1007
|
+
# Check if cache is expired
|
|
1008
|
+
cache_expired = False
|
|
1009
|
+
if cache_timestamp_key in component.cache:
|
|
1010
|
+
time_since_cache = time.time() - component.cache[cache_timestamp_key]
|
|
1011
|
+
cache_expired = time_since_cache > cache_ttl
|
|
1012
|
+
|
|
1013
|
+
# Check if we need to refresh
|
|
1014
|
+
should_refresh = (
|
|
1015
|
+
field_name == "api_key" # API key changed
|
|
1016
|
+
or field_name is None # Initial load
|
|
1017
|
+
or field_name == "model" # Model field refresh button clicked
|
|
1018
|
+
or cache_key not in component.cache # Cache miss
|
|
1019
|
+
or cache_expired # Cache expired
|
|
1020
|
+
)
|
|
1021
|
+
|
|
1022
|
+
if should_refresh:
|
|
1023
|
+
# Fetch options based on user's enabled models
|
|
1024
|
+
try:
|
|
1025
|
+
options = get_options_func(user_id=component.user_id)
|
|
1026
|
+
# Cache the results with timestamp
|
|
1027
|
+
component.cache[cache_key] = {"options": options}
|
|
1028
|
+
component.cache[cache_timestamp_key] = time.time()
|
|
1029
|
+
except KeyError as exc:
|
|
1030
|
+
# If we can't get user-specific options, fall back to empty
|
|
1031
|
+
component.log("Failed to fetch user-specific model options: %s", exc)
|
|
1032
|
+
component.cache[cache_key] = {"options": []}
|
|
1033
|
+
component.cache[cache_timestamp_key] = time.time()
|
|
1034
|
+
|
|
1035
|
+
# Use cached results
|
|
1036
|
+
cached = component.cache.get(cache_key, {"options": []})
|
|
1037
|
+
build_config["model"]["options"] = cached["options"]
|
|
1038
|
+
|
|
1039
|
+
# Set default value on initial load when field is empty
|
|
1040
|
+
# Fetch from user's default model setting in the database
|
|
1041
|
+
if not field_value or field_value == "":
|
|
1042
|
+
options = cached.get("options", [])
|
|
1043
|
+
if options:
|
|
1044
|
+
# Determine model type based on cache_key_prefix
|
|
1045
|
+
model_type = "embeddings" if cache_key_prefix == "embedding_model_options" else "language"
|
|
1046
|
+
|
|
1047
|
+
# Try to get user's default model from the variable service
|
|
1048
|
+
default_model_name = None
|
|
1049
|
+
default_model_provider = None
|
|
1050
|
+
try:
|
|
1051
|
+
|
|
1052
|
+
async def _get_default_model():
|
|
1053
|
+
async with session_scope() as session:
|
|
1054
|
+
variable_service = get_variable_service()
|
|
1055
|
+
if variable_service is None:
|
|
1056
|
+
return None, None
|
|
1057
|
+
from langflow.services.variable.service import DatabaseVariableService
|
|
1058
|
+
|
|
1059
|
+
if not isinstance(variable_service, DatabaseVariableService):
|
|
1060
|
+
return None, None
|
|
1061
|
+
|
|
1062
|
+
# Variable names match those in the API
|
|
1063
|
+
var_name = (
|
|
1064
|
+
"__default_embedding_model__"
|
|
1065
|
+
if model_type == "embeddings"
|
|
1066
|
+
else "__default_language_model__"
|
|
1067
|
+
)
|
|
1068
|
+
|
|
1069
|
+
try:
|
|
1070
|
+
var = await variable_service.get_variable_object(
|
|
1071
|
+
user_id=UUID(component.user_id)
|
|
1072
|
+
if isinstance(component.user_id, str)
|
|
1073
|
+
else component.user_id,
|
|
1074
|
+
name=var_name,
|
|
1075
|
+
session=session,
|
|
1076
|
+
)
|
|
1077
|
+
if var and var.value:
|
|
1078
|
+
import json
|
|
1079
|
+
|
|
1080
|
+
parsed_value = json.loads(var.value)
|
|
1081
|
+
if isinstance(parsed_value, dict):
|
|
1082
|
+
return parsed_value.get("model_name"), parsed_value.get("provider")
|
|
1083
|
+
except (ValueError, json.JSONDecodeError, TypeError):
|
|
1084
|
+
# Variable not found or invalid format
|
|
1085
|
+
logger.info("Variable not found or invalid format", exc_info=True)
|
|
1086
|
+
return None, None
|
|
1087
|
+
|
|
1088
|
+
default_model_name, default_model_provider = run_until_complete(_get_default_model())
|
|
1089
|
+
except Exception: # noqa: BLE001
|
|
1090
|
+
# If we can't get default model, continue without it
|
|
1091
|
+
logger.info("Failed to get default model, continue without it", exc_info=True)
|
|
1092
|
+
|
|
1093
|
+
# Find the default model in options
|
|
1094
|
+
default_model = None
|
|
1095
|
+
if default_model_name and default_model_provider:
|
|
1096
|
+
# Look for the user's preferred default model
|
|
1097
|
+
for opt in options:
|
|
1098
|
+
if opt.get("name") == default_model_name and opt.get("provider") == default_model_provider:
|
|
1099
|
+
default_model = opt
|
|
1100
|
+
break
|
|
1101
|
+
|
|
1102
|
+
# If user's default not found, fallback to first option
|
|
1103
|
+
if not default_model and options:
|
|
1104
|
+
default_model = options[0]
|
|
1105
|
+
|
|
1106
|
+
# Set the value
|
|
1107
|
+
if default_model:
|
|
1108
|
+
build_config["model"]["value"] = [default_model]
|
|
1109
|
+
|
|
1110
|
+
# Handle visibility logic:
|
|
1111
|
+
# - Show handle ONLY when field_value is "connect_other_models"
|
|
1112
|
+
# - Hide handle in all other cases (default, model selection, etc.)
|
|
1113
|
+
if field_value == "connect_other_models":
|
|
1114
|
+
# User explicitly selected "Connect other models", show the handle
|
|
1115
|
+
if cache_key_prefix == "embedding_model_options":
|
|
1116
|
+
build_config["model"]["input_types"] = ["Embeddings"]
|
|
1117
|
+
else:
|
|
1118
|
+
build_config["model"]["input_types"] = ["LanguageModel"]
|
|
1119
|
+
else:
|
|
1120
|
+
# Default case or model selection: hide the handle
|
|
1121
|
+
build_config["model"]["input_types"] = []
|
|
1122
|
+
|
|
1123
|
+
return build_config
|