Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
19 changes: 18 additions & 1 deletion src/backend/base/langflow/api/v1/openai_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,13 +171,30 @@ async def openai_stream_generator() -> AsyncGenerator[str, None]:
sender = data.get("sender", "")
content_blocks = data.get("content_blocks", [])

# Get message state from properties
properties = data.get("properties", {})
message_state = properties.get("state") if isinstance(properties, dict) else None

await logger.adebug(
"[OpenAIResponses][stream] add_message: sender=%s sender_name=%s text_len=%d",
(
"[OpenAIResponses][stream] add_message: "
"sender=%s sender_name=%s text_len=%d state=%s"
),
sender,
sender_name,
len(text) if isinstance(text, str) else -1,
message_state,
)

# Skip processing text content if state is "complete"
# All content has already been streamed via token events
if message_state == "complete":
await logger.adebug(
"[OpenAIResponses][stream] skipping add_message with state=complete"
)
# Still process content_blocks for tool calls, but skip text content
text = ""

# Look for Agent Steps in content_blocks
for block in content_blocks:
if block.get("title") == "Agent Steps":
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import pytest
from lfx.components.models.embedding_model import (
OLLAMA_EMBEDDING_MODELS,
OPENAI_EMBEDDING_MODEL_NAMES,
WATSONX_EMBEDDING_MODEL_NAMES,
EmbeddingModelComponent,
Expand Down Expand Up @@ -61,8 +60,6 @@ async def test_update_build_config_ollama(self, component_class, default_kwargs)
"base_url_ibm_watsonx": {"show": False},
}
updated_config = component.update_build_config(build_config, "Ollama", "provider")
assert updated_config["model"]["options"] == OLLAMA_EMBEDDING_MODELS
assert updated_config["model"]["value"] == OLLAMA_EMBEDDING_MODELS[0]
assert updated_config["api_key"]["display_name"] == "API Key (Optional)"
assert updated_config["api_key"]["required"] is False
assert updated_config["api_key"]["show"] is False
Expand Down
2 changes: 1 addition & 1 deletion src/lfx/src/lfx/_assets/component_index.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/lfx/src/lfx/base/models/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ async def _handle_stream(self, runnable, inputs):
)
model_message.properties.source = self._build_source(self._id, self.display_name, self)
lf_message = await self.send_message(model_message)
result = lf_message.text
result = lf_message.text or ""
else:
message = await runnable.ainvoke(inputs)
result = message.content if hasattr(message, "content") else message
Expand Down
100 changes: 100 additions & 0 deletions src/lfx/src/lfx/base/models/model_utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
import asyncio
from urllib.parse import urljoin

import httpx

from lfx.log.logger import logger
from lfx.utils.util import transform_localhost_url

HTTP_STATUS_OK = 200


def get_model_name(llm, display_name: str | None = "Custom"):
attributes_to_check = ["model_name", "model", "model_id", "deployment_name"]

Expand All @@ -6,3 +17,92 @@ def get_model_name(llm, display_name: str | None = "Custom"):

# If no matching attribute is found, return the class name as a fallback
return model_name if model_name is not None else display_name


async def is_valid_ollama_url(url: str) -> bool:
"""Check if the provided URL is a valid Ollama API endpoint."""
try:
url = transform_localhost_url(url)
if not url:
return False
# Strip /v1 suffix if present, as Ollama API endpoints are at root level
url = url.rstrip("/").removesuffix("/v1")
if not url.endswith("/"):
url = url + "/"
async with httpx.AsyncClient() as client:
return (await client.get(url=urljoin(url, "api/tags"))).status_code == HTTP_STATUS_OK
except httpx.RequestError:
logger.debug(f"Invalid Ollama URL: {url}")
return False


async def get_ollama_models(
base_url_value: str, desired_capability: str, json_models_key: str, json_name_key: str, json_capabilities_key: str
) -> list[str]:
"""Fetch available completion models from the Ollama API.

Filters out embedding models and only returns models with completion capability.

Args:
base_url_value (str): The base URL of the Ollama API.
desired_capability (str): The desired capability of the model.
json_models_key (str): The key in the JSON response that contains the models.
json_name_key (str): The key in the JSON response that contains the model names.
json_capabilities_key (str): The key in the JSON response that contains the model capabilities.

Returns:
list[str]: A sorted list of model names that support completion.

Raises:
ValueError: If there is an issue with the API request or response.
"""
try:
# Strip /v1 suffix if present, as Ollama API endpoints are at root level
base_url = base_url_value.rstrip("/").removesuffix("/v1")
if not base_url.endswith("/"):
base_url = base_url + "/"
base_url = transform_localhost_url(base_url)

