From 9ae3b61695c4e9c2e4691d7396fd0ebe58524499 Mon Sep 17 00:00:00 2001 From: Edwin Jose Date: Mon, 24 Nov 2025 11:36:17 -0500 Subject: [PATCH 01/24] Update base image to langflow-nightly:1.7.0.dev19 Dockerfile now uses the newer langflow-nightly:1.7.0.dev19 image instead of 1.7.0.dev5 to ensure the latest features and fixes are included. --- Dockerfile.langflow | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.langflow b/Dockerfile.langflow index 5343585a..6119de84 100644 --- a/Dockerfile.langflow +++ b/Dockerfile.langflow @@ -1,4 +1,4 @@ -FROM langflowai/langflow-nightly:1.7.0.dev5 +FROM langflowai/langflow-nightly:1.7.0.dev19 EXPOSE 7860 From 71b67f418db1cf90556acfde72c64f12e2e263c8 Mon Sep 17 00:00:00 2001 From: Edwin Jose Date: Mon, 24 Nov 2025 16:40:36 -0500 Subject: [PATCH 02/24] Enhance embedding model component and Docker builds Switches Docker Compose services to local builds for backend, frontend, and langflow. Updates embedding model component to support IBM watsonx.ai features, including input token truncation and original text output, adds new dependencies, and improves configuration options in ingestion and agent flows. --- docker-compose.yml | 18 ++--- flows/ingestion_flow.json | 99 +++++++++++++++++++---- flows/openrag_agent.json | 162 +++++++++++++++++++++++++++++++------ flows/openrag_nudges.json | 97 ++++++++++++++++++---- flows/openrag_url_mcp.json | 95 ++++++++++++++++++---- 5 files changed, 390 insertions(+), 81 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 7ba0cea8..c3382bf3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -45,9 +45,9 @@ services: openrag-backend: image: phact/openrag-backend:${OPENRAG_VERSION:-latest} - # build: - # context: . - # dockerfile: Dockerfile.backend + build: + context: . + dockerfile: Dockerfile.backend container_name: openrag-backend depends_on: - langflow @@ -89,9 +89,9 @@ services: openrag-frontend: image: phact/openrag-frontend:${OPENRAG_VERSION:-latest} - # build: - # context: . - # dockerfile: Dockerfile.frontend + build: + context: . + dockerfile: Dockerfile.frontend container_name: openrag-frontend depends_on: - openrag-backend @@ -104,9 +104,9 @@ services: volumes: - ./flows:/app/flows:U,z image: phact/openrag-langflow:${LANGFLOW_VERSION:-latest} - # build: - # context: . - # dockerfile: Dockerfile.langflow + build: + context: . + dockerfile: Dockerfile.langflow container_name: langflow ports: - "7860:7860" diff --git a/flows/ingestion_flow.json b/flows/ingestion_flow.json index abf51e90..d517c1aa 100644 --- a/flows/ingestion_flow.json +++ b/flows/ingestion_flow.json @@ -319,6 +319,7 @@ }, { "animated": false, + "className": "", "data": { "sourceHandle": { "dataType": "EmbeddingModel", @@ -1231,7 +1232,7 @@ "x": 2218.9287723423276, "y": 1332.2598463956504 }, - "selected": true, + "selected": false, "type": "genericNode" }, { @@ -1255,7 +1256,7 @@ ], "frozen": false, "icon": "braces", - "last_updated": "2025-11-11T15:06:45.096Z", + "last_updated": "2025-11-24T18:01:42.358Z", "legacy": false, "lf_version": "1.6.3.dev0", "metadata": {}, @@ -1345,6 +1346,7 @@ "tool_mode": false, "trace_as_input": true, "trace_as_metadata": true, + "track_in_telemetry": false, "type": "str", "value": "" }, @@ -1375,6 +1377,7 @@ "tool_mode": false, "trace_as_input": true, "trace_as_metadata": true, + "track_in_telemetry": false, "type": "str", "value": "" }, @@ -1405,6 +1408,7 @@ "tool_mode": false, "trace_as_input": true, "trace_as_metadata": true, + "track_in_telemetry": false, "type": "str", "value": "" }, @@ -1435,6 +1439,7 @@ "tool_mode": false, "trace_as_input": true, "trace_as_metadata": true, + "track_in_telemetry": false, "type": "str", "value": "" }, @@ -2617,7 +2622,7 @@ ], "frozen": false, "icon": "table", - "last_updated": "2025-11-11T15:06:45.203Z", + "last_updated": "2025-11-24T18:01:42.468Z", "legacy": false, "lf_version": "1.6.3.dev0", "metadata": { @@ -3033,7 +3038,7 @@ ], "frozen": false, "icon": "table", - "last_updated": "2025-11-11T15:06:45.204Z", + "last_updated": "2025-11-24T18:01:42.469Z", "legacy": false, "lf_version": "1.6.3.dev0", "metadata": { @@ -3449,7 +3454,7 @@ ], "frozen": false, "icon": "table", - "last_updated": "2025-11-11T15:06:45.205Z", + "last_updated": "2025-11-24T18:01:42.469Z", "legacy": false, "lf_version": "1.6.3.dev0", "metadata": { @@ -3889,23 +3894,32 @@ "request_timeout", "max_retries", "show_progress_bar", - "model_kwargs" + "model_kwargs", + "truncate_input_tokens", + "input_text" ], "frozen": false, "icon": "binary", - "last_updated": "2025-11-11T15:06:53.974Z", "legacy": false, "metadata": { - "code_hash": "bb03f97be707", + "code_hash": "c5e0a4535a27", "dependencies": { "dependencies": [ + { + "name": "requests", + "version": "2.32.5" + }, + { + "name": "ibm_watsonx_ai", + "version": "1.4.2" + }, { "name": "langchain_openai", "version": "0.3.23" }, { "name": "lfx", - "version": null + "version": "0.2.0.dev19" }, { "name": "langchain_ollama", @@ -3920,9 +3934,9 @@ "version": "0.3.19" } ], - "total_dependencies": 5 + "total_dependencies": 7 }, - "module": "lfx.components.models.embedding_model.EmbeddingModelComponent" + "module": "custom_components.embedding_model" }, "minimized": false, "output_types": [], @@ -3967,6 +3981,7 @@ "tool_mode": false, "trace_as_input": true, "trace_as_metadata": true, + "track_in_telemetry": false, "type": "str", "value": "" }, @@ -3985,6 +4000,7 @@ "required": true, "show": true, "title_case": false, + "track_in_telemetry": false, "type": "str", "value": "OPENAI_API_KEY" }, @@ -4015,6 +4031,7 @@ "toggle": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "str", "value": "https://us-south.ml.cloud.ibm.com" }, @@ -4033,6 +4050,7 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "int", "value": 1000 }, @@ -4052,7 +4070,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from typing import Any\n\nfrom langchain_openai import OpenAIEmbeddings\n\nfrom lfx.base.embeddings.model import LCEmbeddingsModel\nfrom lfx.base.models.model_utils import get_ollama_models, is_valid_ollama_url\nfrom lfx.base.models.openai_constants import OPENAI_EMBEDDING_MODEL_NAMES\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS, WATSONX_EMBEDDING_MODEL_NAMES\nfrom lfx.field_typing import Embeddings\nfrom lfx.io import (\n BoolInput,\n DictInput,\n DropdownInput,\n FloatInput,\n IntInput,\n MessageTextInput,\n SecretStrInput,\n)\nfrom lfx.log.logger import logger\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.utils.util import transform_localhost_url\n\n# Ollama API constants\nHTTP_STATUS_OK = 200\nJSON_MODELS_KEY = \"models\"\nJSON_NAME_KEY = \"name\"\nJSON_CAPABILITIES_KEY = \"capabilities\"\nDESIRED_CAPABILITY = \"embedding\"\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass EmbeddingModelComponent(LCEmbeddingsModel):\n display_name = \"Embedding Model\"\n description = \"Generate embeddings using a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-embedding-models\"\n icon = \"binary\"\n name = \"EmbeddingModel\"\n category = \"models\"\n\n inputs = [\n DropdownInput(\n name=\"provider\",\n display_name=\"Model Provider\",\n options=[\"OpenAI\", \"Ollama\", \"IBM watsonx.ai\"],\n value=\"OpenAI\",\n info=\"Select the embedding model provider\",\n real_time_refresh=True,\n options_metadata=[{\"icon\": \"OpenAI\"}, {\"icon\": \"Ollama\"}, {\"icon\": \"WatsonxAI\"}],\n ),\n MessageTextInput(\n name=\"api_base\",\n display_name=\"API Base URL\",\n info=\"Base URL for the API. Leave empty for default.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n load_from_db=True,\n ),\n DropdownInput(\n name=\"base_url_ibm_watsonx\",\n display_name=\"watsonx API Endpoint\",\n info=\"The base URL of the API (IBM watsonx.ai only)\",\n options=IBM_WATSONX_URLS,\n value=IBM_WATSONX_URLS[0],\n show=False,\n real_time_refresh=True,\n ),\n DropdownInput(\n name=\"model\",\n display_name=\"Model Name\",\n options=OPENAI_EMBEDDING_MODEL_NAMES,\n value=OPENAI_EMBEDDING_MODEL_NAMES[0],\n info=\"Select the embedding model to use\",\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"OpenAI API Key\",\n info=\"Model Provider API key\",\n required=True,\n show=True,\n real_time_refresh=True,\n ),\n # Watson-specific inputs\n MessageTextInput(\n name=\"project_id\",\n display_name=\"Project ID\",\n info=\"IBM watsonx.ai Project ID (required for IBM watsonx.ai)\",\n show=False,\n ),\n IntInput(\n name=\"dimensions\",\n display_name=\"Dimensions\",\n info=\"The number of dimensions the resulting output embeddings should have. \"\n \"Only supported by certain models.\",\n advanced=True,\n ),\n IntInput(name=\"chunk_size\", display_name=\"Chunk Size\", advanced=True, value=1000),\n FloatInput(name=\"request_timeout\", display_name=\"Request Timeout\", advanced=True),\n IntInput(name=\"max_retries\", display_name=\"Max Retries\", advanced=True, value=3),\n BoolInput(name=\"show_progress_bar\", display_name=\"Show Progress Bar\", advanced=True),\n DictInput(\n name=\"model_kwargs\",\n display_name=\"Model Kwargs\",\n advanced=True,\n info=\"Additional keyword arguments to pass to the model.\",\n ),\n ]\n\n def build_embeddings(self) -> Embeddings:\n provider = self.provider\n model = self.model\n api_key = self.api_key\n api_base = self.api_base\n base_url_ibm_watsonx = self.base_url_ibm_watsonx\n ollama_base_url = self.ollama_base_url\n dimensions = self.dimensions\n chunk_size = self.chunk_size\n request_timeout = self.request_timeout\n max_retries = self.max_retries\n show_progress_bar = self.show_progress_bar\n model_kwargs = self.model_kwargs or {}\n\n if provider == \"OpenAI\":\n if not api_key:\n msg = \"OpenAI API key is required when using OpenAI provider\"\n raise ValueError(msg)\n return OpenAIEmbeddings(\n model=model,\n dimensions=dimensions or None,\n base_url=api_base or None,\n api_key=api_key,\n chunk_size=chunk_size,\n max_retries=max_retries,\n timeout=request_timeout or None,\n show_progress_bar=show_progress_bar,\n model_kwargs=model_kwargs,\n )\n\n if provider == \"Ollama\":\n try:\n from langchain_ollama import OllamaEmbeddings\n except ImportError:\n try:\n from langchain_community.embeddings import OllamaEmbeddings\n except ImportError:\n msg = \"Please install langchain-ollama: pip install langchain-ollama\"\n raise ImportError(msg) from None\n\n transformed_base_url = transform_localhost_url(ollama_base_url)\n\n # Check if URL contains /v1 suffix (OpenAI-compatible mode)\n if transformed_base_url and transformed_base_url.rstrip(\"/\").endswith(\"/v1\"):\n # Strip /v1 suffix and log warning\n transformed_base_url = transformed_base_url.rstrip(\"/\").removesuffix(\"/v1\")\n logger.warning(\n \"Detected '/v1' suffix in base URL. The Ollama component uses the native Ollama API, \"\n \"not the OpenAI-compatible API. The '/v1' suffix has been automatically removed. \"\n \"If you want to use the OpenAI-compatible API, please use the OpenAI component instead. \"\n \"Learn more at https://docs.ollama.com/openai#openai-compatibility\"\n )\n\n return OllamaEmbeddings(\n model=model,\n base_url=transformed_base_url or \"http://localhost:11434\",\n **model_kwargs,\n )\n\n if provider == \"IBM watsonx.ai\":\n try:\n from langchain_ibm import WatsonxEmbeddings\n except ImportError:\n msg = \"Please install langchain-ibm: pip install langchain-ibm\"\n raise ImportError(msg) from None\n\n if not api_key:\n msg = \"IBM watsonx.ai API key is required when using IBM watsonx.ai provider\"\n raise ValueError(msg)\n\n project_id = self.project_id\n\n if not project_id:\n msg = \"Project ID is required for IBM watsonx.ai provider\"\n raise ValueError(msg)\n\n params = {\n \"model_id\": model,\n \"url\": base_url_ibm_watsonx or \"https://us-south.ml.cloud.ibm.com\",\n \"apikey\": api_key,\n }\n\n params[\"project_id\"] = project_id\n\n return WatsonxEmbeddings(**params)\n\n msg = f\"Unknown provider: {provider}\"\n raise ValueError(msg)\n\n async def update_build_config(\n self, build_config: dotdict, field_value: Any, field_name: str | None = None\n ) -> dotdict:\n if field_name == \"provider\":\n if field_value == \"OpenAI\":\n build_config[\"model\"][\"options\"] = OPENAI_EMBEDDING_MODEL_NAMES\n build_config[\"model\"][\"value\"] = OPENAI_EMBEDDING_MODEL_NAMES[0]\n build_config[\"api_key\"][\"display_name\"] = \"OpenAI API Key\"\n build_config[\"api_key\"][\"required\"] = True\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"api_base\"][\"display_name\"] = \"OpenAI API Base URL\"\n build_config[\"api_base\"][\"advanced\"] = True\n build_config[\"api_base\"][\"show\"] = True\n build_config[\"ollama_base_url\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n\n elif field_value == \"Ollama\":\n build_config[\"ollama_base_url\"][\"show\"] = True\n\n if await is_valid_ollama_url(url=self.ollama_base_url):\n try:\n models = await get_ollama_models(\n base_url_value=self.ollama_base_url,\n desired_capability=DESIRED_CAPABILITY,\n json_models_key=JSON_MODELS_KEY,\n json_name_key=JSON_NAME_KEY,\n json_capabilities_key=JSON_CAPABILITIES_KEY,\n )\n build_config[\"model\"][\"options\"] = models\n build_config[\"model\"][\"value\"] = models[0] if models else \"\"\n except ValueError:\n build_config[\"model\"][\"options\"] = []\n build_config[\"model\"][\"value\"] = \"\"\n else:\n build_config[\"model\"][\"options\"] = []\n build_config[\"model\"][\"value\"] = \"\"\n\n build_config[\"api_key\"][\"display_name\"] = \"API Key (Optional)\"\n build_config[\"api_key\"][\"required\"] = False\n build_config[\"api_key\"][\"show\"] = False\n build_config[\"api_base\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n\n elif field_value == \"IBM watsonx.ai\":\n build_config[\"model\"][\"options\"] = WATSONX_EMBEDDING_MODEL_NAMES\n build_config[\"model\"][\"value\"] = WATSONX_EMBEDDING_MODEL_NAMES[0]\n build_config[\"api_key\"][\"display_name\"] = \"IBM watsonx.ai API Key\"\n build_config[\"api_key\"][\"required\"] = True\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"api_base\"][\"show\"] = False\n build_config[\"ollama_base_url\"][\"show\"] = False\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = True\n build_config[\"project_id\"][\"show\"] = True\n\n elif field_name == \"ollama_base_url\":\n # # Refresh Ollama models when base URL changes\n # if hasattr(self, \"provider\") and self.provider == \"Ollama\":\n # Use field_value if provided, otherwise fall back to instance attribute\n ollama_url = self.ollama_base_url\n if await is_valid_ollama_url(url=ollama_url):\n try:\n models = await get_ollama_models(\n base_url_value=ollama_url,\n desired_capability=DESIRED_CAPABILITY,\n json_models_key=JSON_MODELS_KEY,\n json_name_key=JSON_NAME_KEY,\n json_capabilities_key=JSON_CAPABILITIES_KEY,\n )\n build_config[\"model\"][\"options\"] = models\n build_config[\"model\"][\"value\"] = models[0] if models else \"\"\n except ValueError:\n await logger.awarning(\"Failed to fetch Ollama embedding models.\")\n build_config[\"model\"][\"options\"] = []\n build_config[\"model\"][\"value\"] = \"\"\n\n elif field_name == \"model\" and self.provider == \"Ollama\":\n ollama_url = self.ollama_base_url\n if await is_valid_ollama_url(url=ollama_url):\n try:\n models = await get_ollama_models(\n base_url_value=ollama_url,\n desired_capability=DESIRED_CAPABILITY,\n json_models_key=JSON_MODELS_KEY,\n json_name_key=JSON_NAME_KEY,\n json_capabilities_key=JSON_CAPABILITIES_KEY,\n )\n build_config[\"model\"][\"options\"] = models\n except ValueError:\n await logger.awarning(\"Failed to refresh Ollama embedding models.\")\n build_config[\"model\"][\"options\"] = []\n\n return build_config\n" + "value": "from typing import Any\n\nimport requests\nfrom ibm_watsonx_ai.metanames import EmbedTextParamsMetaNames\nfrom langchain_openai import OpenAIEmbeddings\n\nfrom lfx.base.embeddings.model import LCEmbeddingsModel\nfrom lfx.base.models.model_utils import get_ollama_models, is_valid_ollama_url\nfrom lfx.base.models.openai_constants import OPENAI_EMBEDDING_MODEL_NAMES\nfrom lfx.base.models.watsonx_constants import (\n IBM_WATSONX_URLS,\n WATSONX_EMBEDDING_MODEL_NAMES,\n)\nfrom lfx.field_typing import Embeddings\nfrom lfx.io import (\n BoolInput,\n DictInput,\n DropdownInput,\n FloatInput,\n IntInput,\n MessageTextInput,\n SecretStrInput,\n)\nfrom lfx.log.logger import logger\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.utils.util import transform_localhost_url\n\n# Ollama API constants\nHTTP_STATUS_OK = 200\nJSON_MODELS_KEY = \"models\"\nJSON_NAME_KEY = \"name\"\nJSON_CAPABILITIES_KEY = \"capabilities\"\nDESIRED_CAPABILITY = \"embedding\"\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass EmbeddingModelComponent(LCEmbeddingsModel):\n display_name = \"Embedding Model\"\n description = \"Generate embeddings using a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-embedding-models\"\n icon = \"binary\"\n name = \"EmbeddingModel\"\n category = \"models\"\n\n inputs = [\n DropdownInput(\n name=\"provider\",\n display_name=\"Model Provider\",\n options=[\"OpenAI\", \"Ollama\", \"IBM watsonx.ai\"],\n value=\"OpenAI\",\n info=\"Select the embedding model provider\",\n real_time_refresh=True,\n options_metadata=[{\"icon\": \"OpenAI\"}, {\"icon\": \"Ollama\"}, {\"icon\": \"WatsonxAI\"}],\n ),\n MessageTextInput(\n name=\"api_base\",\n display_name=\"API Base URL\",\n info=\"Base URL for the API. Leave empty for default.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n load_from_db=True,\n ),\n DropdownInput(\n name=\"base_url_ibm_watsonx\",\n display_name=\"watsonx API Endpoint\",\n info=\"The base URL of the API (IBM watsonx.ai only)\",\n options=IBM_WATSONX_URLS,\n value=IBM_WATSONX_URLS[0],\n show=False,\n real_time_refresh=True,\n ),\n DropdownInput(\n name=\"model\",\n display_name=\"Model Name\",\n options=OPENAI_EMBEDDING_MODEL_NAMES,\n value=OPENAI_EMBEDDING_MODEL_NAMES[0],\n info=\"Select the embedding model to use\",\n real_time_refresh=True,\n refresh_button=True,\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"OpenAI API Key\",\n info=\"Model Provider API key\",\n required=True,\n show=True,\n real_time_refresh=True,\n ),\n # Watson-specific inputs\n MessageTextInput(\n name=\"project_id\",\n display_name=\"Project ID\",\n info=\"IBM watsonx.ai Project ID (required for IBM watsonx.ai)\",\n show=False,\n ),\n IntInput(\n name=\"dimensions\",\n display_name=\"Dimensions\",\n info=\"The number of dimensions the resulting output embeddings should have. \"\n \"Only supported by certain models.\",\n advanced=True,\n ),\n IntInput(name=\"chunk_size\", display_name=\"Chunk Size\", advanced=True, value=1000),\n FloatInput(name=\"request_timeout\", display_name=\"Request Timeout\", advanced=True),\n IntInput(name=\"max_retries\", display_name=\"Max Retries\", advanced=True, value=3),\n BoolInput(name=\"show_progress_bar\", display_name=\"Show Progress Bar\", advanced=True),\n DictInput(\n name=\"model_kwargs\",\n display_name=\"Model Kwargs\",\n advanced=True,\n info=\"Additional keyword arguments to pass to the model.\",\n ),\n IntInput(\n name=\"truncate_input_tokens\",\n display_name=\"Truncate Input Tokens\",\n advanced=True,\n value=200,\n show=False,\n ),\n BoolInput(\n name=\"input_text\",\n display_name=\"Include the original text in the output\",\n value=True,\n advanced=True,\n show=False,\n ),\n ]\n\n @staticmethod\n def fetch_ibm_models(base_url: str) -> list[str]:\n \"\"\"Fetch available models from the watsonx.ai API.\"\"\"\n try:\n endpoint = f\"{base_url}/ml/v1/foundation_model_specs\"\n params = {\n \"version\": \"2024-09-16\",\n \"filters\": \"function_embedding,!lifecycle_withdrawn:and\",\n }\n response = requests.get(endpoint, params=params, timeout=10)\n response.raise_for_status()\n data = response.json()\n models = [model[\"model_id\"] for model in data.get(\"resources\", [])]\n return sorted(models)\n except Exception: # noqa: BLE001\n logger.exception(\"Error fetching models\")\n return WATSONX_EMBEDDING_MODEL_NAMES\n\n def build_embeddings(self) -> Embeddings:\n provider = self.provider\n model = self.model\n api_key = self.api_key\n api_base = self.api_base\n base_url_ibm_watsonx = self.base_url_ibm_watsonx\n ollama_base_url = self.ollama_base_url\n dimensions = self.dimensions\n chunk_size = self.chunk_size\n request_timeout = self.request_timeout\n max_retries = self.max_retries\n show_progress_bar = self.show_progress_bar\n model_kwargs = self.model_kwargs or {}\n\n if provider == \"OpenAI\":\n if not api_key:\n msg = \"OpenAI API key is required when using OpenAI provider\"\n raise ValueError(msg)\n return OpenAIEmbeddings(\n model=model,\n dimensions=dimensions or None,\n base_url=api_base or None,\n api_key=api_key,\n chunk_size=chunk_size,\n max_retries=max_retries,\n timeout=request_timeout or None,\n show_progress_bar=show_progress_bar,\n model_kwargs=model_kwargs,\n )\n\n if provider == \"Ollama\":\n try:\n from langchain_ollama import OllamaEmbeddings\n except ImportError:\n try:\n from langchain_community.embeddings import OllamaEmbeddings\n except ImportError:\n msg = \"Please install langchain-ollama: pip install langchain-ollama\"\n raise ImportError(msg) from None\n\n transformed_base_url = transform_localhost_url(ollama_base_url)\n\n # Check if URL contains /v1 suffix (OpenAI-compatible mode)\n if transformed_base_url and transformed_base_url.rstrip(\"/\").endswith(\"/v1\"):\n # Strip /v1 suffix and log warning\n transformed_base_url = transformed_base_url.rstrip(\"/\").removesuffix(\"/v1\")\n logger.warning(\n \"Detected '/v1' suffix in base URL. The Ollama component uses the native Ollama API, \"\n \"not the OpenAI-compatible API. The '/v1' suffix has been automatically removed. \"\n \"If you want to use the OpenAI-compatible API, please use the OpenAI component instead. \"\n \"Learn more at https://docs.ollama.com/openai#openai-compatibility\"\n )\n\n return OllamaEmbeddings(\n model=model,\n base_url=transformed_base_url or \"http://localhost:11434\",\n **model_kwargs,\n )\n\n if provider == \"IBM watsonx.ai\":\n try:\n from langchain_ibm import WatsonxEmbeddings\n except ImportError:\n msg = \"Please install langchain-ibm: pip install langchain-ibm\"\n raise ImportError(msg) from None\n\n if not api_key:\n msg = \"IBM watsonx.ai API key is required when using IBM watsonx.ai provider\"\n raise ValueError(msg)\n\n project_id = self.project_id\n\n if not project_id:\n msg = \"Project ID is required for IBM watsonx.ai provider\"\n raise ValueError(msg)\n\n from ibm_watsonx_ai import APIClient, Credentials\n\n credentials = Credentials(\n api_key=self.api_key,\n url=base_url_ibm_watsonx or \"https://us-south.ml.cloud.ibm.com\",\n )\n\n api_client = APIClient(credentials)\n\n params = {\n EmbedTextParamsMetaNames.TRUNCATE_INPUT_TOKENS: self.truncate_input_tokens,\n EmbedTextParamsMetaNames.RETURN_OPTIONS: {\"input_text\": self.input_text},\n }\n\n return WatsonxEmbeddings(\n model_id=model,\n params=params,\n watsonx_client=api_client,\n project_id=project_id,\n )\n\n msg = f\"Unknown provider: {provider}\"\n raise ValueError(msg)\n\n async def update_build_config(\n self, build_config: dotdict, field_value: Any, field_name: str | None = None\n ) -> dotdict:\n if field_name == \"provider\":\n if field_value == \"OpenAI\":\n build_config[\"model\"][\"options\"] = OPENAI_EMBEDDING_MODEL_NAMES\n build_config[\"model\"][\"value\"] = OPENAI_EMBEDDING_MODEL_NAMES[0]\n build_config[\"api_key\"][\"display_name\"] = \"OpenAI API Key\"\n build_config[\"api_key\"][\"required\"] = True\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"api_base\"][\"display_name\"] = \"OpenAI API Base URL\"\n build_config[\"api_base\"][\"advanced\"] = True\n build_config[\"api_base\"][\"show\"] = True\n build_config[\"ollama_base_url\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n build_config[\"truncate_input_tokens\"][\"show\"] = False\n build_config[\"input_text\"][\"show\"] = False\n elif field_value == \"Ollama\":\n build_config[\"ollama_base_url\"][\"show\"] = True\n\n if await is_valid_ollama_url(url=self.ollama_base_url):\n try:\n models = await get_ollama_models(\n base_url_value=self.ollama_base_url,\n desired_capability=DESIRED_CAPABILITY,\n json_models_key=JSON_MODELS_KEY,\n json_name_key=JSON_NAME_KEY,\n json_capabilities_key=JSON_CAPABILITIES_KEY,\n )\n build_config[\"model\"][\"options\"] = models\n build_config[\"model\"][\"value\"] = models[0] if models else \"\"\n except ValueError:\n build_config[\"model\"][\"options\"] = []\n build_config[\"model\"][\"value\"] = \"\"\n else:\n build_config[\"model\"][\"options\"] = []\n build_config[\"model\"][\"value\"] = \"\"\n build_config[\"truncate_input_tokens\"][\"show\"] = False\n build_config[\"input_text\"][\"show\"] = False\n build_config[\"api_key\"][\"display_name\"] = \"API Key (Optional)\"\n build_config[\"api_key\"][\"required\"] = False\n build_config[\"api_key\"][\"show\"] = False\n build_config[\"api_base\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n\n elif field_value == \"IBM watsonx.ai\":\n build_config[\"model\"][\"options\"] = self.fetch_ibm_models(base_url=self.base_url_ibm_watsonx)\n build_config[\"model\"][\"value\"] = self.fetch_ibm_models(base_url=self.base_url_ibm_watsonx)[0]\n build_config[\"api_key\"][\"display_name\"] = \"IBM watsonx.ai API Key\"\n build_config[\"api_key\"][\"required\"] = True\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"api_base\"][\"show\"] = False\n build_config[\"ollama_base_url\"][\"show\"] = False\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = True\n build_config[\"project_id\"][\"show\"] = True\n build_config[\"truncate_input_tokens\"][\"show\"] = True\n build_config[\"input_text\"][\"show\"] = True\n elif field_name == \"base_url_ibm_watsonx\":\n build_config[\"model\"][\"options\"] = self.fetch_ibm_models(base_url=field_value)\n build_config[\"model\"][\"value\"] = self.fetch_ibm_models(base_url=field_value)[0]\n elif field_name == \"ollama_base_url\":\n # # Refresh Ollama models when base URL changes\n # if hasattr(self, \"provider\") and self.provider == \"Ollama\":\n # Use field_value if provided, otherwise fall back to instance attribute\n ollama_url = self.ollama_base_url\n if await is_valid_ollama_url(url=ollama_url):\n try:\n models = await get_ollama_models(\n base_url_value=ollama_url,\n desired_capability=DESIRED_CAPABILITY,\n json_models_key=JSON_MODELS_KEY,\n json_name_key=JSON_NAME_KEY,\n json_capabilities_key=JSON_CAPABILITIES_KEY,\n )\n build_config[\"model\"][\"options\"] = models\n build_config[\"model\"][\"value\"] = models[0] if models else \"\"\n except ValueError:\n await logger.awarning(\"Failed to fetch Ollama embedding models.\")\n build_config[\"model\"][\"options\"] = []\n build_config[\"model\"][\"value\"] = \"\"\n\n elif field_name == \"model\" and self.provider == \"Ollama\":\n ollama_url = self.ollama_base_url\n if await is_valid_ollama_url(url=ollama_url):\n try:\n models = await get_ollama_models(\n base_url_value=ollama_url,\n desired_capability=DESIRED_CAPABILITY,\n json_models_key=JSON_MODELS_KEY,\n json_name_key=JSON_NAME_KEY,\n json_capabilities_key=JSON_CAPABILITIES_KEY,\n )\n build_config[\"model\"][\"options\"] = models\n except ValueError:\n await logger.awarning(\"Failed to refresh Ollama embedding models.\")\n build_config[\"model\"][\"options\"] = []\n\n return build_config\n" }, "dimensions": { "_input_type": "IntInput", @@ -4069,9 +4087,29 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "int", "value": "" }, + "input_text": { + "_input_type": "BoolInput", + "advanced": true, + "display_name": "Include the original text in the output", + "dynamic": false, + "info": "", + "list": false, + "list_add_label": "Add More", + "name": "input_text", + "placeholder": "", + "required": false, + "show": false, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "bool", + "value": true + }, "max_retries": { "_input_type": "IntInput", "advanced": true, @@ -4087,6 +4125,7 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "int", "value": 3 }, @@ -4107,12 +4146,15 @@ ], "options_metadata": [], "placeholder": "", + "real_time_refresh": true, + "refresh_button": true, "required": false, "show": true, "title_case": false, "toggle": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "str", "value": "text-embedding-3-small" }, @@ -4131,6 +4173,7 @@ "title_case": false, "tool_mode": false, "trace_as_input": true, + "track_in_telemetry": false, "type": "dict", "value": {} }, @@ -4155,6 +4198,7 @@ "tool_mode": false, "trace_as_input": true, "trace_as_metadata": true, + "track_in_telemetry": false, "type": "str", "value": "" }, @@ -4178,6 +4222,7 @@ "tool_mode": false, "trace_as_input": true, "trace_as_metadata": true, + "track_in_telemetry": false, "type": "str", "value": "" }, @@ -4215,6 +4260,7 @@ "toggle": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "str", "value": "OpenAI" }, @@ -4233,6 +4279,7 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "float", "value": "" }, @@ -4251,8 +4298,28 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "bool", "value": false + }, + "truncate_input_tokens": { + "_input_type": "IntInput", + "advanced": true, + "display_name": "Truncate Input Tokens", + "dynamic": false, + "info": "", + "list": false, + "list_add_label": "Add More", + "name": "truncate_input_tokens", + "placeholder": "", + "required": false, + "show": false, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "int", + "value": 200 } }, "tool_mode": false @@ -4275,16 +4342,16 @@ } ], "viewport": { - "x": -35.55006677196684, - "y": -678.0353419327157, - "zoom": 0.5646686365360081 + "x": 354.22064006192994, + "y": -436.1821097171422, + "zoom": 0.45965778621327413 } }, "description": "Load your data for chat context with Retrieval Augmented Generation.", "endpoint_name": null, "id": "5488df7c-b93f-4f87-a446-b67028bc0813", "is_component": false, - "last_tested_version": "1.7.0", + "last_tested_version": "1.7.0.dev19", "name": "OpenSearch Ingestion Flow", "tags": [ "openai", diff --git a/flows/openrag_agent.json b/flows/openrag_agent.json index 9640c6a8..af0ed600 100644 --- a/flows/openrag_agent.json +++ b/flows/openrag_agent.json @@ -173,6 +173,7 @@ }, { "animated": false, + "className": "", "data": { "sourceHandle": { "dataType": "CalculatorComponent", @@ -242,7 +243,7 @@ ], "frozen": false, "icon": "OpenSearch", - "last_updated": "2025-11-11T21:40:35.507Z", + "last_updated": "2025-11-24T18:02:41.464Z", "legacy": false, "lf_version": "1.6.0", "metadata": { @@ -713,6 +714,7 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": false, "type": "tools", "value": [ { @@ -992,7 +994,7 @@ "frozen": false, "icon": "Mcp", "key": "mcp_lf-starter_project", - "last_updated": "2025-11-11T21:40:35.508Z", + "last_updated": "2025-11-24T18:02:41.465Z", "legacy": false, "mcpServerName": "lf-starter_project", "metadata": { @@ -1141,6 +1143,7 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": false, "type": "tools", "value": [ { @@ -1821,23 +1824,32 @@ "request_timeout", "max_retries", "show_progress_bar", - "model_kwargs" + "model_kwargs", + "truncate_input_tokens", + "input_text" ], "frozen": false, "icon": "binary", - "last_updated": "2025-11-11T21:40:35.510Z", "legacy": false, "metadata": { - "code_hash": "bb03f97be707", + "code_hash": "c5e0a4535a27", "dependencies": { "dependencies": [ + { + "name": "requests", + "version": "2.32.5" + }, + { + "name": "ibm_watsonx_ai", + "version": "1.4.2" + }, { "name": "langchain_openai", "version": "0.3.23" }, { "name": "lfx", - "version": null + "version": "0.2.0.dev19" }, { "name": "langchain_ollama", @@ -1852,9 +1864,9 @@ "version": "0.3.19" } ], - "total_dependencies": 5 + "total_dependencies": 7 }, - "module": "lfx.components.models.embedding_model.EmbeddingModelComponent" + "module": "custom_components.embedding_model" }, "minimized": false, "output_types": [], @@ -1899,6 +1911,7 @@ "tool_mode": false, "trace_as_input": true, "trace_as_metadata": true, + "track_in_telemetry": false, "type": "str", "value": "" }, @@ -1917,6 +1930,7 @@ "required": true, "show": true, "title_case": false, + "track_in_telemetry": false, "type": "str", "value": "OPENAI_API_KEY" }, @@ -1947,6 +1961,7 @@ "toggle": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "str", "value": "https://us-south.ml.cloud.ibm.com" }, @@ -1965,6 +1980,7 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "int", "value": 1000 }, @@ -1984,7 +2000,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from typing import Any\n\nfrom langchain_openai import OpenAIEmbeddings\n\nfrom lfx.base.embeddings.model import LCEmbeddingsModel\nfrom lfx.base.models.model_utils import get_ollama_models, is_valid_ollama_url\nfrom lfx.base.models.openai_constants import OPENAI_EMBEDDING_MODEL_NAMES\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS, WATSONX_EMBEDDING_MODEL_NAMES\nfrom lfx.field_typing import Embeddings\nfrom lfx.io import (\n BoolInput,\n DictInput,\n DropdownInput,\n FloatInput,\n IntInput,\n MessageTextInput,\n SecretStrInput,\n)\nfrom lfx.log.logger import logger\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.utils.util import transform_localhost_url\n\n# Ollama API constants\nHTTP_STATUS_OK = 200\nJSON_MODELS_KEY = \"models\"\nJSON_NAME_KEY = \"name\"\nJSON_CAPABILITIES_KEY = \"capabilities\"\nDESIRED_CAPABILITY = \"embedding\"\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass EmbeddingModelComponent(LCEmbeddingsModel):\n display_name = \"Embedding Model\"\n description = \"Generate embeddings using a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-embedding-models\"\n icon = \"binary\"\n name = \"EmbeddingModel\"\n category = \"models\"\n\n inputs = [\n DropdownInput(\n name=\"provider\",\n display_name=\"Model Provider\",\n options=[\"OpenAI\", \"Ollama\", \"IBM watsonx.ai\"],\n value=\"OpenAI\",\n info=\"Select the embedding model provider\",\n real_time_refresh=True,\n options_metadata=[{\"icon\": \"OpenAI\"}, {\"icon\": \"Ollama\"}, {\"icon\": \"WatsonxAI\"}],\n ),\n MessageTextInput(\n name=\"api_base\",\n display_name=\"API Base URL\",\n info=\"Base URL for the API. Leave empty for default.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n load_from_db=True,\n ),\n DropdownInput(\n name=\"base_url_ibm_watsonx\",\n display_name=\"watsonx API Endpoint\",\n info=\"The base URL of the API (IBM watsonx.ai only)\",\n options=IBM_WATSONX_URLS,\n value=IBM_WATSONX_URLS[0],\n show=False,\n real_time_refresh=True,\n ),\n DropdownInput(\n name=\"model\",\n display_name=\"Model Name\",\n options=OPENAI_EMBEDDING_MODEL_NAMES,\n value=OPENAI_EMBEDDING_MODEL_NAMES[0],\n info=\"Select the embedding model to use\",\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"OpenAI API Key\",\n info=\"Model Provider API key\",\n required=True,\n show=True,\n real_time_refresh=True,\n ),\n # Watson-specific inputs\n MessageTextInput(\n name=\"project_id\",\n display_name=\"Project ID\",\n info=\"IBM watsonx.ai Project ID (required for IBM watsonx.ai)\",\n show=False,\n ),\n IntInput(\n name=\"dimensions\",\n display_name=\"Dimensions\",\n info=\"The number of dimensions the resulting output embeddings should have. \"\n \"Only supported by certain models.\",\n advanced=True,\n ),\n IntInput(name=\"chunk_size\", display_name=\"Chunk Size\", advanced=True, value=1000),\n FloatInput(name=\"request_timeout\", display_name=\"Request Timeout\", advanced=True),\n IntInput(name=\"max_retries\", display_name=\"Max Retries\", advanced=True, value=3),\n BoolInput(name=\"show_progress_bar\", display_name=\"Show Progress Bar\", advanced=True),\n DictInput(\n name=\"model_kwargs\",\n display_name=\"Model Kwargs\",\n advanced=True,\n info=\"Additional keyword arguments to pass to the model.\",\n ),\n ]\n\n def build_embeddings(self) -> Embeddings:\n provider = self.provider\n model = self.model\n api_key = self.api_key\n api_base = self.api_base\n base_url_ibm_watsonx = self.base_url_ibm_watsonx\n ollama_base_url = self.ollama_base_url\n dimensions = self.dimensions\n chunk_size = self.chunk_size\n request_timeout = self.request_timeout\n max_retries = self.max_retries\n show_progress_bar = self.show_progress_bar\n model_kwargs = self.model_kwargs or {}\n\n if provider == \"OpenAI\":\n if not api_key:\n msg = \"OpenAI API key is required when using OpenAI provider\"\n raise ValueError(msg)\n return OpenAIEmbeddings(\n model=model,\n dimensions=dimensions or None,\n base_url=api_base or None,\n api_key=api_key,\n chunk_size=chunk_size,\n max_retries=max_retries,\n timeout=request_timeout or None,\n show_progress_bar=show_progress_bar,\n model_kwargs=model_kwargs,\n )\n\n if provider == \"Ollama\":\n try:\n from langchain_ollama import OllamaEmbeddings\n except ImportError:\n try:\n from langchain_community.embeddings import OllamaEmbeddings\n except ImportError:\n msg = \"Please install langchain-ollama: pip install langchain-ollama\"\n raise ImportError(msg) from None\n\n transformed_base_url = transform_localhost_url(ollama_base_url)\n\n # Check if URL contains /v1 suffix (OpenAI-compatible mode)\n if transformed_base_url and transformed_base_url.rstrip(\"/\").endswith(\"/v1\"):\n # Strip /v1 suffix and log warning\n transformed_base_url = transformed_base_url.rstrip(\"/\").removesuffix(\"/v1\")\n logger.warning(\n \"Detected '/v1' suffix in base URL. The Ollama component uses the native Ollama API, \"\n \"not the OpenAI-compatible API. The '/v1' suffix has been automatically removed. \"\n \"If you want to use the OpenAI-compatible API, please use the OpenAI component instead. \"\n \"Learn more at https://docs.ollama.com/openai#openai-compatibility\"\n )\n\n return OllamaEmbeddings(\n model=model,\n base_url=transformed_base_url or \"http://localhost:11434\",\n **model_kwargs,\n )\n\n if provider == \"IBM watsonx.ai\":\n try:\n from langchain_ibm import WatsonxEmbeddings\n except ImportError:\n msg = \"Please install langchain-ibm: pip install langchain-ibm\"\n raise ImportError(msg) from None\n\n if not api_key:\n msg = \"IBM watsonx.ai API key is required when using IBM watsonx.ai provider\"\n raise ValueError(msg)\n\n project_id = self.project_id\n\n if not project_id:\n msg = \"Project ID is required for IBM watsonx.ai provider\"\n raise ValueError(msg)\n\n params = {\n \"model_id\": model,\n \"url\": base_url_ibm_watsonx or \"https://us-south.ml.cloud.ibm.com\",\n \"apikey\": api_key,\n }\n\n params[\"project_id\"] = project_id\n\n return WatsonxEmbeddings(**params)\n\n msg = f\"Unknown provider: {provider}\"\n raise ValueError(msg)\n\n async def update_build_config(\n self, build_config: dotdict, field_value: Any, field_name: str | None = None\n ) -> dotdict:\n if field_name == \"provider\":\n if field_value == \"OpenAI\":\n build_config[\"model\"][\"options\"] = OPENAI_EMBEDDING_MODEL_NAMES\n build_config[\"model\"][\"value\"] = OPENAI_EMBEDDING_MODEL_NAMES[0]\n build_config[\"api_key\"][\"display_name\"] = \"OpenAI API Key\"\n build_config[\"api_key\"][\"required\"] = True\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"api_base\"][\"display_name\"] = \"OpenAI API Base URL\"\n build_config[\"api_base\"][\"advanced\"] = True\n build_config[\"api_base\"][\"show\"] = True\n build_config[\"ollama_base_url\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n\n elif field_value == \"Ollama\":\n build_config[\"ollama_base_url\"][\"show\"] = True\n\n if await is_valid_ollama_url(url=self.ollama_base_url):\n try:\n models = await get_ollama_models(\n base_url_value=self.ollama_base_url,\n desired_capability=DESIRED_CAPABILITY,\n json_models_key=JSON_MODELS_KEY,\n json_name_key=JSON_NAME_KEY,\n json_capabilities_key=JSON_CAPABILITIES_KEY,\n )\n build_config[\"model\"][\"options\"] = models\n build_config[\"model\"][\"value\"] = models[0] if models else \"\"\n except ValueError:\n build_config[\"model\"][\"options\"] = []\n build_config[\"model\"][\"value\"] = \"\"\n else:\n build_config[\"model\"][\"options\"] = []\n build_config[\"model\"][\"value\"] = \"\"\n\n build_config[\"api_key\"][\"display_name\"] = \"API Key (Optional)\"\n build_config[\"api_key\"][\"required\"] = False\n build_config[\"api_key\"][\"show\"] = False\n build_config[\"api_base\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n\n elif field_value == \"IBM watsonx.ai\":\n build_config[\"model\"][\"options\"] = WATSONX_EMBEDDING_MODEL_NAMES\n build_config[\"model\"][\"value\"] = WATSONX_EMBEDDING_MODEL_NAMES[0]\n build_config[\"api_key\"][\"display_name\"] = \"IBM watsonx.ai API Key\"\n build_config[\"api_key\"][\"required\"] = True\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"api_base\"][\"show\"] = False\n build_config[\"ollama_base_url\"][\"show\"] = False\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = True\n build_config[\"project_id\"][\"show\"] = True\n\n elif field_name == \"ollama_base_url\":\n # # Refresh Ollama models when base URL changes\n # if hasattr(self, \"provider\") and self.provider == \"Ollama\":\n # Use field_value if provided, otherwise fall back to instance attribute\n ollama_url = self.ollama_base_url\n if await is_valid_ollama_url(url=ollama_url):\n try:\n models = await get_ollama_models(\n base_url_value=ollama_url,\n desired_capability=DESIRED_CAPABILITY,\n json_models_key=JSON_MODELS_KEY,\n json_name_key=JSON_NAME_KEY,\n json_capabilities_key=JSON_CAPABILITIES_KEY,\n )\n build_config[\"model\"][\"options\"] = models\n build_config[\"model\"][\"value\"] = models[0] if models else \"\"\n except ValueError:\n await logger.awarning(\"Failed to fetch Ollama embedding models.\")\n build_config[\"model\"][\"options\"] = []\n build_config[\"model\"][\"value\"] = \"\"\n\n elif field_name == \"model\" and self.provider == \"Ollama\":\n ollama_url = self.ollama_base_url\n if await is_valid_ollama_url(url=ollama_url):\n try:\n models = await get_ollama_models(\n base_url_value=ollama_url,\n desired_capability=DESIRED_CAPABILITY,\n json_models_key=JSON_MODELS_KEY,\n json_name_key=JSON_NAME_KEY,\n json_capabilities_key=JSON_CAPABILITIES_KEY,\n )\n build_config[\"model\"][\"options\"] = models\n except ValueError:\n await logger.awarning(\"Failed to refresh Ollama embedding models.\")\n build_config[\"model\"][\"options\"] = []\n\n return build_config\n" + "value": "from typing import Any\n\nimport requests\nfrom ibm_watsonx_ai.metanames import EmbedTextParamsMetaNames\nfrom langchain_openai import OpenAIEmbeddings\n\nfrom lfx.base.embeddings.model import LCEmbeddingsModel\nfrom lfx.base.models.model_utils import get_ollama_models, is_valid_ollama_url\nfrom lfx.base.models.openai_constants import OPENAI_EMBEDDING_MODEL_NAMES\nfrom lfx.base.models.watsonx_constants import (\n IBM_WATSONX_URLS,\n WATSONX_EMBEDDING_MODEL_NAMES,\n)\nfrom lfx.field_typing import Embeddings\nfrom lfx.io import (\n BoolInput,\n DictInput,\n DropdownInput,\n FloatInput,\n IntInput,\n MessageTextInput,\n SecretStrInput,\n)\nfrom lfx.log.logger import logger\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.utils.util import transform_localhost_url\n\n# Ollama API constants\nHTTP_STATUS_OK = 200\nJSON_MODELS_KEY = \"models\"\nJSON_NAME_KEY = \"name\"\nJSON_CAPABILITIES_KEY = \"capabilities\"\nDESIRED_CAPABILITY = \"embedding\"\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass EmbeddingModelComponent(LCEmbeddingsModel):\n display_name = \"Embedding Model\"\n description = \"Generate embeddings using a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-embedding-models\"\n icon = \"binary\"\n name = \"EmbeddingModel\"\n category = \"models\"\n\n inputs = [\n DropdownInput(\n name=\"provider\",\n display_name=\"Model Provider\",\n options=[\"OpenAI\", \"Ollama\", \"IBM watsonx.ai\"],\n value=\"OpenAI\",\n info=\"Select the embedding model provider\",\n real_time_refresh=True,\n options_metadata=[{\"icon\": \"OpenAI\"}, {\"icon\": \"Ollama\"}, {\"icon\": \"WatsonxAI\"}],\n ),\n MessageTextInput(\n name=\"api_base\",\n display_name=\"API Base URL\",\n info=\"Base URL for the API. Leave empty for default.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n load_from_db=True,\n ),\n DropdownInput(\n name=\"base_url_ibm_watsonx\",\n display_name=\"watsonx API Endpoint\",\n info=\"The base URL of the API (IBM watsonx.ai only)\",\n options=IBM_WATSONX_URLS,\n value=IBM_WATSONX_URLS[0],\n show=False,\n real_time_refresh=True,\n ),\n DropdownInput(\n name=\"model\",\n display_name=\"Model Name\",\n options=OPENAI_EMBEDDING_MODEL_NAMES,\n value=OPENAI_EMBEDDING_MODEL_NAMES[0],\n info=\"Select the embedding model to use\",\n real_time_refresh=True,\n refresh_button=True,\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"OpenAI API Key\",\n info=\"Model Provider API key\",\n required=True,\n show=True,\n real_time_refresh=True,\n ),\n # Watson-specific inputs\n MessageTextInput(\n name=\"project_id\",\n display_name=\"Project ID\",\n info=\"IBM watsonx.ai Project ID (required for IBM watsonx.ai)\",\n show=False,\n ),\n IntInput(\n name=\"dimensions\",\n display_name=\"Dimensions\",\n info=\"The number of dimensions the resulting output embeddings should have. \"\n \"Only supported by certain models.\",\n advanced=True,\n ),\n IntInput(name=\"chunk_size\", display_name=\"Chunk Size\", advanced=True, value=1000),\n FloatInput(name=\"request_timeout\", display_name=\"Request Timeout\", advanced=True),\n IntInput(name=\"max_retries\", display_name=\"Max Retries\", advanced=True, value=3),\n BoolInput(name=\"show_progress_bar\", display_name=\"Show Progress Bar\", advanced=True),\n DictInput(\n name=\"model_kwargs\",\n display_name=\"Model Kwargs\",\n advanced=True,\n info=\"Additional keyword arguments to pass to the model.\",\n ),\n IntInput(\n name=\"truncate_input_tokens\",\n display_name=\"Truncate Input Tokens\",\n advanced=True,\n value=200,\n show=False,\n ),\n BoolInput(\n name=\"input_text\",\n display_name=\"Include the original text in the output\",\n value=True,\n advanced=True,\n show=False,\n ),\n ]\n\n @staticmethod\n def fetch_ibm_models(base_url: str) -> list[str]:\n \"\"\"Fetch available models from the watsonx.ai API.\"\"\"\n try:\n endpoint = f\"{base_url}/ml/v1/foundation_model_specs\"\n params = {\n \"version\": \"2024-09-16\",\n \"filters\": \"function_embedding,!lifecycle_withdrawn:and\",\n }\n response = requests.get(endpoint, params=params, timeout=10)\n response.raise_for_status()\n data = response.json()\n models = [model[\"model_id\"] for model in data.get(\"resources\", [])]\n return sorted(models)\n except Exception: # noqa: BLE001\n logger.exception(\"Error fetching models\")\n return WATSONX_EMBEDDING_MODEL_NAMES\n\n def build_embeddings(self) -> Embeddings:\n provider = self.provider\n model = self.model\n api_key = self.api_key\n api_base = self.api_base\n base_url_ibm_watsonx = self.base_url_ibm_watsonx\n ollama_base_url = self.ollama_base_url\n dimensions = self.dimensions\n chunk_size = self.chunk_size\n request_timeout = self.request_timeout\n max_retries = self.max_retries\n show_progress_bar = self.show_progress_bar\n model_kwargs = self.model_kwargs or {}\n\n if provider == \"OpenAI\":\n if not api_key:\n msg = \"OpenAI API key is required when using OpenAI provider\"\n raise ValueError(msg)\n return OpenAIEmbeddings(\n model=model,\n dimensions=dimensions or None,\n base_url=api_base or None,\n api_key=api_key,\n chunk_size=chunk_size,\n max_retries=max_retries,\n timeout=request_timeout or None,\n show_progress_bar=show_progress_bar,\n model_kwargs=model_kwargs,\n )\n\n if provider == \"Ollama\":\n try:\n from langchain_ollama import OllamaEmbeddings\n except ImportError:\n try:\n from langchain_community.embeddings import OllamaEmbeddings\n except ImportError:\n msg = \"Please install langchain-ollama: pip install langchain-ollama\"\n raise ImportError(msg) from None\n\n transformed_base_url = transform_localhost_url(ollama_base_url)\n\n # Check if URL contains /v1 suffix (OpenAI-compatible mode)\n if transformed_base_url and transformed_base_url.rstrip(\"/\").endswith(\"/v1\"):\n # Strip /v1 suffix and log warning\n transformed_base_url = transformed_base_url.rstrip(\"/\").removesuffix(\"/v1\")\n logger.warning(\n \"Detected '/v1' suffix in base URL. The Ollama component uses the native Ollama API, \"\n \"not the OpenAI-compatible API. The '/v1' suffix has been automatically removed. \"\n \"If you want to use the OpenAI-compatible API, please use the OpenAI component instead. \"\n \"Learn more at https://docs.ollama.com/openai#openai-compatibility\"\n )\n\n return OllamaEmbeddings(\n model=model,\n base_url=transformed_base_url or \"http://localhost:11434\",\n **model_kwargs,\n )\n\n if provider == \"IBM watsonx.ai\":\n try:\n from langchain_ibm import WatsonxEmbeddings\n except ImportError:\n msg = \"Please install langchain-ibm: pip install langchain-ibm\"\n raise ImportError(msg) from None\n\n if not api_key:\n msg = \"IBM watsonx.ai API key is required when using IBM watsonx.ai provider\"\n raise ValueError(msg)\n\n project_id = self.project_id\n\n if not project_id:\n msg = \"Project ID is required for IBM watsonx.ai provider\"\n raise ValueError(msg)\n\n from ibm_watsonx_ai import APIClient, Credentials\n\n credentials = Credentials(\n api_key=self.api_key,\n url=base_url_ibm_watsonx or \"https://us-south.ml.cloud.ibm.com\",\n )\n\n api_client = APIClient(credentials)\n\n params = {\n EmbedTextParamsMetaNames.TRUNCATE_INPUT_TOKENS: self.truncate_input_tokens,\n EmbedTextParamsMetaNames.RETURN_OPTIONS: {\"input_text\": self.input_text},\n }\n\n return WatsonxEmbeddings(\n model_id=model,\n params=params,\n watsonx_client=api_client,\n project_id=project_id,\n )\n\n msg = f\"Unknown provider: {provider}\"\n raise ValueError(msg)\n\n async def update_build_config(\n self, build_config: dotdict, field_value: Any, field_name: str | None = None\n ) -> dotdict:\n if field_name == \"provider\":\n if field_value == \"OpenAI\":\n build_config[\"model\"][\"options\"] = OPENAI_EMBEDDING_MODEL_NAMES\n build_config[\"model\"][\"value\"] = OPENAI_EMBEDDING_MODEL_NAMES[0]\n build_config[\"api_key\"][\"display_name\"] = \"OpenAI API Key\"\n build_config[\"api_key\"][\"required\"] = True\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"api_base\"][\"display_name\"] = \"OpenAI API Base URL\"\n build_config[\"api_base\"][\"advanced\"] = True\n build_config[\"api_base\"][\"show\"] = True\n build_config[\"ollama_base_url\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n build_config[\"truncate_input_tokens\"][\"show\"] = False\n build_config[\"input_text\"][\"show\"] = False\n elif field_value == \"Ollama\":\n build_config[\"ollama_base_url\"][\"show\"] = True\n\n if await is_valid_ollama_url(url=self.ollama_base_url):\n try:\n models = await get_ollama_models(\n base_url_value=self.ollama_base_url,\n desired_capability=DESIRED_CAPABILITY,\n json_models_key=JSON_MODELS_KEY,\n json_name_key=JSON_NAME_KEY,\n json_capabilities_key=JSON_CAPABILITIES_KEY,\n )\n build_config[\"model\"][\"options\"] = models\n build_config[\"model\"][\"value\"] = models[0] if models else \"\"\n except ValueError:\n build_config[\"model\"][\"options\"] = []\n build_config[\"model\"][\"value\"] = \"\"\n else:\n build_config[\"model\"][\"options\"] = []\n build_config[\"model\"][\"value\"] = \"\"\n build_config[\"truncate_input_tokens\"][\"show\"] = False\n build_config[\"input_text\"][\"show\"] = False\n build_config[\"api_key\"][\"display_name\"] = \"API Key (Optional)\"\n build_config[\"api_key\"][\"required\"] = False\n build_config[\"api_key\"][\"show\"] = False\n build_config[\"api_base\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n\n elif field_value == \"IBM watsonx.ai\":\n build_config[\"model\"][\"options\"] = self.fetch_ibm_models(base_url=self.base_url_ibm_watsonx)\n build_config[\"model\"][\"value\"] = self.fetch_ibm_models(base_url=self.base_url_ibm_watsonx)[0]\n build_config[\"api_key\"][\"display_name\"] = \"IBM watsonx.ai API Key\"\n build_config[\"api_key\"][\"required\"] = True\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"api_base\"][\"show\"] = False\n build_config[\"ollama_base_url\"][\"show\"] = False\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = True\n build_config[\"project_id\"][\"show\"] = True\n build_config[\"truncate_input_tokens\"][\"show\"] = True\n build_config[\"input_text\"][\"show\"] = True\n elif field_name == \"base_url_ibm_watsonx\":\n build_config[\"model\"][\"options\"] = self.fetch_ibm_models(base_url=field_value)\n build_config[\"model\"][\"value\"] = self.fetch_ibm_models(base_url=field_value)[0]\n elif field_name == \"ollama_base_url\":\n # # Refresh Ollama models when base URL changes\n # if hasattr(self, \"provider\") and self.provider == \"Ollama\":\n # Use field_value if provided, otherwise fall back to instance attribute\n ollama_url = self.ollama_base_url\n if await is_valid_ollama_url(url=ollama_url):\n try:\n models = await get_ollama_models(\n base_url_value=ollama_url,\n desired_capability=DESIRED_CAPABILITY,\n json_models_key=JSON_MODELS_KEY,\n json_name_key=JSON_NAME_KEY,\n json_capabilities_key=JSON_CAPABILITIES_KEY,\n )\n build_config[\"model\"][\"options\"] = models\n build_config[\"model\"][\"value\"] = models[0] if models else \"\"\n except ValueError:\n await logger.awarning(\"Failed to fetch Ollama embedding models.\")\n build_config[\"model\"][\"options\"] = []\n build_config[\"model\"][\"value\"] = \"\"\n\n elif field_name == \"model\" and self.provider == \"Ollama\":\n ollama_url = self.ollama_base_url\n if await is_valid_ollama_url(url=ollama_url):\n try:\n models = await get_ollama_models(\n base_url_value=ollama_url,\n desired_capability=DESIRED_CAPABILITY,\n json_models_key=JSON_MODELS_KEY,\n json_name_key=JSON_NAME_KEY,\n json_capabilities_key=JSON_CAPABILITIES_KEY,\n )\n build_config[\"model\"][\"options\"] = models\n except ValueError:\n await logger.awarning(\"Failed to refresh Ollama embedding models.\")\n build_config[\"model\"][\"options\"] = []\n\n return build_config\n" }, "dimensions": { "_input_type": "IntInput", @@ -2001,9 +2017,29 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "int", "value": "" }, + "input_text": { + "_input_type": "BoolInput", + "advanced": true, + "display_name": "Include the original text in the output", + "dynamic": false, + "info": "", + "list": false, + "list_add_label": "Add More", + "name": "input_text", + "placeholder": "", + "required": false, + "show": false, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "bool", + "value": true + }, "max_retries": { "_input_type": "IntInput", "advanced": true, @@ -2019,6 +2055,7 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "int", "value": 3 }, @@ -2033,16 +2070,21 @@ "info": "Select the embedding model to use", "name": "model", "options": [ - "text-embedding-3-small" + "text-embedding-3-small", + "text-embedding-3-large", + "text-embedding-ada-002" ], "options_metadata": [], "placeholder": "", + "real_time_refresh": true, + "refresh_button": true, "required": false, "show": true, "title_case": false, "toggle": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "str", "value": "text-embedding-3-small" }, @@ -2061,6 +2103,7 @@ "title_case": false, "tool_mode": false, "trace_as_input": true, + "track_in_telemetry": false, "type": "dict", "value": {} }, @@ -2085,6 +2128,7 @@ "tool_mode": false, "trace_as_input": true, "trace_as_metadata": true, + "track_in_telemetry": false, "type": "str", "value": "" }, @@ -2108,6 +2152,7 @@ "tool_mode": false, "trace_as_input": true, "trace_as_metadata": true, + "track_in_telemetry": false, "type": "str", "value": "" }, @@ -2145,6 +2190,7 @@ "toggle": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "str", "value": "OpenAI" }, @@ -2163,6 +2209,7 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "float", "value": "" }, @@ -2181,8 +2228,28 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "bool", "value": false + }, + "truncate_input_tokens": { + "_input_type": "IntInput", + "advanced": true, + "display_name": "Truncate Input Tokens", + "dynamic": false, + "info": "", + "list": false, + "list_add_label": "Add More", + "name": "truncate_input_tokens", + "placeholder": "", + "required": false, + "show": false, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "int", + "value": 200 } }, "tool_mode": false @@ -2193,7 +2260,7 @@ "dragging": false, "id": "EmbeddingModel-oPi95", "measured": { - "height": 451, + "height": 369, "width": 320 }, "position": { @@ -2247,10 +2314,9 @@ ], "frozen": false, "icon": "bot", - "last_updated": "2025-11-11T21:40:35.510Z", "legacy": false, "metadata": { - "code_hash": "adf733969280", + "code_hash": "d64b11c24a1c", "dependencies": { "dependencies": [ { @@ -2263,12 +2329,12 @@ }, { "name": "lfx", - "version": null + "version": "0.2.0.dev19" } ], "total_dependencies": 3 }, - "module": "lfx.components.agents.agent.AgentComponent" + "module": "custom_components.agent" }, "minimized": false, "output_types": [], @@ -2308,6 +2374,7 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "bool", "value": true }, @@ -2333,6 +2400,7 @@ "tool_mode": false, "trace_as_input": true, "trace_as_metadata": true, + "track_in_telemetry": false, "type": "str", "value": "A helpful assistant with access to the following tools:" }, @@ -2390,6 +2458,7 @@ "toggle": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "str", "value": "OpenAI" }, @@ -2408,6 +2477,7 @@ "required": false, "show": true, "title_case": false, + "track_in_telemetry": false, "type": "str", "value": "OPENAI_API_KEY" }, @@ -2427,6 +2497,7 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": false, "type": "str", "value": "" }, @@ -2446,7 +2517,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import json\nimport re\n\nfrom langchain_core.tools import StructuredTool, Tool\nfrom pydantic import ValidationError\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.model_input_constants import (\n ALL_PROVIDER_FIELDS,\n MODEL_DYNAMIC_UPDATE_FIELDS,\n MODEL_PROVIDERS_DICT,\n MODEL_PROVIDERS_LIST,\n MODELS_METADATA,\n)\nfrom lfx.base.models.model_utils import get_model_name\nfrom lfx.components.helpers.current_date import CurrentDateComponent\nfrom lfx.components.helpers.memory import MemoryComponent\nfrom lfx.components.langchain_utilities.tool_calling import ToolCallingAgentComponent\nfrom lfx.custom.custom_component.component import get_component_toolkit\nfrom lfx.custom.utils import update_component_build_config\nfrom lfx.helpers.base_model import build_model_from_schema\nfrom lfx.inputs.inputs import BoolInput, SecretStrInput, StrInput\nfrom lfx.io import DropdownInput, IntInput, MessageTextInput, MultilineInput, Output, TableInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(ToolCallingAgentComponent):\n display_name: str = \"Agent\"\n description: str = \"Define the agent's instructions, then enter a task to complete using tools.\"\n documentation: str = \"https://docs.langflow.org/agents\"\n icon = \"bot\"\n beta = False\n name = \"Agent\"\n\n memory_inputs = [set_advanced_true(component_input) for component_input in MemoryComponent().inputs]\n\n # Filter out json_mode from OpenAI inputs since we handle structured output differently\n if \"OpenAI\" in MODEL_PROVIDERS_DICT:\n openai_inputs_filtered = [\n input_field\n for input_field in MODEL_PROVIDERS_DICT[\"OpenAI\"][\"inputs\"]\n if not (hasattr(input_field, \"name\") and input_field.name == \"json_mode\")\n ]\n else:\n openai_inputs_filtered = []\n\n inputs = [\n DropdownInput(\n name=\"agent_llm\",\n display_name=\"Model Provider\",\n info=\"The provider of the language model that the agent will use to generate responses.\",\n options=[*MODEL_PROVIDERS_LIST],\n value=\"OpenAI\",\n real_time_refresh=True,\n refresh_button=False,\n input_types=[],\n options_metadata=[MODELS_METADATA[key] for key in MODEL_PROVIDERS_LIST if key in MODELS_METADATA],\n external_options={\n \"fields\": {\n \"data\": {\n \"node\": {\n \"name\": \"connect_other_models\",\n \"display_name\": \"Connect other models\",\n \"icon\": \"CornerDownLeft\",\n }\n }\n },\n },\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"The API key to use for the model.\",\n required=True,\n ),\n StrInput(\n name=\"base_url\",\n display_name=\"Base URL\",\n info=\"The base URL of the API.\",\n required=True,\n show=False,\n ),\n StrInput(\n name=\"project_id\",\n display_name=\"Project ID\",\n info=\"The project ID of the model.\",\n required=True,\n show=False,\n ),\n IntInput(\n name=\"max_output_tokens\",\n display_name=\"Max Output Tokens\",\n info=\"The maximum number of tokens to generate.\",\n show=False,\n ),\n *openai_inputs_filtered,\n MultilineInput(\n name=\"system_prompt\",\n display_name=\"Agent Instructions\",\n info=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\n advanced=False,\n ),\n MessageTextInput(\n name=\"context_id\",\n display_name=\"Context ID\",\n info=\"The context ID of the chat. Adds an extra layer to the local memory.\",\n value=\"\",\n advanced=True,\n ),\n IntInput(\n name=\"n_messages\",\n display_name=\"Number of Chat History Messages\",\n value=100,\n info=\"Number of chat history messages to retrieve.\",\n advanced=True,\n show=True,\n ),\n MultilineInput(\n name=\"format_instructions\",\n display_name=\"Output Format Instructions\",\n info=\"Generic Template for structured output formatting. Valid only with Structured response.\",\n value=(\n \"You are an AI that extracts structured JSON objects from unstructured text. \"\n \"Use a predefined schema with expected types (str, int, float, bool, dict). \"\n \"Extract ALL relevant instances that match the schema - if multiple patterns exist, capture them all. \"\n \"Fill missing or ambiguous values with defaults: null for missing values. \"\n \"Remove exact duplicates but keep variations that have different field values. \"\n \"Always return valid JSON in the expected format, never throw errors. \"\n \"If multiple objects can be extracted, return them all in the structured format.\"\n ),\n advanced=True,\n ),\n TableInput(\n name=\"output_schema\",\n display_name=\"Output Schema\",\n info=(\n \"Schema Validation: Define the structure and data types for structured output. \"\n \"No validation if no output schema.\"\n ),\n advanced=True,\n required=False,\n value=[],\n table_schema=[\n {\n \"name\": \"name\",\n \"display_name\": \"Name\",\n \"type\": \"str\",\n \"description\": \"Specify the name of the output field.\",\n \"default\": \"field\",\n \"edit_mode\": EditMode.INLINE,\n },\n {\n \"name\": \"description\",\n \"display_name\": \"Description\",\n \"type\": \"str\",\n \"description\": \"Describe the purpose of the output field.\",\n \"default\": \"description of field\",\n \"edit_mode\": EditMode.POPOVER,\n },\n {\n \"name\": \"type\",\n \"display_name\": \"Type\",\n \"type\": \"str\",\n \"edit_mode\": EditMode.INLINE,\n \"description\": (\"Indicate the data type of the output field (e.g., str, int, float, bool, dict).\"),\n \"options\": [\"str\", \"int\", \"float\", \"bool\", \"dict\"],\n \"default\": \"str\",\n },\n {\n \"name\": \"multiple\",\n \"display_name\": \"As List\",\n \"type\": \"boolean\",\n \"description\": \"Set to True if this output field should be a list of the specified type.\",\n \"default\": \"False\",\n \"edit_mode\": EditMode.INLINE,\n },\n ],\n ),\n *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\n BoolInput(\n name=\"add_current_date_tool\",\n display_name=\"Current Date\",\n advanced=True,\n info=\"If true, will add a tool to the agent that returns the current date.\",\n value=True,\n ),\n ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\n ]\n\n async def get_agent_requirements(self):\n \"\"\"Get the agent requirements for the agent.\"\"\"\n llm_model, display_name = await self.get_llm()\n if llm_model is None:\n msg = \"No language model selected. Please choose a model to proceed.\"\n raise ValueError(msg)\n self.model_name = get_model_name(llm_model, display_name=display_name)\n\n # Get memory data\n self.chat_history = await self.get_memory_data()\n await logger.adebug(f\"Retrieved {len(self.chat_history)} chat history messages\")\n if isinstance(self.chat_history, Message):\n self.chat_history = [self.chat_history]\n\n # Add current date tool if enabled\n if self.add_current_date_tool:\n if not isinstance(self.tools, list): # type: ignore[has-type]\n self.tools = []\n current_date_tool = (await CurrentDateComponent(**self.get_base_args()).to_toolkit()).pop(0)\n\n if not isinstance(current_date_tool, StructuredTool):\n msg = \"CurrentDateComponent must be converted to a StructuredTool\"\n raise TypeError(msg)\n self.tools.append(current_date_tool)\n\n # Set shared callbacks for tracing the tools used by the agent\n self.set_tools_callbacks(self.tools, self._get_shared_callbacks())\n\n return llm_model, self.chat_history, self.tools\n\n async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\n self.set(\n llm=llm_model,\n tools=self.tools or [],\n chat_history=self.chat_history,\n input_value=self.input_value,\n system_prompt=self.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components else \"\"\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n self.set(\n llm=llm_model,\n tools=self.tools or [],\n chat_history=self.chat_history,\n input_value=self.input_value,\n system_prompt=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\n )\n return [\n message for message in messages if getattr(message, \"id\", None) != getattr(self.input_value, \"id\", None)\n ]\n\n async def get_llm(self):\n if not isinstance(self.agent_llm, str):\n return self.agent_llm, None\n\n try:\n provider_info = MODEL_PROVIDERS_DICT.get(self.agent_llm)\n if not provider_info:\n msg = f\"Invalid model provider: {self.agent_llm}\"\n raise ValueError(msg)\n\n component_class = provider_info.get(\"component_class\")\n display_name = component_class.display_name\n inputs = provider_info.get(\"inputs\")\n prefix = provider_info.get(\"prefix\", \"\")\n\n return self._build_llm_model(component_class, inputs, prefix), display_name\n\n except (AttributeError, ValueError, TypeError, RuntimeError) as e:\n await logger.aerror(f\"Error building {self.agent_llm} language model: {e!s}\")\n msg = f\"Failed to initialize language model: {e!s}\"\n raise ValueError(msg) from e\n\n def _build_llm_model(self, component, inputs, prefix=\"\"):\n model_kwargs = {}\n for input_ in inputs:\n if hasattr(self, f\"{prefix}{input_.name}\"):\n model_kwargs[input_.name] = getattr(self, f\"{prefix}{input_.name}\")\n return component.set(**model_kwargs).build_model()\n\n def set_component_params(self, component):\n provider_info = MODEL_PROVIDERS_DICT.get(self.agent_llm)\n if provider_info:\n inputs = provider_info.get(\"inputs\")\n prefix = provider_info.get(\"prefix\")\n # Filter out json_mode and only use attributes that exist on this component\n model_kwargs = {}\n for input_ in inputs:\n if hasattr(self, f\"{prefix}{input_.name}\"):\n model_kwargs[input_.name] = getattr(self, f\"{prefix}{input_.name}\")\n\n return component.set(**model_kwargs)\n return component\n\n def delete_fields(self, build_config: dotdict, fields: dict | list[str]) -> None:\n \"\"\"Delete specified fields from build_config.\"\"\"\n for field in fields:\n if build_config is not None and field in build_config:\n build_config.pop(field, None)\n\n def update_input_types(self, build_config: dotdict) -> dotdict:\n \"\"\"Update input types for all fields in build_config.\"\"\"\n for key, value in build_config.items():\n if isinstance(value, dict):\n if value.get(\"input_types\") is None:\n build_config[key][\"input_types\"] = []\n elif hasattr(value, \"input_types\") and value.input_types is None:\n value.input_types = []\n return build_config\n\n async def update_build_config(\n self, build_config: dotdict, field_value: str, field_name: str | None = None\n ) -> dotdict:\n # Iterate over all providers in the MODEL_PROVIDERS_DICT\n # Existing logic for updating build_config\n if field_name in (\"agent_llm\",):\n build_config[\"agent_llm\"][\"value\"] = field_value\n provider_info = MODEL_PROVIDERS_DICT.get(field_value)\n if provider_info:\n component_class = provider_info.get(\"component_class\")\n if component_class and hasattr(component_class, \"update_build_config\"):\n # Call the component class's update_build_config method\n build_config = await update_component_build_config(\n component_class, build_config, field_value, \"model_name\"\n )\n\n provider_configs: dict[str, tuple[dict, list[dict]]] = {\n provider: (\n MODEL_PROVIDERS_DICT[provider][\"fields\"],\n [\n MODEL_PROVIDERS_DICT[other_provider][\"fields\"]\n for other_provider in MODEL_PROVIDERS_DICT\n if other_provider != provider\n ],\n )\n for provider in MODEL_PROVIDERS_DICT\n }\n if field_value in provider_configs:\n fields_to_add, fields_to_delete = provider_configs[field_value]\n\n # Delete fields from other providers\n for fields in fields_to_delete:\n self.delete_fields(build_config, fields)\n\n # Add provider-specific fields\n if field_value == \"OpenAI\" and not any(field in build_config for field in fields_to_add):\n build_config.update(fields_to_add)\n else:\n build_config.update(fields_to_add)\n # Reset input types for agent_llm\n build_config[\"agent_llm\"][\"input_types\"] = []\n build_config[\"agent_llm\"][\"display_name\"] = \"Model Provider\"\n elif field_value == \"connect_other_models\":\n # Delete all provider fields\n self.delete_fields(build_config, ALL_PROVIDER_FIELDS)\n # # Update with custom component\n custom_component = DropdownInput(\n name=\"agent_llm\",\n display_name=\"Language Model\",\n info=\"The provider of the language model that the agent will use to generate responses.\",\n options=[*MODEL_PROVIDERS_LIST],\n real_time_refresh=True,\n refresh_button=False,\n input_types=[\"LanguageModel\"],\n placeholder=\"Awaiting model input.\",\n options_metadata=[MODELS_METADATA[key] for key in MODEL_PROVIDERS_LIST if key in MODELS_METADATA],\n external_options={\n \"fields\": {\n \"data\": {\n \"node\": {\n \"name\": \"connect_other_models\",\n \"display_name\": \"Connect other models\",\n \"icon\": \"CornerDownLeft\",\n },\n }\n },\n },\n )\n build_config.update({\"agent_llm\": custom_component.to_dict()})\n # Update input types for all fields\n build_config = self.update_input_types(build_config)\n\n # Validate required keys\n default_keys = [\n \"code\",\n \"_type\",\n \"agent_llm\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\n ]\n missing_keys = [key for key in default_keys if key not in build_config]\n if missing_keys:\n msg = f\"Missing required keys in build_config: {missing_keys}\"\n raise ValueError(msg)\n if (\n isinstance(self.agent_llm, str)\n and self.agent_llm in MODEL_PROVIDERS_DICT\n and field_name in MODEL_DYNAMIC_UPDATE_FIELDS\n ):\n provider_info = MODEL_PROVIDERS_DICT.get(self.agent_llm)\n if provider_info:\n component_class = provider_info.get(\"component_class\")\n component_class = self.set_component_params(component_class)\n prefix = provider_info.get(\"prefix\")\n if component_class and hasattr(component_class, \"update_build_config\"):\n # Call each component class's update_build_config method\n # remove the prefix from the field_name\n if isinstance(field_name, str) and isinstance(prefix, str):\n field_name = field_name.replace(prefix, \"\")\n build_config = await update_component_build_config(\n component_class, build_config, field_value, \"model_name\"\n )\n return dotdict({k: v.to_dict() if hasattr(v, \"to_dict\") else v for k, v in build_config.items()})\n\n async def _get_tools(self) -> list[Tool]:\n component_toolkit = get_component_toolkit()\n tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\n # here we do not use the shared callbacks as we are exposing the agent as a tool\n callbacks=self.get_langchain_callbacks(),\n )\n if hasattr(self, \"tools_metadata\"):\n tools = component_toolkit(component=self, metadata=self.tools_metadata).update_tools_metadata(tools=tools)\n\n return tools\n" + "value": "import json\nimport re\n\nfrom langchain_core.tools import StructuredTool, Tool\nfrom pydantic import ValidationError\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.model_input_constants import (\n ALL_PROVIDER_FIELDS,\n MODEL_DYNAMIC_UPDATE_FIELDS,\n MODEL_PROVIDERS_DICT,\n MODEL_PROVIDERS_LIST,\n MODELS_METADATA,\n)\nfrom lfx.base.models.model_utils import get_model_name\nfrom lfx.components.helpers import CurrentDateComponent\nfrom lfx.components.langchain_utilities.tool_calling import ToolCallingAgentComponent\nfrom lfx.components.models_and_agents.memory import MemoryComponent\nfrom lfx.custom.custom_component.component import get_component_toolkit\nfrom lfx.custom.utils import update_component_build_config\nfrom lfx.helpers.base_model import build_model_from_schema\nfrom lfx.inputs.inputs import BoolInput, SecretStrInput, StrInput\nfrom lfx.io import DropdownInput, IntInput, MessageTextInput, MultilineInput, Output, TableInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(ToolCallingAgentComponent):\n display_name: str = \"Agent\"\n description: str = \"Define the agent's instructions, then enter a task to complete using tools.\"\n documentation: str = \"https://docs.langflow.org/agents\"\n icon = \"bot\"\n beta = False\n name = \"Agent\"\n\n memory_inputs = [set_advanced_true(component_input) for component_input in MemoryComponent().inputs]\n\n # Filter out json_mode from OpenAI inputs since we handle structured output differently\n if \"OpenAI\" in MODEL_PROVIDERS_DICT:\n openai_inputs_filtered = [\n input_field\n for input_field in MODEL_PROVIDERS_DICT[\"OpenAI\"][\"inputs\"]\n if not (hasattr(input_field, \"name\") and input_field.name == \"json_mode\")\n ]\n else:\n openai_inputs_filtered = []\n\n inputs = [\n DropdownInput(\n name=\"agent_llm\",\n display_name=\"Model Provider\",\n info=\"The provider of the language model that the agent will use to generate responses.\",\n options=[*MODEL_PROVIDERS_LIST],\n value=\"OpenAI\",\n real_time_refresh=True,\n refresh_button=False,\n input_types=[],\n options_metadata=[MODELS_METADATA[key] for key in MODEL_PROVIDERS_LIST if key in MODELS_METADATA],\n external_options={\n \"fields\": {\n \"data\": {\n \"node\": {\n \"name\": \"connect_other_models\",\n \"display_name\": \"Connect other models\",\n \"icon\": \"CornerDownLeft\",\n }\n }\n },\n },\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"The API key to use for the model.\",\n required=True,\n ),\n StrInput(\n name=\"base_url\",\n display_name=\"Base URL\",\n info=\"The base URL of the API.\",\n required=True,\n show=False,\n ),\n StrInput(\n name=\"project_id\",\n display_name=\"Project ID\",\n info=\"The project ID of the model.\",\n required=True,\n show=False,\n ),\n IntInput(\n name=\"max_output_tokens\",\n display_name=\"Max Output Tokens\",\n info=\"The maximum number of tokens to generate.\",\n show=False,\n ),\n *openai_inputs_filtered,\n MultilineInput(\n name=\"system_prompt\",\n display_name=\"Agent Instructions\",\n info=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\n advanced=False,\n ),\n MessageTextInput(\n name=\"context_id\",\n display_name=\"Context ID\",\n info=\"The context ID of the chat. Adds an extra layer to the local memory.\",\n value=\"\",\n advanced=True,\n ),\n IntInput(\n name=\"n_messages\",\n display_name=\"Number of Chat History Messages\",\n value=100,\n info=\"Number of chat history messages to retrieve.\",\n advanced=True,\n show=True,\n ),\n MultilineInput(\n name=\"format_instructions\",\n display_name=\"Output Format Instructions\",\n info=\"Generic Template for structured output formatting. Valid only with Structured response.\",\n value=(\n \"You are an AI that extracts structured JSON objects from unstructured text. \"\n \"Use a predefined schema with expected types (str, int, float, bool, dict). \"\n \"Extract ALL relevant instances that match the schema - if multiple patterns exist, capture them all. \"\n \"Fill missing or ambiguous values with defaults: null for missing values. \"\n \"Remove exact duplicates but keep variations that have different field values. \"\n \"Always return valid JSON in the expected format, never throw errors. \"\n \"If multiple objects can be extracted, return them all in the structured format.\"\n ),\n advanced=True,\n ),\n TableInput(\n name=\"output_schema\",\n display_name=\"Output Schema\",\n info=(\n \"Schema Validation: Define the structure and data types for structured output. \"\n \"No validation if no output schema.\"\n ),\n advanced=True,\n required=False,\n value=[],\n table_schema=[\n {\n \"name\": \"name\",\n \"display_name\": \"Name\",\n \"type\": \"str\",\n \"description\": \"Specify the name of the output field.\",\n \"default\": \"field\",\n \"edit_mode\": EditMode.INLINE,\n },\n {\n \"name\": \"description\",\n \"display_name\": \"Description\",\n \"type\": \"str\",\n \"description\": \"Describe the purpose of the output field.\",\n \"default\": \"description of field\",\n \"edit_mode\": EditMode.POPOVER,\n },\n {\n \"name\": \"type\",\n \"display_name\": \"Type\",\n \"type\": \"str\",\n \"edit_mode\": EditMode.INLINE,\n \"description\": (\"Indicate the data type of the output field (e.g., str, int, float, bool, dict).\"),\n \"options\": [\"str\", \"int\", \"float\", \"bool\", \"dict\"],\n \"default\": \"str\",\n },\n {\n \"name\": \"multiple\",\n \"display_name\": \"As List\",\n \"type\": \"boolean\",\n \"description\": \"Set to True if this output field should be a list of the specified type.\",\n \"default\": \"False\",\n \"edit_mode\": EditMode.INLINE,\n },\n ],\n ),\n *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\n BoolInput(\n name=\"add_current_date_tool\",\n display_name=\"Current Date\",\n advanced=True,\n info=\"If true, will add a tool to the agent that returns the current date.\",\n value=True,\n ),\n ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\n ]\n\n async def get_agent_requirements(self):\n \"\"\"Get the agent requirements for the agent.\"\"\"\n llm_model, display_name = await self.get_llm()\n if llm_model is None:\n msg = \"No language model selected. Please choose a model to proceed.\"\n raise ValueError(msg)\n self.model_name = get_model_name(llm_model, display_name=display_name)\n\n # Get memory data\n self.chat_history = await self.get_memory_data()\n await logger.adebug(f\"Retrieved {len(self.chat_history)} chat history messages\")\n if isinstance(self.chat_history, Message):\n self.chat_history = [self.chat_history]\n\n # Add current date tool if enabled\n if self.add_current_date_tool:\n if not isinstance(self.tools, list): # type: ignore[has-type]\n self.tools = []\n current_date_tool = (await CurrentDateComponent(**self.get_base_args()).to_toolkit()).pop(0)\n\n if not isinstance(current_date_tool, StructuredTool):\n msg = \"CurrentDateComponent must be converted to a StructuredTool\"\n raise TypeError(msg)\n self.tools.append(current_date_tool)\n\n # Set shared callbacks for tracing the tools used by the agent\n self.set_tools_callbacks(self.tools, self._get_shared_callbacks())\n\n return llm_model, self.chat_history, self.tools\n\n async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\n self.set(\n llm=llm_model,\n tools=self.tools or [],\n chat_history=self.chat_history,\n input_value=self.input_value,\n system_prompt=self.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components else \"\"\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n self.set(\n llm=llm_model,\n tools=self.tools or [],\n chat_history=self.chat_history,\n input_value=self.input_value,\n system_prompt=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\n )\n return [\n message for message in messages if getattr(message, \"id\", None) != getattr(self.input_value, \"id\", None)\n ]\n\n async def get_llm(self):\n if not isinstance(self.agent_llm, str):\n return self.agent_llm, None\n\n try:\n provider_info = MODEL_PROVIDERS_DICT.get(self.agent_llm)\n if not provider_info:\n msg = f\"Invalid model provider: {self.agent_llm}\"\n raise ValueError(msg)\n\n component_class = provider_info.get(\"component_class\")\n display_name = component_class.display_name\n inputs = provider_info.get(\"inputs\")\n prefix = provider_info.get(\"prefix\", \"\")\n\n return self._build_llm_model(component_class, inputs, prefix), display_name\n\n except (AttributeError, ValueError, TypeError, RuntimeError) as e:\n await logger.aerror(f\"Error building {self.agent_llm} language model: {e!s}\")\n msg = f\"Failed to initialize language model: {e!s}\"\n raise ValueError(msg) from e\n\n def _build_llm_model(self, component, inputs, prefix=\"\"):\n model_kwargs = {}\n for input_ in inputs:\n if hasattr(self, f\"{prefix}{input_.name}\"):\n model_kwargs[input_.name] = getattr(self, f\"{prefix}{input_.name}\")\n return component.set(**model_kwargs).build_model()\n\n def set_component_params(self, component):\n provider_info = MODEL_PROVIDERS_DICT.get(self.agent_llm)\n if provider_info:\n inputs = provider_info.get(\"inputs\")\n prefix = provider_info.get(\"prefix\")\n # Filter out json_mode and only use attributes that exist on this component\n model_kwargs = {}\n for input_ in inputs:\n if hasattr(self, f\"{prefix}{input_.name}\"):\n model_kwargs[input_.name] = getattr(self, f\"{prefix}{input_.name}\")\n\n return component.set(**model_kwargs)\n return component\n\n def delete_fields(self, build_config: dotdict, fields: dict | list[str]) -> None:\n \"\"\"Delete specified fields from build_config.\"\"\"\n for field in fields:\n if build_config is not None and field in build_config:\n build_config.pop(field, None)\n\n def update_input_types(self, build_config: dotdict) -> dotdict:\n \"\"\"Update input types for all fields in build_config.\"\"\"\n for key, value in build_config.items():\n if isinstance(value, dict):\n if value.get(\"input_types\") is None:\n build_config[key][\"input_types\"] = []\n elif hasattr(value, \"input_types\") and value.input_types is None:\n value.input_types = []\n return build_config\n\n async def update_build_config(\n self, build_config: dotdict, field_value: str, field_name: str | None = None\n ) -> dotdict:\n # Iterate over all providers in the MODEL_PROVIDERS_DICT\n # Existing logic for updating build_config\n if field_name in (\"agent_llm\",):\n build_config[\"agent_llm\"][\"value\"] = field_value\n provider_info = MODEL_PROVIDERS_DICT.get(field_value)\n if provider_info:\n component_class = provider_info.get(\"component_class\")\n if component_class and hasattr(component_class, \"update_build_config\"):\n # Call the component class's update_build_config method\n build_config = await update_component_build_config(\n component_class, build_config, field_value, \"model_name\"\n )\n\n provider_configs: dict[str, tuple[dict, list[dict]]] = {\n provider: (\n MODEL_PROVIDERS_DICT[provider][\"fields\"],\n [\n MODEL_PROVIDERS_DICT[other_provider][\"fields\"]\n for other_provider in MODEL_PROVIDERS_DICT\n if other_provider != provider\n ],\n )\n for provider in MODEL_PROVIDERS_DICT\n }\n if field_value in provider_configs:\n fields_to_add, fields_to_delete = provider_configs[field_value]\n\n # Delete fields from other providers\n for fields in fields_to_delete:\n self.delete_fields(build_config, fields)\n\n # Add provider-specific fields\n if field_value == \"OpenAI\" and not any(field in build_config for field in fields_to_add):\n build_config.update(fields_to_add)\n else:\n build_config.update(fields_to_add)\n # Reset input types for agent_llm\n build_config[\"agent_llm\"][\"input_types\"] = []\n build_config[\"agent_llm\"][\"display_name\"] = \"Model Provider\"\n elif field_value == \"connect_other_models\":\n # Delete all provider fields\n self.delete_fields(build_config, ALL_PROVIDER_FIELDS)\n # # Update with custom component\n custom_component = DropdownInput(\n name=\"agent_llm\",\n display_name=\"Language Model\",\n info=\"The provider of the language model that the agent will use to generate responses.\",\n options=[*MODEL_PROVIDERS_LIST],\n real_time_refresh=True,\n refresh_button=False,\n input_types=[\"LanguageModel\"],\n placeholder=\"Awaiting model input.\",\n options_metadata=[MODELS_METADATA[key] for key in MODEL_PROVIDERS_LIST if key in MODELS_METADATA],\n external_options={\n \"fields\": {\n \"data\": {\n \"node\": {\n \"name\": \"connect_other_models\",\n \"display_name\": \"Connect other models\",\n \"icon\": \"CornerDownLeft\",\n },\n }\n },\n },\n )\n build_config.update({\"agent_llm\": custom_component.to_dict()})\n # Update input types for all fields\n build_config = self.update_input_types(build_config)\n\n # Validate required keys\n default_keys = [\n \"code\",\n \"_type\",\n \"agent_llm\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\n ]\n missing_keys = [key for key in default_keys if key not in build_config]\n if missing_keys:\n msg = f\"Missing required keys in build_config: {missing_keys}\"\n raise ValueError(msg)\n if (\n isinstance(self.agent_llm, str)\n and self.agent_llm in MODEL_PROVIDERS_DICT\n and field_name in MODEL_DYNAMIC_UPDATE_FIELDS\n ):\n provider_info = MODEL_PROVIDERS_DICT.get(self.agent_llm)\n if provider_info:\n component_class = provider_info.get(\"component_class\")\n component_class = self.set_component_params(component_class)\n prefix = provider_info.get(\"prefix\")\n if component_class and hasattr(component_class, \"update_build_config\"):\n # Call each component class's update_build_config method\n # remove the prefix from the field_name\n if isinstance(field_name, str) and isinstance(prefix, str):\n field_name = field_name.replace(prefix, \"\")\n build_config = await update_component_build_config(\n component_class, build_config, field_value, \"model_name\"\n )\n return dotdict({k: v.to_dict() if hasattr(v, \"to_dict\") else v for k, v in build_config.items()})\n\n async def _get_tools(self) -> list[Tool]:\n component_toolkit = get_component_toolkit()\n tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\n # here we do not use the shared callbacks as we are exposing the agent as a tool\n callbacks=self.get_langchain_callbacks(),\n )\n if hasattr(self, \"tools_metadata\"):\n tools = component_toolkit(component=self, metadata=self.tools_metadata).update_tools_metadata(tools=tools)\n\n return tools\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -2468,6 +2539,7 @@ "tool_mode": false, "trace_as_input": true, "trace_as_metadata": true, + "track_in_telemetry": false, "type": "str", "value": "" }, @@ -2493,6 +2565,7 @@ "tool_mode": false, "trace_as_input": true, "trace_as_metadata": true, + "track_in_telemetry": false, "type": "str", "value": "You are an AI that extracts structured JSON objects from unstructured text. Use a predefined schema with expected types (str, int, float, bool, dict). Extract ALL relevant instances that match the schema - if multiple patterns exist, capture them all. Fill missing or ambiguous values with defaults: null for missing values. Remove exact duplicates but keep variations that have different field values. Always return valid JSON in the expected format, never throw errors. If multiple objects can be extracted, return them all in the structured format." }, @@ -2511,6 +2584,7 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "bool", "value": true }, @@ -2534,6 +2608,7 @@ "tool_mode": true, "trace_as_input": true, "trace_as_metadata": true, + "track_in_telemetry": false, "type": "str", "value": "" }, @@ -2552,6 +2627,7 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "int", "value": 15 }, @@ -2570,6 +2646,7 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "int", "value": "" }, @@ -2588,6 +2665,7 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "int", "value": 5 }, @@ -2612,6 +2690,7 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "int", "value": "" }, @@ -2630,6 +2709,7 @@ "title_case": false, "tool_mode": false, "trace_as_input": true, + "track_in_telemetry": false, "type": "dict", "value": {} }, @@ -2642,9 +2722,29 @@ "dynamic": false, "external_options": {}, "info": "To see the model names, first choose a provider. Then, enter your API key and click the refresh button next to the model name.", + "load_from_db": false, "name": "model_name", "options": [ - "gpt-4o" + "gpt-4o-mini", + "gpt-4o", + "gpt-4.1", + "gpt-4.1-mini", + "gpt-4.1-nano", + "gpt-4-turbo", + "gpt-4-turbo-preview", + "gpt-4", + "gpt-3.5-turbo", + "gpt-5.1", + "gpt-5", + "gpt-5-mini", + "gpt-5-nano", + "gpt-5-chat-latest", + "o1", + "o3-mini", + "o3", + "o3-pro", + "o4-mini", + "o4-mini-high" ], "options_metadata": [], "placeholder": "", @@ -2655,6 +2755,7 @@ "toggle": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "str", "value": "gpt-4o" }, @@ -2673,6 +2774,7 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "int", "value": 100 }, @@ -2692,6 +2794,7 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": false, "type": "str", "value": "" }, @@ -2752,6 +2855,7 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": false, "trigger_icon": "Table", "trigger_text": "Open table", "type": "table", @@ -2773,6 +2877,7 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": false, "type": "str", "value": "" }, @@ -2791,6 +2896,7 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "int", "value": 1 }, @@ -2816,6 +2922,7 @@ "tool_mode": false, "trace_as_input": true, "trace_as_metadata": true, + "track_in_telemetry": false, "type": "str", "value": "You are the OpenRAG Agent. You answer questions using retrieval, reasoning, and tool use.\nYou have access to several tools. Your job is to determine **which tool to use and when**.\n### Available Tools\n- OpenSearch Retrieval Tool:\n Use this to search the indexed knowledge base. Use when the user asks about product details, internal concepts, processes, architecture, documentation, roadmaps, or anything that may be stored in the index.\n- Conversation History:\n Use this to maintain continuity when the user is referring to previous turns. \n Do not treat history as a factual source.\n- Conversation File Context:\n Use this when the user asks about a document they uploaded or refers directly to its contents.\n- URL Ingestion Tool:\n Use this **only** when the user explicitly asks you to read, summarize, or analyze the content of a URL.\n Do not ingest URLs automatically.\n- Calculator / Expression Evaluation Tool:\n Use this when the user asks to compare numbers, compute estimates, calculate totals, analyze pricing, or answer any question requiring mathematics or quantitative reasoning.\n If the answer requires arithmetic, call the calculator tool rather than calculating internally.\n### Retrieval Decision Rules\nUse OpenSearch **whenever**:\n1. The question may be answered from internal or indexed data.\n2. The user references team names, product names, release plans, configurations, requirements, or official information.\n3. The user needs a factual, grounded answer.\nDo **not** use retrieval if:\n- The question is purely creative (e.g., storytelling, analogies) or personal preference.\n- The user simply wants text reformatted or rewritten from what is already present in the conversation.\nWhen uncertain → **Retrieve.** Retrieval is low risk and improves grounding.\n### URL Ingestion Rules\nOnly ingest URLs when the user explicitly says:\n- \"Read this link\"\n- \"Summarize this webpage\"\n- \"What does this site say?\"\n- \"Ingest this URL\"\nIf unclear → ask a clarifying question.\n### Calculator Usage Rules\nUse the calculator when:\n- Performing arithmetic\n- Estimating totals\n- Comparing values\n- Modeling cost, time, effort, scale, or projections\nDo not perform math internally. **Call the calculator tool instead.**\n### Answer Construction Rules\n1. When asked: \"What is OpenRAG\", answer the following:\n\"OpenRAG is an open-source package for building agentic RAG systems. It supports integration with a wide range of orchestration tools, vector databases, and LLM providers. OpenRAG connects and amplifies three popular, proven open-source projects into one powerful platform:\n**Langflow** – Langflow is a powerful tool to build and deploy AI agents and MCP servers. [Read more](https://www.langflow.org/)\n**OpenSearch** – OpenSearch is an open source, search and observability suite that brings order to unstructured data at scale. [Read more](https://opensearch.org/)\n**Docling** – Docling simplifies document processing with advanced PDF understanding, OCR support, and seamless AI integrations. Parse PDFs, DOCX, PPTX, images & more. [Read more](https://www.docling.ai/)\"\n2. Synthesize retrieved or ingested content in your own words.\n3. Support factual claims with citations in the format:\n (Source: )\n4. If no supporting evidence is found:\n Say: \"No relevant supporting sources were found for that request.\"\n5. Never invent facts or hallucinate details.\n6. Be concise, direct, and confident. \n7. Do not reveal internal chain-of-thought." }, @@ -2844,6 +2951,7 @@ "slider_input": false, "title_case": false, "tool_mode": false, + "track_in_telemetry": false, "type": "slider", "value": 0.1 }, @@ -2862,6 +2970,7 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "int", "value": 700 }, @@ -2882,6 +2991,7 @@ "show": true, "title_case": false, "trace_as_metadata": true, + "track_in_telemetry": false, "type": "other", "value": "" }, @@ -2900,6 +3010,7 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "bool", "value": true } @@ -2919,7 +3030,7 @@ "x": 1629.578423203229, "y": 451.946400444934 }, - "selected": true, + "selected": false, "type": "genericNode" }, { @@ -2941,7 +3052,7 @@ ], "frozen": false, "icon": "calculator", - "last_updated": "2025-11-11T21:40:50.133Z", + "last_updated": "2025-11-24T18:02:41.468Z", "legacy": false, "metadata": { "code_hash": "5fcfa26be77d", @@ -3037,6 +3148,7 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": false, "type": "tools", "value": [ { @@ -3074,24 +3186,24 @@ "width": 320 }, "position": { - "x": 720.5458956206228, - "y": 1220.8583072055585 + "x": 740.6385299138288, + "y": 1287.3185590984713 }, "selected": false, "type": "genericNode" } ], "viewport": { - "x": -37.553133726465376, - "y": 28.26764925979927, - "zoom": 0.6144649479685779 + "x": 91.72060325190853, + "y": -32.331644762411656, + "zoom": 0.6206869895512208 } }, "description": "OpenRAG OpenSearch Agent", "endpoint_name": null, "id": "1098eea1-6649-4e1d-aed1-b77249fb8dd0", "is_component": false, - "last_tested_version": "1.7.0", + "last_tested_version": "1.7.0.dev19", "name": "OpenRAG OpenSearch Agent", "tags": [ "assistants", diff --git a/flows/openrag_nudges.json b/flows/openrag_nudges.json index 5bbfef8d..702353a3 100644 --- a/flows/openrag_nudges.json +++ b/flows/openrag_nudges.json @@ -174,6 +174,7 @@ }, { "animated": false, + "className": "", "data": { "sourceHandle": { "dataType": "TextInput", @@ -1784,23 +1785,32 @@ "request_timeout", "max_retries", "show_progress_bar", - "model_kwargs" + "model_kwargs", + "truncate_input_tokens", + "input_text" ], "frozen": false, "icon": "binary", - "last_updated": "2025-11-11T21:49:56.409Z", "legacy": false, "metadata": { - "code_hash": "bb03f97be707", + "code_hash": "c5e0a4535a27", "dependencies": { "dependencies": [ + { + "name": "requests", + "version": "2.32.5" + }, + { + "name": "ibm_watsonx_ai", + "version": "1.4.2" + }, { "name": "langchain_openai", "version": "0.3.23" }, { "name": "lfx", - "version": null + "version": "0.2.0.dev19" }, { "name": "langchain_ollama", @@ -1815,9 +1825,9 @@ "version": "0.3.19" } ], - "total_dependencies": 5 + "total_dependencies": 7 }, - "module": "lfx.components.models.embedding_model.EmbeddingModelComponent" + "module": "custom_components.embedding_model" }, "minimized": false, "output_types": [], @@ -1844,7 +1854,7 @@ "_type": "Component", "api_base": { "_input_type": "MessageTextInput", - "advanced": false, + "advanced": true, "display_name": "API Base URL", "dynamic": false, "info": "Base URL for the API. Leave empty for default.", @@ -1862,6 +1872,7 @@ "tool_mode": false, "trace_as_input": true, "trace_as_metadata": true, + "track_in_telemetry": false, "type": "str", "value": "" }, @@ -1880,6 +1891,7 @@ "required": true, "show": true, "title_case": false, + "track_in_telemetry": false, "type": "str", "value": "OPENAI_API_KEY" }, @@ -1910,6 +1922,7 @@ "toggle": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "str", "value": "https://us-south.ml.cloud.ibm.com" }, @@ -1928,6 +1941,7 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "int", "value": 1000 }, @@ -1947,7 +1961,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from typing import Any\n\nfrom langchain_openai import OpenAIEmbeddings\n\nfrom lfx.base.embeddings.model import LCEmbeddingsModel\nfrom lfx.base.models.model_utils import get_ollama_models, is_valid_ollama_url\nfrom lfx.base.models.openai_constants import OPENAI_EMBEDDING_MODEL_NAMES\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS, WATSONX_EMBEDDING_MODEL_NAMES\nfrom lfx.field_typing import Embeddings\nfrom lfx.io import (\n BoolInput,\n DictInput,\n DropdownInput,\n FloatInput,\n IntInput,\n MessageTextInput,\n SecretStrInput,\n)\nfrom lfx.log.logger import logger\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.utils.util import transform_localhost_url\n\n# Ollama API constants\nHTTP_STATUS_OK = 200\nJSON_MODELS_KEY = \"models\"\nJSON_NAME_KEY = \"name\"\nJSON_CAPABILITIES_KEY = \"capabilities\"\nDESIRED_CAPABILITY = \"embedding\"\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass EmbeddingModelComponent(LCEmbeddingsModel):\n display_name = \"Embedding Model\"\n description = \"Generate embeddings using a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-embedding-models\"\n icon = \"binary\"\n name = \"EmbeddingModel\"\n category = \"models\"\n\n inputs = [\n DropdownInput(\n name=\"provider\",\n display_name=\"Model Provider\",\n options=[\"OpenAI\", \"Ollama\", \"IBM watsonx.ai\"],\n value=\"OpenAI\",\n info=\"Select the embedding model provider\",\n real_time_refresh=True,\n options_metadata=[{\"icon\": \"OpenAI\"}, {\"icon\": \"Ollama\"}, {\"icon\": \"WatsonxAI\"}],\n ),\n MessageTextInput(\n name=\"api_base\",\n display_name=\"API Base URL\",\n info=\"Base URL for the API. Leave empty for default.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n load_from_db=True,\n ),\n DropdownInput(\n name=\"base_url_ibm_watsonx\",\n display_name=\"watsonx API Endpoint\",\n info=\"The base URL of the API (IBM watsonx.ai only)\",\n options=IBM_WATSONX_URLS,\n value=IBM_WATSONX_URLS[0],\n show=False,\n real_time_refresh=True,\n ),\n DropdownInput(\n name=\"model\",\n display_name=\"Model Name\",\n options=OPENAI_EMBEDDING_MODEL_NAMES,\n value=OPENAI_EMBEDDING_MODEL_NAMES[0],\n info=\"Select the embedding model to use\",\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"OpenAI API Key\",\n info=\"Model Provider API key\",\n required=True,\n show=True,\n real_time_refresh=True,\n ),\n # Watson-specific inputs\n MessageTextInput(\n name=\"project_id\",\n display_name=\"Project ID\",\n info=\"IBM watsonx.ai Project ID (required for IBM watsonx.ai)\",\n show=False,\n ),\n IntInput(\n name=\"dimensions\",\n display_name=\"Dimensions\",\n info=\"The number of dimensions the resulting output embeddings should have. \"\n \"Only supported by certain models.\",\n advanced=True,\n ),\n IntInput(name=\"chunk_size\", display_name=\"Chunk Size\", advanced=True, value=1000),\n FloatInput(name=\"request_timeout\", display_name=\"Request Timeout\", advanced=True),\n IntInput(name=\"max_retries\", display_name=\"Max Retries\", advanced=True, value=3),\n BoolInput(name=\"show_progress_bar\", display_name=\"Show Progress Bar\", advanced=True),\n DictInput(\n name=\"model_kwargs\",\n display_name=\"Model Kwargs\",\n advanced=True,\n info=\"Additional keyword arguments to pass to the model.\",\n ),\n ]\n\n def build_embeddings(self) -> Embeddings:\n provider = self.provider\n model = self.model\n api_key = self.api_key\n api_base = self.api_base\n base_url_ibm_watsonx = self.base_url_ibm_watsonx\n ollama_base_url = self.ollama_base_url\n dimensions = self.dimensions\n chunk_size = self.chunk_size\n request_timeout = self.request_timeout\n max_retries = self.max_retries\n show_progress_bar = self.show_progress_bar\n model_kwargs = self.model_kwargs or {}\n\n if provider == \"OpenAI\":\n if not api_key:\n msg = \"OpenAI API key is required when using OpenAI provider\"\n raise ValueError(msg)\n return OpenAIEmbeddings(\n model=model,\n dimensions=dimensions or None,\n base_url=api_base or None,\n api_key=api_key,\n chunk_size=chunk_size,\n max_retries=max_retries,\n timeout=request_timeout or None,\n show_progress_bar=show_progress_bar,\n model_kwargs=model_kwargs,\n )\n\n if provider == \"Ollama\":\n try:\n from langchain_ollama import OllamaEmbeddings\n except ImportError:\n try:\n from langchain_community.embeddings import OllamaEmbeddings\n except ImportError:\n msg = \"Please install langchain-ollama: pip install langchain-ollama\"\n raise ImportError(msg) from None\n\n transformed_base_url = transform_localhost_url(ollama_base_url)\n\n # Check if URL contains /v1 suffix (OpenAI-compatible mode)\n if transformed_base_url and transformed_base_url.rstrip(\"/\").endswith(\"/v1\"):\n # Strip /v1 suffix and log warning\n transformed_base_url = transformed_base_url.rstrip(\"/\").removesuffix(\"/v1\")\n logger.warning(\n \"Detected '/v1' suffix in base URL. The Ollama component uses the native Ollama API, \"\n \"not the OpenAI-compatible API. The '/v1' suffix has been automatically removed. \"\n \"If you want to use the OpenAI-compatible API, please use the OpenAI component instead. \"\n \"Learn more at https://docs.ollama.com/openai#openai-compatibility\"\n )\n\n return OllamaEmbeddings(\n model=model,\n base_url=transformed_base_url or \"http://localhost:11434\",\n **model_kwargs,\n )\n\n if provider == \"IBM watsonx.ai\":\n try:\n from langchain_ibm import WatsonxEmbeddings\n except ImportError:\n msg = \"Please install langchain-ibm: pip install langchain-ibm\"\n raise ImportError(msg) from None\n\n if not api_key:\n msg = \"IBM watsonx.ai API key is required when using IBM watsonx.ai provider\"\n raise ValueError(msg)\n\n project_id = self.project_id\n\n if not project_id:\n msg = \"Project ID is required for IBM watsonx.ai provider\"\n raise ValueError(msg)\n\n params = {\n \"model_id\": model,\n \"url\": base_url_ibm_watsonx or \"https://us-south.ml.cloud.ibm.com\",\n \"apikey\": api_key,\n }\n\n params[\"project_id\"] = project_id\n\n return WatsonxEmbeddings(**params)\n\n msg = f\"Unknown provider: {provider}\"\n raise ValueError(msg)\n\n async def update_build_config(\n self, build_config: dotdict, field_value: Any, field_name: str | None = None\n ) -> dotdict:\n if field_name == \"provider\":\n if field_value == \"OpenAI\":\n build_config[\"model\"][\"options\"] = OPENAI_EMBEDDING_MODEL_NAMES\n build_config[\"model\"][\"value\"] = OPENAI_EMBEDDING_MODEL_NAMES[0]\n build_config[\"api_key\"][\"display_name\"] = \"OpenAI API Key\"\n build_config[\"api_key\"][\"required\"] = True\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"api_base\"][\"display_name\"] = \"OpenAI API Base URL\"\n build_config[\"api_base\"][\"advanced\"] = True\n build_config[\"api_base\"][\"show\"] = True\n build_config[\"ollama_base_url\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n\n elif field_value == \"Ollama\":\n build_config[\"ollama_base_url\"][\"show\"] = True\n\n if await is_valid_ollama_url(url=self.ollama_base_url):\n try:\n models = await get_ollama_models(\n base_url_value=self.ollama_base_url,\n desired_capability=DESIRED_CAPABILITY,\n json_models_key=JSON_MODELS_KEY,\n json_name_key=JSON_NAME_KEY,\n json_capabilities_key=JSON_CAPABILITIES_KEY,\n )\n build_config[\"model\"][\"options\"] = models\n build_config[\"model\"][\"value\"] = models[0] if models else \"\"\n except ValueError:\n build_config[\"model\"][\"options\"] = []\n build_config[\"model\"][\"value\"] = \"\"\n else:\n build_config[\"model\"][\"options\"] = []\n build_config[\"model\"][\"value\"] = \"\"\n\n build_config[\"api_key\"][\"display_name\"] = \"API Key (Optional)\"\n build_config[\"api_key\"][\"required\"] = False\n build_config[\"api_key\"][\"show\"] = False\n build_config[\"api_base\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n\n elif field_value == \"IBM watsonx.ai\":\n build_config[\"model\"][\"options\"] = WATSONX_EMBEDDING_MODEL_NAMES\n build_config[\"model\"][\"value\"] = WATSONX_EMBEDDING_MODEL_NAMES[0]\n build_config[\"api_key\"][\"display_name\"] = \"IBM watsonx.ai API Key\"\n build_config[\"api_key\"][\"required\"] = True\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"api_base\"][\"show\"] = False\n build_config[\"ollama_base_url\"][\"show\"] = False\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = True\n build_config[\"project_id\"][\"show\"] = True\n\n elif field_name == \"ollama_base_url\":\n # # Refresh Ollama models when base URL changes\n # if hasattr(self, \"provider\") and self.provider == \"Ollama\":\n # Use field_value if provided, otherwise fall back to instance attribute\n ollama_url = self.ollama_base_url\n if await is_valid_ollama_url(url=ollama_url):\n try:\n models = await get_ollama_models(\n base_url_value=ollama_url,\n desired_capability=DESIRED_CAPABILITY,\n json_models_key=JSON_MODELS_KEY,\n json_name_key=JSON_NAME_KEY,\n json_capabilities_key=JSON_CAPABILITIES_KEY,\n )\n build_config[\"model\"][\"options\"] = models\n build_config[\"model\"][\"value\"] = models[0] if models else \"\"\n except ValueError:\n await logger.awarning(\"Failed to fetch Ollama embedding models.\")\n build_config[\"model\"][\"options\"] = []\n build_config[\"model\"][\"value\"] = \"\"\n\n elif field_name == \"model\" and self.provider == \"Ollama\":\n ollama_url = self.ollama_base_url\n if await is_valid_ollama_url(url=ollama_url):\n try:\n models = await get_ollama_models(\n base_url_value=ollama_url,\n desired_capability=DESIRED_CAPABILITY,\n json_models_key=JSON_MODELS_KEY,\n json_name_key=JSON_NAME_KEY,\n json_capabilities_key=JSON_CAPABILITIES_KEY,\n )\n build_config[\"model\"][\"options\"] = models\n except ValueError:\n await logger.awarning(\"Failed to refresh Ollama embedding models.\")\n build_config[\"model\"][\"options\"] = []\n\n return build_config\n" + "value": "from typing import Any\n\nimport requests\nfrom ibm_watsonx_ai.metanames import EmbedTextParamsMetaNames\nfrom langchain_openai import OpenAIEmbeddings\n\nfrom lfx.base.embeddings.model import LCEmbeddingsModel\nfrom lfx.base.models.model_utils import get_ollama_models, is_valid_ollama_url\nfrom lfx.base.models.openai_constants import OPENAI_EMBEDDING_MODEL_NAMES\nfrom lfx.base.models.watsonx_constants import (\n IBM_WATSONX_URLS,\n WATSONX_EMBEDDING_MODEL_NAMES,\n)\nfrom lfx.field_typing import Embeddings\nfrom lfx.io import (\n BoolInput,\n DictInput,\n DropdownInput,\n FloatInput,\n IntInput,\n MessageTextInput,\n SecretStrInput,\n)\nfrom lfx.log.logger import logger\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.utils.util import transform_localhost_url\n\n# Ollama API constants\nHTTP_STATUS_OK = 200\nJSON_MODELS_KEY = \"models\"\nJSON_NAME_KEY = \"name\"\nJSON_CAPABILITIES_KEY = \"capabilities\"\nDESIRED_CAPABILITY = \"embedding\"\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass EmbeddingModelComponent(LCEmbeddingsModel):\n display_name = \"Embedding Model\"\n description = \"Generate embeddings using a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-embedding-models\"\n icon = \"binary\"\n name = \"EmbeddingModel\"\n category = \"models\"\n\n inputs = [\n DropdownInput(\n name=\"provider\",\n display_name=\"Model Provider\",\n options=[\"OpenAI\", \"Ollama\", \"IBM watsonx.ai\"],\n value=\"OpenAI\",\n info=\"Select the embedding model provider\",\n real_time_refresh=True,\n options_metadata=[{\"icon\": \"OpenAI\"}, {\"icon\": \"Ollama\"}, {\"icon\": \"WatsonxAI\"}],\n ),\n MessageTextInput(\n name=\"api_base\",\n display_name=\"API Base URL\",\n info=\"Base URL for the API. Leave empty for default.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n load_from_db=True,\n ),\n DropdownInput(\n name=\"base_url_ibm_watsonx\",\n display_name=\"watsonx API Endpoint\",\n info=\"The base URL of the API (IBM watsonx.ai only)\",\n options=IBM_WATSONX_URLS,\n value=IBM_WATSONX_URLS[0],\n show=False,\n real_time_refresh=True,\n ),\n DropdownInput(\n name=\"model\",\n display_name=\"Model Name\",\n options=OPENAI_EMBEDDING_MODEL_NAMES,\n value=OPENAI_EMBEDDING_MODEL_NAMES[0],\n info=\"Select the embedding model to use\",\n real_time_refresh=True,\n refresh_button=True,\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"OpenAI API Key\",\n info=\"Model Provider API key\",\n required=True,\n show=True,\n real_time_refresh=True,\n ),\n # Watson-specific inputs\n MessageTextInput(\n name=\"project_id\",\n display_name=\"Project ID\",\n info=\"IBM watsonx.ai Project ID (required for IBM watsonx.ai)\",\n show=False,\n ),\n IntInput(\n name=\"dimensions\",\n display_name=\"Dimensions\",\n info=\"The number of dimensions the resulting output embeddings should have. \"\n \"Only supported by certain models.\",\n advanced=True,\n ),\n IntInput(name=\"chunk_size\", display_name=\"Chunk Size\", advanced=True, value=1000),\n FloatInput(name=\"request_timeout\", display_name=\"Request Timeout\", advanced=True),\n IntInput(name=\"max_retries\", display_name=\"Max Retries\", advanced=True, value=3),\n BoolInput(name=\"show_progress_bar\", display_name=\"Show Progress Bar\", advanced=True),\n DictInput(\n name=\"model_kwargs\",\n display_name=\"Model Kwargs\",\n advanced=True,\n info=\"Additional keyword arguments to pass to the model.\",\n ),\n IntInput(\n name=\"truncate_input_tokens\",\n display_name=\"Truncate Input Tokens\",\n advanced=True,\n value=200,\n show=False,\n ),\n BoolInput(\n name=\"input_text\",\n display_name=\"Include the original text in the output\",\n value=True,\n advanced=True,\n show=False,\n ),\n ]\n\n @staticmethod\n def fetch_ibm_models(base_url: str) -> list[str]:\n \"\"\"Fetch available models from the watsonx.ai API.\"\"\"\n try:\n endpoint = f\"{base_url}/ml/v1/foundation_model_specs\"\n params = {\n \"version\": \"2024-09-16\",\n \"filters\": \"function_embedding,!lifecycle_withdrawn:and\",\n }\n response = requests.get(endpoint, params=params, timeout=10)\n response.raise_for_status()\n data = response.json()\n models = [model[\"model_id\"] for model in data.get(\"resources\", [])]\n return sorted(models)\n except Exception: # noqa: BLE001\n logger.exception(\"Error fetching models\")\n return WATSONX_EMBEDDING_MODEL_NAMES\n\n def build_embeddings(self) -> Embeddings:\n provider = self.provider\n model = self.model\n api_key = self.api_key\n api_base = self.api_base\n base_url_ibm_watsonx = self.base_url_ibm_watsonx\n ollama_base_url = self.ollama_base_url\n dimensions = self.dimensions\n chunk_size = self.chunk_size\n request_timeout = self.request_timeout\n max_retries = self.max_retries\n show_progress_bar = self.show_progress_bar\n model_kwargs = self.model_kwargs or {}\n\n if provider == \"OpenAI\":\n if not api_key:\n msg = \"OpenAI API key is required when using OpenAI provider\"\n raise ValueError(msg)\n return OpenAIEmbeddings(\n model=model,\n dimensions=dimensions or None,\n base_url=api_base or None,\n api_key=api_key,\n chunk_size=chunk_size,\n max_retries=max_retries,\n timeout=request_timeout or None,\n show_progress_bar=show_progress_bar,\n model_kwargs=model_kwargs,\n )\n\n if provider == \"Ollama\":\n try:\n from langchain_ollama import OllamaEmbeddings\n except ImportError:\n try:\n from langchain_community.embeddings import OllamaEmbeddings\n except ImportError:\n msg = \"Please install langchain-ollama: pip install langchain-ollama\"\n raise ImportError(msg) from None\n\n transformed_base_url = transform_localhost_url(ollama_base_url)\n\n # Check if URL contains /v1 suffix (OpenAI-compatible mode)\n if transformed_base_url and transformed_base_url.rstrip(\"/\").endswith(\"/v1\"):\n # Strip /v1 suffix and log warning\n transformed_base_url = transformed_base_url.rstrip(\"/\").removesuffix(\"/v1\")\n logger.warning(\n \"Detected '/v1' suffix in base URL. The Ollama component uses the native Ollama API, \"\n \"not the OpenAI-compatible API. The '/v1' suffix has been automatically removed. \"\n \"If you want to use the OpenAI-compatible API, please use the OpenAI component instead. \"\n \"Learn more at https://docs.ollama.com/openai#openai-compatibility\"\n )\n\n return OllamaEmbeddings(\n model=model,\n base_url=transformed_base_url or \"http://localhost:11434\",\n **model_kwargs,\n )\n\n if provider == \"IBM watsonx.ai\":\n try:\n from langchain_ibm import WatsonxEmbeddings\n except ImportError:\n msg = \"Please install langchain-ibm: pip install langchain-ibm\"\n raise ImportError(msg) from None\n\n if not api_key:\n msg = \"IBM watsonx.ai API key is required when using IBM watsonx.ai provider\"\n raise ValueError(msg)\n\n project_id = self.project_id\n\n if not project_id:\n msg = \"Project ID is required for IBM watsonx.ai provider\"\n raise ValueError(msg)\n\n from ibm_watsonx_ai import APIClient, Credentials\n\n credentials = Credentials(\n api_key=self.api_key,\n url=base_url_ibm_watsonx or \"https://us-south.ml.cloud.ibm.com\",\n )\n\n api_client = APIClient(credentials)\n\n params = {\n EmbedTextParamsMetaNames.TRUNCATE_INPUT_TOKENS: self.truncate_input_tokens,\n EmbedTextParamsMetaNames.RETURN_OPTIONS: {\"input_text\": self.input_text},\n }\n\n return WatsonxEmbeddings(\n model_id=model,\n params=params,\n watsonx_client=api_client,\n project_id=project_id,\n )\n\n msg = f\"Unknown provider: {provider}\"\n raise ValueError(msg)\n\n async def update_build_config(\n self, build_config: dotdict, field_value: Any, field_name: str | None = None\n ) -> dotdict:\n if field_name == \"provider\":\n if field_value == \"OpenAI\":\n build_config[\"model\"][\"options\"] = OPENAI_EMBEDDING_MODEL_NAMES\n build_config[\"model\"][\"value\"] = OPENAI_EMBEDDING_MODEL_NAMES[0]\n build_config[\"api_key\"][\"display_name\"] = \"OpenAI API Key\"\n build_config[\"api_key\"][\"required\"] = True\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"api_base\"][\"display_name\"] = \"OpenAI API Base URL\"\n build_config[\"api_base\"][\"advanced\"] = True\n build_config[\"api_base\"][\"show\"] = True\n build_config[\"ollama_base_url\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n build_config[\"truncate_input_tokens\"][\"show\"] = False\n build_config[\"input_text\"][\"show\"] = False\n elif field_value == \"Ollama\":\n build_config[\"ollama_base_url\"][\"show\"] = True\n\n if await is_valid_ollama_url(url=self.ollama_base_url):\n try:\n models = await get_ollama_models(\n base_url_value=self.ollama_base_url,\n desired_capability=DESIRED_CAPABILITY,\n json_models_key=JSON_MODELS_KEY,\n json_name_key=JSON_NAME_KEY,\n json_capabilities_key=JSON_CAPABILITIES_KEY,\n )\n build_config[\"model\"][\"options\"] = models\n build_config[\"model\"][\"value\"] = models[0] if models else \"\"\n except ValueError:\n build_config[\"model\"][\"options\"] = []\n build_config[\"model\"][\"value\"] = \"\"\n else:\n build_config[\"model\"][\"options\"] = []\n build_config[\"model\"][\"value\"] = \"\"\n build_config[\"truncate_input_tokens\"][\"show\"] = False\n build_config[\"input_text\"][\"show\"] = False\n build_config[\"api_key\"][\"display_name\"] = \"API Key (Optional)\"\n build_config[\"api_key\"][\"required\"] = False\n build_config[\"api_key\"][\"show\"] = False\n build_config[\"api_base\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n\n elif field_value == \"IBM watsonx.ai\":\n build_config[\"model\"][\"options\"] = self.fetch_ibm_models(base_url=self.base_url_ibm_watsonx)\n build_config[\"model\"][\"value\"] = self.fetch_ibm_models(base_url=self.base_url_ibm_watsonx)[0]\n build_config[\"api_key\"][\"display_name\"] = \"IBM watsonx.ai API Key\"\n build_config[\"api_key\"][\"required\"] = True\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"api_base\"][\"show\"] = False\n build_config[\"ollama_base_url\"][\"show\"] = False\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = True\n build_config[\"project_id\"][\"show\"] = True\n build_config[\"truncate_input_tokens\"][\"show\"] = True\n build_config[\"input_text\"][\"show\"] = True\n elif field_name == \"base_url_ibm_watsonx\":\n build_config[\"model\"][\"options\"] = self.fetch_ibm_models(base_url=field_value)\n build_config[\"model\"][\"value\"] = self.fetch_ibm_models(base_url=field_value)[0]\n elif field_name == \"ollama_base_url\":\n # # Refresh Ollama models when base URL changes\n # if hasattr(self, \"provider\") and self.provider == \"Ollama\":\n # Use field_value if provided, otherwise fall back to instance attribute\n ollama_url = self.ollama_base_url\n if await is_valid_ollama_url(url=ollama_url):\n try:\n models = await get_ollama_models(\n base_url_value=ollama_url,\n desired_capability=DESIRED_CAPABILITY,\n json_models_key=JSON_MODELS_KEY,\n json_name_key=JSON_NAME_KEY,\n json_capabilities_key=JSON_CAPABILITIES_KEY,\n )\n build_config[\"model\"][\"options\"] = models\n build_config[\"model\"][\"value\"] = models[0] if models else \"\"\n except ValueError:\n await logger.awarning(\"Failed to fetch Ollama embedding models.\")\n build_config[\"model\"][\"options\"] = []\n build_config[\"model\"][\"value\"] = \"\"\n\n elif field_name == \"model\" and self.provider == \"Ollama\":\n ollama_url = self.ollama_base_url\n if await is_valid_ollama_url(url=ollama_url):\n try:\n models = await get_ollama_models(\n base_url_value=ollama_url,\n desired_capability=DESIRED_CAPABILITY,\n json_models_key=JSON_MODELS_KEY,\n json_name_key=JSON_NAME_KEY,\n json_capabilities_key=JSON_CAPABILITIES_KEY,\n )\n build_config[\"model\"][\"options\"] = models\n except ValueError:\n await logger.awarning(\"Failed to refresh Ollama embedding models.\")\n build_config[\"model\"][\"options\"] = []\n\n return build_config\n" }, "dimensions": { "_input_type": "IntInput", @@ -1964,9 +1978,29 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "int", "value": "" }, + "input_text": { + "_input_type": "BoolInput", + "advanced": true, + "display_name": "Include the original text in the output", + "dynamic": false, + "info": "", + "list": false, + "list_add_label": "Add More", + "name": "input_text", + "placeholder": "", + "required": false, + "show": false, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "bool", + "value": true + }, "max_retries": { "_input_type": "IntInput", "advanced": true, @@ -1982,6 +2016,7 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "int", "value": 3 }, @@ -1996,16 +2031,21 @@ "info": "Select the embedding model to use", "name": "model", "options": [ - "text-embedding-3-small" + "text-embedding-3-small", + "text-embedding-3-large", + "text-embedding-ada-002" ], "options_metadata": [], "placeholder": "", + "real_time_refresh": true, + "refresh_button": true, "required": false, "show": true, "title_case": false, "toggle": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "str", "value": "text-embedding-3-small" }, @@ -2024,6 +2064,7 @@ "title_case": false, "tool_mode": false, "trace_as_input": true, + "track_in_telemetry": false, "type": "dict", "value": {} }, @@ -2048,6 +2089,7 @@ "tool_mode": false, "trace_as_input": true, "trace_as_metadata": true, + "track_in_telemetry": false, "type": "str", "value": "" }, @@ -2071,6 +2113,7 @@ "tool_mode": false, "trace_as_input": true, "trace_as_metadata": true, + "track_in_telemetry": false, "type": "str", "value": "" }, @@ -2108,6 +2151,7 @@ "toggle": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "str", "value": "OpenAI" }, @@ -2126,6 +2170,7 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "float", "value": "" }, @@ -2144,8 +2189,28 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "bool", "value": false + }, + "truncate_input_tokens": { + "_input_type": "IntInput", + "advanced": true, + "display_name": "Truncate Input Tokens", + "dynamic": false, + "info": "", + "list": false, + "list_add_label": "Add More", + "name": "truncate_input_tokens", + "placeholder": "", + "required": false, + "show": false, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "int", + "value": 200 } }, "tool_mode": false @@ -2156,7 +2221,7 @@ "dragging": false, "id": "EmbeddingModel-26o1e", "measured": { - "height": 451, + "height": 369, "width": 320 }, "position": { @@ -2195,7 +2260,7 @@ ], "frozen": false, "icon": "brain-circuit", - "last_updated": "2025-11-11T21:51:53.901Z", + "last_updated": "2025-11-24T18:03:45.208Z", "legacy": false, "metadata": { "code_hash": "694ffc4b17b8", @@ -2589,7 +2654,7 @@ "x": 2151.7120459180214, "y": 419.8653051925172 }, - "selected": true, + "selected": false, "type": "genericNode" }, { @@ -2707,16 +2772,16 @@ } ], "viewport": { - "x": -98.41517694131858, - "y": 122.09140842027057, - "zoom": 0.5043780525896302 + "x": -206.89994136968403, + "y": 74.41941219497068, + "zoom": 0.6126010241500153 } }, "description": "OpenRAG OpenSearch Nudges generator, based on the OpenSearch documents and the chat history.", "endpoint_name": null, "id": "ebc01d31-1976-46ce-a385-b0240327226c", "is_component": false, - "last_tested_version": "1.7.0", + "last_tested_version": "1.7.0.dev19", "name": "OpenRAG OpenSearch Nudges", "tags": [ "assistants", diff --git a/flows/openrag_url_mcp.json b/flows/openrag_url_mcp.json index 278ee2c8..0333faf1 100644 --- a/flows/openrag_url_mcp.json +++ b/flows/openrag_url_mcp.json @@ -231,6 +231,8 @@ "targetHandle": "{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-0XHyoœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œotherœ}" }, { + "animated": false, + "className": "", "data": { "sourceHandle": { "dataType": "EmbeddingModel", @@ -250,6 +252,7 @@ } }, "id": "xy-edge__EmbeddingModel-XjV5v{œdataTypeœ:œEmbeddingModelœ,œidœ:œEmbeddingModel-XjV5vœ,œnameœ:œembeddingsœ,œoutput_typesœ:[œEmbeddingsœ]}-OpenSearchHybrid-Ve6bS{œfieldNameœ:œembeddingœ,œidœ:œOpenSearchHybrid-Ve6bSœ,œinputTypesœ:[œEmbeddingsœ],œtypeœ:œotherœ}", + "selected": false, "source": "EmbeddingModel-XjV5v", "sourceHandle": "{œdataTypeœ:œEmbeddingModelœ,œidœ:œEmbeddingModel-XjV5vœ,œnameœ:œembeddingsœ,œoutput_typesœ:[œEmbeddingsœ]}", "target": "OpenSearchHybrid-Ve6bS", @@ -1553,7 +1556,7 @@ ], "frozen": false, "icon": "table", - "last_updated": "2025-11-11T15:07:55.352Z", + "last_updated": "2025-11-24T17:58:32.464Z", "legacy": false, "lf_version": "1.6.0", "metadata": { @@ -1969,7 +1972,7 @@ ], "frozen": false, "icon": "table", - "last_updated": "2025-11-11T15:07:55.353Z", + "last_updated": "2025-11-24T17:58:32.465Z", "legacy": false, "lf_version": "1.6.0", "metadata": { @@ -2385,7 +2388,7 @@ ], "frozen": false, "icon": "table", - "last_updated": "2025-11-11T15:07:55.353Z", + "last_updated": "2025-11-24T17:58:32.465Z", "legacy": false, "metadata": { "code_hash": "b4d6b19b6eef", @@ -3355,23 +3358,32 @@ "request_timeout", "max_retries", "show_progress_bar", - "model_kwargs" + "model_kwargs", + "truncate_input_tokens", + "input_text" ], "frozen": false, "icon": "binary", - "last_updated": "2025-11-11T15:08:05.989Z", "legacy": false, "metadata": { - "code_hash": "bb03f97be707", + "code_hash": "c5e0a4535a27", "dependencies": { "dependencies": [ + { + "name": "requests", + "version": "2.32.5" + }, + { + "name": "ibm_watsonx_ai", + "version": "1.4.2" + }, { "name": "langchain_openai", "version": "0.3.23" }, { "name": "lfx", - "version": null + "version": "0.2.0.dev19" }, { "name": "langchain_ollama", @@ -3386,9 +3398,9 @@ "version": "0.3.19" } ], - "total_dependencies": 5 + "total_dependencies": 7 }, - "module": "lfx.components.models.embedding_model.EmbeddingModelComponent" + "module": "custom_components.embedding_model" }, "minimized": false, "output_types": [], @@ -3433,6 +3445,7 @@ "tool_mode": false, "trace_as_input": true, "trace_as_metadata": true, + "track_in_telemetry": false, "type": "str", "value": "" }, @@ -3451,6 +3464,7 @@ "required": true, "show": true, "title_case": false, + "track_in_telemetry": false, "type": "str", "value": "OPENAI_API_KEY" }, @@ -3481,6 +3495,7 @@ "toggle": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "str", "value": "https://us-south.ml.cloud.ibm.com" }, @@ -3499,6 +3514,7 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "int", "value": 1000 }, @@ -3518,7 +3534,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from typing import Any\n\nfrom langchain_openai import OpenAIEmbeddings\n\nfrom lfx.base.embeddings.model import LCEmbeddingsModel\nfrom lfx.base.models.model_utils import get_ollama_models, is_valid_ollama_url\nfrom lfx.base.models.openai_constants import OPENAI_EMBEDDING_MODEL_NAMES\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS, WATSONX_EMBEDDING_MODEL_NAMES\nfrom lfx.field_typing import Embeddings\nfrom lfx.io import (\n BoolInput,\n DictInput,\n DropdownInput,\n FloatInput,\n IntInput,\n MessageTextInput,\n SecretStrInput,\n)\nfrom lfx.log.logger import logger\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.utils.util import transform_localhost_url\n\n# Ollama API constants\nHTTP_STATUS_OK = 200\nJSON_MODELS_KEY = \"models\"\nJSON_NAME_KEY = \"name\"\nJSON_CAPABILITIES_KEY = \"capabilities\"\nDESIRED_CAPABILITY = \"embedding\"\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass EmbeddingModelComponent(LCEmbeddingsModel):\n display_name = \"Embedding Model\"\n description = \"Generate embeddings using a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-embedding-models\"\n icon = \"binary\"\n name = \"EmbeddingModel\"\n category = \"models\"\n\n inputs = [\n DropdownInput(\n name=\"provider\",\n display_name=\"Model Provider\",\n options=[\"OpenAI\", \"Ollama\", \"IBM watsonx.ai\"],\n value=\"OpenAI\",\n info=\"Select the embedding model provider\",\n real_time_refresh=True,\n options_metadata=[{\"icon\": \"OpenAI\"}, {\"icon\": \"Ollama\"}, {\"icon\": \"WatsonxAI\"}],\n ),\n MessageTextInput(\n name=\"api_base\",\n display_name=\"API Base URL\",\n info=\"Base URL for the API. Leave empty for default.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n load_from_db=True,\n ),\n DropdownInput(\n name=\"base_url_ibm_watsonx\",\n display_name=\"watsonx API Endpoint\",\n info=\"The base URL of the API (IBM watsonx.ai only)\",\n options=IBM_WATSONX_URLS,\n value=IBM_WATSONX_URLS[0],\n show=False,\n real_time_refresh=True,\n ),\n DropdownInput(\n name=\"model\",\n display_name=\"Model Name\",\n options=OPENAI_EMBEDDING_MODEL_NAMES,\n value=OPENAI_EMBEDDING_MODEL_NAMES[0],\n info=\"Select the embedding model to use\",\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"OpenAI API Key\",\n info=\"Model Provider API key\",\n required=True,\n show=True,\n real_time_refresh=True,\n ),\n # Watson-specific inputs\n MessageTextInput(\n name=\"project_id\",\n display_name=\"Project ID\",\n info=\"IBM watsonx.ai Project ID (required for IBM watsonx.ai)\",\n show=False,\n ),\n IntInput(\n name=\"dimensions\",\n display_name=\"Dimensions\",\n info=\"The number of dimensions the resulting output embeddings should have. \"\n \"Only supported by certain models.\",\n advanced=True,\n ),\n IntInput(name=\"chunk_size\", display_name=\"Chunk Size\", advanced=True, value=1000),\n FloatInput(name=\"request_timeout\", display_name=\"Request Timeout\", advanced=True),\n IntInput(name=\"max_retries\", display_name=\"Max Retries\", advanced=True, value=3),\n BoolInput(name=\"show_progress_bar\", display_name=\"Show Progress Bar\", advanced=True),\n DictInput(\n name=\"model_kwargs\",\n display_name=\"Model Kwargs\",\n advanced=True,\n info=\"Additional keyword arguments to pass to the model.\",\n ),\n ]\n\n def build_embeddings(self) -> Embeddings:\n provider = self.provider\n model = self.model\n api_key = self.api_key\n api_base = self.api_base\n base_url_ibm_watsonx = self.base_url_ibm_watsonx\n ollama_base_url = self.ollama_base_url\n dimensions = self.dimensions\n chunk_size = self.chunk_size\n request_timeout = self.request_timeout\n max_retries = self.max_retries\n show_progress_bar = self.show_progress_bar\n model_kwargs = self.model_kwargs or {}\n\n if provider == \"OpenAI\":\n if not api_key:\n msg = \"OpenAI API key is required when using OpenAI provider\"\n raise ValueError(msg)\n return OpenAIEmbeddings(\n model=model,\n dimensions=dimensions or None,\n base_url=api_base or None,\n api_key=api_key,\n chunk_size=chunk_size,\n max_retries=max_retries,\n timeout=request_timeout or None,\n show_progress_bar=show_progress_bar,\n model_kwargs=model_kwargs,\n )\n\n if provider == \"Ollama\":\n try:\n from langchain_ollama import OllamaEmbeddings\n except ImportError:\n try:\n from langchain_community.embeddings import OllamaEmbeddings\n except ImportError:\n msg = \"Please install langchain-ollama: pip install langchain-ollama\"\n raise ImportError(msg) from None\n\n transformed_base_url = transform_localhost_url(ollama_base_url)\n\n # Check if URL contains /v1 suffix (OpenAI-compatible mode)\n if transformed_base_url and transformed_base_url.rstrip(\"/\").endswith(\"/v1\"):\n # Strip /v1 suffix and log warning\n transformed_base_url = transformed_base_url.rstrip(\"/\").removesuffix(\"/v1\")\n logger.warning(\n \"Detected '/v1' suffix in base URL. The Ollama component uses the native Ollama API, \"\n \"not the OpenAI-compatible API. The '/v1' suffix has been automatically removed. \"\n \"If you want to use the OpenAI-compatible API, please use the OpenAI component instead. \"\n \"Learn more at https://docs.ollama.com/openai#openai-compatibility\"\n )\n\n return OllamaEmbeddings(\n model=model,\n base_url=transformed_base_url or \"http://localhost:11434\",\n **model_kwargs,\n )\n\n if provider == \"IBM watsonx.ai\":\n try:\n from langchain_ibm import WatsonxEmbeddings\n except ImportError:\n msg = \"Please install langchain-ibm: pip install langchain-ibm\"\n raise ImportError(msg) from None\n\n if not api_key:\n msg = \"IBM watsonx.ai API key is required when using IBM watsonx.ai provider\"\n raise ValueError(msg)\n\n project_id = self.project_id\n\n if not project_id:\n msg = \"Project ID is required for IBM watsonx.ai provider\"\n raise ValueError(msg)\n\n params = {\n \"model_id\": model,\n \"url\": base_url_ibm_watsonx or \"https://us-south.ml.cloud.ibm.com\",\n \"apikey\": api_key,\n }\n\n params[\"project_id\"] = project_id\n\n return WatsonxEmbeddings(**params)\n\n msg = f\"Unknown provider: {provider}\"\n raise ValueError(msg)\n\n async def update_build_config(\n self, build_config: dotdict, field_value: Any, field_name: str | None = None\n ) -> dotdict:\n if field_name == \"provider\":\n if field_value == \"OpenAI\":\n build_config[\"model\"][\"options\"] = OPENAI_EMBEDDING_MODEL_NAMES\n build_config[\"model\"][\"value\"] = OPENAI_EMBEDDING_MODEL_NAMES[0]\n build_config[\"api_key\"][\"display_name\"] = \"OpenAI API Key\"\n build_config[\"api_key\"][\"required\"] = True\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"api_base\"][\"display_name\"] = \"OpenAI API Base URL\"\n build_config[\"api_base\"][\"advanced\"] = True\n build_config[\"api_base\"][\"show\"] = True\n build_config[\"ollama_base_url\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n\n elif field_value == \"Ollama\":\n build_config[\"ollama_base_url\"][\"show\"] = True\n\n if await is_valid_ollama_url(url=self.ollama_base_url):\n try:\n models = await get_ollama_models(\n base_url_value=self.ollama_base_url,\n desired_capability=DESIRED_CAPABILITY,\n json_models_key=JSON_MODELS_KEY,\n json_name_key=JSON_NAME_KEY,\n json_capabilities_key=JSON_CAPABILITIES_KEY,\n )\n build_config[\"model\"][\"options\"] = models\n build_config[\"model\"][\"value\"] = models[0] if models else \"\"\n except ValueError:\n build_config[\"model\"][\"options\"] = []\n build_config[\"model\"][\"value\"] = \"\"\n else:\n build_config[\"model\"][\"options\"] = []\n build_config[\"model\"][\"value\"] = \"\"\n\n build_config[\"api_key\"][\"display_name\"] = \"API Key (Optional)\"\n build_config[\"api_key\"][\"required\"] = False\n build_config[\"api_key\"][\"show\"] = False\n build_config[\"api_base\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n\n elif field_value == \"IBM watsonx.ai\":\n build_config[\"model\"][\"options\"] = WATSONX_EMBEDDING_MODEL_NAMES\n build_config[\"model\"][\"value\"] = WATSONX_EMBEDDING_MODEL_NAMES[0]\n build_config[\"api_key\"][\"display_name\"] = \"IBM watsonx.ai API Key\"\n build_config[\"api_key\"][\"required\"] = True\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"api_base\"][\"show\"] = False\n build_config[\"ollama_base_url\"][\"show\"] = False\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = True\n build_config[\"project_id\"][\"show\"] = True\n\n elif field_name == \"ollama_base_url\":\n # # Refresh Ollama models when base URL changes\n # if hasattr(self, \"provider\") and self.provider == \"Ollama\":\n # Use field_value if provided, otherwise fall back to instance attribute\n ollama_url = self.ollama_base_url\n if await is_valid_ollama_url(url=ollama_url):\n try:\n models = await get_ollama_models(\n base_url_value=ollama_url,\n desired_capability=DESIRED_CAPABILITY,\n json_models_key=JSON_MODELS_KEY,\n json_name_key=JSON_NAME_KEY,\n json_capabilities_key=JSON_CAPABILITIES_KEY,\n )\n build_config[\"model\"][\"options\"] = models\n build_config[\"model\"][\"value\"] = models[0] if models else \"\"\n except ValueError:\n await logger.awarning(\"Failed to fetch Ollama embedding models.\")\n build_config[\"model\"][\"options\"] = []\n build_config[\"model\"][\"value\"] = \"\"\n\n elif field_name == \"model\" and self.provider == \"Ollama\":\n ollama_url = self.ollama_base_url\n if await is_valid_ollama_url(url=ollama_url):\n try:\n models = await get_ollama_models(\n base_url_value=ollama_url,\n desired_capability=DESIRED_CAPABILITY,\n json_models_key=JSON_MODELS_KEY,\n json_name_key=JSON_NAME_KEY,\n json_capabilities_key=JSON_CAPABILITIES_KEY,\n )\n build_config[\"model\"][\"options\"] = models\n except ValueError:\n await logger.awarning(\"Failed to refresh Ollama embedding models.\")\n build_config[\"model\"][\"options\"] = []\n\n return build_config\n" + "value": "from typing import Any\n\nimport requests\nfrom ibm_watsonx_ai.metanames import EmbedTextParamsMetaNames\nfrom langchain_openai import OpenAIEmbeddings\n\nfrom lfx.base.embeddings.model import LCEmbeddingsModel\nfrom lfx.base.models.model_utils import get_ollama_models, is_valid_ollama_url\nfrom lfx.base.models.openai_constants import OPENAI_EMBEDDING_MODEL_NAMES\nfrom lfx.base.models.watsonx_constants import (\n IBM_WATSONX_URLS,\n WATSONX_EMBEDDING_MODEL_NAMES,\n)\nfrom lfx.field_typing import Embeddings\nfrom lfx.io import (\n BoolInput,\n DictInput,\n DropdownInput,\n FloatInput,\n IntInput,\n MessageTextInput,\n SecretStrInput,\n)\nfrom lfx.log.logger import logger\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.utils.util import transform_localhost_url\n\n# Ollama API constants\nHTTP_STATUS_OK = 200\nJSON_MODELS_KEY = \"models\"\nJSON_NAME_KEY = \"name\"\nJSON_CAPABILITIES_KEY = \"capabilities\"\nDESIRED_CAPABILITY = \"embedding\"\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass EmbeddingModelComponent(LCEmbeddingsModel):\n display_name = \"Embedding Model\"\n description = \"Generate embeddings using a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-embedding-models\"\n icon = \"binary\"\n name = \"EmbeddingModel\"\n category = \"models\"\n\n inputs = [\n DropdownInput(\n name=\"provider\",\n display_name=\"Model Provider\",\n options=[\"OpenAI\", \"Ollama\", \"IBM watsonx.ai\"],\n value=\"OpenAI\",\n info=\"Select the embedding model provider\",\n real_time_refresh=True,\n options_metadata=[{\"icon\": \"OpenAI\"}, {\"icon\": \"Ollama\"}, {\"icon\": \"WatsonxAI\"}],\n ),\n MessageTextInput(\n name=\"api_base\",\n display_name=\"API Base URL\",\n info=\"Base URL for the API. Leave empty for default.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n load_from_db=True,\n ),\n DropdownInput(\n name=\"base_url_ibm_watsonx\",\n display_name=\"watsonx API Endpoint\",\n info=\"The base URL of the API (IBM watsonx.ai only)\",\n options=IBM_WATSONX_URLS,\n value=IBM_WATSONX_URLS[0],\n show=False,\n real_time_refresh=True,\n ),\n DropdownInput(\n name=\"model\",\n display_name=\"Model Name\",\n options=OPENAI_EMBEDDING_MODEL_NAMES,\n value=OPENAI_EMBEDDING_MODEL_NAMES[0],\n info=\"Select the embedding model to use\",\n real_time_refresh=True,\n refresh_button=True,\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"OpenAI API Key\",\n info=\"Model Provider API key\",\n required=True,\n show=True,\n real_time_refresh=True,\n ),\n # Watson-specific inputs\n MessageTextInput(\n name=\"project_id\",\n display_name=\"Project ID\",\n info=\"IBM watsonx.ai Project ID (required for IBM watsonx.ai)\",\n show=False,\n ),\n IntInput(\n name=\"dimensions\",\n display_name=\"Dimensions\",\n info=\"The number of dimensions the resulting output embeddings should have. \"\n \"Only supported by certain models.\",\n advanced=True,\n ),\n IntInput(name=\"chunk_size\", display_name=\"Chunk Size\", advanced=True, value=1000),\n FloatInput(name=\"request_timeout\", display_name=\"Request Timeout\", advanced=True),\n IntInput(name=\"max_retries\", display_name=\"Max Retries\", advanced=True, value=3),\n BoolInput(name=\"show_progress_bar\", display_name=\"Show Progress Bar\", advanced=True),\n DictInput(\n name=\"model_kwargs\",\n display_name=\"Model Kwargs\",\n advanced=True,\n info=\"Additional keyword arguments to pass to the model.\",\n ),\n IntInput(\n name=\"truncate_input_tokens\",\n display_name=\"Truncate Input Tokens\",\n advanced=True,\n value=200,\n show=False,\n ),\n BoolInput(\n name=\"input_text\",\n display_name=\"Include the original text in the output\",\n value=True,\n advanced=True,\n show=False,\n ),\n ]\n\n @staticmethod\n def fetch_ibm_models(base_url: str) -> list[str]:\n \"\"\"Fetch available models from the watsonx.ai API.\"\"\"\n try:\n endpoint = f\"{base_url}/ml/v1/foundation_model_specs\"\n params = {\n \"version\": \"2024-09-16\",\n \"filters\": \"function_embedding,!lifecycle_withdrawn:and\",\n }\n response = requests.get(endpoint, params=params, timeout=10)\n response.raise_for_status()\n data = response.json()\n models = [model[\"model_id\"] for model in data.get(\"resources\", [])]\n return sorted(models)\n except Exception: # noqa: BLE001\n logger.exception(\"Error fetching models\")\n return WATSONX_EMBEDDING_MODEL_NAMES\n\n def build_embeddings(self) -> Embeddings:\n provider = self.provider\n model = self.model\n api_key = self.api_key\n api_base = self.api_base\n base_url_ibm_watsonx = self.base_url_ibm_watsonx\n ollama_base_url = self.ollama_base_url\n dimensions = self.dimensions\n chunk_size = self.chunk_size\n request_timeout = self.request_timeout\n max_retries = self.max_retries\n show_progress_bar = self.show_progress_bar\n model_kwargs = self.model_kwargs or {}\n\n if provider == \"OpenAI\":\n if not api_key:\n msg = \"OpenAI API key is required when using OpenAI provider\"\n raise ValueError(msg)\n return OpenAIEmbeddings(\n model=model,\n dimensions=dimensions or None,\n base_url=api_base or None,\n api_key=api_key,\n chunk_size=chunk_size,\n max_retries=max_retries,\n timeout=request_timeout or None,\n show_progress_bar=show_progress_bar,\n model_kwargs=model_kwargs,\n )\n\n if provider == \"Ollama\":\n try:\n from langchain_ollama import OllamaEmbeddings\n except ImportError:\n try:\n from langchain_community.embeddings import OllamaEmbeddings\n except ImportError:\n msg = \"Please install langchain-ollama: pip install langchain-ollama\"\n raise ImportError(msg) from None\n\n transformed_base_url = transform_localhost_url(ollama_base_url)\n\n # Check if URL contains /v1 suffix (OpenAI-compatible mode)\n if transformed_base_url and transformed_base_url.rstrip(\"/\").endswith(\"/v1\"):\n # Strip /v1 suffix and log warning\n transformed_base_url = transformed_base_url.rstrip(\"/\").removesuffix(\"/v1\")\n logger.warning(\n \"Detected '/v1' suffix in base URL. The Ollama component uses the native Ollama API, \"\n \"not the OpenAI-compatible API. The '/v1' suffix has been automatically removed. \"\n \"If you want to use the OpenAI-compatible API, please use the OpenAI component instead. \"\n \"Learn more at https://docs.ollama.com/openai#openai-compatibility\"\n )\n\n return OllamaEmbeddings(\n model=model,\n base_url=transformed_base_url or \"http://localhost:11434\",\n **model_kwargs,\n )\n\n if provider == \"IBM watsonx.ai\":\n try:\n from langchain_ibm import WatsonxEmbeddings\n except ImportError:\n msg = \"Please install langchain-ibm: pip install langchain-ibm\"\n raise ImportError(msg) from None\n\n if not api_key:\n msg = \"IBM watsonx.ai API key is required when using IBM watsonx.ai provider\"\n raise ValueError(msg)\n\n project_id = self.project_id\n\n if not project_id:\n msg = \"Project ID is required for IBM watsonx.ai provider\"\n raise ValueError(msg)\n\n from ibm_watsonx_ai import APIClient, Credentials\n\n credentials = Credentials(\n api_key=self.api_key,\n url=base_url_ibm_watsonx or \"https://us-south.ml.cloud.ibm.com\",\n )\n\n api_client = APIClient(credentials)\n\n params = {\n EmbedTextParamsMetaNames.TRUNCATE_INPUT_TOKENS: self.truncate_input_tokens,\n EmbedTextParamsMetaNames.RETURN_OPTIONS: {\"input_text\": self.input_text},\n }\n\n return WatsonxEmbeddings(\n model_id=model,\n params=params,\n watsonx_client=api_client,\n project_id=project_id,\n )\n\n msg = f\"Unknown provider: {provider}\"\n raise ValueError(msg)\n\n async def update_build_config(\n self, build_config: dotdict, field_value: Any, field_name: str | None = None\n ) -> dotdict:\n if field_name == \"provider\":\n if field_value == \"OpenAI\":\n build_config[\"model\"][\"options\"] = OPENAI_EMBEDDING_MODEL_NAMES\n build_config[\"model\"][\"value\"] = OPENAI_EMBEDDING_MODEL_NAMES[0]\n build_config[\"api_key\"][\"display_name\"] = \"OpenAI API Key\"\n build_config[\"api_key\"][\"required\"] = True\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"api_base\"][\"display_name\"] = \"OpenAI API Base URL\"\n build_config[\"api_base\"][\"advanced\"] = True\n build_config[\"api_base\"][\"show\"] = True\n build_config[\"ollama_base_url\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n build_config[\"truncate_input_tokens\"][\"show\"] = False\n build_config[\"input_text\"][\"show\"] = False\n elif field_value == \"Ollama\":\n build_config[\"ollama_base_url\"][\"show\"] = True\n\n if await is_valid_ollama_url(url=self.ollama_base_url):\n try:\n models = await get_ollama_models(\n base_url_value=self.ollama_base_url,\n desired_capability=DESIRED_CAPABILITY,\n json_models_key=JSON_MODELS_KEY,\n json_name_key=JSON_NAME_KEY,\n json_capabilities_key=JSON_CAPABILITIES_KEY,\n )\n build_config[\"model\"][\"options\"] = models\n build_config[\"model\"][\"value\"] = models[0] if models else \"\"\n except ValueError:\n build_config[\"model\"][\"options\"] = []\n build_config[\"model\"][\"value\"] = \"\"\n else:\n build_config[\"model\"][\"options\"] = []\n build_config[\"model\"][\"value\"] = \"\"\n build_config[\"truncate_input_tokens\"][\"show\"] = False\n build_config[\"input_text\"][\"show\"] = False\n build_config[\"api_key\"][\"display_name\"] = \"API Key (Optional)\"\n build_config[\"api_key\"][\"required\"] = False\n build_config[\"api_key\"][\"show\"] = False\n build_config[\"api_base\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n\n elif field_value == \"IBM watsonx.ai\":\n build_config[\"model\"][\"options\"] = self.fetch_ibm_models(base_url=self.base_url_ibm_watsonx)\n build_config[\"model\"][\"value\"] = self.fetch_ibm_models(base_url=self.base_url_ibm_watsonx)[0]\n build_config[\"api_key\"][\"display_name\"] = \"IBM watsonx.ai API Key\"\n build_config[\"api_key\"][\"required\"] = True\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"api_base\"][\"show\"] = False\n build_config[\"ollama_base_url\"][\"show\"] = False\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = True\n build_config[\"project_id\"][\"show\"] = True\n build_config[\"truncate_input_tokens\"][\"show\"] = True\n build_config[\"input_text\"][\"show\"] = True\n elif field_name == \"base_url_ibm_watsonx\":\n build_config[\"model\"][\"options\"] = self.fetch_ibm_models(base_url=field_value)\n build_config[\"model\"][\"value\"] = self.fetch_ibm_models(base_url=field_value)[0]\n elif field_name == \"ollama_base_url\":\n # # Refresh Ollama models when base URL changes\n # if hasattr(self, \"provider\") and self.provider == \"Ollama\":\n # Use field_value if provided, otherwise fall back to instance attribute\n ollama_url = self.ollama_base_url\n if await is_valid_ollama_url(url=ollama_url):\n try:\n models = await get_ollama_models(\n base_url_value=ollama_url,\n desired_capability=DESIRED_CAPABILITY,\n json_models_key=JSON_MODELS_KEY,\n json_name_key=JSON_NAME_KEY,\n json_capabilities_key=JSON_CAPABILITIES_KEY,\n )\n build_config[\"model\"][\"options\"] = models\n build_config[\"model\"][\"value\"] = models[0] if models else \"\"\n except ValueError:\n await logger.awarning(\"Failed to fetch Ollama embedding models.\")\n build_config[\"model\"][\"options\"] = []\n build_config[\"model\"][\"value\"] = \"\"\n\n elif field_name == \"model\" and self.provider == \"Ollama\":\n ollama_url = self.ollama_base_url\n if await is_valid_ollama_url(url=ollama_url):\n try:\n models = await get_ollama_models(\n base_url_value=ollama_url,\n desired_capability=DESIRED_CAPABILITY,\n json_models_key=JSON_MODELS_KEY,\n json_name_key=JSON_NAME_KEY,\n json_capabilities_key=JSON_CAPABILITIES_KEY,\n )\n build_config[\"model\"][\"options\"] = models\n except ValueError:\n await logger.awarning(\"Failed to refresh Ollama embedding models.\")\n build_config[\"model\"][\"options\"] = []\n\n return build_config\n" }, "dimensions": { "_input_type": "IntInput", @@ -3535,9 +3551,29 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "int", "value": "" }, + "input_text": { + "_input_type": "BoolInput", + "advanced": true, + "display_name": "Include the original text in the output", + "dynamic": false, + "info": "", + "list": false, + "list_add_label": "Add More", + "name": "input_text", + "placeholder": "", + "required": false, + "show": false, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "bool", + "value": true + }, "max_retries": { "_input_type": "IntInput", "advanced": true, @@ -3553,6 +3589,7 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "int", "value": 3 }, @@ -3573,12 +3610,15 @@ ], "options_metadata": [], "placeholder": "", + "real_time_refresh": true, + "refresh_button": true, "required": false, "show": true, "title_case": false, "toggle": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "str", "value": "text-embedding-3-small" }, @@ -3597,6 +3637,7 @@ "title_case": false, "tool_mode": false, "trace_as_input": true, + "track_in_telemetry": false, "type": "dict", "value": {} }, @@ -3621,6 +3662,7 @@ "tool_mode": false, "trace_as_input": true, "trace_as_metadata": true, + "track_in_telemetry": false, "type": "str", "value": "" }, @@ -3644,6 +3686,7 @@ "tool_mode": false, "trace_as_input": true, "trace_as_metadata": true, + "track_in_telemetry": false, "type": "str", "value": "" }, @@ -3681,6 +3724,7 @@ "toggle": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "str", "value": "OpenAI" }, @@ -3699,6 +3743,7 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "float", "value": "" }, @@ -3717,8 +3762,28 @@ "title_case": false, "tool_mode": false, "trace_as_metadata": true, + "track_in_telemetry": true, "type": "bool", "value": false + }, + "truncate_input_tokens": { + "_input_type": "IntInput", + "advanced": true, + "display_name": "Truncate Input Tokens", + "dynamic": false, + "info": "", + "list": false, + "list_add_label": "Add More", + "name": "truncate_input_tokens", + "placeholder": "", + "required": false, + "show": false, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "int", + "value": 200 } }, "tool_mode": false @@ -3736,22 +3801,22 @@ "x": 2066.3681917820168, "y": 2053.0594731518368 }, - "selected": false, + "selected": true, "type": "genericNode" } ], "viewport": { - "x": -376.03081395180425, - "y": -548.1133759270245, - "zoom": 0.5197673958703216 + "x": -425.92312760235086, + "y": -699.5243463797829, + "zoom": 0.5890166507565666 } }, "description": "This flow is to ingest the URL to open search.", "endpoint_name": null, "id": "72c3d17c-2dac-4a73-b48a-6518473d7830", "is_component": false, - "last_tested_version": "1.7.0", "mcp_enabled": true, + "last_tested_version": "1.7.0.dev19", "name": "OpenSearch URL Ingestion Flow", "tags": [ "openai", From 6495cd96e98d0b5ae7ebbc87055fb282eceb39b6 Mon Sep 17 00:00:00 2001 From: phact Date: Mon, 24 Nov 2025 18:04:13 -0500 Subject: [PATCH 03/24] switch to langflowai/ in dockerhub --- .github/workflows/build-multiarch.yml | 64 +++++++++++++-------------- Makefile | 10 ++--- docker-compose-cpu.yml | 8 ++-- docker-compose.yml | 8 ++-- 4 files changed, 45 insertions(+), 45 deletions(-) diff --git a/.github/workflows/build-multiarch.yml b/.github/workflows/build-multiarch.yml index b8ee1a28..f516b62d 100644 --- a/.github/workflows/build-multiarch.yml +++ b/.github/workflows/build-multiarch.yml @@ -53,13 +53,13 @@ jobs: # backend - image: backend file: ./Dockerfile.backend - tag: phact/openrag-backend + tag: langflowai/openrag-backend platform: linux/amd64 arch: amd64 runs-on: ubuntu-latest-16-cores - image: backend file: ./Dockerfile.backend - tag: phact/openrag-backend + tag: langflowai/openrag-backend platform: linux/arm64 arch: arm64 runs-on: [self-hosted, linux, ARM64, langflow-ai-arm64-2] @@ -67,13 +67,13 @@ jobs: # frontend - image: frontend file: ./Dockerfile.frontend - tag: phact/openrag-frontend + tag: langflowai/openrag-frontend platform: linux/amd64 arch: amd64 runs-on: ubuntu-latest-16-cores - image: frontend file: ./Dockerfile.frontend - tag: phact/openrag-frontend + tag: langflowai/openrag-frontend platform: linux/arm64 arch: arm64 runs-on: [self-hosted, linux, ARM64, langflow-ai-arm64-2] @@ -81,13 +81,13 @@ jobs: # langflow - image: langflow file: ./Dockerfile.langflow - tag: phact/openrag-langflow + tag: langflowai/openrag-langflow platform: linux/amd64 arch: amd64 runs-on: ubuntu-latest-16-cores - image: langflow file: ./Dockerfile.langflow - tag: phact/openrag-langflow + tag: langflowai/openrag-langflow platform: linux/arm64 arch: arm64 runs-on: self-hosted @@ -95,13 +95,13 @@ jobs: # opensearch - image: opensearch file: ./Dockerfile - tag: phact/openrag-opensearch + tag: langflowai/openrag-opensearch platform: linux/amd64 arch: amd64 runs-on: ubuntu-latest-16-cores - image: opensearch file: ./Dockerfile - tag: phact/openrag-opensearch + tag: langflowai/openrag-opensearch platform: linux/arm64 arch: arm64 runs-on: [self-hosted, linux, ARM64, langflow-ai-arm64-2] @@ -165,40 +165,40 @@ jobs: VERSION=${{ steps.version.outputs.version }} # Create versioned tags - docker buildx imagetools create -t phact/openrag-backend:$VERSION \ - phact/openrag-backend:$VERSION-amd64 \ - phact/openrag-backend:$VERSION-arm64 + docker buildx imagetools create -t langflowai/openrag-backend:$VERSION \ + langflowai/openrag-backend:$VERSION-amd64 \ + langflowai/openrag-backend:$VERSION-arm64 - docker buildx imagetools create -t phact/openrag-frontend:$VERSION \ - phact/openrag-frontend:$VERSION-amd64 \ - phact/openrag-frontend:$VERSION-arm64 + docker buildx imagetools create -t langflowai/openrag-frontend:$VERSION \ + langflowai/openrag-frontend:$VERSION-amd64 \ + langflowai/openrag-frontend:$VERSION-arm64 - docker buildx imagetools create -t phact/openrag-langflow:$VERSION \ - phact/openrag-langflow:$VERSION-amd64 \ - phact/openrag-langflow:$VERSION-arm64 + docker buildx imagetools create -t langflowai/openrag-langflow:$VERSION \ + langflowai/openrag-langflow:$VERSION-amd64 \ + langflowai/openrag-langflow:$VERSION-arm64 - docker buildx imagetools create -t phact/openrag-opensearch:$VERSION \ - phact/openrag-opensearch:$VERSION-amd64 \ - phact/openrag-opensearch:$VERSION-arm64 + docker buildx imagetools create -t langflowai/openrag-opensearch:$VERSION \ + langflowai/openrag-opensearch:$VERSION-amd64 \ + langflowai/openrag-opensearch:$VERSION-arm64 # Only update latest tags if version is numeric if [[ "$VERSION" =~ ^[0-9.-]+$ ]]; then echo "Updating latest tags for production release: $VERSION" - docker buildx imagetools create -t phact/openrag-backend:latest \ - phact/openrag-backend:$VERSION-amd64 \ - phact/openrag-backend:$VERSION-arm64 + docker buildx imagetools create -t langflowai/openrag-backend:latest \ + langflowai/openrag-backend:$VERSION-amd64 \ + langflowai/openrag-backend:$VERSION-arm64 - docker buildx imagetools create -t phact/openrag-frontend:latest \ - phact/openrag-frontend:$VERSION-amd64 \ - phact/openrag-frontend:$VERSION-arm64 + docker buildx imagetools create -t langflowai/openrag-frontend:latest \ + langflowai/openrag-frontend:$VERSION-amd64 \ + langflowai/openrag-frontend:$VERSION-arm64 - docker buildx imagetools create -t phact/openrag-langflow:latest \ - phact/openrag-langflow:$VERSION-amd64 \ - phact/openrag-langflow:$VERSION-arm64 + docker buildx imagetools create -t langflowai/openrag-langflow:latest \ + langflowai/openrag-langflow:$VERSION-amd64 \ + langflowai/openrag-langflow:$VERSION-arm64 - docker buildx imagetools create -t phact/openrag-opensearch:latest \ - phact/openrag-opensearch:$VERSION-amd64 \ - phact/openrag-opensearch:$VERSION-arm64 + docker buildx imagetools create -t langflowai/openrag-opensearch:latest \ + langflowai/openrag-opensearch:$VERSION-amd64 \ + langflowai/openrag-opensearch:$VERSION-arm64 else echo "Skipping latest tags - version: $VERSION (not numeric)" fi diff --git a/Makefile b/Makefile index f47bba28..b5804f77 100644 --- a/Makefile +++ b/Makefile @@ -210,7 +210,7 @@ test-ci: echo "Pulling latest images..."; \ docker compose -f docker-compose-cpu.yml pull; \ echo "Building OpenSearch image override..."; \ - docker build --no-cache -t phact/openrag-opensearch:latest -f Dockerfile .; \ + docker build --no-cache -t langflowai/openrag-opensearch:latest -f Dockerfile .; \ echo "Starting infra (OpenSearch + Dashboards + Langflow) with CPU containers"; \ docker compose -f docker-compose-cpu.yml up -d opensearch dashboards langflow; \ echo "Starting docling-serve..."; \ @@ -288,10 +288,10 @@ test-ci-local: echo "Cleaning up old containers and volumes..."; \ docker compose -f docker-compose-cpu.yml down -v 2>/dev/null || true; \ echo "Building all images locally..."; \ - docker build -t phact/openrag-opensearch:latest -f Dockerfile .; \ - docker build -t phact/openrag-backend:latest -f Dockerfile.backend .; \ - docker build -t phact/openrag-frontend:latest -f Dockerfile.frontend .; \ - docker build -t phact/openrag-langflow:latest -f Dockerfile.langflow .; \ + docker build -t langflowai/openrag-opensearch:latest -f Dockerfile .; \ + docker build -t langflowai/openrag-backend:latest -f Dockerfile.backend .; \ + docker build -t langflowai/openrag-frontend:latest -f Dockerfile.frontend .; \ + docker build -t langflowai/openrag-langflow:latest -f Dockerfile.langflow .; \ echo "Starting infra (OpenSearch + Dashboards + Langflow) with CPU containers"; \ docker compose -f docker-compose-cpu.yml up -d opensearch dashboards langflow; \ echo "Starting docling-serve..."; \ diff --git a/docker-compose-cpu.yml b/docker-compose-cpu.yml index 50e118b7..c5cd15f3 100644 --- a/docker-compose-cpu.yml +++ b/docker-compose-cpu.yml @@ -1,6 +1,6 @@ services: opensearch: - image: phact/openrag-opensearch:${OPENRAG_VERSION:-latest} + image: langflowai/openrag-opensearch:${OPENRAG_VERSION:-latest} #build: # context: . # dockerfile: Dockerfile @@ -44,7 +44,7 @@ services: - "5601:5601" openrag-backend: - image: phact/openrag-backend:${OPENRAG_VERSION:-latest} + image: langflowai/openrag-backend:${OPENRAG_VERSION:-latest} # build: # context: . # dockerfile: Dockerfile.backend @@ -86,7 +86,7 @@ services: - ./flows:/app/flows:U,z openrag-frontend: - image: phact/openrag-frontend:${OPENRAG_VERSION:-latest} + image: langflowai/openrag-frontend:${OPENRAG_VERSION:-latest} # build: # context: . # dockerfile: Dockerfile.frontend @@ -101,7 +101,7 @@ services: langflow: volumes: - ./flows:/app/flows:U,z - image: phact/openrag-langflow:${LANGFLOW_VERSION:-latest} + image: langflowai/openrag-langflow:${LANGFLOW_VERSION:-latest} # build: # context: . # dockerfile: Dockerfile.langflow diff --git a/docker-compose.yml b/docker-compose.yml index 7ba0cea8..fbba580f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: opensearch: - image: phact/openrag-opensearch:${OPENRAG_VERSION:-latest} + image: langflowai/openrag-opensearch:${OPENRAG_VERSION:-latest} #build: #context: . #dockerfile: Dockerfile @@ -44,7 +44,7 @@ services: - "5601:5601" openrag-backend: - image: phact/openrag-backend:${OPENRAG_VERSION:-latest} + image: langflowai/openrag-backend:${OPENRAG_VERSION:-latest} # build: # context: . # dockerfile: Dockerfile.backend @@ -88,7 +88,7 @@ services: gpus: all openrag-frontend: - image: phact/openrag-frontend:${OPENRAG_VERSION:-latest} + image: langflowai/openrag-frontend:${OPENRAG_VERSION:-latest} # build: # context: . # dockerfile: Dockerfile.frontend @@ -103,7 +103,7 @@ services: langflow: volumes: - ./flows:/app/flows:U,z - image: phact/openrag-langflow:${LANGFLOW_VERSION:-latest} + image: langflowai/openrag-langflow:${LANGFLOW_VERSION:-latest} # build: # context: . # dockerfile: Dockerfile.langflow From 88b90a17b2a7aeebf7d7fdbf9e0d3ec9438cf1bf Mon Sep 17 00:00:00 2001 From: phact Date: Mon, 24 Nov 2025 18:10:25 -0500 Subject: [PATCH 04/24] v0.1.39-rc1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 09ce2e9b..8065286e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "openrag" -version = "0.1.38" +version = "0.1.39-rc1" description = "Add your description here" readme = "README.md" requires-python = ">=3.13" From f370afbd221c779e08a097b9d999dc28552c53de Mon Sep 17 00:00:00 2001 From: phact Date: Mon, 24 Nov 2025 21:27:19 -0500 Subject: [PATCH 05/24] rc2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8065286e..b4003082 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "openrag" -version = "0.1.39-rc1" +version = "0.1.39-rc2" description = "Add your description here" readme = "README.md" requires-python = ">=3.13" From 608fc798b065b1f73b696d5f56449ff3f819a6c7 Mon Sep 17 00:00:00 2001 From: Lucas Oliveira Date: Tue, 25 Nov 2025 18:09:47 -0300 Subject: [PATCH 06/24] Deleted docker compose cpu, added gpu override --- docker-compose-cpu.yml | 141 ----------------------------------------- docker-compose.gpu.yml | 7 ++ docker-compose.yml | 6 +- 3 files changed, 9 insertions(+), 145 deletions(-) delete mode 100644 docker-compose-cpu.yml create mode 100644 docker-compose.gpu.yml diff --git a/docker-compose-cpu.yml b/docker-compose-cpu.yml deleted file mode 100644 index 50e118b7..00000000 --- a/docker-compose-cpu.yml +++ /dev/null @@ -1,141 +0,0 @@ -services: - opensearch: - image: phact/openrag-opensearch:${OPENRAG_VERSION:-latest} - #build: - # context: . - # dockerfile: Dockerfile - container_name: os - depends_on: - - openrag-backend - environment: - - discovery.type=single-node - - OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_PASSWORD} - # Run security setup in background after OpenSearch starts - command: > - bash -c " - # Ensure data directory has correct permissions - sudo chown -R opensearch:opensearch /usr/share/opensearch/data || true - - # Start OpenSearch in background - /usr/share/opensearch/opensearch-docker-entrypoint.sh opensearch & - - # Wait a bit for OpenSearch to start, then apply security config - sleep 10 && /usr/share/opensearch/setup-security.sh & - - # Wait for background processes - wait - " - ports: - - "9200:9200" - - "9600:9600" - volumes: - - ${OPENSEARCH_DATA_PATH:-./opensearch-data}:/usr/share/opensearch/data:Z - - dashboards: - image: opensearchproject/opensearch-dashboards:3.0.0 - container_name: osdash - depends_on: - - opensearch - environment: - OPENSEARCH_HOSTS: '["https://opensearch:9200"]' - OPENSEARCH_USERNAME: "admin" - OPENSEARCH_PASSWORD: ${OPENSEARCH_PASSWORD} - ports: - - "5601:5601" - - openrag-backend: - image: phact/openrag-backend:${OPENRAG_VERSION:-latest} - # build: - # context: . - # dockerfile: Dockerfile.backend - container_name: openrag-backend - depends_on: - - langflow - environment: - - OPENSEARCH_HOST=opensearch - - LANGFLOW_URL=http://langflow:7860 - - LANGFLOW_PUBLIC_URL=${LANGFLOW_PUBLIC_URL} - - LANGFLOW_AUTO_LOGIN=${LANGFLOW_AUTO_LOGIN} - - LANGFLOW_SECRET_KEY=${LANGFLOW_SECRET_KEY} - - LANGFLOW_SUPERUSER=${LANGFLOW_SUPERUSER} - - LANGFLOW_SUPERUSER_PASSWORD=${LANGFLOW_SUPERUSER_PASSWORD} - - LANGFLOW_CHAT_FLOW_ID=${LANGFLOW_CHAT_FLOW_ID} - - LANGFLOW_INGEST_FLOW_ID=${LANGFLOW_INGEST_FLOW_ID} - - LANGFLOW_URL_INGEST_FLOW_ID=${LANGFLOW_URL_INGEST_FLOW_ID} - - DISABLE_INGEST_WITH_LANGFLOW=${DISABLE_INGEST_WITH_LANGFLOW:-false} - - NUDGES_FLOW_ID=${NUDGES_FLOW_ID} - - OPENSEARCH_PORT=9200 - - OPENSEARCH_USERNAME=admin - - OPENSEARCH_PASSWORD=${OPENSEARCH_PASSWORD} - - OPENAI_API_KEY=${OPENAI_API_KEY} - - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} - - WATSONX_API_KEY=${WATSONX_API_KEY} - - WATSONX_ENDPOINT=${WATSONX_ENDPOINT} - - WATSONX_PROJECT_ID=${WATSONX_PROJECT_ID} - - OLLAMA_ENDPOINT=${OLLAMA_ENDPOINT} - - GOOGLE_OAUTH_CLIENT_ID=${GOOGLE_OAUTH_CLIENT_ID} - - GOOGLE_OAUTH_CLIENT_SECRET=${GOOGLE_OAUTH_CLIENT_SECRET} - - MICROSOFT_GRAPH_OAUTH_CLIENT_ID=${MICROSOFT_GRAPH_OAUTH_CLIENT_ID} - - MICROSOFT_GRAPH_OAUTH_CLIENT_SECRET=${MICROSOFT_GRAPH_OAUTH_CLIENT_SECRET} - - WEBHOOK_BASE_URL=${WEBHOOK_BASE_URL} - - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} - - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} - volumes: - - ./documents:/app/documents:Z - - ./keys:/app/keys:Z - - ./flows:/app/flows:U,z - - openrag-frontend: - image: phact/openrag-frontend:${OPENRAG_VERSION:-latest} - # build: - # context: . - # dockerfile: Dockerfile.frontend - container_name: openrag-frontend - depends_on: - - openrag-backend - environment: - - OPENRAG_BACKEND_HOST=openrag-backend - ports: - - "3000:3000" - - langflow: - volumes: - - ./flows:/app/flows:U,z - image: phact/openrag-langflow:${LANGFLOW_VERSION:-latest} - # build: - # context: . - # dockerfile: Dockerfile.langflow - container_name: langflow - ports: - - "7860:7860" - environment: - - LANGFLOW_DEACTIVATE_TRACING=true - - OPENAI_API_KEY=${OPENAI_API_KEY} - - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} - - WATSONX_API_KEY=${WATSONX_API_KEY} - - WATSONX_ENDPOINT=${WATSONX_ENDPOINT} - - WATSONX_PROJECT_ID=${WATSONX_PROJECT_ID} - - OLLAMA_BASE_URL=${OLLAMA_ENDPOINT} - - LANGFLOW_LOAD_FLOWS_PATH=/app/flows - - LANGFLOW_SECRET_KEY=${LANGFLOW_SECRET_KEY} - - JWT=None - - OWNER=None - - OWNER_NAME=None - - OWNER_EMAIL=None - - CONNECTOR_TYPE=system - - CONNECTOR_TYPE_URL=url - - OPENRAG-QUERY-FILTER="{}" - - OPENSEARCH_PASSWORD=${OPENSEARCH_PASSWORD} - - FILENAME=None - - MIMETYPE=None - - FILESIZE=0 - - LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT=JWT,OPENRAG-QUERY-FILTER,OPENSEARCH_PASSWORD,OWNER,OWNER_NAME,OWNER_EMAIL,CONNECTOR_TYPE,FILENAME,MIMETYPE,FILESIZE - - LANGFLOW_LOG_LEVEL=DEBUG - - LANGFLOW_AUTO_LOGIN=${LANGFLOW_AUTO_LOGIN} - - LANGFLOW_SUPERUSER=${LANGFLOW_SUPERUSER} - - LANGFLOW_SUPERUSER_PASSWORD=${LANGFLOW_SUPERUSER_PASSWORD} - - LANGFLOW_NEW_USER_IS_ACTIVE=${LANGFLOW_NEW_USER_IS_ACTIVE} - - LANGFLOW_ENABLE_SUPERUSER_CLI=${LANGFLOW_ENABLE_SUPERUSER_CLI} - # - DEFAULT_FOLDER_NAME=OpenRAG - - HIDE_GETTING_STARTED_PROGRESS=true - diff --git a/docker-compose.gpu.yml b/docker-compose.gpu.yml new file mode 100644 index 00000000..4496c4ac --- /dev/null +++ b/docker-compose.gpu.yml @@ -0,0 +1,7 @@ +services: + openrag-backend: + environment: + - NVIDIA_DRIVER_CAPABILITIES=compute,utility + - NVIDIA_VISIBLE_DEVICES=all + gpus: all + diff --git a/docker-compose.yml b/docker-compose.yml index 7ba0cea8..2627aa14 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -56,6 +56,7 @@ services: - LANGFLOW_URL=http://langflow:7860 - LANGFLOW_PUBLIC_URL=${LANGFLOW_PUBLIC_URL} - LANGFLOW_AUTO_LOGIN=${LANGFLOW_AUTO_LOGIN} + - LANGFLOW_SECRET_KEY=${LANGFLOW_SECRET_KEY} - LANGFLOW_SUPERUSER=${LANGFLOW_SUPERUSER} - LANGFLOW_SUPERUSER_PASSWORD=${LANGFLOW_SUPERUSER_PASSWORD} - LANGFLOW_CHAT_FLOW_ID=${LANGFLOW_CHAT_FLOW_ID} @@ -72,8 +73,6 @@ services: - WATSONX_ENDPOINT=${WATSONX_ENDPOINT} - WATSONX_PROJECT_ID=${WATSONX_PROJECT_ID} - OLLAMA_ENDPOINT=${OLLAMA_ENDPOINT} - - NVIDIA_DRIVER_CAPABILITIES=compute,utility - - NVIDIA_VISIBLE_DEVICES=all - GOOGLE_OAUTH_CLIENT_ID=${GOOGLE_OAUTH_CLIENT_ID} - GOOGLE_OAUTH_CLIENT_SECRET=${GOOGLE_OAUTH_CLIENT_SECRET} - MICROSOFT_GRAPH_OAUTH_CLIENT_ID=${MICROSOFT_GRAPH_OAUTH_CLIENT_ID} @@ -85,7 +84,6 @@ services: - ./documents:/app/documents:Z - ./keys:/app/keys:Z - ./flows:/app/flows:U,z - gpus: all openrag-frontend: image: phact/openrag-frontend:${OPENRAG_VERSION:-latest} @@ -127,10 +125,10 @@ services: - CONNECTOR_TYPE=system - CONNECTOR_TYPE_URL=url - OPENRAG-QUERY-FILTER="{}" + - OPENSEARCH_PASSWORD=${OPENSEARCH_PASSWORD} - FILENAME=None - MIMETYPE=None - FILESIZE=0 - - OPENSEARCH_PASSWORD=${OPENSEARCH_PASSWORD} - LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT=JWT,OPENRAG-QUERY-FILTER,OPENSEARCH_PASSWORD,OWNER,OWNER_NAME,OWNER_EMAIL,CONNECTOR_TYPE,FILENAME,MIMETYPE,FILESIZE - LANGFLOW_LOG_LEVEL=DEBUG - LANGFLOW_AUTO_LOGIN=${LANGFLOW_AUTO_LOGIN} From 28cb9fc26b80367cd00d85b8f16cdec38d202e21 Mon Sep 17 00:00:00 2001 From: Lucas Oliveira Date: Tue, 25 Nov 2025 18:10:02 -0300 Subject: [PATCH 07/24] Changed TUI implementation to support new docker compose override --- src/tui/main.py | 2 +- src/tui/managers/container_manager.py | 132 ++++++++++++++++---------- src/tui/screens/monitor.py | 12 +-- 3 files changed, 91 insertions(+), 55 deletions(-) diff --git a/src/tui/main.py b/src/tui/main.py index 19468473..d27db184 100644 --- a/src/tui/main.py +++ b/src/tui/main.py @@ -485,7 +485,7 @@ def copy_compose_files(*, force: bool = False) -> None: logger.debug(f"Could not access compose assets: {e}") return - for filename in ("docker-compose.yml", "docker-compose-cpu.yml"): + for filename in ("docker-compose.yml", "docker-compose.gpu.yml"): destination = Path(filename) if destination.exists() and not force: continue diff --git a/src/tui/managers/container_manager.py b/src/tui/managers/container_manager.py index 2953d550..53fc385e 100644 --- a/src/tui/managers/container_manager.py +++ b/src/tui/managers/container_manager.py @@ -56,15 +56,15 @@ class ContainerManager: self.platform_detector = PlatformDetector() self.runtime_info = self.platform_detector.detect_runtime() self.compose_file = compose_file or self._find_compose_file("docker-compose.yml") - self.cpu_compose_file = self._find_compose_file("docker-compose-cpu.yml") + self.gpu_compose_file = self._find_compose_file("docker-compose.gpu.yml") self.services_cache: Dict[str, ServiceInfo] = {} self.last_status_update = 0 - # Auto-select CPU compose if no GPU available + # Auto-select GPU override if GPU is available try: has_gpu, _ = detect_gpu_devices() - self.use_cpu_compose = not has_gpu + self.use_gpu_compose = has_gpu except Exception: - self.use_cpu_compose = True + self.use_gpu_compose = False # Expected services based on compose files self.expected_services = [ @@ -143,9 +143,15 @@ class ContainerManager: return False, "", "No container runtime available" if cpu_mode is None: - cpu_mode = self.use_cpu_compose - compose_file = self.cpu_compose_file if cpu_mode else self.compose_file - cmd = self.runtime_info.compose_command + ["-f", str(compose_file)] + args + use_gpu = self.use_gpu_compose + else: + use_gpu = not cpu_mode + + # Build compose command with override pattern + cmd = self.runtime_info.compose_command + ["-f", str(self.compose_file)] + if use_gpu and self.gpu_compose_file.exists(): + cmd.extend(["-f", str(self.gpu_compose_file)]) + cmd.extend(args) try: process = await asyncio.create_subprocess_exec( @@ -179,9 +185,15 @@ class ContainerManager: return if cpu_mode is None: - cpu_mode = self.use_cpu_compose - compose_file = self.cpu_compose_file if cpu_mode else self.compose_file - cmd = self.runtime_info.compose_command + ["-f", str(compose_file)] + args + use_gpu = self.use_gpu_compose + else: + use_gpu = not cpu_mode + + # Build compose command with override pattern + cmd = self.runtime_info.compose_command + ["-f", str(self.compose_file)] + if use_gpu and self.gpu_compose_file.exists(): + cmd.extend(["-f", str(self.gpu_compose_file)]) + cmd.extend(args) try: process = await asyncio.create_subprocess_exec( @@ -242,9 +254,15 @@ class ContainerManager: return if cpu_mode is None: - cpu_mode = self.use_cpu_compose - compose_file = self.cpu_compose_file if cpu_mode else self.compose_file - cmd = self.runtime_info.compose_command + ["-f", str(compose_file)] + args + use_gpu = self.use_gpu_compose + else: + use_gpu = not cpu_mode + + # Build compose command with override pattern + cmd = self.runtime_info.compose_command + ["-f", str(self.compose_file)] + if use_gpu and self.gpu_compose_file.exists(): + cmd.extend(["-f", str(self.gpu_compose_file)]) + cmd.extend(args) try: process = await asyncio.create_subprocess_exec( @@ -551,44 +569,61 @@ class ContainerManager: """Get resolved image names from compose files using docker/podman compose, with robust fallbacks.""" images: set[str] = set() - compose_files = [self.compose_file, self.cpu_compose_file] - for compose_file in compose_files: + # Try both GPU and CPU modes to get all images + for use_gpu in [True, False]: try: - if not compose_file or not compose_file.exists(): - continue + # Build compose command with override pattern + cmd = self.runtime_info.compose_command + ["-f", str(self.compose_file)] + if use_gpu and self.gpu_compose_file.exists(): + cmd.extend(["-f", str(self.gpu_compose_file)]) + cmd.extend(["config", "--format", "json"]) - cpu_mode = (compose_file == self.cpu_compose_file) - - # Try JSON format first - success, stdout, _ = await self._run_compose_command( - ["config", "--format", "json"], - cpu_mode=cpu_mode + process = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + cwd=Path.cwd(), ) + stdout, stderr = await process.communicate() + stdout_text = stdout.decode() if stdout else "" - if success and stdout.strip(): - from_cfg = self._extract_images_from_compose_config(stdout, tried_json=True) + if process.returncode == 0 and stdout_text.strip(): + from_cfg = self._extract_images_from_compose_config(stdout_text, tried_json=True) if from_cfg: images.update(from_cfg) - continue # this compose file succeeded; move to next file + continue # Fallback to YAML output (for older compose versions) - success, stdout, _ = await self._run_compose_command( - ["config"], - cpu_mode=cpu_mode - ) + cmd = self.runtime_info.compose_command + ["-f", str(self.compose_file)] + if use_gpu and self.gpu_compose_file.exists(): + cmd.extend(["-f", str(self.gpu_compose_file)]) + cmd.append("config") - if success and stdout.strip(): - from_cfg = self._extract_images_from_compose_config(stdout, tried_json=False) + process = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + cwd=Path.cwd(), + ) + stdout, stderr = await process.communicate() + stdout_text = stdout.decode() if stdout else "" + + if process.returncode == 0 and stdout_text.strip(): + from_cfg = self._extract_images_from_compose_config(stdout_text, tried_json=False) if from_cfg: images.update(from_cfg) continue except Exception: - # Keep behavior resilient—just continue to next file + # Keep behavior resilient—just continue to next mode continue # Fallback: manual parsing if compose config didn't work if not images: + compose_files = [self.compose_file] + if self.gpu_compose_file.exists(): + compose_files.append(self.gpu_compose_file) + for compose in compose_files: try: if not compose.exists(): @@ -638,8 +673,11 @@ class ContainerManager: yield False, "No container runtime available" return - # Diagnostic info about compose files - compose_file = self.cpu_compose_file if (cpu_mode if cpu_mode is not None else self.use_cpu_compose) else self.compose_file + # Determine GPU mode + if cpu_mode is None: + use_gpu = self.use_gpu_compose + else: + use_gpu = not cpu_mode # Show the search process for debugging if hasattr(self, '_compose_search_log'): @@ -650,9 +688,12 @@ class ContainerManager: # Show runtime detection info runtime_cmd_str = " ".join(self.runtime_info.compose_command) yield False, f"Using compose command: {runtime_cmd_str}", False - yield False, f"Final compose file: {compose_file.absolute()}", False - if not compose_file.exists(): - yield False, f"ERROR: Compose file not found at {compose_file.absolute()}", False + compose_files_str = str(self.compose_file.absolute()) + if use_gpu and self.gpu_compose_file.exists(): + compose_files_str += f" + {self.gpu_compose_file.absolute()}" + yield False, f"Compose files: {compose_files_str}", False + if not self.compose_file.exists(): + yield False, f"ERROR: Base compose file not found at {self.compose_file.absolute()}", False return yield False, "Starting OpenRAG services...", False @@ -786,16 +827,11 @@ class ContainerManager: yield "No container runtime available" return - compose_file = ( - self.cpu_compose_file if self.use_cpu_compose else self.compose_file - ) - cmd = self.runtime_info.compose_command + [ - "-f", - str(compose_file), - "logs", - "-f", - service_name, - ] + # Build compose command with override pattern + cmd = self.runtime_info.compose_command + ["-f", str(self.compose_file)] + if self.use_gpu_compose and self.gpu_compose_file.exists(): + cmd.extend(["-f", str(self.gpu_compose_file)]) + cmd.extend(["logs", "-f", service_name]) try: process = await asyncio.create_subprocess_exec( diff --git a/src/tui/screens/monitor.py b/src/tui/screens/monitor.py index 01c243c6..99e7a040 100644 --- a/src/tui/screens/monitor.py +++ b/src/tui/screens/monitor.py @@ -581,22 +581,22 @@ class MonitorScreen(Screen): def _update_mode_row(self) -> None: """Update the mode indicator and toggle button label.""" try: - use_cpu = getattr(self.container_manager, "use_cpu_compose", True) + use_gpu = getattr(self.container_manager, "use_gpu_compose", False) indicator = self.query_one("#mode-indicator", Static) - mode_text = "Mode: CPU (no GPU detected)" if use_cpu else "Mode: GPU" + mode_text = "Mode: GPU" if use_gpu else "Mode: CPU (no GPU detected)" indicator.update(mode_text) toggle_btn = self.query_one("#toggle-mode-btn", Button) - toggle_btn.label = "Switch to GPU Mode" if use_cpu else "Switch to CPU Mode" + toggle_btn.label = "Switch to CPU Mode" if use_gpu else "Switch to GPU Mode" except Exception: pass def action_toggle_mode(self) -> None: """Toggle between CPU/GPU compose files and refresh view.""" try: - current = getattr(self.container_manager, "use_cpu_compose", True) - self.container_manager.use_cpu_compose = not current + current = getattr(self.container_manager, "use_gpu_compose", False) + self.container_manager.use_gpu_compose = not current self.notify( - "Switched to GPU compose" if not current else "Switched to CPU compose", + "Switched to GPU mode" if not current else "Switched to CPU mode", severity="information", ) self._update_mode_row() From d5bf5b0c1821f6d18f91a3be856d2ab4e9baa792 Mon Sep 17 00:00:00 2001 From: Edwin Jose Date: Tue, 25 Nov 2025 17:06:06 -0500 Subject: [PATCH 08/24] Update docker-compose.yml --- docker-compose.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index c3382bf3..7ba0cea8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -45,9 +45,9 @@ services: openrag-backend: image: phact/openrag-backend:${OPENRAG_VERSION:-latest} - build: - context: . - dockerfile: Dockerfile.backend + # build: + # context: . + # dockerfile: Dockerfile.backend container_name: openrag-backend depends_on: - langflow @@ -89,9 +89,9 @@ services: openrag-frontend: image: phact/openrag-frontend:${OPENRAG_VERSION:-latest} - build: - context: . - dockerfile: Dockerfile.frontend + # build: + # context: . + # dockerfile: Dockerfile.frontend container_name: openrag-frontend depends_on: - openrag-backend @@ -104,9 +104,9 @@ services: volumes: - ./flows:/app/flows:U,z image: phact/openrag-langflow:${LANGFLOW_VERSION:-latest} - build: - context: . - dockerfile: Dockerfile.langflow + # build: + # context: . + # dockerfile: Dockerfile.langflow container_name: langflow ports: - "7860:7860" From 63d6979eb1a24900f257a7f158d371a0471f2759 Mon Sep 17 00:00:00 2001 From: Eric Hare Date: Tue, 25 Nov 2025 17:20:30 -0800 Subject: [PATCH 09/24] fix: More graceful handling of port conflicts --- src/tui/managers/container_manager.py | 126 ++++++++++++++++++++++++++ src/tui/screens/monitor.py | 31 ++++++- src/tui/widgets/command_modal.py | 26 ++++++ 3 files changed, 182 insertions(+), 1 deletion(-) diff --git a/src/tui/managers/container_manager.py b/src/tui/managers/container_manager.py index 2953d550..7e75f4a0 100644 --- a/src/tui/managers/container_manager.py +++ b/src/tui/managers/container_manager.py @@ -43,6 +43,7 @@ class ServiceInfo: image: Optional[str] = None image_digest: Optional[str] = None created: Optional[str] = None + error_message: Optional[str] = None def __post_init__(self): if self.ports is None: @@ -135,6 +136,96 @@ class ContainerManager: return self.platform_detector.get_compose_installation_instructions() return self.platform_detector.get_installation_instructions() + def _extract_ports_from_compose(self) -> Dict[str, List[int]]: + """Extract port mappings from compose files. + + Returns: + Dict mapping service name to list of host ports + """ + service_ports: Dict[str, List[int]] = {} + + compose_files = [self.compose_file] + if hasattr(self, 'cpu_compose_file') and self.cpu_compose_file and self.cpu_compose_file.exists(): + compose_files.append(self.cpu_compose_file) + + for compose_file in compose_files: + if not compose_file.exists(): + continue + + try: + import re + content = compose_file.read_text() + current_service = None + in_ports_section = False + + for line in content.splitlines(): + # Detect service names + service_match = re.match(r'^ (\w[\w-]*):$', line) + if service_match: + current_service = service_match.group(1) + in_ports_section = False + if current_service not in service_ports: + service_ports[current_service] = [] + continue + + # Detect ports section + if current_service and re.match(r'^ ports:$', line): + in_ports_section = True + continue + + # Exit ports section on new top-level key + if in_ports_section and re.match(r'^ \w+:', line): + in_ports_section = False + + # Extract port mappings + if in_ports_section and current_service: + # Match patterns like: - "3000:3000", - "9200:9200", - 7860:7860 + port_match = re.search(r'["\']?(\d+):\d+["\']?', line) + if port_match: + host_port = int(port_match.group(1)) + if host_port not in service_ports[current_service]: + service_ports[current_service].append(host_port) + + except Exception as e: + logger.debug(f"Error parsing {compose_file} for ports: {e}") + continue + + return service_ports + + async def check_ports_available(self) -> tuple[bool, List[tuple[str, int, str]]]: + """Check if required ports are available. + + Returns: + Tuple of (all_available, conflicts) where conflicts is a list of + (service_name, port, error_message) tuples + """ + import socket + + service_ports = self._extract_ports_from_compose() + conflicts: List[tuple[str, int, str]] = [] + + for service_name, ports in service_ports.items(): + for port in ports: + try: + # Try to bind to the port to check if it's available + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(0.5) + result = sock.connect_ex(('127.0.0.1', port)) + sock.close() + + if result == 0: + # Port is in use + conflicts.append(( + service_name, + port, + f"Port {port} is already in use" + )) + except Exception as e: + logger.debug(f"Error checking port {port}: {e}") + continue + + return (len(conflicts) == 0, conflicts) + async def _run_compose_command( self, args: List[str], cpu_mode: Optional[bool] = None ) -> tuple[bool, str, str]: @@ -655,6 +746,17 @@ class ContainerManager: yield False, f"ERROR: Compose file not found at {compose_file.absolute()}", False return + # Check for port conflicts before starting + yield False, "Checking port availability...", False + ports_available, conflicts = await self.check_ports_available() + if not ports_available: + yield False, "ERROR: Port conflicts detected:", False + for service_name, port, error_msg in conflicts: + yield False, f" - {service_name}: {error_msg}", False + yield False, "Please stop the conflicting services and try again.", False + yield False, "Services not started due to port conflicts.", False + return + yield False, "Starting OpenRAG services...", False missing_images: List[str] = [] @@ -677,13 +779,37 @@ class ContainerManager: yield False, "Creating and starting containers...", False up_success = {"value": True} + error_messages = [] + async for message, replace_last in self._stream_compose_command(["up", "-d"], up_success, cpu_mode): + # Detect error patterns in the output + import re + lower_msg = message.lower() + + # Check for common error patterns + if any(pattern in lower_msg for pattern in [ + "port.*already.*allocated", + "address already in use", + "bind.*address already in use", + "port is already allocated" + ]): + error_messages.append("Port conflict detected") + up_success["value"] = False + elif "error" in lower_msg or "failed" in lower_msg: + # Generic error detection + if message not in error_messages: + error_messages.append(message) + yield False, message, replace_last if up_success["value"]: yield True, "Services started successfully", False else: yield False, "Failed to start services. See output above for details.", False + if error_messages: + yield False, "\nDetected errors:", False + for err in error_messages[:5]: # Limit to first 5 errors + yield False, f" - {err}", False async def stop_services(self) -> AsyncIterator[tuple[bool, str]]: """Stop all services and yield progress updates.""" diff --git a/src/tui/screens/monitor.py b/src/tui/screens/monitor.py index 01c243c6..601566b7 100644 --- a/src/tui/screens/monitor.py +++ b/src/tui/screens/monitor.py @@ -311,17 +311,46 @@ class MonitorScreen(Screen): """Start services with progress updates.""" self.operation_in_progress = True try: + # Check for port conflicts before attempting to start + ports_available, conflicts = await self.container_manager.check_ports_available() + if not ports_available: + # Show error notification instead of modal + conflict_msgs = [] + for service_name, port, error_msg in conflicts[:3]: # Show first 3 + conflict_msgs.append(f"{service_name} (port {port})") + + conflict_str = ", ".join(conflict_msgs) + if len(conflicts) > 3: + conflict_str += f" and {len(conflicts) - 3} more" + + self.notify( + f"Cannot start services: Port conflicts detected for {conflict_str}. " + f"Please stop the conflicting services first.", + severity="error", + timeout=10 + ) + # Refresh to show current state + await self._refresh_services() + return + # Show command output in modal dialog command_generator = self.container_manager.start_services(cpu_mode) modal = CommandOutputModal( "Starting Services", command_generator, - on_complete=None, # We'll refresh in on_screen_resume instead + on_complete=self._on_start_complete, # Refresh after completion ) self.app.push_screen(modal) + except Exception as e: + self.notify(f"Error starting services: {str(e)}", severity="error") + await self._refresh_services() finally: self.operation_in_progress = False + async def _on_start_complete(self) -> None: + """Callback after service start completes.""" + await self._refresh_services() + async def _stop_services(self) -> None: """Stop services with progress updates.""" self.operation_in_progress = True diff --git a/src/tui/widgets/command_modal.py b/src/tui/widgets/command_modal.py index a5013031..a75a46ee 100644 --- a/src/tui/widgets/command_modal.py +++ b/src/tui/widgets/command_modal.py @@ -23,6 +23,7 @@ class CommandOutputModal(ModalScreen): ("p", "pause_waves", "Pause"), ("f", "speed_up", "Faster"), ("s", "speed_down", "Slower"), + ("escape", "close_modal", "Close"), ] DEFAULT_CSS = """ @@ -188,6 +189,8 @@ class CommandOutputModal(ModalScreen): self._output_lines: list[str] = [] self._layer_line_map: dict[str, int] = {} # Maps layer ID to line index self._status_task: Optional[asyncio.Task] = None + self._error_detected = False + self._command_complete = False def compose(self) -> ComposeResult: """Create the modal dialog layout.""" @@ -254,6 +257,12 @@ class CommandOutputModal(ModalScreen): for w in waves.wavelets: w.speed = max(0.1, w.speed * 0.8) + def action_close_modal(self) -> None: + """Close the modal (only if error detected or command complete).""" + close_btn = self.query_one("#close-btn", Button) + if not close_btn.disabled: + self.dismiss() + async def _run_command(self) -> None: """Run the command and update the output in real-time.""" output = self.query_one("#command-output", TextArea) @@ -273,8 +282,25 @@ class CommandOutputModal(ModalScreen): # Move cursor to end to trigger scroll output.move_cursor((len(self._output_lines), 0)) + # Detect error patterns in messages + import re + lower_msg = message.lower() if message else "" + if not self._error_detected and any(pattern in lower_msg for pattern in [ + "error:", + "failed", + "port.*already.*allocated", + "address already in use", + "not found", + "permission denied" + ]): + self._error_detected = True + # Enable close button when error detected + close_btn = self.query_one("#close-btn", Button) + close_btn.disabled = False + # If command is complete, update UI if is_complete: + self._command_complete = True self._update_output("Command completed successfully", False) output.text = "\n".join(self._output_lines) output.move_cursor((len(self._output_lines), 0)) From c7427b1ad2989013231f146c5b89719b482d001a Mon Sep 17 00:00:00 2001 From: phact Date: Wed, 26 Nov 2025 02:31:44 -0500 Subject: [PATCH 10/24] use RagRunner for docker build step --- .github/workflows/build-multiarch.yml | 13 +++++++++---- pyproject.toml | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-multiarch.yml b/.github/workflows/build-multiarch.yml index f516b62d..f9a83400 100644 --- a/.github/workflows/build-multiarch.yml +++ b/.github/workflows/build-multiarch.yml @@ -62,7 +62,8 @@ jobs: tag: langflowai/openrag-backend platform: linux/arm64 arch: arm64 - runs-on: [self-hosted, linux, ARM64, langflow-ai-arm64-2] + #runs-on: [self-hosted, linux, ARM64, langflow-ai-arm64-2] + runs-on: RagRunner # frontend - image: frontend @@ -76,7 +77,8 @@ jobs: tag: langflowai/openrag-frontend platform: linux/arm64 arch: arm64 - runs-on: [self-hosted, linux, ARM64, langflow-ai-arm64-2] + #runs-on: [self-hosted, linux, ARM64, langflow-ai-arm64-2] + runs-on: RagRunner # langflow - image: langflow @@ -90,7 +92,8 @@ jobs: tag: langflowai/openrag-langflow platform: linux/arm64 arch: arm64 - runs-on: self-hosted + #runs-on: self-hosted + runs-on: RagRunner # opensearch - image: opensearch @@ -104,7 +107,9 @@ jobs: tag: langflowai/openrag-opensearch platform: linux/arm64 arch: arm64 - runs-on: [self-hosted, linux, ARM64, langflow-ai-arm64-2] + #runs-on: [self-hosted, linux, ARM64, langflow-ai-arm64-2] + #runs-on: self-hosted + runs-on: RagRunner runs-on: ${{ matrix.runs-on }} diff --git a/pyproject.toml b/pyproject.toml index b4003082..334e045e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "openrag" -version = "0.1.39-rc2" +version = "0.1.39-rc3" description = "Add your description here" readme = "README.md" requires-python = ">=3.13" From fefcede8b5645c9932a586ffffb2e69273e1ab73 Mon Sep 17 00:00:00 2001 From: phact Date: Wed, 26 Nov 2025 03:47:47 -0500 Subject: [PATCH 11/24] also proactively check native port --- src/tui/managers/docling_manager.py | 23 ++++++++++++++++ src/tui/screens/monitor.py | 13 +++++++++ src/tui/screens/welcome.py | 42 +++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+) diff --git a/src/tui/managers/docling_manager.py b/src/tui/managers/docling_manager.py index 109cb7c1..7f7c2a78 100644 --- a/src/tui/managers/docling_manager.py +++ b/src/tui/managers/docling_manager.py @@ -143,6 +143,29 @@ class DoclingManager: self._external_process = False return False + def check_port_available(self) -> tuple[bool, Optional[str]]: + """Check if the native service port is available. + + Returns: + Tuple of (available, error_message) + """ + import socket + + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(0.5) + result = sock.connect_ex(('127.0.0.1', self._port)) + sock.close() + + if result == 0: + # Port is in use + return False, f"Port {self._port} is already in use" + return True, None + except Exception as e: + logger.debug(f"Error checking port {self._port}: {e}") + # If we can't check, assume it's available + return True, None + def get_status(self) -> Dict[str, Any]: """Get current status of docling serve.""" # Check for starting state first diff --git a/src/tui/screens/monitor.py b/src/tui/screens/monitor.py index 601566b7..d72ba619 100644 --- a/src/tui/screens/monitor.py +++ b/src/tui/screens/monitor.py @@ -415,6 +415,19 @@ class MonitorScreen(Screen): """Start docling serve.""" self.operation_in_progress = True try: + # Check for port conflicts before attempting to start + port_available, error_msg = self.docling_manager.check_port_available() + if not port_available: + self.notify( + f"Cannot start docling serve: {error_msg}. " + f"Please stop the conflicting service first.", + severity="error", + timeout=10 + ) + # Refresh to show current state + await self._refresh_services() + return + # Start the service (this sets _starting = True internally at the start) # Create task and let it begin executing (which sets the flag) start_task = asyncio.create_task(self.docling_manager.start()) diff --git a/src/tui/screens/welcome.py b/src/tui/screens/welcome.py index 64ad888a..673fae5b 100644 --- a/src/tui/screens/welcome.py +++ b/src/tui/screens/welcome.py @@ -385,6 +385,34 @@ class WelcomeScreen(Screen): async def _start_all_services(self) -> None: """Start all services: containers first, then native services.""" + # Check for port conflicts before attempting to start anything + conflicts = [] + + # Check container ports + if self.container_manager.is_available(): + ports_available, port_conflicts = await self.container_manager.check_ports_available() + if not ports_available: + for service_name, port, error_msg in port_conflicts[:3]: # Show first 3 + conflicts.append(f"{service_name} (port {port})") + if len(port_conflicts) > 3: + conflicts.append(f"and {len(port_conflicts) - 3} more") + + # Check native service port + port_available, error_msg = self.docling_manager.check_port_available() + if not port_available: + conflicts.append(f"docling (port {self.docling_manager._port})") + + # If there are any conflicts, show error and return + if conflicts: + conflict_str = ", ".join(conflicts) + self.notify( + f"Cannot start services: Port conflicts detected for {conflict_str}. " + f"Please stop the conflicting services first.", + severity="error", + timeout=10 + ) + return + # Step 1: Start container services first (to create the network) if self.container_manager.is_available(): command_generator = self.container_manager.start_services() @@ -410,6 +438,20 @@ class WelcomeScreen(Screen): async def _start_native_services_after_containers(self) -> None: """Start native services after containers have been started.""" if not self.docling_manager.is_running(): + # Check for port conflicts before attempting to start + port_available, error_msg = self.docling_manager.check_port_available() + if not port_available: + self.notify( + f"Cannot start native services: {error_msg}. " + f"Please stop the conflicting service first.", + severity="error", + timeout=10 + ) + # Update state and return + self.docling_running = False + await self._refresh_welcome_content() + return + self.notify("Starting native services...", severity="information") success, message = await self.docling_manager.start() if success: From 58528dc541cf05b8c5072dd85656d2190d43dd98 Mon Sep 17 00:00:00 2001 From: phact Date: Wed, 26 Nov 2025 03:04:53 -0500 Subject: [PATCH 12/24] clean up runner --- .github/workflows/test-integration.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-integration.yml b/.github/workflows/test-integration.yml index 1b4c6162..28641819 100644 --- a/.github/workflows/test-integration.yml +++ b/.github/workflows/test-integration.yml @@ -31,11 +31,14 @@ jobs: steps: - run: df -h - #- name: "node-cleanup" - #run: | - # sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL - # sudo docker image prune --all --force - # sudo docker builder prune -a + + - name: Clean runner workspace + run: | + # Clean old job workspaces + rm -rf ${GITHUB_WORKSPACE}/../../../_work/* || true + # Clean runner logs + rm -rf ${GITHUB_WORKSPACE}/../../../_diag/* || true + - run: df -h - name: Checkout uses: actions/checkout@v4 From c276060cb1e0fbc15f272fdea1149c483e07929e Mon Sep 17 00:00:00 2001 From: phact Date: Wed, 26 Nov 2025 03:53:43 -0500 Subject: [PATCH 13/24] fix cleanup --- .github/workflows/test-integration.yml | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-integration.yml b/.github/workflows/test-integration.yml index 28641819..2614699c 100644 --- a/.github/workflows/test-integration.yml +++ b/.github/workflows/test-integration.yml @@ -34,14 +34,23 @@ jobs: - name: Clean runner workspace run: | - # Clean old job workspaces - rm -rf ${GITHUB_WORKSPACE}/../../../_work/* || true - # Clean runner logs - rm -rf ${GITHUB_WORKSPACE}/../../../_diag/* || true + # Save current workspace path + CURRENT_WORKSPACE="${GITHUB_WORKSPACE}" + # Clean old job workspaces except current one + find ${GITHUB_WORKSPACE}/../../../_work -maxdepth 2 -type d -mtime +1 -not -path "${CURRENT_WORKSPACE}*" -exec rm -rf {} + 2>/dev/null || true + # Clean runner logs older than 1 day + find ${GITHUB_WORKSPACE}/../../../_diag -type f -mtime +1 -delete 2>/dev/null || true - run: df -h + - name: Checkout uses: actions/checkout@v4 + + - name: Verify workspace + run: | + echo "Current directory: $(pwd)" + echo "Workspace: ${GITHUB_WORKSPACE}" + ls -la - name: Set up UV uses: astral-sh/setup-uv@v3 From bd36727eb018abe78fdb6647e3f9959a22d73de3 Mon Sep 17 00:00:00 2001 From: phact Date: Wed, 26 Nov 2025 03:58:58 -0500 Subject: [PATCH 14/24] cut v0.1.40 --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 334e045e..faa5820b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "openrag" -version = "0.1.39-rc3" +version = "0.1.40" description = "Add your description here" readme = "README.md" requires-python = ">=3.13" diff --git a/uv.lock b/uv.lock index e99e9dc8..788cb852 100644 --- a/uv.lock +++ b/uv.lock @@ -2352,7 +2352,7 @@ wheels = [ [[package]] name = "openrag" -version = "0.1.37" +version = "0.1.40" source = { editable = "." } dependencies = [ { name = "agentd" }, From a5c21e1ffca9d09b12060dba92e25f74debf76a3 Mon Sep 17 00:00:00 2001 From: phact Date: Wed, 26 Nov 2025 04:04:00 -0500 Subject: [PATCH 15/24] du --- .github/workflows/test-integration.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/test-integration.yml b/.github/workflows/test-integration.yml index 2614699c..80d2d169 100644 --- a/.github/workflows/test-integration.yml +++ b/.github/workflows/test-integration.yml @@ -32,6 +32,14 @@ jobs: steps: - run: df -h + - name: Disk space troubleshooting + run: | + echo "=== Top-level disk usage ===" + sudo du -h -d1 / 2>/dev/null | sort -h + echo "" + echo "=== Docker disk usage ===" + docker system df + - name: Clean runner workspace run: | # Save current workspace path From bd0ea37253792e83a1f346cf06e5cf94f87a1301 Mon Sep 17 00:00:00 2001 From: phact Date: Wed, 26 Nov 2025 04:08:28 -0500 Subject: [PATCH 16/24] integration tests docker cache --- .github/workflows/test-integration.yml | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/.github/workflows/test-integration.yml b/.github/workflows/test-integration.yml index 80d2d169..92c9a976 100644 --- a/.github/workflows/test-integration.yml +++ b/.github/workflows/test-integration.yml @@ -32,23 +32,11 @@ jobs: steps: - run: df -h - - name: Disk space troubleshooting + - name: Cleanup Docker cache run: | - echo "=== Top-level disk usage ===" - sudo du -h -d1 / 2>/dev/null | sort -h - echo "" - echo "=== Docker disk usage ===" - docker system df - - - name: Clean runner workspace - run: | - # Save current workspace path - CURRENT_WORKSPACE="${GITHUB_WORKSPACE}" - # Clean old job workspaces except current one - find ${GITHUB_WORKSPACE}/../../../_work -maxdepth 2 -type d -mtime +1 -not -path "${CURRENT_WORKSPACE}*" -exec rm -rf {} + 2>/dev/null || true - # Clean runner logs older than 1 day - find ${GITHUB_WORKSPACE}/../../../_diag -type f -mtime +1 -delete 2>/dev/null || true - + docker system prune -af || true + docker builder prune -af || true + - run: df -h - name: Checkout From 67e31dbbe42f25959c8de5e9c87895993f0ebd97 Mon Sep 17 00:00:00 2001 From: phact Date: Wed, 26 Nov 2025 06:01:04 -0500 Subject: [PATCH 17/24] symlinks --- src/tui/_assets/docker-compose-cpu.yml | 1 - src/tui/_assets/docker-compose.gpu.yml | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 120000 src/tui/_assets/docker-compose-cpu.yml create mode 120000 src/tui/_assets/docker-compose.gpu.yml diff --git a/src/tui/_assets/docker-compose-cpu.yml b/src/tui/_assets/docker-compose-cpu.yml deleted file mode 120000 index 5ad7a663..00000000 --- a/src/tui/_assets/docker-compose-cpu.yml +++ /dev/null @@ -1 +0,0 @@ -../../../docker-compose-cpu.yml \ No newline at end of file diff --git a/src/tui/_assets/docker-compose.gpu.yml b/src/tui/_assets/docker-compose.gpu.yml new file mode 120000 index 00000000..bfebbedd --- /dev/null +++ b/src/tui/_assets/docker-compose.gpu.yml @@ -0,0 +1 @@ +../../../docker-compose.gpu.yml \ No newline at end of file From 71eaa1c1e2d9e1468350e06ae1e6e6e222b38381 Mon Sep 17 00:00:00 2001 From: phact Date: Wed, 26 Nov 2025 06:17:20 -0500 Subject: [PATCH 18/24] monitor actually displays and sticks across navigation --- src/tui/screens/monitor.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/tui/screens/monitor.py b/src/tui/screens/monitor.py index 99e7a040..7d7269ab 100644 --- a/src/tui/screens/monitor.py +++ b/src/tui/screens/monitor.py @@ -33,13 +33,14 @@ class MonitorScreen(Screen): ("u", "upgrade", "Upgrade"), ("x", "reset", "Reset"), ("l", "logs", "View Logs"), + ("g", "toggle_mode", "Toggle GPU/CPU"), ("j", "cursor_down", "Move Down"), ("k", "cursor_up", "Move Up"), ] def __init__(self): super().__init__() - self.container_manager = ContainerManager() + self._container_manager = None # Use app's shared instance self.docling_manager = DoclingManager() self.services_table = None self.docling_table = None @@ -52,6 +53,13 @@ class MonitorScreen(Screen): # Track which table was last selected for mutual exclusion self._last_selected_table = None + @property + def container_manager(self) -> ContainerManager: + """Get the shared container manager from the app.""" + if self._container_manager is None: + self._container_manager = self.app.container_manager + return self._container_manager + def on_unmount(self) -> None: """Clean up when the screen is unmounted.""" if hasattr(self, 'docling_manager'): @@ -69,10 +77,10 @@ class MonitorScreen(Screen): def _create_services_tab(self) -> ComposeResult: """Create the services monitoring tab.""" - # Current mode indicator + toggle + # GPU/CPU mode section + yield Static("GPU Mode", id="mode-indicator", classes="tab-header") yield Horizontal( - Static("", id="mode-indicator"), - Button("Toggle Mode", id="toggle-mode-btn"), + Button("Switch to CPU Mode", id="toggle-mode-btn"), classes="button-row", id="mode-row", ) @@ -583,8 +591,7 @@ class MonitorScreen(Screen): try: use_gpu = getattr(self.container_manager, "use_gpu_compose", False) indicator = self.query_one("#mode-indicator", Static) - mode_text = "Mode: GPU" if use_gpu else "Mode: CPU (no GPU detected)" - indicator.update(mode_text) + indicator.update("GPU Mode" if use_gpu else "CPU Mode") toggle_btn = self.query_one("#toggle-mode-btn", Button) toggle_btn.label = "Switch to CPU Mode" if use_gpu else "Switch to GPU Mode" except Exception: From 24c006128cbacda15f37bd8b87c345648363aea5 Mon Sep 17 00:00:00 2001 From: phact Date: Wed, 26 Nov 2025 06:31:15 -0500 Subject: [PATCH 19/24] remove the secret --- docker-compose.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 2627aa14..7954d1bc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -56,7 +56,6 @@ services: - LANGFLOW_URL=http://langflow:7860 - LANGFLOW_PUBLIC_URL=${LANGFLOW_PUBLIC_URL} - LANGFLOW_AUTO_LOGIN=${LANGFLOW_AUTO_LOGIN} - - LANGFLOW_SECRET_KEY=${LANGFLOW_SECRET_KEY} - LANGFLOW_SUPERUSER=${LANGFLOW_SUPERUSER} - LANGFLOW_SUPERUSER_PASSWORD=${LANGFLOW_SUPERUSER_PASSWORD} - LANGFLOW_CHAT_FLOW_ID=${LANGFLOW_CHAT_FLOW_ID} From d1b7a36801b341f23596816233f86b256cb4f541 Mon Sep 17 00:00:00 2001 From: phact Date: Wed, 26 Nov 2025 06:32:38 -0500 Subject: [PATCH 20/24] integration test cleanup --- .github/workflows/test-integration.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test-integration.yml b/.github/workflows/test-integration.yml index 92c9a976..0ef50684 100644 --- a/.github/workflows/test-integration.yml +++ b/.github/workflows/test-integration.yml @@ -39,6 +39,9 @@ jobs: - run: df -h + - name: Cleanup + run: sudo rm -rf opensearch-data + - name: Checkout uses: actions/checkout@v4 From 9e8936e2b84933af099cd36b56d1b7a92526f86e Mon Sep 17 00:00:00 2001 From: phact Date: Wed, 26 Nov 2025 06:33:51 -0500 Subject: [PATCH 21/24] comment - integration test cleanup --- .github/workflows/test-integration.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-integration.yml b/.github/workflows/test-integration.yml index 0ef50684..635552f0 100644 --- a/.github/workflows/test-integration.yml +++ b/.github/workflows/test-integration.yml @@ -39,8 +39,8 @@ jobs: - run: df -h - - name: Cleanup - run: sudo rm -rf opensearch-data + #- name: Cleanup + # run: sudo rm -rf opensearch-data - name: Checkout uses: actions/checkout@v4 From 5df25e21fe935451082110c2341bed2f010cdb88 Mon Sep 17 00:00:00 2001 From: phact Date: Wed, 26 Nov 2025 06:35:36 -0500 Subject: [PATCH 22/24] chown instead --- .github/workflows/test-integration.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-integration.yml b/.github/workflows/test-integration.yml index 635552f0..aab34d2a 100644 --- a/.github/workflows/test-integration.yml +++ b/.github/workflows/test-integration.yml @@ -39,8 +39,8 @@ jobs: - run: df -h - #- name: Cleanup - # run: sudo rm -rf opensearch-data + - name: Fix permissions for opensearch-data + run: sudo chown -R $(whoami) opensearch-data && chmod -R u+w opensearch-data - name: Checkout uses: actions/checkout@v4 From b92f4fdfb3ae4480e9d51bb1977245d8d27c01d4 Mon Sep 17 00:00:00 2001 From: phact Date: Wed, 26 Nov 2025 06:40:30 -0500 Subject: [PATCH 23/24] rm full path --- .github/workflows/test-integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-integration.yml b/.github/workflows/test-integration.yml index aab34d2a..604f608a 100644 --- a/.github/workflows/test-integration.yml +++ b/.github/workflows/test-integration.yml @@ -40,7 +40,7 @@ jobs: - run: df -h - name: Fix permissions for opensearch-data - run: sudo chown -R $(whoami) opensearch-data && chmod -R u+w opensearch-data + run: sudo rm -rf /opt/actions-runner/_work/openrag/openrag/opensearch-data/* - name: Checkout uses: actions/checkout@v4 From 157cebff4e705b9af97fdf2ce608e439a514e920 Mon Sep 17 00:00:00 2001 From: phact Date: Wed, 26 Nov 2025 06:43:50 -0500 Subject: [PATCH 24/24] rm with docker compose --- .github/workflows/test-integration.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/test-integration.yml b/.github/workflows/test-integration.yml index 604f608a..a70dd24d 100644 --- a/.github/workflows/test-integration.yml +++ b/.github/workflows/test-integration.yml @@ -36,12 +36,10 @@ jobs: run: | docker system prune -af || true docker builder prune -af || true + docker-compose -f docker-compose.yml down -v --remove-orphans || true - run: df -h - - name: Fix permissions for opensearch-data - run: sudo rm -rf /opt/actions-runner/_work/openrag/openrag/opensearch-data/* - - name: Checkout uses: actions/checkout@v4