Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/lfx/src/lfx/_assets/component_index.json

Large diffs are not rendered by default.

264 changes: 264 additions & 0 deletions src/lfx/src/lfx/components/ibm/watsonx_cpd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
import json
from typing import Any

import requests
from ibm_watsonx_ai import APIClient, Credentials
from langchain_ibm import ChatWatsonx
from loguru import logger
from pydantic.v1 import SecretStr

from lfx.base.models.model import LCModelComponent
from lfx.field_typing import LanguageModel
from lfx.field_typing.range_spec import RangeSpec
from lfx.inputs.inputs import BoolInput, IntInput, SecretStrInput, SliderInput, StrInput, TabInput
from lfx.schema.dotdict import dotdict


class WatsonxAIComponentCPD(LCModelComponent):
display_name = "IBM watsonx.ai / CPD"
description = "Generate text using IBM watsonx.ai foundation models (SaaS or On-Prem / Cloud Pak for Data)."
icon = "WatsonxAI"
name = "IBMwatsonxModel"
beta = False

# These LLMs are used only for SaaS - On-Prem models as input field according to what has been deployed on-prem
_default_models = [
"ibm/granite-3-2b-instruct",
"ibm/granite-3-8b-instruct",
"ibm/granite-13b-instruct-v2",
]

inputs = [
TabInput(
name="deployment_type",
display_name="Deployment Type",
info="Choose SaaS (IBM Cloud) or On-Prem (Cloud Pak for Data).",
options=["On-Prem (CPD)", "SaaS"],
value="On-Prem (CPD)",
real_time_refresh=True,
),
StrInput(
name="url",
display_name="watsonx API Endpoint",
info="The base URL of the API (e.g. https://eu-de.ml.cloud.ibm.com or your CPD URL).",
required=True,
),
StrInput(
name="project_id",
display_name="Project / Space ID",
required=True,
info="The project or deployment space ID associated with the model.",
),
SecretStrInput(
name="api_key",
display_name="API Key",
info="IBM Cloud API key for SaaS use.",
required=True,
show=False,
),
StrInput(
name="username",
display_name="Username",
info="Cloud Pak for Data username.",
required=True,
show=True,
),
SecretStrInput(
name="password",
display_name="Password",
info="Cloud Pak for Data password.",
required=True,
show=True,
),
StrInput(
name="model_name",
display_name="Model Name",
dynamic=True,
required=True,
),
IntInput(
name="max_tokens",
display_name="Max Tokens",
advanced=True,
info="The maximum number of tokens to generate.",
range_spec=RangeSpec(min=1, max=4096),
value=1000,
),
StrInput(
name="stop_sequence",
display_name="Stop Sequence",
advanced=True,
info="Sequence where generation should stop.",
field_type="str",
),
SliderInput(
name="temperature",
display_name="Temperature",
info="Controls randomness, higher values increase diversity.",
value=0.1,
range_spec=RangeSpec(min=0, max=2, step=0.01),
advanced=True,
),
SliderInput(
name="top_p",
display_name="Top P",
info="Cumulative probability cutoff for token sampling.",
value=0.9,
range_spec=RangeSpec(min=0, max=1, step=0.01),
advanced=True,
),
SliderInput(
name="frequency_penalty",
display_name="Frequency Penalty",
info="Penalty for frequent token usage.",
value=0.5,
range_spec=RangeSpec(min=-2.0, max=2.0, step=0.01),
advanced=True,
),
SliderInput(
name="presence_penalty",
display_name="Presence Penalty",
info="Penalty for token presence in prior text.",
value=0.3,
range_spec=RangeSpec(min=-2.0, max=2.0, step=0.01),
advanced=True,
),
IntInput(
name="seed",
display_name="Random Seed",
advanced=True,
value=8,
),
BoolInput(
name="logprobs",
display_name="Log Probabilities",
advanced=True,
value=True,
),
IntInput(
name="top_logprobs",
display_name="Top Log Probabilities",
advanced=True,
value=3,
range_spec=RangeSpec(min=1, max=20),
),
StrInput(
name="logit_bias",
display_name="Logit Bias",
advanced=True,
info='JSON string of token IDs to bias/suppress, e.g. {"1003": -100, "1004": 100}.',
field_type="str",
),
]

@staticmethod
def fetch_models(base_url: str) -> list[str]:
"""Fetch available SaaS models."""
try:
endpoint = f"{base_url}/ml/v1/foundation_model_specs"
params = {"version": "2024-09-16", "filters": "function_text_chat,!lifecycle_withdrawn"}
response = requests.get(endpoint, params=params, timeout=10)
response.raise_for_status()
data = response.json()
models = [model["model_id"] for model in data.get("resources", [])]
return sorted(models)
except Exception:

Check failure on line 165 in src/lfx/src/lfx/components/ibm/watsonx_cpd.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.13)

Ruff (BLE001)