# Ollama REST API to return models
tags_url = urljoin(base_url, "api/tags")

# Ollama REST API to return model capabilities
show_url = urljoin(base_url, "api/show")
tags_response = None

async with httpx.AsyncClient() as client:
# Fetch available models
tags_response = await client.get(url=tags_url)
tags_response.raise_for_status()
models = tags_response.json()
if asyncio.iscoroutine(models):
models = await models
await logger.adebug(f"Available models: {models}")
Comment thread
lucaseduoli marked this conversation as resolved.

# Filter models that are NOT embedding models
model_ids = []
for model in models.get(json_models_key, []):
model_name = model.get(json_name_key)
if not model_name:
continue
await logger.adebug(f"Checking model: {model_name}")

payload = {"model": model_name}
show_response = await client.post(url=show_url, json=payload)
show_response.raise_for_status()
json_data = show_response.json()
if asyncio.iscoroutine(json_data):
json_data = await json_data

capabilities = json_data.get(json_capabilities_key, [])
await logger.adebug(f"Model: {model_name}, Capabilities: {capabilities}")

if desired_capability in capabilities:
model_ids.append(model_name)

return sorted(model_ids)

except (httpx.RequestError, ValueError) as e:
msg = "Could not get model names from Ollama."
await logger.aexception(msg)
raise ValueError(msg) from e
92 changes: 83 additions & 9 deletions src/lfx/src/lfx/components/models/embedding_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from langchain_openai import OpenAIEmbeddings

from lfx.base.embeddings.model import LCEmbeddingsModel
from lfx.base.models.ollama_constants import OLLAMA_EMBEDDING_MODELS
from lfx.base.models.model_utils import get_ollama_models, is_valid_ollama_url
from lfx.base.models.openai_constants import OPENAI_EMBEDDING_MODEL_NAMES
from lfx.base.models.watsonx_constants import IBM_WATSONX_URLS, WATSONX_EMBEDDING_MODEL_NAMES
from lfx.field_typing import Embeddings
Expand All @@ -20,6 +20,14 @@
from lfx.schema.dotdict import dotdict
from lfx.utils.util import transform_localhost_url

# Ollama API constants
HTTP_STATUS_OK = 200
JSON_MODELS_KEY = "models"
JSON_NAME_KEY = "name"
JSON_CAPABILITIES_KEY = "capabilities"
DESIRED_CAPABILITY = "embedding"
DEFAULT_OLLAMA_URL = "http://localhost:11434"


