diff --git a/CLAUDE.md b/CLAUDE.md index 9febf3a..c481928 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,14 +43,14 @@ All operations go through `agentctl` (add `scripts/` to PATH): ```bash agentctl local-up # Start local dev environment -agentctl local-down [--force] # Stop local (--force cleans stuck containers) -agentctl deploy [--namespace ns] # Deploy full stack to OpenShift +agentctl local-down [--force] # Stop local (--force removes volumes too) +agentctl deploy [--image img] [-n ns] # Deploy to OpenShift (--image uses built image, without it pulls default from Docker Hub) agentctl destroy [--namespace ns] # Remove all cluster resources agentctl flows save # Local Langflow -> flows/ dir agentctl flows load # flows/ dir -> Local Langflow agentctl flows pull [-n ns] # Cluster Langflow -> flows/ dir agentctl flows push [-n ns] # flows/ dir -> Cluster Langflow -agentctl build [reg] # Build flow into container image +agentctl build [--prod] [reg] [tag] # Build flow image (--prod for API-only runtime) agentctl list [--all-namespaces] # List deployed agents agentctl status [-n ns] # Show agent status ``` @@ -78,17 +78,27 @@ Both local and cluster use `LANGFUSE_INIT_*` env vars to auto-create org, projec - `flows push/load` uploads via `POST /api/v1/flows/upload/` (multipart file) - Auth via `POST /api/v1/login` with default credentials `langflow/langflow` +### Build & Deploy +- `agentctl build [registry] [tag]` — builds full Langflow image (UI + flow baked in) +- `agentctl build --prod` — builds **Langflow Runtime** image (backend-only, no UI) +- `-n ` on build rewrites model endpoints (api_base, model_name) in the flow JSON to point to the cluster's KServe URL +- Images are built for `linux/amd64` via `podman build --platform linux/amd64` +- `agentctl deploy --image ` — deploys with the built image +- `agentctl deploy` (no `--image`) — no image is built or pushed; OpenShift pulls the default Langflow image from Docker Hub. Use `agentctl flows push` to upload flows after deploy. +- The Helm template conditionally sets `LANGFLOW_BACKEND_ONLY=true` and `LANGFLOW_SKIP_AUTH_AUTO_LOGIN=true` +- `LANGFLOW_LOAD_FLOWS_PATH` must point to a **directory** (not a file) — Langflow calls `iterdir()` on it +- `LANGFLOW_SKIP_AUTH_AUTO_LOGIN=true` is required for Langflow >= 1.5 to allow unauthenticated API access with auto-login +- Registry images must be public or the cluster needs a pull secret + ### Custom vLLM Component Flows using the cluster's model serving have a custom `VLLMModel` component with hardcoded `base_url`. When moving flows between environments, the model endpoint URL must be changed: -- Local: `http://ollama:11434/v1` with model `llama3.2` -- Cluster: `http://llama-31-8b-instruct-predictor.langflow-agent.svc.cluster.local:8080/v1` with model `llama-31-8b-instruct` +- Local: `http://ollama:11434/v1` with model `qwen2.5:7b` +- Cluster: `http://qwen25-7b-instruct-predictor..svc.cluster.local:8080/v1` with model `qwen25-7b-instruct` ## Known Issues -- `flows save`/`flows pull` downloads ALL flows including Langflow's built-in starter templates (34+), not just user-created flows - `flows push`/`flows load` creates duplicates if the flow already exists on the target - Langfuse INIT vars only run on first database creation — if the database already exists from a prior run, wipe volumes and restart -- The README.md is outdated (still references MLflow which was removed) ## Images Used diff --git a/README.md b/README.md index c25bc4c..fbcef79 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,22 @@ To check its status: podman machine list ``` +**Memory**: The Podman VM needs at least 8GB of memory. If it's set lower, increase it: + +```bash +podman machine stop +podman machine set --memory 8192 +podman machine start +``` + +**Platform**: The `platform` field in `local/podman-compose.yml` must match your machine's architecture. Update it if needed: + +| Machine | Platform | +|---------|----------| +| Mac (Apple Silicon) | `linux/arm64` | +| Mac (Intel) | `linux/amd64` | +| Linux (x86_64) | `linux/amd64` | + ### Cluster Login To get the `oc login` command for your cluster: @@ -163,6 +179,53 @@ agentctl destroy # Remove cluster resources agentctl local-down # Stop local environment ``` +### Build & Deploy (Production) + +Develop locally, then package and deploy as a container image. + +```bash +# ── Inner Loop (Development) ────────────────────────── + +# 1. Start local environment +agentctl local-up + +# 2. Build and test your agent flow in the Langflow UI +open http://localhost:7860 + +# 3. Save flows to the flows/ directory +agentctl flows save + +# 4. Commit flows to Git +git add flows/ && git commit -m "Add agent flow" + +# ── Outer Loop (Production) ────────────────────────── + +# 5. Build a container image with the flow baked in +podman login quay.io +agentctl build flows/my-flow.json quay.io/myorg v1.0 # full UI +agentctl build --prod flows/my-flow.json quay.io/myorg v1.0 # headless API only + +# 6. Deploy to OpenShift with the built image +oc login https://your-cluster:6443 +agentctl deploy --image quay.io/myorg/langflow-my-flow:v1.0 + +# 7. Test the agent via API (URL printed by deploy) +curl -X POST https:///api/v1/run/ \ + -H "Content-Type: application/json" \ + -d '{"input_value": "Hello", "output_type": "chat", "input_type": "chat"}' + +# Cleanup +agentctl destroy +agentctl local-down +``` + +- `agentctl build`: builds a full Langflow image with UI + flow baked in +- `agentctl build --prod`: builds a **Langflow Runtime** image — headless API server, no UI +- `agentctl deploy --image`: deploys with your built image +- `agentctl deploy` (no `--image`): no image is built or pushed — OpenShift pulls the default Langflow image directly from Docker Hub. Use `agentctl flows push` to upload your flows after deploy. + +> **Note:** `agentctl build` will create a new repository in Quay.io when pushing. The repository defaults to **private**. You need to make it **public** in the Quay.io UI so that OpenShift can pull the image without a pull secret. + ## CLI Reference All operations go through `agentctl`: @@ -170,14 +233,14 @@ All operations go through `agentctl`: | Command | Description | |---------|-------------| | `agentctl local-up` | Start local dev environment | -| `agentctl local-down [--force]` | Stop local environment (`--force` cleans stuck containers) | -| `agentctl deploy [--namespace ns]` | Deploy full stack to OpenShift | +| `agentctl local-down [--force]` | Stop local environment (`--force` removes volumes too) | +| `agentctl deploy [--image img] [--namespace ns]` | Deploy full stack to OpenShift | | `agentctl destroy [--namespace ns]` | Remove all cluster resources | | `agentctl flows save` | Local Langflow → `flows/` directory | | `agentctl flows load` | `flows/` directory → Local Langflow | | `agentctl flows pull [-n ns]` | Cluster Langflow → `flows/` directory | | `agentctl flows push [-n ns]` | `flows/` directory → Cluster Langflow | -| `agentctl build [registry] [tag]` | Build flow into a container image and push to registry | +| `agentctl build [--prod] [registry] [tag]` | Build flow into a container image (`--prod` for API-only runtime) | | `agentctl list [--all-namespaces]` | List deployed agents | | `agentctl status [-n ns]` | Show agent status and metadata | @@ -191,9 +254,10 @@ Key values in `helm/langflow-agent/values.yaml`: |-----------|-------------|---------| | `langflow.image` | Langflow container image | `langflowai/langflow:1.7.1` | | `langflow.replicas` | Number of Langflow replicas | `1` | +| `langflow.backendOnly` | Run as Langflow Runtime (API-only, no UI) | `false` | | `langfuse.enabled` | Deploy Langfuse for tracing | `true` | | `modelServing.enabled` | Deploy vLLM + KServe | `false` | -| `modelServing.modelName` | Model to serve | `meta-llama/Llama-3.1-8B-Instruct` | +| `modelServing.modelName` | Model to serve | `Qwen/Qwen2.5-7B-Instruct` | | `modelServing.gpu.count` | GPUs for model serving | `1` | ### Deploy with Model Serving @@ -221,8 +285,26 @@ To enable model serving, set `modelServing.enabled: true` in `values.yaml`. Requ | Langfuse | http://localhost:3000 | | Ollama API | http://localhost:11434 | +## Using a Third-Party Model (OpenAI, Anthropic, etc.) + +You don't need Ollama or vLLM if you want to use a third-party model provider. Just configure the model component directly in the Langflow UI: + +1. Open your flow in Langflow +2. Use a built-in **OpenAI** / **Anthropic** / **OpenAI-compatible** component +3. Set `api_base` to the provider's URL and `api_key` to your key + +This works the same locally and on cluster — no infrastructure changes needed. + +Alternatively, set the model endpoint in `local/.env`: + +```bash +OPENAI_API_BASE=https://api.openai.com/v1 +OPENAI_API_KEY=sk-... +``` + ## Notes - Flows pulled from the cluster may contain model components pointing to cluster-internal URLs. When loading these locally, update the model endpoint in the Langflow UI to point to Ollama (`http://ollama:11434/v1`). -- `flows save` and `flows pull` download all flows including Langflow's built-in starter templates. Only user-created flows are relevant for version control. -- Langfuse auto-provisioning (org, project, API keys) only runs on first database creation. If you need to reset, remove the PostgreSQL volume and restart. \ No newline at end of file +- Langfuse auto-provisioning (org, project, API keys) only runs on first database creation. If you need to reset, remove the PostgreSQL volume and restart. +- When pushing images to Quay.io or other registries, ensure the repository is **public** or create a pull secret on the cluster (`oc create secret docker-registry ...`). +- Images are built for `linux/amd64` by default to match typical OpenShift cluster architecture. \ No newline at end of file diff --git a/custom_components/kserve_vllm.py b/custom_components/kserve_vllm.py new file mode 100644 index 0000000..a201081 --- /dev/null +++ b/custom_components/kserve_vllm.py @@ -0,0 +1,55 @@ +from langchain_openai import ChatOpenAI + +from langflow.base.models.model import LCModelComponent +from langflow.field_typing import LanguageModel +from langflow.io import FloatInput, IntInput, SecretStrInput, StrInput + + +class KServeVLLMComponent(LCModelComponent): + display_name = "KServe vLLM" + description = "Language model served via KServe + vLLM (OpenAI-compatible API)." + icon = "server" + name = "KServeVLLM" + + inputs = LCModelComponent._base_inputs + [ + StrInput( + name="api_base", + display_name="API Base URL", + info="KServe vLLM OpenAI-compatible endpoint.", + value="", + required=True, + ), + StrInput( + name="model_name", + display_name="Model Name", + value="", + required=True, + ), + SecretStrInput( + name="api_key", + display_name="API Key", + info="API key (use 'EMPTY' if not required).", + value="EMPTY", + ), + FloatInput( + name="temperature", + display_name="Temperature", + value=0.1, + ), + IntInput( + name="max_tokens", + display_name="Max Tokens", + value=4096, + advanced=True, + ), + ] + + def build_model(self) -> LanguageModel: + return ChatOpenAI( + api_key=self.api_key or "EMPTY", + model=self.model_name, + base_url=self.api_base, + temperature=self.temperature, + max_tokens=self.max_tokens if self.max_tokens > 0 else None, + timeout=120, + ) diff --git a/flows/outdoor-activity-agent.json b/flows/outdoor-activity-agent.json new file mode 100644 index 0000000..624b12f --- /dev/null +++ b/flows/outdoor-activity-agent.json @@ -0,0 +1,2286 @@ +{ + "name": "Outdoor Activity Agent", + "description": "Find outdoor activities in a specific area based on nearby national parks, upcoming weather, and park alerts.", + "icon": null, + "icon_bg_color": null, + "gradient": null, + "data": { + "nodes": [ + { + "data": { + "id": "ChatInput-42lm9", + "node": { + "base_classes": [ + "Message" + ], + "beta": false, + "category": "inputs", + "conditional_paths": [], + "custom_fields": {}, + "description": "Get chat inputs from the Playground.", + "display_name": "Chat Input", + "documentation": "", + "edited": false, + "field_order": [ + "input_value", + "should_store_message", + "sender", + "sender_name", + "session_id", + "files", + "background_color", + "chat_icon", + "text_color" + ], + "frozen": false, + "icon": "MessagesSquare", + "key": "ChatInput", + "legacy": false, + "lf_version": "1.7.1", + "metadata": { + "code_hash": "7a26c54d89ed", + "dependencies": { + "dependencies": [ + { + "name": "lfx", + "version": "0.2.2" + } + ], + "total_dependencies": 1 + }, + "module": "lfx.components.input_output.chat.ChatInput" + }, + "minimized": true, + "output_types": [], + "outputs": [ + { + "allows_loop": false, + "cache": true, + "display_name": "Chat Message", + "group_outputs": false, + "method": "message_response", + "name": "message", + "selected": "Message", + "tool_mode": true, + "types": [ + "Message" + ], + "value": "__UNDEFINED__" + } + ], + "pinned": false, + "score": 0.0020353564437605998, + "template": { + "_type": "Component", + "code": { + "advanced": true, + "dynamic": true, + "fileTypes": [], + "file_path": "", + "info": "", + "list": false, + "load_from_db": false, + "multiline": true, + "name": "code", + "password": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "type": "code", + "value": "from lfx.base.data.utils import IMG_FILE_TYPES, TEXT_FILE_TYPES\nfrom lfx.base.io.chat import ChatComponent\nfrom lfx.inputs.inputs import BoolInput\nfrom lfx.io import (\n DropdownInput,\n FileInput,\n MessageTextInput,\n MultilineInput,\n Output,\n)\nfrom lfx.schema.message import Message\nfrom lfx.utils.constants import (\n MESSAGE_SENDER_AI,\n MESSAGE_SENDER_NAME_USER,\n MESSAGE_SENDER_USER,\n)\n\n\nclass ChatInput(ChatComponent):\n display_name = \"Chat Input\"\n description = \"Get chat inputs from the Playground.\"\n documentation: str = \"https://docs.langflow.org/chat-input-and-output\"\n icon = \"MessagesSquare\"\n name = \"ChatInput\"\n minimized = True\n\n inputs = [\n MultilineInput(\n name=\"input_value\",\n display_name=\"Input Text\",\n value=\"\",\n info=\"Message to be passed as input.\",\n input_types=[],\n ),\n BoolInput(\n name=\"should_store_message\",\n display_name=\"Store Messages\",\n info=\"Store the message in the history.\",\n value=True,\n advanced=True,\n ),\n DropdownInput(\n name=\"sender\",\n display_name=\"Sender Type\",\n options=[MESSAGE_SENDER_AI, MESSAGE_SENDER_USER],\n value=MESSAGE_SENDER_USER,\n info=\"Type of sender.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"sender_name\",\n display_name=\"Sender Name\",\n info=\"Name of the sender.\",\n value=MESSAGE_SENDER_NAME_USER,\n advanced=True,\n ),\n MessageTextInput(\n name=\"session_id\",\n display_name=\"Session ID\",\n info=\"The session ID of the chat. If empty, the current session ID parameter will be used.\",\n advanced=True,\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 FileInput(\n name=\"files\",\n display_name=\"Files\",\n file_types=TEXT_FILE_TYPES + IMG_FILE_TYPES,\n info=\"Files to be sent with the message.\",\n advanced=True,\n is_list=True,\n temp_file=True,\n ),\n ]\n outputs = [\n Output(display_name=\"Chat Message\", name=\"message\", method=\"message_response\"),\n ]\n\n async def message_response(self) -> Message:\n # Ensure files is a list and filter out empty/None values\n files = self.files if self.files else []\n if files and not isinstance(files, list):\n files = [files]\n # Filter out None/empty values\n files = [f for f in files if f is not None and f != \"\"]\n\n session_id = self.session_id or self.graph.session_id or \"\"\n message = await Message.create(\n text=self.input_value,\n sender=self.sender,\n sender_name=self.sender_name,\n session_id=session_id,\n context_id=self.context_id,\n files=files,\n )\n if session_id and isinstance(message, Message) and self.should_store_message:\n stored_message = await self.send_message(\n message,\n )\n self.message.value = stored_message\n message = stored_message\n\n self.status = message\n return message\n" + }, + "context_id": { + "_input_type": "MessageTextInput", + "advanced": true, + "display_name": "Context ID", + "dynamic": false, + "info": "The context ID of the chat. Adds an extra layer to the local memory.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "context_id", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "type": "str", + "value": "" + }, + "files": { + "_input_type": "FileInput", + "advanced": true, + "display_name": "Files", + "dynamic": false, + "fileTypes": [ + "csv", + "json", + "pdf", + "txt", + "md", + "mdx", + "yaml", + "yml", + "xml", + "html", + "htm", + "docx", + "py", + "sh", + "sql", + "js", + "ts", + "tsx", + "jpg", + "jpeg", + "png", + "bmp", + "image" + ], + "file_path": "", + "info": "Files to be sent with the message.", + "list": true, + "list_add_label": "Add More", + "name": "files", + "placeholder": "", + "required": false, + "show": true, + "temp_file": true, + "title_case": false, + "trace_as_metadata": true, + "type": "file", + "value": "" + }, + "input_value": { + "_input_type": "MultilineInput", + "advanced": false, + "copy_field": false, + "display_name": "Input Text", + "dynamic": false, + "info": "Message to be passed as input.", + "input_types": [], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "multiline": true, + "name": "input_value", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "type": "str", + "value": "Hello, how are you?" + }, + "sender": { + "_input_type": "DropdownInput", + "advanced": true, + "combobox": false, + "dialog_inputs": {}, + "display_name": "Sender Type", + "dynamic": false, + "info": "Type of sender.", + "name": "sender", + "options": [ + "Machine", + "User" + ], + "options_metadata": [], + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "type": "str", + "value": "User" + }, + "sender_name": { + "_input_type": "MessageTextInput", + "advanced": true, + "display_name": "Sender Name", + "dynamic": false, + "info": "Name of the sender.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "sender_name", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "type": "str", + "value": "User" + }, + "session_id": { + "_input_type": "MessageTextInput", + "advanced": true, + "display_name": "Session ID", + "dynamic": false, + "info": "The session ID of the chat. If empty, the current session ID parameter will be used.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "session_id", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "type": "str", + "value": "" + }, + "should_store_message": { + "_input_type": "BoolInput", + "advanced": true, + "display_name": "Store Messages", + "dynamic": false, + "info": "Store the message in the history.", + "list": false, + "list_add_label": "Add More", + "name": "should_store_message", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "type": "bool", + "value": true + } + }, + "tool_mode": false + }, + "selected_output": "message", + "showNode": false, + "type": "ChatInput" + }, + "dragging": false, + "id": "ChatInput-42lm9", + "measured": { + "height": 48, + "width": 192 + }, + "position": { + "x": 1654.7124260397243, + "y": 791.3003176826047 + }, + "selected": false, + "type": "genericNode" + }, + { + "data": { + "id": "ChatOutput-QSU1M", + "node": { + "base_classes": [ + "Message" + ], + "beta": false, + "category": "outputs", + "conditional_paths": [], + "custom_fields": {}, + "description": "Display a chat message in the Playground.", + "display_name": "Chat Output", + "documentation": "", + "edited": false, + "field_order": [ + "input_value", + "should_store_message", + "sender", + "sender_name", + "session_id", + "data_template", + "background_color", + "chat_icon", + "text_color", + "clean_data" + ], + "frozen": false, + "icon": "MessagesSquare", + "key": "ChatOutput", + "legacy": false, + "lf_version": "1.7.1", + "metadata": { + "code_hash": "8c87e536cca4", + "dependencies": { + "dependencies": [ + { + "name": "orjson", + "version": "3.10.15" + }, + { + "name": "fastapi", + "version": "0.128.0" + }, + { + "name": "lfx", + "version": "0.2.2" + } + ], + "total_dependencies": 3 + }, + "module": "lfx.components.input_output.chat_output.ChatOutput" + }, + "minimized": true, + "output_types": [], + "outputs": [ + { + "allows_loop": false, + "cache": true, + "display_name": "Output Message", + "group_outputs": false, + "method": "message_response", + "name": "message", + "selected": "Message", + "tool_mode": true, + "types": [ + "Message" + ], + "value": "__UNDEFINED__" + } + ], + "pinned": false, + "score": 0.003169567463043492, + "template": { + "_type": "Component", + "clean_data": { + "_input_type": "BoolInput", + "advanced": true, + "display_name": "Basic Clean Data", + "dynamic": false, + "info": "Whether to clean data before converting to string.", + "list": false, + "list_add_label": "Add More", + "name": "clean_data", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "type": "bool", + "value": true + }, + "code": { + "advanced": true, + "dynamic": true, + "fileTypes": [], + "file_path": "", + "info": "", + "list": false, + "load_from_db": false, + "multiline": true, + "name": "code", + "password": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "type": "code", + "value": "from collections.abc import Generator\nfrom typing import Any\n\nimport orjson\nfrom fastapi.encoders import jsonable_encoder\n\nfrom lfx.base.io.chat import ChatComponent\nfrom lfx.helpers.data import safe_convert\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, HandleInput, MessageTextInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.schema.properties import Source\nfrom lfx.template.field.base import Output\nfrom lfx.utils.constants import (\n MESSAGE_SENDER_AI,\n MESSAGE_SENDER_NAME_AI,\n MESSAGE_SENDER_USER,\n)\n\n\nclass ChatOutput(ChatComponent):\n display_name = \"Chat Output\"\n description = \"Display a chat message in the Playground.\"\n documentation: str = \"https://docs.langflow.org/chat-input-and-output\"\n icon = \"MessagesSquare\"\n name = \"ChatOutput\"\n minimized = True\n\n inputs = [\n HandleInput(\n name=\"input_value\",\n display_name=\"Inputs\",\n info=\"Message to be passed as output.\",\n input_types=[\"Data\", \"DataFrame\", \"Message\"],\n required=True,\n ),\n BoolInput(\n name=\"should_store_message\",\n display_name=\"Store Messages\",\n info=\"Store the message in the history.\",\n value=True,\n advanced=True,\n ),\n DropdownInput(\n name=\"sender\",\n display_name=\"Sender Type\",\n options=[MESSAGE_SENDER_AI, MESSAGE_SENDER_USER],\n value=MESSAGE_SENDER_AI,\n advanced=True,\n info=\"Type of sender.\",\n ),\n MessageTextInput(\n name=\"sender_name\",\n display_name=\"Sender Name\",\n info=\"Name of the sender.\",\n value=MESSAGE_SENDER_NAME_AI,\n advanced=True,\n ),\n MessageTextInput(\n name=\"session_id\",\n display_name=\"Session ID\",\n info=\"The session ID of the chat. If empty, the current session ID parameter will be used.\",\n advanced=True,\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 MessageTextInput(\n name=\"data_template\",\n display_name=\"Data Template\",\n value=\"{text}\",\n advanced=True,\n info=\"Template to convert Data to Text. If left empty, it will be dynamically set to the Data's text key.\",\n ),\n BoolInput(\n name=\"clean_data\",\n display_name=\"Basic Clean Data\",\n value=True,\n advanced=True,\n info=\"Whether to clean data before converting to string.\",\n ),\n ]\n outputs = [\n Output(\n display_name=\"Output Message\",\n name=\"message\",\n method=\"message_response\",\n ),\n ]\n\n def _build_source(self, id_: str | None, display_name: str | None, source: str | None) -> Source:\n source_dict = {}\n if id_:\n source_dict[\"id\"] = id_\n if display_name:\n source_dict[\"display_name\"] = display_name\n if source:\n # Handle case where source is a ChatOpenAI object\n if hasattr(source, \"model_name\"):\n source_dict[\"source\"] = source.model_name\n elif hasattr(source, \"model\"):\n source_dict[\"source\"] = str(source.model)\n else:\n source_dict[\"source\"] = str(source)\n return Source(**source_dict)\n\n async def message_response(self) -> Message:\n # First convert the input to string if needed\n text = self.convert_to_string()\n\n # Get source properties\n source, _, display_name, source_id = self.get_properties_from_source_component()\n\n # Create or use existing Message object\n if isinstance(self.input_value, Message) and not self.is_connected_to_chat_input():\n message = self.input_value\n # Update message properties\n message.text = text\n # Preserve existing session_id from the incoming message if it exists\n existing_session_id = message.session_id\n else:\n message = Message(text=text)\n existing_session_id = None\n\n # Set message properties\n message.sender = self.sender\n message.sender_name = self.sender_name\n # Preserve session_id from incoming message, or use component/graph session_id\n message.session_id = (\n self.session_id or existing_session_id or (self.graph.session_id if hasattr(self, \"graph\") else None) or \"\"\n )\n message.context_id = self.context_id\n message.flow_id = self.graph.flow_id if hasattr(self, \"graph\") else None\n message.properties.source = self._build_source(source_id, display_name, source)\n\n # Store message if needed\n if message.session_id and self.should_store_message:\n stored_message = await self.send_message(message)\n self.message.value = stored_message\n message = stored_message\n\n self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + }, + "context_id": { + "_input_type": "MessageTextInput", + "advanced": true, + "display_name": "Context ID", + "dynamic": false, + "info": "The context ID of the chat. Adds an extra layer to the local memory.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "context_id", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "type": "str", + "value": "" + }, + "data_template": { + "_input_type": "MessageTextInput", + "advanced": true, + "display_name": "Data Template", + "dynamic": false, + "info": "Template to convert Data to Text. If left empty, it will be dynamically set to the Data's text key.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "data_template", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "type": "str", + "value": "{text}" + }, + "input_value": { + "_input_type": "HandleInput", + "advanced": false, + "display_name": "Inputs", + "dynamic": false, + "info": "Message to be passed as output.", + "input_types": [ + "Data", + "DataFrame", + "Message" + ], + "list": false, + "list_add_label": "Add More", + "name": "input_value", + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "trace_as_metadata": true, + "type": "other", + "value": "" + }, + "sender": { + "_input_type": "DropdownInput", + "advanced": true, + "combobox": false, + "dialog_inputs": {}, + "display_name": "Sender Type", + "dynamic": false, + "info": "Type of sender.", + "name": "sender", + "options": [ + "Machine", + "User" + ], + "options_metadata": [], + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "type": "str", + "value": "Machine" + }, + "sender_name": { + "_input_type": "MessageTextInput", + "advanced": true, + "display_name": "Sender Name", + "dynamic": false, + "info": "Name of the sender.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "sender_name", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "type": "str", + "value": "AI" + }, + "session_id": { + "_input_type": "MessageTextInput", + "advanced": true, + "display_name": "Session ID", + "dynamic": false, + "info": "The session ID of the chat. If empty, the current session ID parameter will be used.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "session_id", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "type": "str", + "value": "" + }, + "should_store_message": { + "_input_type": "BoolInput", + "advanced": true, + "display_name": "Store Messages", + "dynamic": false, + "info": "Store the message in the history.", + "list": false, + "list_add_label": "Add More", + "name": "should_store_message", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "type": "bool", + "value": true + } + }, + "tool_mode": false + }, + "showNode": false, + "type": "ChatOutput" + }, + "id": "ChatOutput-QSU1M", + "measured": { + "height": 48, + "width": 192 + }, + "position": { + "x": 2443.6872188885436, + "y": 619.8424622330226 + }, + "selected": false, + "type": "genericNode", + "dragging": false + }, + { + "data": { + "id": "Agent-Np81j", + "node": { + "base_classes": [ + "Message" + ], + "beta": false, + "conditional_paths": [], + "custom_fields": {}, + "description": "Define the agent's instructions, then enter a task to complete using tools.", + "display_name": "Agent", + "documentation": "", + "edited": false, + "field_order": [ + "agent_llm", + "system_prompt", + "n_messages", + "tools", + "input_value", + "handle_parsing_errors", + "verbose", + "max_iterations", + "agent_description", + "add_current_date_tool" + ], + "frozen": false, + "icon": "bot", + "last_updated": "2026-02-26T18:49:15.566Z", + "legacy": false, + "lf_version": "1.7.1", + "metadata": { + "code_hash": "fba2d73636e5", + "dependencies": { + "dependencies": [ + { + "name": "langchain_core", + "version": "0.3.83" + }, + { + "name": "pydantic", + "version": "2.11.10" + }, + { + "name": "lfx", + "version": "0.2.2" + } + ], + "total_dependencies": 3 + }, + "module": "lfx.components.models_and_agents.agent.AgentComponent" + }, + "minimized": false, + "output_types": [], + "outputs": [ + { + "types": [ + "Message" + ], + "selected": "Message", + "name": "response", + "display_name": "Response", + "method": "message_response", + "value": "__UNDEFINED__", + "cache": true, + "required_inputs": null, + "allows_loop": false, + "loop_types": null, + "group_outputs": false, + "options": null, + "tool_mode": true + } + ], + "pinned": false, + "template": { + "_frontend_node_flow_id": { + "value": "78698891-d8b6-4fb8-ad64-e0335063e62a", + "input_types": [] + }, + "_frontend_node_folder_id": { + "value": "676d9518-21e7-4e3e-a53b-283fd2122418", + "input_types": [] + }, + "_type": "Component", + "add_current_date_tool": { + "_input_type": "BoolInput", + "advanced": true, + "display_name": "Current Date", + "dynamic": false, + "info": "If true, will add a tool to the agent that returns the current date.", + "input_types": [], + "list": false, + "list_add_label": "Add More", + "name": "add_current_date_tool", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "type": "bool", + "value": true + }, + "agent_description": { + "_input_type": "MultilineInput", + "advanced": true, + "copy_field": false, + "display_name": "Agent Description [Deprecated]", + "dynamic": false, + "info": "The description of the agent. This is only used when in Tool Mode. Defaults to 'A helpful assistant with access to the following tools:' and tools are added dynamically. This feature is deprecated and will be removed in future versions.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "multiline": true, + "name": "agent_description", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "type": "str", + "value": "A helpful assistant with access to the following tools:" + }, + "agent_llm": { + "_input_type": "DropdownInput", + "advanced": false, + "combobox": false, + "dialog_inputs": {}, + "display_name": "Language Model", + "dynamic": false, + "external_options": { + "fields": { + "data": { + "node": { + "display_name": "Connect other models", + "icon": "CornerDownLeft", + "name": "connect_other_models" + } + } + } + }, + "info": "The provider of the language model that the agent will use to generate responses.", + "input_types": [ + "LanguageModel" + ], + "name": "agent_llm", + "options": [ + "Anthropic", + "Google Generative AI", + "OpenAI", + "IBM watsonx.ai", + "Ollama" + ], + "options_metadata": [ + { + "icon": "Anthropic" + }, + { + "icon": "GoogleGenerativeAI" + }, + { + "icon": "OpenAI" + }, + { + "icon": "WatsonxAI" + }, + { + "icon": "Ollama" + } + ], + "override_skip": false, + "placeholder": "Awaiting model input.", + "real_time_refresh": true, + "refresh_button": false, + "required": false, + "show": true, + "title_case": false, + "toggle": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "str", + "value": "" + }, + "code": { + "advanced": true, + "dynamic": true, + "fileTypes": [], + "file_path": "", + "info": "", + "input_types": [], + "list": false, + "load_from_db": false, + "multiline": true, + "name": "code", + "password": false, + "placeholder": "", + "required": true, + "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 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_without_prefix = field_name.replace(prefix, \"\")\n else:\n field_name_without_prefix = field_name\n build_config = await update_component_build_config(\n component_class, build_config, field_value, field_name_without_prefix\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", + "advanced": true, + "display_name": "Context ID", + "dynamic": false, + "info": "The context ID of the chat. Adds an extra layer to the local memory.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "context_id", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "type": "str", + "value": "" + }, + "format_instructions": { + "_input_type": "MultilineInput", + "advanced": true, + "copy_field": false, + "display_name": "Output Format Instructions", + "dynamic": false, + "info": "Generic Template for structured output formatting. Valid only with Structured response.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "multiline": true, + "name": "format_instructions", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "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." + }, + "handle_parsing_errors": { + "_input_type": "BoolInput", + "advanced": true, + "display_name": "Handle Parse Errors", + "dynamic": false, + "info": "Should the Agent fix errors when reading user input for better processing?", + "input_types": [], + "list": false, + "list_add_label": "Add More", + "name": "handle_parsing_errors", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "type": "bool", + "value": true + }, + "input_value": { + "_input_type": "MessageTextInput", + "advanced": false, + "display_name": "Input", + "dynamic": false, + "info": "The input provided by the user for the agent to process.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "input_value", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": true, + "trace_as_input": true, + "trace_as_metadata": true, + "type": "str", + "value": "" + }, + "is_refresh": false, + "max_iterations": { + "_input_type": "IntInput", + "advanced": true, + "display_name": "Max Iterations", + "dynamic": false, + "info": "The maximum number of attempts the agent can make to complete its task before it stops.", + "input_types": [], + "list": false, + "list_add_label": "Add More", + "name": "max_iterations", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "type": "int", + "value": 15 + }, + "n_messages": { + "_input_type": "IntInput", + "advanced": true, + "display_name": "Number of Chat History Messages", + "dynamic": false, + "info": "Number of chat history messages to retrieve.", + "input_types": [], + "list": false, + "list_add_label": "Add More", + "name": "n_messages", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "type": "int", + "value": 0 + }, + "output_schema": { + "_input_type": "TableInput", + "advanced": true, + "display_name": "Output Schema", + "dynamic": false, + "info": "Schema Validation: Define the structure and data types for structured output. No validation if no output schema.", + "input_types": [], + "is_list": true, + "list_add_label": "Add More", + "name": "output_schema", + "placeholder": "", + "required": false, + "show": true, + "table_icon": "Table", + "table_schema": [ + { + "default": "field", + "description": "Specify the name of the output field.", + "display_name": "Name", + "edit_mode": "inline", + "name": "name", + "type": "str" + }, + { + "default": "description of field", + "description": "Describe the purpose of the output field.", + "display_name": "Description", + "edit_mode": "popover", + "name": "description", + "type": "str" + }, + { + "default": "str", + "description": "Indicate the data type of the output field (e.g., str, int, float, bool, dict).", + "display_name": "Type", + "edit_mode": "inline", + "name": "type", + "options": [ + "str", + "int", + "float", + "bool", + "dict" + ], + "type": "str" + }, + { + "default": "False", + "description": "Set to True if this output field should be a list of the specified type.", + "display_name": "As List", + "edit_mode": "inline", + "name": "multiple", + "type": "boolean" + } + ], + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "trigger_icon": "Table", + "trigger_text": "Open table", + "type": "table", + "value": [] + }, + "system_prompt": { + "_input_type": "MultilineInput", + "advanced": false, + "copy_field": false, + "display_name": "Agent Instructions", + "dynamic": false, + "info": "System Prompt: Initial instructions and context provided to guide the agent's behavior.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "multiline": true, + "name": "system_prompt", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "type": "str", + "value": "You are a national park trip planner and weather assistant. You MUST use the available tools to get real data for every question - never guess or make up information about alerts, weather, or park details.\n\nIMPORTANT: For every question about a park, call the appropriate tool. Do not answer from memory.\n\nTools:\nNPS Search Parks \u2014 find parks by state code and activity keyword\nNPS Park Alerts \u2014 get active closures/hazards for a park (uses four-letter park code)\nOpen-Meteo Forecast \u2014 get weather forecast for coordinates (latitude, longitude)\n\nWorkflow:\n\nParse the request. Identify: state(s), activity/interest, travel dates (if given), and group needs (e.g., kids, accessibility).\n\nSearch parks. Call NPS Search Parks with the state code and activity. Extract the park code, coordinates, and description from results. If the user mentions a specific park by name, still search to confirm its park code and coordinates.\n\nCheck alerts for each candidate park. Call NPS Park Alerts using the park code. Classify alerts as:\n\nBlocking (closure, danger): disqualifies the park unless the closure is partial and doesn't affect the user's activity.\nInformational (caution, info): mention but don't disqualify.\nCheck weather. Call Open-Meteo Forecast using the park's latitude and longitude. Evaluate conditions against the user's planned activity \u2014 e.g., rain matters more for hiking than museum visits. If the user provided travel dates, focus on that date range.\n\nRecommend. Pick the best park considering all three factors. If the top choice has blocking alerts or severe weather, suggest an alternative from the search results.\n\nShortcuts:\n\nWeather-only question \u2192 skip NPS tools, forecast directly.\nAlert-only question \u2192 skip weather tool.\nUser names a specific park \u2192 still check alerts and weather, skip search only if you already have the park code and coordinates.\n\nResponse format:\n\nProvide your recommendation in this structure:\n\nPark: name, location, and why it fits the activity\n\nAlerts: any active alerts (or \"No active alerts\")\n\nWeather Outlook: conditions for the travel dates, with a note on suitability for the activity\n\nTips: 1-2 practical tips (best trails, gear, timing, alternatives if weather turns)" + }, + "tools": { + "_input_type": "HandleInput", + "advanced": false, + "display_name": "Tools", + "dynamic": false, + "info": "These are the tools that the agent can use to help with tasks.", + "input_types": [ + "Tool" + ], + "list": true, + "list_add_label": "Add More", + "name": "tools", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "trace_as_metadata": true, + "type": "other", + "value": "" + }, + "verbose": { + "_input_type": "BoolInput", + "advanced": true, + "display_name": "Verbose", + "dynamic": false, + "info": "", + "input_types": [], + "list": false, + "list_add_label": "Add More", + "name": "verbose", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "type": "bool", + "value": true + } + }, + "tool_mode": false + }, + "selected_output": "response", + "showNode": true, + "type": "Agent" + }, + "dragging": false, + "id": "Agent-Np81j", + "measured": { + "width": 320, + "height": 429 + }, + "position": { + "x": 1981.5611428837763, + "y": 112.39787837448219 + }, + "selected": true, + "type": "genericNode" + }, + { + "data": { + "id": "OpenMeteoForecastComponent-UCBro", + "node": { + "base_classes": [ + "Data" + ], + "beta": false, + "conditional_paths": [], + "custom_fields": {}, + "description": "Fetch weather forecast for a given latitude and longitude", + "display_name": "Open-Meteo Forecast", + "documentation": "", + "edited": true, + "field_order": [ + "latitude", + "longitude", + "forecast_days", + "timezone", + "daily_variables", + "hourly_variables" + ], + "frozen": false, + "icon": "cloud", + "last_updated": "2026-02-26T18:49:15.533Z", + "legacy": false, + "lf_version": "1.7.1", + "metadata": { + "code_hash": "debf08d0a0d5", + "dependencies": { + "dependencies": [ + { + "name": "lfx", + "version": "0.2.2" + }, + { + "name": "requests", + "version": "2.32.5" + } + ], + "total_dependencies": 2 + }, + "module": "custom_components.openmeteo_forecast" + }, + "minimized": false, + "output_types": [], + "outputs": [ + { + "types": [ + "Tool" + ], + "selected": "Tool", + "name": "component_as_tool", + "hidden": null, + "display_name": "Toolset", + "method": "to_toolkit", + "value": "__UNDEFINED__", + "cache": true, + "required_inputs": null, + "allows_loop": false, + "loop_types": null, + "group_outputs": false, + "options": null, + "tool_mode": true + } + ], + "pinned": false, + "template": { + "_frontend_node_flow_id": { + "value": "78698891-d8b6-4fb8-ad64-e0335063e62a" + }, + "_frontend_node_folder_id": { + "value": "676d9518-21e7-4e3e-a53b-283fd2122418" + }, + "_type": "Component", + "code": { + "advanced": true, + "dynamic": true, + "fileTypes": [], + "file_path": "", + "info": "", + "list": false, + "load_from_db": false, + "multiline": true, + "name": "code", + "password": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "type": "code", + "value": "from lfx.custom import Component\nfrom lfx.inputs import StrInput, IntInput, MultilineInput\nfrom lfx.template import Output\nfrom lfx.schema import Data\nimport requests\n\nclass OpenMeteoForecastComponent(Component):\n display_name = \"Open-Meteo Forecast\"\n description = \"Fetch weather forecast for a given latitude and longitude\"\n icon = \"cloud\"\n\n inputs = [\n StrInput(\n name=\"latitude\",\n display_name=\"Latitude\",\n info=\"Decimal latitude, e.g. 40.3428\",\n value=\"40.3428\"\n ),\n StrInput(\n name=\"longitude\",\n display_name=\"Longitude\",\n info=\"Decimal longitude, e.g. -105.6836\",\n value=\"-105.6836\"\n ),\n IntInput(\n name=\"forecast_days\",\n display_name=\"Forecast Days\",\n info=\"Number of days to forecast (1-16)\",\n value=7\n ),\n StrInput(\n name=\"timezone\",\n display_name=\"Timezone\",\n info=\"Timezone string, e.g. America/Denver\",\n value=\"America/New_York\"\n ),\n MultilineInput(\n name=\"daily_variables\",\n display_name=\"Daily Variables\",\n info=\"Comma-separated Open-Meteo daily variables to fetch\",\n value=\"temperature_2m_max,temperature_2m_min,precipitation_sum,windspeed_10m_max,weathercode\",\n tool_mode=True,\n ),\n MultilineInput(\n name=\"hourly_variables\",\n display_name=\"Hourly Variables\",\n info=\"Comma-separated Open-Meteo hourly variables (leave blank to skip)\",\n value=\"\"\n ),\n ]\n\n outputs = [\n Output(display_name=\"Forecast Tool\", name=\"forecast\", method=\"get_forecast\")\n ]\n\n def get_forecast(self) -> Data:\n url = \"https://api.open-meteo.com/v1/forecast\"\n\n params = {\n \"latitude\": self.latitude,\n \"longitude\": self.longitude,\n \"forecast_days\": self.forecast_days,\n \"timezone\": self.timezone,\n }\n\n if self.daily_variables.strip():\n params[\"daily\"] = self.daily_variables.strip()\n\n if self.hourly_variables.strip():\n params[\"hourly\"] = self.hourly_variables.strip()\n\n try:\n response = requests.get(url, params=params, timeout=10)\n response.raise_for_status()\n data = response.json()\n\n result = {\n \"location\": {\n \"latitude\": data.get(\"latitude\"),\n \"longitude\": data.get(\"longitude\"),\n \"timezone\": data.get(\"timezone\"),\n \"elevation_m\": data.get(\"elevation\")\n },\n \"forecast_days\": self.forecast_days,\n \"daily\": data.get(\"daily\", {}),\n \"hourly\": data.get(\"hourly\", {})\n }\n\n return Data(data=result)\n\n except requests.exceptions.Timeout:\n return Data(data={\"error\": \"Request timed out. Try again.\"})\n except requests.exceptions.HTTPError as e:\n return Data(data={\"error\": \"HTTP error: \" + str(e)})\n except Exception as e:\n return Data(data={\"error\": \"Unexpected error: \" + str(e)})" + }, + "daily_variables": { + "_input_type": "MultilineInput", + "advanced": false, + "ai_enabled": false, + "copy_field": false, + "display_name": "Daily Variables", + "dynamic": false, + "info": "Comma-separated Open-Meteo daily variables to fetch", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "multiline": true, + "name": "daily_variables", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": true, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "temperature_2m_max,temperature_2m_min,precipitation_sum,windspeed_10m_max,weathercode" + }, + "forecast_days": { + "_input_type": "IntInput", + "advanced": false, + "display_name": "Forecast Days", + "dynamic": false, + "info": "Number of days to forecast (1-16)", + "list": false, + "list_add_label": "Add More", + "name": "forecast_days", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "int", + "value": 7 + }, + "hourly_variables": { + "_input_type": "MultilineInput", + "advanced": false, + "ai_enabled": false, + "copy_field": false, + "display_name": "Hourly Variables", + "dynamic": false, + "info": "Comma-separated Open-Meteo hourly variables (leave blank to skip)", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "multiline": true, + "name": "hourly_variables", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "is_refresh": false, + "latitude": { + "_input_type": "StrInput", + "advanced": false, + "display_name": "Latitude", + "dynamic": false, + "info": "Decimal latitude, e.g. 40.3428", + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "latitude", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "40.3428" + }, + "longitude": { + "_input_type": "StrInput", + "advanced": false, + "display_name": "Longitude", + "dynamic": false, + "info": "Decimal longitude, e.g. -105.6836", + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "longitude", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "-105.6836" + }, + "timezone": { + "_input_type": "StrInput", + "advanced": false, + "display_name": "Timezone", + "dynamic": false, + "info": "Timezone string, e.g. America/Denver", + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "timezone", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "America/New_York" + }, + "tools_metadata": { + "tool_mode": false, + "trace_as_metadata": true, + "is_list": true, + "list_add_label": "Add More", + "override_skip": false, + "required": false, + "placeholder": "", + "show": true, + "name": "tools_metadata", + "value": [ + { + "args": { + "daily_variables": { + "default": "temperature_2m_max,temperature_2m_min,precipitation_sum,windspeed_10m_max,weathercode", + "description": "Comma-separated Open-Meteo daily variables to fetch", + "title": "Daily Variables", + "type": "string" + } + }, + "description": "Fetch weather forecast for a given latitude and longitude", + "display_description": "Fetch weather forecast for a given latitude and longitude", + "display_name": "get_forecast", + "name": "get_forecast", + "readonly": false, + "status": true, + "tags": [ + "get_forecast" + ] + } + ], + "display_name": "Actions", + "advanced": false, + "dynamic": false, + "info": "Modify tool names and descriptions to help agents understand when to use each tool.", + "real_time_refresh": true, + "title_case": false, + "track_in_telemetry": false, + "type": "tools", + "_input_type": "ToolsInput" + } + }, + "tool_mode": true + }, + "showNode": true, + "type": "OpenMeteoForecastComponent" + }, + "dragging": false, + "id": "OpenMeteoForecastComponent-UCBro", + "measured": { + "width": 320, + "height": 630 + }, + "position": { + "x": 511.32090434332224, + "y": 11.281442436767321 + }, + "selected": false, + "type": "genericNode" + }, + { + "data": { + "id": "NPSSearchParksComponent-lPuSF", + "node": { + "base_classes": [ + "Data" + ], + "beta": false, + "conditional_paths": [], + "custom_fields": {}, + "description": "Search national parks by state, activity, or keyword using the NPS API", + "display_name": "NPS Search Parks", + "documentation": "", + "edited": true, + "field_order": [ + "state_code", + "api_key", + "limit", + "query" + ], + "frozen": false, + "icon": "map", + "legacy": false, + "lf_version": "1.7.1", + "metadata": { + "code_hash": "10229bbf5ea9", + "dependencies": { + "dependencies": [ + { + "name": "lfx", + "version": "0.2.2" + }, + { + "name": "requests", + "version": "2.32.5" + } + ], + "total_dependencies": 2 + }, + "module": "custom_components.nps_search_parks" + }, + "minimized": false, + "output_types": [], + "outputs": [ + { + "types": [ + "Tool" + ], + "selected": "Tool", + "name": "component_as_tool", + "hidden": null, + "display_name": "Toolset", + "method": "to_toolkit", + "value": "__UNDEFINED__", + "cache": true, + "required_inputs": null, + "allows_loop": false, + "loop_types": null, + "group_outputs": false, + "options": null, + "tool_mode": true + } + ], + "pinned": false, + "template": { + "_type": "Component", + "api_key": { + "_input_type": "SecretStrInput", + "advanced": false, + "display_name": "NPS API Key", + "dynamic": false, + "info": "Get a free key at developer.nps.gov or use DEMO_KEY to start", + "input_types": [], + "load_from_db": false, + "name": "api_key", + "override_skip": false, + "password": true, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "track_in_telemetry": false, + "type": "str", + "value": "Ca4E8GV8ZsgO2DDAumGXgbaeASRFEIIddaaEjtkU" + }, + "code": { + "advanced": true, + "dynamic": true, + "fileTypes": [], + "file_path": "", + "info": "", + "list": false, + "load_from_db": false, + "multiline": true, + "name": "code", + "password": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "type": "code", + "value": "from lfx.custom import Component\nfrom lfx.inputs import StrInput, IntInput, MultilineInput\nfrom lfx.template import Output\nfrom lfx.schema import Data\nimport requests\n\nclass NPSSearchParksComponent(Component):\n display_name = \"NPS Search Parks\"\n description = \"Search national parks by state, activity, or keyword using the NPS API\"\n icon = \"map\"\n\n inputs = [\n StrInput(\n name=\"state_code\",\n display_name=\"State Code\",\n info=\"Two-letter state code, e.g. CO, CA, NY\",\n value=\"CO\"\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"NPS API Key\",\n info=\"Get a free key at developer.nps.gov or use DEMO_KEY to start\",\n value=\"DEMO_KEY\"\n ),\n IntInput(\n name=\"limit\",\n display_name=\"Limit\",\n info=\"Maximum number of parks to return\",\n value=5\n ),\n MultilineInput(\n name=\"query\",\n display_name=\"Query\",\n info=\"Keyword search, e.g. hiking, camping, waterfall\",\n value=\"hiking\",\n tool_mode=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Parks Tool\", name=\"parks\", method=\"search_parks\")\n ]\n\n def search_parks(self) -> Data:\n url = \"https://developer.nps.gov/api/v1/parks\"\n\n params = {\n \"api_key\": self.api_key,\n \"limit\": self.limit,\n }\n\n if self.state_code.strip():\n params[\"stateCode\"] = self.state_code.strip().upper()\n\n if self.query.strip():\n params[\"q\"] = self.query.strip()\n\n try:\n response = requests.get(url, params=params, timeout=10)\n response.raise_for_status()\n data = response.json()\n\n parks = []\n for p in data.get(\"data\", []):\n parks.append({\n \"name\": p.get(\"fullName\", \"\"),\n \"code\": p.get(\"parkCode\", \"\"),\n \"description\": p.get(\"description\", \"\")[:200],\n \"states\": p.get(\"states\", \"\"),\n \"designation\": p.get(\"designation\", \"\"),\n \"latitude\": p.get(\"latitude\", \"\"),\n \"longitude\": p.get(\"longitude\", \"\"),\n \"url\": p.get(\"url\", \"\")\n })\n\n return Data(data={\n \"total\": data.get(\"total\", len(parks)),\n \"parks\": parks\n })\n\n except requests.exceptions.Timeout:\n return Data(data={\"error\": \"Request timed out. Try again.\"})\n except requests.exceptions.HTTPError as e:\n return Data(data={\"error\": \"HTTP error: \" + str(e)})\n except Exception as e:\n return Data(data={\"error\": \"Unexpected error: \" + str(e)})" + }, + "limit": { + "_input_type": "IntInput", + "advanced": false, + "display_name": "Limit", + "dynamic": false, + "info": "Maximum number of parks to return", + "list": false, + "list_add_label": "Add More", + "name": "limit", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "int", + "value": 5 + }, + "query": { + "_input_type": "MultilineInput", + "advanced": false, + "ai_enabled": false, + "copy_field": false, + "display_name": "Query", + "dynamic": false, + "info": "Keyword search, e.g. hiking, camping, waterfall", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "multiline": true, + "name": "query", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": true, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "hiking" + }, + "state_code": { + "_input_type": "StrInput", + "advanced": false, + "display_name": "State Code", + "dynamic": false, + "info": "Two-letter state code, e.g. CO, CA, NY", + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "state_code", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "CO" + }, + "tools_metadata": { + "tool_mode": false, + "trace_as_metadata": true, + "is_list": true, + "list_add_label": "Add More", + "override_skip": false, + "required": false, + "placeholder": "", + "show": true, + "name": "tools_metadata", + "value": [ + { + "args": { + "query": { + "default": "hiking", + "description": "Keyword search, e.g. hiking, camping, waterfall", + "title": "Query", + "type": "string" + } + }, + "description": "Search national parks by state, activity, or keyword using the NPS API", + "display_description": "Search national parks by state, activity, or keyword using the NPS API", + "display_name": "search_parks", + "name": "search_parks", + "readonly": false, + "status": true, + "tags": [ + "search_parks" + ] + } + ], + "display_name": "Actions", + "advanced": false, + "dynamic": false, + "info": "Modify tool names and descriptions to help agents understand when to use each tool.", + "real_time_refresh": true, + "title_case": false, + "track_in_telemetry": false, + "type": "tools", + "_input_type": "ToolsInput" + }, + "_frontend_node_flow_id": { + "value": "78698891-d8b6-4fb8-ad64-e0335063e62a" + }, + "_frontend_node_folder_id": { + "value": "676d9518-21e7-4e3e-a53b-283fd2122418" + }, + "is_refresh": false + }, + "tool_mode": true, + "last_updated": "2026-02-26T18:49:15.534Z" + }, + "showNode": true, + "type": "NPSSearchParksComponent" + }, + "dragging": false, + "id": "NPSSearchParksComponent-lPuSF", + "measured": { + "width": 320, + "height": 465 + }, + "position": { + "x": 919.1199671060897, + "y": -55.01135032295745 + }, + "selected": false, + "type": "genericNode" + }, + { + "data": { + "id": "NPSParkAlertsComponent-L8q8t", + "node": { + "base_classes": [ + "Data" + ], + "beta": false, + "conditional_paths": [], + "custom_fields": {}, + "description": "Get active hazard and closure alerts for a national park", + "display_name": "NPS Park Alerts", + "documentation": "", + "edited": true, + "field_order": [ + "api_key", + "park_code" + ], + "frozen": false, + "icon": "alert-triangle", + "legacy": false, + "lf_version": "1.7.1", + "metadata": { + "code_hash": "3040cf95d296", + "dependencies": { + "dependencies": [ + { + "name": "lfx", + "version": "0.2.2" + }, + { + "name": "requests", + "version": "2.32.5" + } + ], + "total_dependencies": 2 + }, + "module": "custom_components.nps_park_alerts" + }, + "minimized": false, + "output_types": [], + "outputs": [ + { + "types": [ + "Tool" + ], + "selected": "Tool", + "name": "component_as_tool", + "hidden": null, + "display_name": "Toolset", + "method": "to_toolkit", + "value": "__UNDEFINED__", + "cache": true, + "required_inputs": null, + "allows_loop": false, + "loop_types": null, + "group_outputs": false, + "options": null, + "tool_mode": true + } + ], + "pinned": false, + "template": { + "_type": "Component", + "api_key": { + "_input_type": "SecretStrInput", + "advanced": false, + "display_name": "NPS API Key", + "dynamic": false, + "info": "Get a free key at developer.nps.gov or use DEMO_KEY to start", + "input_types": [], + "load_from_db": false, + "name": "api_key", + "override_skip": false, + "password": true, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "track_in_telemetry": false, + "type": "str", + "value": "Ca4E8GV8ZsgO2DDAumGXgbaeASRFEIIddaaEjtkU" + }, + "code": { + "advanced": true, + "dynamic": true, + "fileTypes": [], + "file_path": "", + "info": "", + "list": false, + "load_from_db": false, + "multiline": true, + "name": "code", + "password": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "type": "code", + "value": "from lfx.custom import Component\nfrom lfx.inputs import StrInput, MultilineInput\nfrom lfx.template import Output\nfrom lfx.schema import Data\nimport requests\n\nclass NPSParkAlertsComponent(Component):\n display_name = \"NPS Park Alerts\"\n description = \"Get active hazard and closure alerts for a national park\"\n icon = \"alert-triangle\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"NPS API Key\",\n info=\"Get a free key at developer.nps.gov or use DEMO_KEY to start\",\n value=\"DEMO_KEY\"\n ),\n MultilineInput(\n name=\"park_code\",\n display_name=\"Park Code\",\n info=\"Four-letter park code, e.g. romo, acad, grca, yell\",\n value=\"romo\",\n tool_mode=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Alerts Tool\", name=\"alerts\", method=\"get_alerts\")\n ]\n\n def get_alerts(self) -> Data:\n url = \"https://developer.nps.gov/api/v1/alerts\"\n\n params = {\n \"api_key\": self.api_key,\n \"parkCode\": self.park_code.strip().lower()\n }\n\n try:\n response = requests.get(url, params=params, timeout=10)\n response.raise_for_status()\n data = response.json()\n\n alerts = []\n for a in data.get(\"data\", []):\n alerts.append({\n \"title\": a.get(\"title\", \"\"),\n \"category\": a.get(\"category\", \"\"),\n \"description\": a.get(\"description\", \"\")[:200],\n \"url\": a.get(\"url\", \"\")\n })\n\n return Data(data={\n \"park_code\": self.park_code.strip().upper(),\n \"total_alerts\": len(alerts),\n \"alerts\": alerts if alerts else [\"No active alerts for this park\"]\n })\n\n except requests.exceptions.Timeout:\n return Data(data={\"error\": \"Request timed out. Try again.\"})\n except requests.exceptions.HTTPError as e:\n return Data(data={\"error\": \"HTTP error: \" + str(e)})\n except Exception as e:\n return Data(data={\"error\": \"Unexpected error: \" + str(e)})" + }, + "park_code": { + "_input_type": "MultilineInput", + "advanced": false, + "ai_enabled": false, + "copy_field": false, + "display_name": "Park Code", + "dynamic": false, + "info": "Four-letter park code, e.g. romo, acad, grca, yell", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "multiline": true, + "name": "park_code", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": true, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "romo" + }, + "tools_metadata": { + "tool_mode": false, + "trace_as_metadata": true, + "is_list": true, + "list_add_label": "Add More", + "override_skip": false, + "required": false, + "placeholder": "", + "show": true, + "name": "tools_metadata", + "value": [ + { + "args": { + "park_code": { + "default": "romo", + "description": "Four-letter park code, e.g. romo, acad, grca, yell", + "title": "Park Code", + "type": "string" + } + }, + "description": "Get active hazard and closure alerts for a national park", + "display_description": "Get active hazard and closure alerts for a national park", + "display_name": "get_alerts", + "name": "get_alerts", + "readonly": false, + "status": true, + "tags": [ + "get_alerts" + ] + } + ], + "display_name": "Actions", + "advanced": false, + "dynamic": false, + "info": "Modify tool names and descriptions to help agents understand when to use each tool.", + "real_time_refresh": true, + "title_case": false, + "track_in_telemetry": false, + "type": "tools", + "_input_type": "ToolsInput" + }, + "_frontend_node_flow_id": { + "value": "78698891-d8b6-4fb8-ad64-e0335063e62a" + }, + "_frontend_node_folder_id": { + "value": "676d9518-21e7-4e3e-a53b-283fd2122418" + }, + "is_refresh": false + }, + "tool_mode": true, + "last_updated": "2026-02-26T18:49:15.535Z" + }, + "showNode": true, + "type": "NPSParkAlertsComponent" + }, + "dragging": false, + "id": "NPSParkAlertsComponent-L8q8t", + "measured": { + "width": 320, + "height": 300 + }, + "position": { + "x": 1049.7070782940123, + "y": 686.6744228431585 + }, + "selected": false, + "type": "genericNode" + }, + { + "data": { + "id": "KServeVLLM-0EoPh", + "node": { + "base_classes": [ + "LanguageModel", + "Message" + ], + "beta": false, + "conditional_paths": [], + "custom_fields": {}, + "description": "Language model served via KServe + vLLM (OpenAI-compatible API).", + "display_name": "KServe vLLM", + "documentation": "", + "edited": true, + "field_order": [ + "input_value", + "system_message", + "stream", + "api_base", + "model_name", + "api_key", + "temperature", + "max_tokens" + ], + "frozen": false, + "icon": "server", + "legacy": false, + "lf_version": "1.7.1", + "metadata": { + "code_hash": "07b5b42b04cc", + "dependencies": { + "dependencies": [ + { + "name": "langchain_openai", + "version": "0.3.23" + }, + { + "name": "langflow", + "version": "1.7.1" + } + ], + "total_dependencies": 2 + }, + "keywords": [ + "model", + "llm", + "language model", + "large language model" + ], + "module": "custom_components.kserve_vllm" + }, + "minimized": false, + "output_types": [], + "outputs": [ + { + "allows_loop": false, + "cache": true, + "display_name": "Model Response", + "group_outputs": false, + "hidden": null, + "loop_types": null, + "method": "text_response", + "name": "text_output", + "options": null, + "required_inputs": null, + "tool_mode": true, + "types": [ + "Message" + ], + "value": "__UNDEFINED__" + }, + { + "allows_loop": false, + "cache": true, + "display_name": "Language Model", + "group_outputs": false, + "hidden": null, + "loop_types": null, + "method": "build_model", + "name": "model_output", + "options": null, + "required_inputs": null, + "selected": "LanguageModel", + "tool_mode": true, + "types": [ + "LanguageModel" + ], + "value": "__UNDEFINED__" + } + ], + "pinned": false, + "template": { + "_type": "Component", + "api_base": { + "_input_type": "StrInput", + "advanced": false, + "display_name": "API Base URL", + "dynamic": false, + "info": "KServe vLLM OpenAI-compatible endpoint.", + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "api_base", + "override_skip": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "http://llama-31-8b-instruct-predictor.langflow-example.svc.cluster.local:8080/v1" + }, + "api_key": { + "_input_type": "SecretStrInput", + "advanced": false, + "display_name": "API Key", + "dynamic": false, + "info": "API key (use 'EMPTY' if not required).", + "input_types": [], + "load_from_db": false, + "name": "api_key", + "override_skip": false, + "password": true, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "track_in_telemetry": false, + "type": "str", + "value": "EMPTY" + }, + "code": { + "advanced": true, + "dynamic": true, + "fileTypes": [], + "file_path": "", + "info": "", + "list": false, + "load_from_db": false, + "multiline": true, + "name": "code", + "password": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "type": "code", + "value": "from langchain_openai import ChatOpenAI\n\nfrom langflow.base.models.model import LCModelComponent\nfrom langflow.field_typing import LanguageModel\nfrom langflow.io import FloatInput, IntInput, SecretStrInput, StrInput\n\n\nclass KServeVLLMComponent(LCModelComponent):\n display_name = \"KServe vLLM\"\n description = \"Language model served via KServe + vLLM (OpenAI-compatible API).\"\n icon = \"server\"\n name = \"KServeVLLM\"\n\n inputs = LCModelComponent._base_inputs + [\n StrInput(\n name=\"api_base\",\n display_name=\"API Base URL\",\n info=\"KServe vLLM OpenAI-compatible endpoint.\",\n value=\"http://llama-31-8b-instruct-predictor.outdoor-activity-agent.svc.cluster.local:8080/v1\",\n required=True,\n ),\n StrInput(\n name=\"model_name\",\n display_name=\"Model Name\",\n value=\"llama-31-8b-instruct\",\n required=True,\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"API key (use 'EMPTY' if not required).\",\n value=\"EMPTY\",\n ),\n FloatInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n value=4096,\n advanced=True,\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return ChatOpenAI(\n api_key=self.api_key or \"EMPTY\",\n model=self.model_name,\n base_url=self.api_base,\n temperature=self.temperature,\n max_tokens=self.max_tokens if self.max_tokens > 0 else None,\n timeout=120,\n )\n" + }, + "input_value": { + "_input_type": "MessageInput", + "advanced": false, + "display_name": "Input", + "dynamic": false, + "info": "", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "input_value", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "Hello, World!" + }, + "max_tokens": { + "_input_type": "IntInput", + "advanced": true, + "display_name": "Max Tokens", + "dynamic": false, + "info": "", + "list": false, + "list_add_label": "Add More", + "name": "max_tokens", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "int", + "value": 4096 + }, + "model_name": { + "_input_type": "StrInput", + "advanced": false, + "display_name": "Model Name", + "dynamic": false, + "info": "", + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "model_name", + "override_skip": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "llama-31-8b-instruct" + }, + "stream": { + "_input_type": "BoolInput", + "advanced": true, + "display_name": "Stream", + "dynamic": false, + "info": "Stream the response from the model. Streaming works only in Chat.", + "list": false, + "list_add_label": "Add More", + "name": "stream", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "bool", + "value": false + }, + "system_message": { + "_input_type": "MultilineInput", + "advanced": false, + "ai_enabled": false, + "copy_field": false, + "display_name": "System Message", + "dynamic": false, + "info": "System message to pass to the model.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "multiline": true, + "name": "system_message", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "temperature": { + "_input_type": "FloatInput", + "advanced": false, + "display_name": "Temperature", + "dynamic": false, + "info": "", + "list": false, + "list_add_label": "Add More", + "name": "temperature", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "float", + "value": 0 + } + }, + "tool_mode": false + }, + "selected_output": "model_output", + "showNode": true, + "type": "KServeVLLM" + }, + "dragging": false, + "id": "KServeVLLM-0EoPh", + "measured": { + "width": 320, + "height": 632 + }, + "position": { + "x": 1360.7832426446166, + "y": -272.11380229635813 + }, + "selected": false, + "type": "genericNode" + } + ], + "edges": [ + { + "animated": false, + "className": "", + "data": { + "sourceHandle": { + "dataType": "Agent", + "id": "Agent-Np81j", + "name": "response", + "output_types": [ + "Message" + ] + }, + "targetHandle": { + "fieldName": "input_value", + "id": "ChatOutput-QSU1M", + "inputTypes": [ + "Data", + "DataFrame", + "Message" + ], + "type": "other" + } + }, + "id": "reactflow__edge-Agent-Np81j{\u0153dataType\u0153:\u0153Agent\u0153,\u0153id\u0153:\u0153Agent-Np81j\u0153,\u0153name\u0153:\u0153response\u0153,\u0153output_types\u0153:[\u0153Message\u0153]}-ChatOutput-QSU1M{\u0153fieldName\u0153:\u0153input_value\u0153,\u0153id\u0153:\u0153ChatOutput-QSU1M\u0153,\u0153inputTypes\u0153:[\u0153Data\u0153,\u0153DataFrame\u0153,\u0153Message\u0153],\u0153type\u0153:\u0153other\u0153}", + "selected": false, + "source": "Agent-Np81j", + "sourceHandle": "{\u0153dataType\u0153:\u0153Agent\u0153,\u0153id\u0153:\u0153Agent-Np81j\u0153,\u0153name\u0153:\u0153response\u0153,\u0153output_types\u0153:[\u0153Message\u0153]}", + "target": "ChatOutput-QSU1M", + "targetHandle": "{\u0153fieldName\u0153:\u0153input_value\u0153,\u0153id\u0153:\u0153ChatOutput-QSU1M\u0153,\u0153inputTypes\u0153:[\u0153Data\u0153,\u0153DataFrame\u0153,\u0153Message\u0153],\u0153type\u0153:\u0153other\u0153}" + }, + { + "animated": false, + "className": "", + "data": { + "sourceHandle": { + "dataType": "ChatInput", + "id": "ChatInput-42lm9", + "name": "message", + "output_types": [ + "Message" + ] + }, + "targetHandle": { + "fieldName": "input_value", + "id": "Agent-Np81j", + "inputTypes": [ + "Message" + ], + "type": "str" + } + }, + "id": "reactflow__edge-ChatInput-42lm9{\u0153dataType\u0153:\u0153ChatInput\u0153,\u0153id\u0153:\u0153ChatInput-42lm9\u0153,\u0153name\u0153:\u0153message\u0153,\u0153output_types\u0153:[\u0153Message\u0153]}-Agent-Np81j{\u0153fieldName\u0153:\u0153input_value\u0153,\u0153id\u0153:\u0153Agent-Np81j\u0153,\u0153inputTypes\u0153:[\u0153Message\u0153],\u0153type\u0153:\u0153str\u0153}", + "selected": false, + "source": "ChatInput-42lm9", + "sourceHandle": "{\u0153dataType\u0153:\u0153ChatInput\u0153,\u0153id\u0153:\u0153ChatInput-42lm9\u0153,\u0153name\u0153:\u0153message\u0153,\u0153output_types\u0153:[\u0153Message\u0153]}", + "target": "Agent-Np81j", + "targetHandle": "{\u0153fieldName\u0153:\u0153input_value\u0153,\u0153id\u0153:\u0153Agent-Np81j\u0153,\u0153inputTypes\u0153:[\u0153Message\u0153],\u0153type\u0153:\u0153str\u0153}" + }, + { + "animated": false, + "className": "", + "data": { + "sourceHandle": { + "dataType": "OpenMeteoForecastComponent", + "id": "OpenMeteoForecastComponent-UCBro", + "name": "component_as_tool", + "output_types": [ + "Tool" + ] + }, + "targetHandle": { + "fieldName": "tools", + "id": "Agent-Np81j", + "inputTypes": [ + "Tool" + ], + "type": "other" + } + }, + "id": "reactflow__edge-OpenMeteoForecastComponent-UCBro{\u0153dataType\u0153:\u0153OpenMeteoForecastComponent\u0153,\u0153id\u0153:\u0153OpenMeteoForecastComponent-UCBro\u0153,\u0153name\u0153:\u0153component_as_tool\u0153,\u0153output_types\u0153:[\u0153Tool\u0153]}-Agent-Np81j{\u0153fieldName\u0153:\u0153tools\u0153,\u0153id\u0153:\u0153Agent-Np81j\u0153,\u0153inputTypes\u0153:[\u0153Tool\u0153],\u0153type\u0153:\u0153other\u0153}", + "selected": false, + "source": "OpenMeteoForecastComponent-UCBro", + "sourceHandle": "{\u0153dataType\u0153:\u0153OpenMeteoForecastComponent\u0153,\u0153id\u0153:\u0153OpenMeteoForecastComponent-UCBro\u0153,\u0153name\u0153:\u0153component_as_tool\u0153,\u0153output_types\u0153:[\u0153Tool\u0153]}", + "target": "Agent-Np81j", + "targetHandle": "{\u0153fieldName\u0153:\u0153tools\u0153,\u0153id\u0153:\u0153Agent-Np81j\u0153,\u0153inputTypes\u0153:[\u0153Tool\u0153],\u0153type\u0153:\u0153other\u0153}" + }, + { + "animated": false, + "className": "", + "data": { + "sourceHandle": { + "dataType": "NPSParkAlertsComponent", + "id": "NPSParkAlertsComponent-L8q8t", + "name": "component_as_tool", + "output_types": [ + "Tool" + ] + }, + "targetHandle": { + "fieldName": "tools", + "id": "Agent-Np81j", + "inputTypes": [ + "Tool" + ], + "type": "other" + } + }, + "id": "reactflow__edge-NPSParkAlertsComponent-L8q8t{\u0153dataType\u0153:\u0153NPSParkAlertsComponent\u0153,\u0153id\u0153:\u0153NPSParkAlertsComponent-L8q8t\u0153,\u0153name\u0153:\u0153component_as_tool\u0153,\u0153output_types\u0153:[\u0153Tool\u0153]}-Agent-Np81j{\u0153fieldName\u0153:\u0153tools\u0153,\u0153id\u0153:\u0153Agent-Np81j\u0153,\u0153inputTypes\u0153:[\u0153Tool\u0153],\u0153type\u0153:\u0153other\u0153}", + "selected": false, + "source": "NPSParkAlertsComponent-L8q8t", + "sourceHandle": "{\u0153dataType\u0153:\u0153NPSParkAlertsComponent\u0153,\u0153id\u0153:\u0153NPSParkAlertsComponent-L8q8t\u0153,\u0153name\u0153:\u0153component_as_tool\u0153,\u0153output_types\u0153:[\u0153Tool\u0153]}", + "target": "Agent-Np81j", + "targetHandle": "{\u0153fieldName\u0153:\u0153tools\u0153,\u0153id\u0153:\u0153Agent-Np81j\u0153,\u0153inputTypes\u0153:[\u0153Tool\u0153],\u0153type\u0153:\u0153other\u0153}" + }, + { + "animated": false, + "className": "", + "data": { + "sourceHandle": { + "dataType": "NPSSearchParksComponent", + "id": "NPSSearchParksComponent-lPuSF", + "name": "component_as_tool", + "output_types": [ + "Tool" + ] + }, + "targetHandle": { + "fieldName": "tools", + "id": "Agent-Np81j", + "inputTypes": [ + "Tool" + ], + "type": "other" + } + }, + "id": "reactflow__edge-NPSSearchParksComponent-lPuSF{\u0153dataType\u0153:\u0153NPSSearchParksComponent\u0153,\u0153id\u0153:\u0153NPSSearchParksComponent-lPuSF\u0153,\u0153name\u0153:\u0153component_as_tool\u0153,\u0153output_types\u0153:[\u0153Tool\u0153]}-Agent-Np81j{\u0153fieldName\u0153:\u0153tools\u0153,\u0153id\u0153:\u0153Agent-Np81j\u0153,\u0153inputTypes\u0153:[\u0153Tool\u0153],\u0153type\u0153:\u0153other\u0153}", + "selected": false, + "source": "NPSSearchParksComponent-lPuSF", + "sourceHandle": "{\u0153dataType\u0153:\u0153NPSSearchParksComponent\u0153,\u0153id\u0153:\u0153NPSSearchParksComponent-lPuSF\u0153,\u0153name\u0153:\u0153component_as_tool\u0153,\u0153output_types\u0153:[\u0153Tool\u0153]}", + "target": "Agent-Np81j", + "targetHandle": "{\u0153fieldName\u0153:\u0153tools\u0153,\u0153id\u0153:\u0153Agent-Np81j\u0153,\u0153inputTypes\u0153:[\u0153Tool\u0153],\u0153type\u0153:\u0153other\u0153}" + }, + { + "source": "KServeVLLM-0EoPh", + "sourceHandle": "{\u0153dataType\u0153:\u0153KServeVLLM\u0153,\u0153id\u0153:\u0153KServeVLLM-0EoPh\u0153,\u0153name\u0153:\u0153model_output\u0153,\u0153output_types\u0153:[\u0153LanguageModel\u0153]}", + "target": "Agent-Np81j", + "targetHandle": "{\u0153fieldName\u0153:\u0153agent_llm\u0153,\u0153id\u0153:\u0153Agent-Np81j\u0153,\u0153inputTypes\u0153:[\u0153LanguageModel\u0153],\u0153type\u0153:\u0153str\u0153}", + "data": { + "targetHandle": { + "fieldName": "agent_llm", + "id": "Agent-Np81j", + "inputTypes": [ + "LanguageModel" + ], + "type": "str" + }, + "sourceHandle": { + "dataType": "KServeVLLM", + "id": "KServeVLLM-0EoPh", + "name": "model_output", + "output_types": [ + "LanguageModel" + ] + } + }, + "id": "xy-edge__KServeVLLM-0EoPh{\u0153dataType\u0153:\u0153KServeVLLM\u0153,\u0153id\u0153:\u0153KServeVLLM-0EoPh\u0153,\u0153name\u0153:\u0153model_output\u0153,\u0153output_types\u0153:[\u0153LanguageModel\u0153]}-Agent-Np81j{\u0153fieldName\u0153:\u0153agent_llm\u0153,\u0153id\u0153:\u0153Agent-Np81j\u0153,\u0153inputTypes\u0153:[\u0153LanguageModel\u0153],\u0153type\u0153:\u0153str\u0153}", + "selected": false, + "animated": false, + "className": "" + } + ], + "viewport": { + "x": -599.8675156460467, + "y": 182.0550649922885, + "zoom": 0.683739903754153 + } + }, + "is_component": false, + "webhook": false, + "endpoint_name": null, + "tags": [ + "assistants", + "agents" + ], + "locked": false, + "mcp_enabled": true, + "action_name": null, + "action_description": null, + "access_type": "PRIVATE" +} \ No newline at end of file diff --git a/helm/langflow-agent/charts/model-serving/templates/inferenceservice.yaml b/helm/langflow-agent/charts/model-serving/templates/inferenceservice.yaml index 1a6f5b6..4341754 100644 --- a/helm/langflow-agent/charts/model-serving/templates/inferenceservice.yaml +++ b/helm/langflow-agent/charts/model-serving/templates/inferenceservice.yaml @@ -2,7 +2,7 @@ apiVersion: serving.kserve.io/v1beta1 kind: InferenceService metadata: - name: llama-31-8b-instruct + name: qwen25-7b-instruct annotations: serving.kserve.io/deploymentMode: RawDeployment labels: @@ -18,8 +18,8 @@ spec: model: modelFormat: name: vLLM - runtime: vllm-llama-runtime - storageUri: oci://registry.redhat.io/rhelai1/modelcar-llama-3-1-8b-instruct:1.5 + runtime: vllm-qwen-runtime + storageUri: hf://Qwen/Qwen2.5-7B-Instruct resources: requests: cpu: "2" @@ -29,4 +29,4 @@ spec: cpu: "4" memory: 16Gi nvidia.com/gpu: "1" -{{- end }} \ No newline at end of file +{{- end }} diff --git a/helm/langflow-agent/charts/model-serving/templates/serving-runtime.yaml b/helm/langflow-agent/charts/model-serving/templates/serving-runtime.yaml index e1f57e6..b9fad64 100644 --- a/helm/langflow-agent/charts/model-serving/templates/serving-runtime.yaml +++ b/helm/langflow-agent/charts/model-serving/templates/serving-runtime.yaml @@ -2,9 +2,9 @@ apiVersion: serving.kserve.io/v1alpha1 kind: ServingRuntime metadata: - name: vllm-llama-runtime + name: vllm-qwen-runtime annotations: - openshift.io/display-name: "vLLM NVIDIA GPU - Llama 3.1" + openshift.io/display-name: "vLLM NVIDIA GPU - Qwen 2.5" opendatahub.io/recommended-accelerators: '["nvidia.com/gpu"]' labels: opendatahub.io/dashboard: 'true' @@ -27,7 +27,7 @@ spec: - "--distributed-executor-backend=mp" - "--max-model-len=16384" - "--enable-auto-tool-choice" - - "--tool-call-parser=llama3_json" + - "--tool-call-parser=hermes" env: - name: HF_HOME value: /tmp/hf_home @@ -44,4 +44,4 @@ spec: memory: 16Gi builtInAdapter: modelLoadingTimeoutMillis: 600000 -{{- end }} \ No newline at end of file +{{- end }} diff --git a/helm/langflow-agent/charts/model-serving/values.yaml b/helm/langflow-agent/charts/model-serving/values.yaml index 1a4cfa1..d4b9dd2 100644 --- a/helm/langflow-agent/charts/model-serving/values.yaml +++ b/helm/langflow-agent/charts/model-serving/values.yaml @@ -1,5 +1,5 @@ enabled: true -modelName: meta-llama/Llama-3.1-8B-Instruct +modelName: Qwen/Qwen2.5-7B-Instruct runtime: vllm gpu: count: 1 diff --git a/helm/langflow-agent/templates/custom-components-cm.yaml b/helm/langflow-agent/templates/custom-components-cm.yaml new file mode 100644 index 0000000..c9ea30c --- /dev/null +++ b/helm/langflow-agent/templates/custom-components-cm.yaml @@ -0,0 +1,65 @@ +{{- if .Values.langflow.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Release.Name }}-custom-components + labels: + {{- include "langflow-agent.labels" . | nindent 4 }} +data: + kserve_vllm.py: | + from langchain_openai import ChatOpenAI + + from langflow.base.models.model import LCModelComponent + from langflow.field_typing import LanguageModel + from langflow.io import FloatInput, IntInput, SecretStrInput, StrInput + + + class KServeVLLMComponent(LCModelComponent): + display_name = "KServe vLLM" + description = "Language model served via KServe + vLLM (OpenAI-compatible API)." + icon = "server" + name = "KServeVLLM" + + inputs = LCModelComponent._base_inputs + [ + StrInput( + name="api_base", + display_name="API Base URL", + info="KServe vLLM OpenAI-compatible endpoint.", + value="", + required=True, + ), + StrInput( + name="model_name", + display_name="Model Name", + value="", + required=True, + ), + SecretStrInput( + name="api_key", + display_name="API Key", + info="API key", + value="EMPTY", + ), + FloatInput( + name="temperature", + display_name="Temperature", + value=0.1, + ), + IntInput( + name="max_tokens", + display_name="Max Tokens", + value=4096, + advanced=True, + ), + ] + + def build_model(self) -> LanguageModel: + return ChatOpenAI( + api_key=self.api_key or "EMPTY", + model=self.model_name, + base_url=self.api_base, + temperature=self.temperature, + max_tokens=self.max_tokens if self.max_tokens > 0 else None, + timeout=120, + ) +{{- end }} diff --git a/helm/langflow-agent/templates/langflow.yaml b/helm/langflow-agent/templates/langflow.yaml index 30ce958..987292e 100644 --- a/helm/langflow-agent/templates/langflow.yaml +++ b/helm/langflow-agent/templates/langflow.yaml @@ -74,6 +74,14 @@ spec: value: {{ include "langflow-agent.postgresUrl" . | quote }} - name: LANGFLOW_ALEMBIC_LOG_FILE value: "/app/data/alembic.log" + {{- if .Values.langflow.backendOnly }} + - name: LANGFLOW_BACKEND_ONLY + value: "true" + - name: LANGFLOW_SKIP_AUTH_AUTO_LOGIN + value: "true" + {{- end }} + - name: LANGFLOW_COMPONENTS_PATH + value: "/app/custom_components" {{- if .Values.langfuse.enabled }} - name: LANGFUSE_HOST value: "http://{{ .Release.Name }}-langfuse:3000" @@ -97,6 +105,8 @@ spec: mountPath: /tmp - name: cache mountPath: /.cache + - name: custom-components + mountPath: /app/custom_components startupProbe: httpGet: path: /health @@ -124,6 +134,9 @@ spec: emptyDir: {} - name: cache emptyDir: {} + - name: custom-components + configMap: + name: {{ .Release.Name }}-custom-components --- # Langflow Service apiVersion: v1 diff --git a/helm/langflow-agent/values.yaml b/helm/langflow-agent/values.yaml index 391d693..51e1246 100644 --- a/helm/langflow-agent/values.yaml +++ b/helm/langflow-agent/values.yaml @@ -10,6 +10,7 @@ langflow: enabled: true image: docker.io/langflowai/langflow:1.7.1 replicas: 1 + backendOnly: false # Set to true for Langflow Runtime (API-only, no UI) resources: requests: cpu: 500m diff --git a/local/.env.example b/local/.env.example index 47da93d..1c10735 100644 --- a/local/.env.example +++ b/local/.env.example @@ -4,7 +4,7 @@ POSTGRES_PASSWORD=langflow POSTGRES_DB=langflow # Ollama — model to pull on first run -OLLAMA_MODEL=llama3.2 +OLLAMA_MODEL=qwen2.5:7b # To use a remote model endpoint instead of Ollama, set: # OPENAI_API_BASE=https://your-cluster-model-endpoint/v1 diff --git a/local/podman-compose.yml b/local/podman-compose.yml index cbcf1c3..7dceac7 100644 --- a/local/podman-compose.yml +++ b/local/podman-compose.yml @@ -4,6 +4,7 @@ services: # ── Langflow (UI + Server) ───────────────────────────────────────── langflow: image: docker.io/langflowai/langflow:1.7.1 + platform: linux/arm64 ports: - "7860:7860" environment: @@ -17,8 +18,10 @@ services: LANGFUSE_HOST: "http://langfuse:3000" LANGFUSE_PUBLIC_KEY: "pk-lf-local-dev" LANGFUSE_SECRET_KEY: "sk-lf-local-dev" + LANGFLOW_COMPONENTS_PATH: "/app/custom_components" volumes: - langflow_data:/var/lib/langflow + - ../custom_components:/app/custom_components:ro depends_on: postgres: condition: service_healthy diff --git a/scripts/agentctl b/scripts/agentctl index 1d1be10..6e382ef 100755 --- a/scripts/agentctl +++ b/scripts/agentctl @@ -14,7 +14,7 @@ ensure_podman() { if [[ "$(uname)" == "Darwin" ]]; then if command -v brew &>/dev/null; then brew install podman podman-compose - podman machine init + podman machine init --memory 8192 podman machine start else echo "Error: Homebrew not found. Install Podman manually: https://podman.io/docs/installation" @@ -43,7 +43,7 @@ usage() { echo "agentctl — CLI for managing Langflow agents on OpenShift" echo "" echo "Usage:" - echo " agentctl deploy [image] [--namespace ] Deploy full stack to the cluster" + echo " agentctl deploy [--image ] [--namespace ] Deploy full stack to the cluster" echo " agentctl destroy [--namespace ] Remove all resources from the cluster" echo " agentctl flows save Local Langflow → flows/ directory" echo " agentctl flows push [-n ] flows/ directory → Cluster Langflow" @@ -53,7 +53,7 @@ usage() { echo " agentctl status [-n ] Show agent status and metadata" echo " agentctl local-up Start local dev environment" echo " agentctl local-down Stop local dev environment" - echo " agentctl build [registry] [tag] Build & push agent image from a flow" + echo " agentctl build [--prod] [-n ns] [reg] [tag] Build agent image (--prod for runtime, -n sets namespace)" echo "" echo "Examples:" echo " agentctl local-up" @@ -78,13 +78,12 @@ cmd_deploy() { local NAMESPACE="langflow-agent" local MODEL_SERVING="true" - # Parse args — image is optional (defaults to stock Langflow) while [[ $# -gt 0 ]]; do case "$1" in --namespace|-n) NAMESPACE="$2"; shift 2 ;; + --image|-i) IMAGE="$2"; shift 2 ;; --no-model-serving) MODEL_SERVING="false"; shift ;; - -*) echo "Unknown option: $1"; exit 1 ;; - *) IMAGE="$1"; shift ;; + *) echo "Unknown option: $1"; exit 1 ;; esac done @@ -105,27 +104,31 @@ cmd_deploy() { fi echo "" - # Create namespace if needed - oc get namespace "$NAMESPACE" &>/dev/null || oc new-project "$NAMESPACE" + # Check if namespace already exists + if oc get namespace "$NAMESPACE" &>/dev/null; then + echo "Namespace '$NAMESPACE' already exists — deploying into it." + read -p "Continue? (Y/n) " -n 1 -r + echo "" + if [[ $REPLY =~ ^[Nn]$ ]]; then + echo "Cancelled." + exit 0 + fi + else + oc new-project "$NAMESPACE" + fi # Build helm args local HELM_ARGS="--set modelServing.enabled=$MODEL_SERVING" - # Use custom image if specified, or check for values-override.yaml from agentctl build - local OVERRIDE_FILE="$PROJECT_ROOT/values-override.yaml" + # Use custom image if specified via --image if [ -n "$IMAGE" ]; then - cat > "$OVERRIDE_FILE" << EOF -langflow: - image: $IMAGE -EOF - HELM_ARGS="$HELM_ARGS -f $OVERRIDE_FILE" - elif [ -f "$OVERRIDE_FILE" ]; then - echo "Found values-override.yaml — using exported flow image." - HELM_ARGS="$HELM_ARGS -f $OVERRIDE_FILE" + HELM_ARGS="$HELM_ARGS --set langflow.image=$IMAGE" fi # Deploy everything echo "Installing/upgrading Helm release..." + echo " (waiting for all pods to be ready — this can take 5-10 minutes on first deploy)" + echo "" helm upgrade --install langflow-agent "$CHART_DIR" \ --namespace "$NAMESPACE" \ $HELM_ARGS \ @@ -147,12 +150,39 @@ EOF echo " Langflow UI: https://$LANGFLOW_HOST" fi if [ -n "$LANGFUSE_HOST" ]; then - echo " Langfuse: https://$LANGFUSE_HOST" + echo " Langfuse: https://$LANGFUSE_HOST (login: admin@langflow.local / admin123)" fi echo "" + echo " Model endpoint (update in Langflow UI > model component, or in flows/*.json):" + echo " Cluster (vLLM): api_base=/v1 model_name=qwen25-7b-instruct" + echo " Local (Ollama): api_base=http://ollama:11434/v1 model_name=qwen2.5:7b" + echo "" echo " Pods:" oc get pods -n "$NAMESPACE" -o custom-columns=' NAME:.metadata.name,STATUS:.status.phase,READY:.status.containerStatuses[0].ready' --no-headers 2>/dev/null echo "" + + # Show ready-to-use API curl command if Langflow route is available + if [ -n "$LANGFLOW_HOST" ]; then + local LANGFLOW_URL="https://$LANGFLOW_HOST" + local FLOW_ID + FLOW_ID=$(curl -s --compressed "$LANGFLOW_URL/api/v1/flows/" | python3 -c " +import sys, json +flows = json.load(sys.stdin) +if flows: + print(flows[0]['id']) +" 2>/dev/null || echo "") + + if [ -n "$FLOW_ID" ]; then + echo " API endpoint:" + echo " POST $LANGFLOW_URL/api/v1/run/$FLOW_ID" + echo "" + echo " Try it:" + echo " curl -X POST '$LANGFLOW_URL/api/v1/run/$FLOW_ID' \\" + echo " -H 'Content-Type: application/json' \\" + echo " -d '{\"input_value\": \"Hello\", \"output_type\": \"chat\", \"input_type\": \"chat\"}'" + echo "" + fi + fi } # ── list ────────────────────────────────────────────────────────────── @@ -234,7 +264,9 @@ cmd_local_down() { podman-compose down --force 2>/dev/null podman pod rm -f -a 2>/dev/null podman rm -f -a 2>/dev/null - echo "Local environment force-stopped and cleaned up." + echo "Removing volumes (all data will be lost)..." + podman volume rm local_postgres_data local_langflow_data local_ollama_data local_mlflow_data 2>/dev/null || true + echo "Local environment force-stopped and volumes removed." else podman-compose down echo "Local environment stopped." @@ -275,7 +307,7 @@ _flows_download_from() { fi local RESPONSE - RESPONSE=$(curl -s --compressed -H "$AUTH_HEADER" "$URL/api/v1/flows/") + RESPONSE=$(curl -sL --compressed -H "$AUTH_HEADER" "$URL/api/v1/flows/?remove_example_flows=true") local COUNT COUNT=$(echo "$RESPONSE" | python3 -c " diff --git a/scripts/deploy-local.sh b/scripts/deploy-local.sh index eb8d578..1879434 100755 --- a/scripts/deploy-local.sh +++ b/scripts/deploy-local.sh @@ -40,7 +40,11 @@ sleep 10 # Pull Ollama model only if enabled and not already downloaded if [ "$USE_OLLAMA" = "yes" ]; then - OLLAMA_MODEL="${OLLAMA_MODEL:-llama3.2}" + # Source .env to get OLLAMA_MODEL if set + if [ -f "$LOCAL_DIR/.env" ]; then + OLLAMA_MODEL=$(grep -E '^OLLAMA_MODEL=' "$LOCAL_DIR/.env" | cut -d= -f2-) + fi + OLLAMA_MODEL="${OLLAMA_MODEL:-qwen2.5:7b}" OLLAMA_CONTAINER=$(podman ps --filter "name=ollama" --format "{{.Names}}" | head -1) # Check if model is already pulled @@ -57,7 +61,14 @@ echo "" echo "Local environment is ready." echo "" echo " Langflow UI: http://localhost:7860" -echo " Langfuse: http://localhost:3000" +echo " Langfuse: http://localhost:3000 (login: admin@langflow.local / admin123)" if [ "$USE_OLLAMA" = "yes" ]; then echo " Ollama API: http://localhost:11434" -fi \ No newline at end of file +fi +echo "" +echo " ⚠ If your flow was pulled from the cluster, update the model component:" +echo " Langflow UI > click model component > change api_base and model_name" +echo "" +echo " Model endpoint:" +echo " Local (Ollama): api_base=http://ollama:11434/v1 model_name=qwen2.5:7b" +echo " Cluster (vLLM): api_base=/v1 model_name=qwen25-7b-instruct" \ No newline at end of file diff --git a/scripts/export-flow.sh b/scripts/export-flow.sh index 5ead1b0..f2e3e9b 100755 --- a/scripts/export-flow.sh +++ b/scripts/export-flow.sh @@ -4,22 +4,39 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" -FLOW_FILE="${1:-}" -REGISTRY="${2:-quay.io/your-org}" -IMAGE_TAG="${3:-latest}" +# Parse flags +PROD_MODE=false +TARGET_NAMESPACE="" +POSITIONAL_ARGS=() +while [[ $# -gt 0 ]]; do + case "$1" in + --prod) PROD_MODE=true; shift ;; + --namespace|-n) TARGET_NAMESPACE="$2"; shift 2 ;; + *) POSITIONAL_ARGS+=("$1"); shift ;; + esac +done + +FLOW_FILE="${POSITIONAL_ARGS[0]:-}" +REGISTRY="${POSITIONAL_ARGS[1]:-quay.io/your-org}" +IMAGE_TAG="${POSITIONAL_ARGS[2]:-latest}" if [ -z "$FLOW_FILE" ]; then - echo "Usage: $0 [registry] [tag]" + echo "Usage: $0 [--prod] [-n ] [registry] [tag]" + echo "" + echo "Options:" + echo " --prod Build for production (Langflow Runtime, backend-only, no UI)" + echo " -n, --namespace Target namespace — rewrites model endpoints in the flow" echo "" echo "Examples:" - echo " $0 flows/example-rag-flow.json" - echo " $0 flows/my-flow.json quay.io/myorg v1.0" + echo " $0 flows/my-flow.json quay.io/myorg v1.0 # IDE mode (full UI)" + echo " $0 --prod -n my-ns flows/my-flow.json quay.io/myorg v1.0 # Production, target namespace" echo "" echo "This script:" echo " 1. Takes a Langflow flow JSON file" - echo " 2. Builds a container image with the flow baked in" - echo " 3. Pushes it to a registry" - echo " 4. The Helm chart on the cluster can then deploy this image" + echo " 2. Rewrites model endpoints for the target namespace (if -n is given)" + echo " 3. Builds a container image with the flow baked in" + echo " 4. Pushes it to a registry" + echo " 5. The Helm chart on the cluster can then deploy this image" exit 1 fi @@ -31,6 +48,13 @@ fi FLOW_NAME=$(basename "$FLOW_FILE" .json) IMAGE_NAME="$REGISTRY/langflow-$FLOW_NAME:$IMAGE_TAG" +if [ "$PROD_MODE" = true ]; then + MODE_LABEL="runtime" + echo "Mode: PRODUCTION (Langflow Runtime — backend-only, no UI)" +else + MODE_LABEL="ide" + echo "Mode: IDE (full Langflow UI)" +fi echo "Exporting flow: $FLOW_FILE" echo "Image: $IMAGE_NAME" echo "" @@ -41,29 +65,113 @@ trap "rm -rf $BUILD_DIR" EXIT cp "$FLOW_FILE" "$BUILD_DIR/flow.json" +# Copy custom components if they exist +if [ -d "$PROJECT_ROOT/custom_components" ]; then + cp -r "$PROJECT_ROOT/custom_components" "$BUILD_DIR/custom_components" +fi + +# Rewrite model endpoints for target namespace +if [ -n "$TARGET_NAMESPACE" ]; then + echo "Rewriting model endpoints for namespace: $TARGET_NAMESPACE" + python3 -c " +import json, re, sys + +MODEL_URL = 'http://qwen25-7b-instruct-predictor.$TARGET_NAMESPACE.svc.cluster.local:8080/v1' +MODEL_NAME = 'qwen25-7b-instruct' + +with open('$BUILD_DIR/flow.json') as f: + flow = json.load(f) + +changed = False +for node in flow.get('data', {}).get('nodes', []): + template = node.get('data', {}).get('node', {}).get('template', {}) + name = node.get('data', {}).get('node', {}).get('display_name', '?') + + # Rewrite api_base field (built-in OpenAI/Ollama components) + if 'api_base' in template: + old_val = template['api_base'].get('value', '') + if old_val and ('ollama' in old_val or 'svc.cluster.local' in old_val or old_val): + template['api_base']['value'] = MODEL_URL + print(f' Updated {name}: api_base -> {MODEL_URL}') + changed = True + if 'model_name' in template: + old_val = template['model_name'].get('value', '') + if old_val: + template['model_name']['value'] = MODEL_NAME + print(f' Updated {name}: model_name -> {MODEL_NAME}') + changed = True + + # Rewrite custom component code (vLLM component with hardcoded URLs) + code = template.get('code', {}).get('value', '') + if '.svc.cluster.local' in code: + updated = re.sub( + r'(predictor\.)[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.svc\.cluster\.local)', + r'\g<1>$TARGET_NAMESPACE\3', + code + ) + if updated != code: + template['code']['value'] = updated + print(f' Updated {name}: custom component code') + changed = True + +if changed: + with open('$BUILD_DIR/flow.json', 'w') as f: + json.dump(flow, f, indent=2) +else: + print(' No model endpoints found to rewrite.') +" + echo "" +fi + # Generate Containerfile with kagenti.* OCI metadata labels cat > "$BUILD_DIR/Containerfile" << EOF -FROM docker.io/langflowai/langflow:latest +FROM docker.io/langflowai/langflow:1.7.1 # kagenti metadata labels — makes this image discoverable by the platform LABEL kagenti.type="agent" LABEL kagenti.name="$FLOW_NAME" LABEL kagenti.version="$IMAGE_TAG" LABEL kagenti.framework="langflow" +LABEL kagenti.mode="$MODE_LABEL" LABEL kagenti.description="Langflow agent: $FLOW_NAME" -# Copy the flow into the Langflow config directory -COPY flow.json /app/flow.json +# Create directories +RUN mkdir -p /app/langflow-config-dir /app/flows + +# Copy the flow into the image +COPY flow.json /app/flows/flow.json + +# Copy custom components for sidebar availability +COPY custom_components/ /app/custom_components/ # Set environment variables for the flow -ENV LANGFLOW_LOAD_FLOWS_PATH=/app/flow.json +ENV LANGFLOW_LOAD_FLOWS_PATH=/app/flows +ENV LANGFLOW_COMPONENTS_PATH=/app/custom_components ENV LANGFLOW_AUTO_LOGIN=true +ENV LANGFLOW_SKIP_AUTH_AUTO_LOGIN=true +ENV LANGFLOW_CONFIG_DIR=/app/langflow-config-dir +ENV LANGFLOW_LOG_ENV=container +EOF + +# Add production-specific configuration +if [ "$PROD_MODE" = true ]; then + cat >> "$BUILD_DIR/Containerfile" << 'EOF' + +# Production: backend-only mode (no UI) +ENV LANGFLOW_BACKEND_ONLY=true + +EXPOSE 7860 +CMD ["langflow", "run", "--backend-only", "--host", "0.0.0.0", "--port", "7860"] +EOF +else + cat >> "$BUILD_DIR/Containerfile" << 'EOF' EXPOSE 7860 EOF +fi -echo "Building container image..." -podman build -t "$IMAGE_NAME" -f "$BUILD_DIR/Containerfile" "$BUILD_DIR" +echo "Building container image (linux/amd64)..." +podman build --platform linux/amd64 -t "$IMAGE_NAME" -f "$BUILD_DIR/Containerfile" "$BUILD_DIR" echo "" echo "Image built: $IMAGE_NAME" @@ -79,13 +187,6 @@ else echo "Skipped push. To push later: podman push $IMAGE_NAME" fi -# Write values override so deploy-cluster.sh picks up this image automatically -OVERRIDE_FILE="$PROJECT_ROOT/values-override.yaml" -cat > "$OVERRIDE_FILE" << EOF -langflow: - image: $IMAGE_NAME -EOF - echo "" -echo "Saved $OVERRIDE_FILE" -echo "Next step: ./scripts/deploy-cluster.sh" +echo "Next step:" +echo " agentctl deploy --image $IMAGE_NAME"