src/lfx/src/lfx/components/ibm/watsonx_cpd.py:165:16: BLE001 Do not catch blind exception: `Exception`
logger.exception("Error fetching SaaS models. Using defaults.")
return WatsonxAIComponent._default_models

Check failure on line 167 in src/lfx/src/lfx/components/ibm/watsonx_cpd.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.13)

Ruff (F821)

src/lfx/src/lfx/components/ibm/watsonx_cpd.py:167:20: F821 Undefined name `WatsonxAIComponent`

Check failure on line 167 in src/lfx/src/lfx/components/ibm/watsonx_cpd.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.13)

Ruff (SLF001)

src/lfx/src/lfx/components/ibm/watsonx_cpd.py:167:20: SLF001 Private member accessed: `_default_models`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix NameError in SaaS model fallback.

If fetch_models hits the exception path we return WatsonxAIComponent._default_models, but that class is not defined in this module, so we raise a NameError instead of falling back. Point the fallback at this class’s _default_models to keep SaaS model loading resilient.

-            return WatsonxAIComponent._default_models
+            return WatsonxAIComponentCPD._default_models
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
except Exception:
logger.exception("Error fetching SaaS models. Using defaults.")
return WatsonxAIComponent._default_models
except Exception:
logger.exception("Error fetching SaaS models. Using defaults.")
return WatsonxAIComponentCPD._default_models
🤖 Prompt for AI Agents
In src/lfx/src/lfx/components/ibm/watsonx_cpd.py around lines 165 to 167, the
exception handler returns WatsonxAIComponent._default_models which is not
defined and causes a NameError; change the fallback to use the current class's
default (e.g., return type(self)._default_models or
self.__class__._default_models) so the method returns the proper _default_models
defined on this class.


def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None):

Check failure on line 169 in src/lfx/src/lfx/components/ibm/watsonx_cpd.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.13)

Ruff (ARG002)

src/lfx/src/lfx/components/ibm/watsonx_cpd.py:169:58: ARG002 Unused method argument: `field_value`
"""Update model dropdown based on environment."""
deployment = build_config.deployment_type.value

if deployment == "SaaS":
build_config.api_key.show = True
build_config.api_key.required = True
build_config.username.show = False
build_config.password.show = False
build_config.username.required = False
build_config.password.required = False
else:
build_config.api_key.show = False
build_config.api_key.required = False
build_config.username.show = True
build_config.password.show = True
build_config.username.required = True
build_config.password.required = True

if field_name in ("url", "deployment_type") and build_config.url.value:
if deployment == "SaaS":
try:
models = self.fetch_models(build_config.url.value)
build_config.model_name.options = models
build_config.model_name.value = models[0] if models else None
logger.info(f"Loaded {len(models)} SaaS models.")
except Exception:

Check failure on line 195 in src/lfx/src/lfx/components/ibm/watsonx_cpd.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.13)

Ruff (BLE001)

src/lfx/src/lfx/components/ibm/watsonx_cpd.py:195:24: BLE001 Do not catch blind exception: `Exception`
logger.exception("Error loading SaaS model list.")
else:
build_config.model_name.options = self._default_models
build_config.model_name.value = self._default_models[0]
logger.info("Using static CPD model list (on-prem).")

def build_model(self) -> LanguageModel:
"""Construct ChatWatsonx client for SaaS or CPD."""
# Parse logit bias
logit_bias = None
if getattr(self, "logit_bias", None):
try:
logit_bias = json.loads(self.logit_bias)
except json.JSONDecodeError:
logger.warning("Invalid logit_bias JSON; ignored.")

chat_params = {
"max_tokens": self.max_tokens,
"temperature": self.temperature,
"top_p": self.top_p,
"frequency_penalty": self.frequency_penalty,
"presence_penalty": self.presence_penalty,
"seed": self.seed,
"stop": [self.stop_sequence] if self.stop_sequence else [],
"n": 1,
"logprobs": self.logprobs,
"top_logprobs": self.top_logprobs,
"time_limit": 600000,
"logit_bias": logit_bias,
}

if self.deployment_type == "On-Prem (CPD)":
# 🟢 CPD auth
username = self.username or ""
password = SecretStr(self.password).get_secret_value() if self.password else None
try:
credentials = Credentials(
url=self.url,
username=username,
password=password,
instance_id="openshift",
auth_type="cpd",
)
except TypeError:
credentials = Credentials(
url=self.url,
username=username,
password=password,
instance_id="openshift",
)

api_client = APIClient(credentials)
return ChatWatsonx(
watsonx_client=api_client,
model_id=self.model_name,
project_id=self.project_id,
params=chat_params,
streaming=self.stream,
)

# 🟢 SaaS auth
return ChatWatsonx(
apikey=SecretStr(self.api_key).get_secret_value(),
url=self.url,
project_id=self.project_id,
model_id=self.model_name,
params=chat_params,
streaming=self.stream,
)
Loading
Loading