class EmbeddingModelComponent(LCEmbeddingsModel):
display_name = "Embedding Model"
Expand All @@ -45,6 +53,15 @@ class EmbeddingModelComponent(LCEmbeddingsModel):
info="Base URL for the API. Leave empty for default.",
advanced=True,
),
MessageTextInput(
name="ollama_base_url",
display_name="Ollama API URL",
info=f"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}",
value=DEFAULT_OLLAMA_URL,
show=False,
real_time_refresh=True,
load_from_db=True,
),
DropdownInput(
name="base_url_ibm_watsonx",
display_name="watsonx API Endpoint",
Expand Down Expand Up @@ -101,6 +118,7 @@ def build_embeddings(self) -> Embeddings:
api_key = self.api_key
api_base = self.api_base
base_url_ibm_watsonx = self.base_url_ibm_watsonx
ollama_base_url = self.ollama_base_url
dimensions = self.dimensions
chunk_size = self.chunk_size
request_timeout = self.request_timeout
Expand Down Expand Up @@ -134,7 +152,7 @@ def build_embeddings(self) -> Embeddings:
msg = "Please install langchain-ollama: pip install langchain-ollama"
raise ImportError(msg) from None

transformed_base_url = transform_localhost_url(api_base)
transformed_base_url = transform_localhost_url(ollama_base_url)

# Check if URL contains /v1 suffix (OpenAI-compatible mode)
if transformed_base_url and transformed_base_url.rstrip("/").endswith("/v1"):
Expand Down Expand Up @@ -183,7 +201,9 @@ def build_embeddings(self) -> Embeddings:
msg = f"Unknown provider: {provider}"
raise ValueError(msg)

def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None) -> dotdict:
async def update_build_config(
self, build_config: dotdict, field_value: Any, field_name: str | None = None
) -> dotdict:
if field_name == "provider":
if field_value == "OpenAI":
build_config["model"]["options"] = OPENAI_EMBEDDING_MODEL_NAMES
Expand All @@ -194,19 +214,35 @@ def update_build_config(self, build_config: dotdict, field_value: Any, field_nam
build_config["api_base"]["display_name"] = "OpenAI API Base URL"
build_config["api_base"]["advanced"] = True
build_config["api_base"]["show"] = True
build_config["ollama_base_url"]["show"] = False
build_config["project_id"]["show"] = False
build_config["base_url_ibm_watsonx"]["show"] = False

elif field_value == "Ollama":
build_config["model"]["options"] = OLLAMA_EMBEDDING_MODELS
build_config["model"]["value"] = OLLAMA_EMBEDDING_MODELS[0]
build_config["ollama_base_url"]["show"] = True

if await is_valid_ollama_url(url=self.ollama_base_url):
try:
models = await get_ollama_models(
base_url_value=self.ollama_base_url,
desired_capability=DESIRED_CAPABILITY,
json_models_key=JSON_MODELS_KEY,
json_name_key=JSON_NAME_KEY,
json_capabilities_key=JSON_CAPABILITIES_KEY,
)
build_config["model"]["options"] = models
build_config["model"]["value"] = models[0] if models else ""
except ValueError:
build_config["model"]["options"] = []
build_config["model"]["value"] = ""
else:
build_config["model"]["options"] = []
build_config["model"]["value"] = ""

build_config["api_key"]["display_name"] = "API Key (Optional)"
build_config["api_key"]["required"] = False
build_config["api_key"]["show"] = False
build_config["api_base"]["display_name"] = "Ollama Base URL"
build_config["api_base"]["value"] = "http://localhost:11434"
build_config["api_base"]["advanced"] = False
build_config["api_base"]["show"] = True
build_config["api_base"]["show"] = False
build_config["project_id"]["show"] = False
build_config["base_url_ibm_watsonx"]["show"] = False

Expand All @@ -217,7 +253,45 @@ def update_build_config(self, build_config: dotdict, field_value: Any, field_nam
build_config["api_key"]["required"] = True
build_config["api_key"]["show"] = True
build_config["api_base"]["show"] = False
build_config["ollama_base_url"]["show"] = False
build_config["base_url_ibm_watsonx"]["show"] = True
build_config["project_id"]["show"] = True

elif field_name == "ollama_base_url":
# # Refresh Ollama models when base URL changes
# if hasattr(self, "provider") and self.provider == "Ollama":
# Use field_value if provided, otherwise fall back to instance attribute
ollama_url = self.ollama_base_url
if await is_valid_ollama_url(url=ollama_url):
try:
models = await get_ollama_models(
base_url_value=ollama_url,
desired_capability=DESIRED_CAPABILITY,
json_models_key=JSON_MODELS_KEY,
json_name_key=JSON_NAME_KEY,
json_capabilities_key=JSON_CAPABILITIES_KEY,
)
build_config["model"]["options"] = models
build_config["model"]["value"] = models[0] if models else ""
except ValueError:
await logger.awarning("Failed to fetch Ollama embedding models.")
build_config["model"]["options"] = []
build_config["model"]["value"] = ""

elif field_name == "model" and self.provider == "Ollama":
ollama_url = self.ollama_base_url
if await is_valid_ollama_url(url=ollama_url):
try:
models = await get_ollama_models(
base_url_value=ollama_url,
desired_capability=DESIRED_CAPABILITY,
json_models_key=JSON_MODELS_KEY,
json_name_key=JSON_NAME_KEY,
json_capabilities_key=JSON_CAPABILITIES_KEY,
)
build_config["model"]["options"] = models
except ValueError:
await logger.awarning("Failed to refresh Ollama embedding models.")
build_config["model"]["options"] = []

return build_config
Loading
Loading