Merge branch 'main' into docs-publish-v0

This commit is contained in:
Mendon Kissling
2025-02-18 11:43:21 -05:00
committed by GitHub
151 changed files with 22569 additions and 7060 deletions

View File

@ -25,6 +25,7 @@ starter-projects:
- "src/backend/base/langflow/components/**"
- "src/backend/base/langflow/services/**"
- "src/backend/base/langflow/custom/**"
- "src/backend/base/langflow/api/v1/chat.py"
- "src/frontend/src/pages/MainPage/**"
- "src/frontend/src/utils/reactflowUtils.ts"
- "src/frontend/tests/extended/features/**"

View File

@ -29,6 +29,7 @@ jobs:
- run: uv sync --extra dev
- name: Run Codeflash Optimizer
working-directory: ./src/backend/base
continue-on-error: true
run: uv run codeflash
- name: Minimize uv cache
run: uv cache prune --ci

View File

@ -463,3 +463,51 @@ alembic-check: ## check migration status
alembic-stamp: ## stamp the database with a specific revision
@echo 'Stamping the database with revision $(revision)'
cd src/backend/base/langflow/ && uv run alembic stamp $(revision)
######################
# LOAD TESTING
######################
# Default values for locust configuration
locust_users ?= 10
locust_spawn_rate ?= 1
locust_host ?= http://localhost:7860
locust_headless ?= true
locust_time ?= 300s
locust_api_key ?= your-api-key
locust_flow_id ?= your-flow-id
locust_file ?= src/backend/tests/locust/locustfile.py
locust_min_wait ?= 2000
locust_max_wait ?= 5000
locust_request_timeout ?= 30.0
locust: ## run locust load tests (options: locust_users=10 locust_spawn_rate=1 locust_host=http://localhost:7860 locust_headless=true locust_time=300s locust_api_key=your-api-key locust_flow_id=your-flow-id locust_file=src/backend/tests/locust/locustfile.py locust_min_wait=2000 locust_max_wait=5000 locust_request_timeout=30.0)
@if [ ! -f "$(locust_file)" ]; then \
echo "$(RED)Error: Locustfile not found at $(locust_file)$(NC)"; \
exit 1; \
fi
@echo "Starting Locust with $(locust_users) users, spawn rate of $(locust_spawn_rate)"
@echo "Testing host: $(locust_host)"
@echo "Using locustfile: $(locust_file)"
@export API_KEY=$(locust_api_key) && \
export FLOW_ID=$(locust_flow_id) && \
export LANGFLOW_HOST=$(locust_host) && \
export MIN_WAIT=$(locust_min_wait) && \
export MAX_WAIT=$(locust_max_wait) && \
export REQUEST_TIMEOUT=$(locust_request_timeout) && \
cd $$(dirname "$(locust_file)") && \
if [ "$(locust_headless)" = "true" ]; then \
uv run locust \
--headless \
-u $(locust_users) \
-r $(locust_spawn_rate) \
--run-time $(locust_time) \
--host $(locust_host) \
-f $$(basename "$(locust_file)"); \
else \
uv run locust \
-u $(locust_users) \
-r $(locust_spawn_rate) \
--host $(locust_host) \
-f $$(basename "$(locust_file)"); \
fi

View File

@ -31,6 +31,7 @@
<a href="./README.ja.md"><img alt="README en Japonés" src="https://img.shields.io/badge/日本語-d9d9d9"></a>
<a href="./README.KR.md"><img alt="README en Coreano" src="https://img.shields.io/badge/한국어-d9d9d9"></a>
<a href="./README.FR.md"><img alt="README en Francès" src="https://img.shields.io/badge/Français-d9d9d9"></a>
<a href="./README.RU.md"><img alt="README en Russian" src="https://img.shields.io/badge/Русский"></a>
</div>
<p align="center">

View File

@ -38,6 +38,7 @@
<a href="./README.ja.md"><img alt="README in Japanese" src="https://img.shields.io/badge/日本語-d9d9d9"></a>
<a href="./README.KR.md"><img alt="README in KOREAN" src="https://img.shields.io/badge/한국어-d9d9d9"></a>
<a href="./README.FR.md"><img alt="README in French" src="https://img.shields.io/badge/Français-d9d9d9"></a>
<a href="./README.RU.md"><img alt="README in Russian" src="https://img.shields.io/badge/Русский"></a>
</div>
<p align="center">

View File

@ -33,6 +33,7 @@
<a href="./README.ja.md"><img alt="README em Japonês" src="https://img.shields.io/badge/日本語-d9d9d9"></a>
<a href="./README.KR.md"><img alt="README em Coreano" src="https://img.shields.io/badge/한국어-d9d9d9"></a>
<a href="./README.FR.md"><img alt="README em Francês" src="https://img.shields.io/badge/Français-d9d9d9"></a>
<a href="./README.RU.md"><img alt="README in Russian" src="https://img.shields.io/badge/Русский"></a>
</div>
<p align="center">

78
README.RU.md Normal file
View File

@ -0,0 +1,78 @@
<!-- markdownlint-disable MD030 -->
![Langflow](./docs/static/img/hero.png)
<p align="center" style="font-size: 12px;">
Langflow — это инструмент для создания приложений с низким уровнем кода для RAG и многоагентных ИИ-приложений. Он основан на Python и не зависит от конкретных моделей, API или баз данных.
</p>
<p align="center" style="font-size: 12px;">
<a href="https://docs.langflow.org" style="text-decoration: underline;">Документация</a> -
<a href="https://astra.datastax.com/signup?type=langflow" style="text-decoration: underline;">Бесплатный облачный сервис</a> -
<a href="https://docs.langflow.org/get-started-installation" style="text-decoration: underline;">Самостоятельное развертывание</a>
</p>
<div align="center">
<a href="./README.md"><img alt="README на английском" src="https://img.shields.io/badge/English-d9d9d9"></a>
<a href="./README.PT.md"><img alt="README на португальском" src="https://img.shields.io/badge/Portuguese-d9d9d9"></a>
<a href="./README.ES.md"><img alt="README на испанском" src="https://img.shields.io/badge/Spanish-d9d9d9"></a>
<a href="./README.zh_CN.md"><img alt="README на упрощенном китайском" src="https://img.shields.io/badge/简体中文-d9d9d9"></a>
<a href="./README.ja.md"><img alt="README на японском" src="https://img.shields.io/badge/日本語-d9d9d9"></a>
<a href="./README.KR.md"><img alt="README на корейском" src="https://img.shields.io/badge/한국어-d9d9d9"></a>
<a href="./README.FR.md"><img alt="README на французском" src="https://img.shields.io/badge/Français-d9d9d9"></a>
<a href="./README.RU.md"><img alt="README на русском" src="https://img.shields.io/badge/Русский-d9d9d9"></a>
</div>
## ✨ Основные возможности
1. **Основан на Python** и не зависит от моделей, API, источников данных или баз данных.
2. **Визуальная IDE** для построения и тестирования рабочих процессов методом drag-and-drop.
3. **Песочница** для мгновенного тестирования и итерации процессов с поэтапным контролем.
4. **Оркестрация многоагентных систем** с управлением диалогами и извлечением информации.
5. **Бесплатный облачный сервис**, позволяющий начать работу за считанные минуты без настройки.
6. **Публикация в виде API** или экспорт в виде Python-приложения.
7. **Наблюдаемость** с интеграцией LangSmith, LangFuse или LangWatch.
8. **Безопасность и масштабируемость уровня предприятия** с бесплатным облачным сервисом DataStax Langflow.
9. **Настройка рабочих процессов** или создание потоков исключительно на Python.
10. **Интеграция с экосистемами** через повторно используемые компоненты для любых моделей, API или баз данных.
![Интеграции](./docs/static/img/integrations.png)
## 📦 Быстрый старт
- **Установка через uv (рекомендуется)** (Python 3.103.12):
```shell
uv pip install langflow
```
- **Установка через pip** (Python 3.103.12):
```shell
pip install langflow
```
- **Облако:** DataStax Langflow — это управляемая среда без необходимости настройки. [Зарегистрируйтесь бесплатно.](https://astra.datastax.com/signup?type=langflow)
- **Самостоятельное развертывание:** Запустите Langflow в своей среде. [Установите Langflow](https://docs.langflow.org/get-started-installation), чтобы запустить локальный сервер Langflow, а затем воспользуйтесь [руководством по быстрому старту](https://docs.langflow.org/get-started-quickstart) для создания и выполнения потока.
- **Hugging Face:** [Клонируйте пространство по этой ссылке](https://huggingface.co/spaces/Langflow/Langflow?duplicate=true), чтобы создать рабочее пространство Langflow.
[![Начало работы](https://github.com/user-attachments/assets/f1adfbe7-3c35-43a4-b265-661f3d4f875f)](https://www.youtube.com/watch?v=kinngWhaUKM)
## ⭐ Будьте в курсе обновлений
Добавьте Langflow в избранное на GitHub, чтобы мгновенно узнавать о новых релизах.
![Star Langflow](https://github.com/user-attachments/assets/03168b17-a11d-4b2a-b0f7-c1cce69e5a2c)
## 👋 Внесите свой вклад
Мы приветствуем вклад разработчиков любого уровня. Если хотите помочь в развитии проекта, ознакомьтесь с нашими [руководящими принципами для участников](./CONTRIBUTING.md) и сделайте Langflow еще доступнее.
---
[![График истории звезд](https://api.star-history.com/svg?repos=langflow-ai/langflow&type=Timeline)](https://star-history.com/#langflow-ai/langflow&Date)
## ❤️ Соавторы
[![Соавторы Langflow](https://contrib.rocks/image?repo=langflow-ai/langflow)](https://github.com/langflow-ai/langflow/graphs/contributors)

View File

@ -38,6 +38,7 @@
<a href="./README.ja.md"><img alt="README in Japanese" src="https://img.shields.io/badge/日本語-d9d9d9"></a>
<a href="./README.KR.md"><img alt="README in KOREAN" src="https://img.shields.io/badge/한국어-d9d9d9"></a>
<a href="./README.FR.md"><img alt="README in French" src="https://img.shields.io/badge/Français-d9d9d9"></a>
<a href="./README.RU.md"><img alt="README in Russian" src="https://img.shields.io/badge/Русский"></a>
</div>
<p align="center">

View File

@ -21,6 +21,7 @@
<a href="./README.ja.md"><img alt="README in Japanese" src="https://img.shields.io/badge/日本語-d9d9d9"></a>
<a href="./README.KR.md"><img alt="README in KOREAN" src="https://img.shields.io/badge/한국어-d9d9d9"></a>
<a href="./README.FR.md"><img alt="README in French" src="https://img.shields.io/badge/Français-d9d9d9"></a>
<a href="./README.RU.md"><img alt="README in Russian" src="https://img.shields.io/badge/Русский"></a>
</div>
## ✨ Core features

View File

@ -33,6 +33,7 @@
<a href="./README.ja.md"><img alt="README in Japanese" src="https://img.shields.io/badge/日本語-d9d9d9"></a>
<a href="./README.KR.md"><img alt="README in KOREAN" src="https://img.shields.io/badge/한국어-d9d9d9"></a>
<a href="./README.FR.md"><img alt="README in French" src="https://img.shields.io/badge/Français-d9d9d9"></a>
<a href="./README.RU.md"><img alt="README in Russian" src="https://img.shields.io/badge/Русский"></a>
</div>
<p align="center">

View File

@ -191,6 +191,7 @@ dev-dependencies = [
"types-aiofiles>=24.1.0.20240626",
"codeflash>=0.8.4",
"hypothesis>=6.123.17",
"locust>=2.32.9",
]

View File

@ -0,0 +1,428 @@
import asyncio
import json
import time
import traceback
import uuid
from collections.abc import AsyncIterator
from fastapi import BackgroundTasks, HTTPException
from fastapi.responses import JSONResponse
from loguru import logger
from sqlmodel import select
from langflow.api.disconnect import DisconnectHandlerStreamingResponse
from langflow.api.utils import (
CurrentActiveUser,
build_graph_from_data,
build_graph_from_db,
format_elapsed_time,
format_exception_message,
get_top_level_vertices,
parse_exception,
)
from langflow.api.v1.schemas import (
FlowDataRequest,
InputValueRequest,
ResultDataResponse,
VertexBuildResponse,
)
from langflow.events.event_manager import EventManager
from langflow.exceptions.component import ComponentBuildError
from langflow.graph.graph.base import Graph
from langflow.graph.utils import log_vertex_build
from langflow.schema.message import ErrorMessage
from langflow.schema.schema import OutputValue
from langflow.services.database.models.flow import Flow
from langflow.services.deps import get_chat_service, get_telemetry_service, session_scope
from langflow.services.job_queue.service import JobQueueService
from langflow.services.telemetry.schema import ComponentPayload, PlaygroundPayload
async def start_flow_build(
*,
flow_id: uuid.UUID,
background_tasks: BackgroundTasks,
inputs: InputValueRequest | None,
data: FlowDataRequest | None,
files: list[str] | None,
stop_component_id: str | None,
start_component_id: str | None,
log_builds: bool,
current_user: CurrentActiveUser,
queue_service: JobQueueService,
) -> str:
"""Start the flow build process by setting up the queue and starting the build task.
Returns:
the job_id.
"""
job_id = str(uuid.uuid4())
try:
_, event_manager = queue_service.create_queue(job_id)
task_coro = generate_flow_events(
flow_id=flow_id,
background_tasks=background_tasks,
event_manager=event_manager,
inputs=inputs,
data=data,
files=files,
stop_component_id=stop_component_id,
start_component_id=start_component_id,
log_builds=log_builds,
current_user=current_user,
)
queue_service.start_job(job_id, task_coro)
except Exception as e:
logger.exception("Failed to create queue and start task")
raise HTTPException(status_code=500, detail=str(e)) from e
return job_id
async def get_flow_events_response(
*,
job_id: str,
queue_service: JobQueueService,
stream: bool = True,
):
"""Get events for a specific build job, either as a stream or single event."""
try:
main_queue, event_manager, event_task = queue_service.get_queue_data(job_id)
if stream:
if event_task is None:
raise HTTPException(status_code=404, detail="No event task found for job")
return await create_flow_response(
queue=main_queue,
event_manager=event_manager,
event_task=event_task,
)
# Polling mode - get exactly one event
_, value, _ = await main_queue.get()
if value is None:
# End of stream, trigger end event
if event_task is not None:
event_task.cancel()
event_manager.on_end(data={})
return JSONResponse({"event": value.decode("utf-8") if value else None})
except ValueError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
async def create_flow_response(
queue: asyncio.Queue,
event_manager: EventManager,
event_task: asyncio.Task,
) -> DisconnectHandlerStreamingResponse:
"""Create a streaming response for the flow build process."""
async def consume_and_yield() -> AsyncIterator[str]:
while True:
try:
event_id, value, put_time = await queue.get()
if value is None:
break
get_time = time.time()
yield value.decode("utf-8")
logger.debug(f"Event {event_id} consumed in {get_time - put_time:.4f}s")
except Exception as exc: # noqa: BLE001
logger.exception(f"Error consuming event: {exc}")
break
def on_disconnect() -> None:
logger.debug("Client disconnected, closing tasks")
event_task.cancel()
event_manager.on_end(data={})
return DisconnectHandlerStreamingResponse(
consume_and_yield(),
media_type="application/x-ndjson",
on_disconnect=on_disconnect,
)
async def generate_flow_events(
*,
flow_id: uuid.UUID,
background_tasks: BackgroundTasks,
event_manager: EventManager,
inputs: InputValueRequest | None,
data: FlowDataRequest | None,
files: list[str] | None,
stop_component_id: str | None,
start_component_id: str | None,
log_builds: bool,
current_user: CurrentActiveUser,
) -> None:
"""Generate events for flow building process.
This function handles the core flow building logic and generates appropriate events:
- Building and validating the graph
- Processing vertices
- Handling errors and cleanup
"""
chat_service = get_chat_service()
telemetry_service = get_telemetry_service()
if not inputs:
inputs = InputValueRequest(session=str(flow_id))
async def build_graph_and_get_order() -> tuple[list[str], list[str], Graph]:
start_time = time.perf_counter()
components_count = 0
graph = None
try:
flow_id_str = str(flow_id)
# Create a fresh session for database operations
async with session_scope() as fresh_session:
graph = await create_graph(fresh_session, flow_id_str)
graph.validate_stream()
first_layer = sort_vertices(graph)
if inputs is not None and getattr(inputs, "session", None) is not None:
graph.session_id = inputs.session
for vertex_id in first_layer:
graph.run_manager.add_to_vertices_being_run(vertex_id)
# Now vertices is a list of lists
# We need to get the id of each vertex
# and return the same structure but only with the ids
components_count = len(graph.vertices)
vertices_to_run = list(graph.vertices_to_run.union(get_top_level_vertices(graph, graph.vertices_to_run)))
await chat_service.set_cache(flow_id_str, graph)
await log_telemetry(start_time, components_count, success=True)
except Exception as exc:
await log_telemetry(start_time, components_count, success=False, error_message=str(exc))
if "stream or streaming set to True" in str(exc):
raise HTTPException(status_code=400, detail=str(exc)) from exc
logger.exception("Error checking build status")
raise HTTPException(status_code=500, detail=str(exc)) from exc
return first_layer, vertices_to_run, graph
async def log_telemetry(
start_time: float, components_count: int, *, success: bool, error_message: str | None = None
):
background_tasks.add_task(
telemetry_service.log_package_playground,
PlaygroundPayload(
playground_seconds=int(time.perf_counter() - start_time),
playground_component_count=components_count,
playground_success=success,
playground_error_message=str(error_message) if error_message else "",
),
)
async def create_graph(fresh_session, flow_id_str: str) -> Graph:
if not data:
return await build_graph_from_db(flow_id=flow_id, session=fresh_session, chat_service=chat_service)
result = await fresh_session.exec(select(Flow.name).where(Flow.id == flow_id))
flow_name = result.first()
return await build_graph_from_data(
flow_id=flow_id_str,
payload=data.model_dump(),
user_id=str(current_user.id),
flow_name=flow_name,
)
def sort_vertices(graph: Graph) -> list[str]:
try:
return graph.sort_vertices(stop_component_id, start_component_id)
except Exception: # noqa: BLE001
logger.exception("Error sorting vertices")
return graph.sort_vertices()
async def _build_vertex(vertex_id: str, graph: Graph, event_manager: EventManager) -> VertexBuildResponse:
flow_id_str = str(flow_id)
next_runnable_vertices = []
top_level_vertices = []
start_time = time.perf_counter()
error_message = None
try:
vertex = graph.get_vertex(vertex_id)
try:
lock = chat_service.async_cache_locks[flow_id_str]
vertex_build_result = await graph.build_vertex(
vertex_id=vertex_id,
user_id=str(current_user.id),
inputs_dict=inputs.model_dump() if inputs else {},
files=files,
get_cache=chat_service.get_cache,
set_cache=chat_service.set_cache,
event_manager=event_manager,
)
result_dict = vertex_build_result.result_dict
params = vertex_build_result.params
valid = vertex_build_result.valid
artifacts = vertex_build_result.artifacts
next_runnable_vertices = await graph.get_next_runnable_vertices(lock, vertex=vertex, cache=False)
top_level_vertices = graph.get_top_level_vertices(next_runnable_vertices)
result_data_response = ResultDataResponse.model_validate(result_dict, from_attributes=True)
except Exception as exc: # noqa: BLE001
if isinstance(exc, ComponentBuildError):
params = exc.message
tb = exc.formatted_traceback
else:
tb = traceback.format_exc()
logger.exception("Error building Component")
params = format_exception_message(exc)
message = {"errorMessage": params, "stackTrace": tb}
valid = False
error_message = params
output_label = vertex.outputs[0]["name"] if vertex.outputs else "output"
outputs = {output_label: OutputValue(message=message, type="error")}
result_data_response = ResultDataResponse(results={}, outputs=outputs)
artifacts = {}
background_tasks.add_task(graph.end_all_traces, error=exc)
result_data_response.message = artifacts
# Log the vertex build
if not vertex.will_stream and log_builds:
background_tasks.add_task(
log_vertex_build,
flow_id=flow_id_str,
vertex_id=vertex_id,
valid=valid,
params=params,
data=result_data_response,
artifacts=artifacts,
)
else:
await chat_service.set_cache(flow_id_str, graph)
timedelta = time.perf_counter() - start_time
duration = format_elapsed_time(timedelta)
result_data_response.duration = duration
result_data_response.timedelta = timedelta
vertex.add_build_time(timedelta)
inactivated_vertices = list(graph.inactivated_vertices)
graph.reset_inactivated_vertices()
graph.reset_activated_vertices()
# graph.stop_vertex tells us if the user asked
# to stop the build of the graph at a certain vertex
# if it is in next_vertices_ids, we need to remove other
# vertices from next_vertices_ids
if graph.stop_vertex and graph.stop_vertex in next_runnable_vertices:
next_runnable_vertices = [graph.stop_vertex]
if not graph.run_manager.vertices_being_run and not next_runnable_vertices:
background_tasks.add_task(graph.end_all_traces)
build_response = VertexBuildResponse(
inactivated_vertices=list(set(inactivated_vertices)),
next_vertices_ids=list(set(next_runnable_vertices)),
top_level_vertices=list(set(top_level_vertices)),
valid=valid,
params=params,
id=vertex.id,
data=result_data_response,
)
background_tasks.add_task(
telemetry_service.log_package_component,
ComponentPayload(
component_name=vertex_id.split("-")[0],
component_seconds=int(time.perf_counter() - start_time),
component_success=valid,
component_error_message=error_message,
),
)
except Exception as exc:
background_tasks.add_task(
telemetry_service.log_package_component,
ComponentPayload(
component_name=vertex_id.split("-")[0],
component_seconds=int(time.perf_counter() - start_time),
component_success=False,
component_error_message=str(exc),
),
)
logger.exception("Error building Component")
message = parse_exception(exc)
raise HTTPException(status_code=500, detail=message) from exc
return build_response
async def build_vertices(
vertex_id: str,
graph: Graph,
event_manager: EventManager,
) -> None:
"""Build vertices and handle their events.
Args:
vertex_id: The ID of the vertex to build
graph: The graph instance
event_manager: Manager for handling events
"""
try:
vertex_build_response: VertexBuildResponse = await _build_vertex(vertex_id, graph, event_manager)
except asyncio.CancelledError as exc:
logger.exception(exc)
raise
# send built event or error event
try:
vertex_build_response_json = vertex_build_response.model_dump_json()
build_data = json.loads(vertex_build_response_json)
except Exception as exc:
msg = f"Error serializing vertex build response: {exc}"
raise ValueError(msg) from exc
event_manager.on_end_vertex(data={"build_data": build_data})
if vertex_build_response.valid and vertex_build_response.next_vertices_ids:
tasks = []
for next_vertex_id in vertex_build_response.next_vertices_ids:
task = asyncio.create_task(
build_vertices(
next_vertex_id,
graph,
event_manager,
)
)
tasks.append(task)
await asyncio.gather(*tasks)
try:
ids, vertices_to_run, graph = await build_graph_and_get_order()
except Exception as e:
error_message = ErrorMessage(
flow_id=flow_id,
exception=e,
)
event_manager.on_error(data=error_message.data)
raise
event_manager.on_vertices_sorted(data={"ids": ids, "to_run": vertices_to_run})
tasks = []
for vertex_id in ids:
task = asyncio.create_task(build_vertices(vertex_id, graph, event_manager))
tasks.append(task)
try:
await asyncio.gather(*tasks)
except asyncio.CancelledError:
background_tasks.add_task(graph.end_all_traces)
raise
except Exception as e:
logger.error(f"Error building vertices: {e}")
custom_component = graph.get_vertex(vertex_id).custom_component
trace_name = getattr(custom_component, "trace_name", None)
error_message = ErrorMessage(
flow_id=flow_id,
exception=e,
session_id=graph.session_id,
trace_name=trace_name,
)
event_manager.on_error(data=error_message.data)
raise
event_manager.on_end(data={})
await event_manager.queue.put((None, None, time.time()))

View File

@ -0,0 +1,31 @@
import asyncio
import typing
from fastapi.responses import StreamingResponse
from starlette.background import BackgroundTask
from starlette.responses import ContentStream
from starlette.types import Receive
class DisconnectHandlerStreamingResponse(StreamingResponse):
def __init__(
self,
content: ContentStream,
status_code: int = 200,
headers: typing.Mapping[str, str] | None = None,
media_type: str | None = None,
background: BackgroundTask | None = None,
on_disconnect: typing.Callable | None = None,
):
super().__init__(content, status_code, headers, media_type, background)
self.on_disconnect = on_disconnect
async def listen_for_disconnect(self, receive: Receive) -> None:
while True:
message = await receive()
if message["type"] == "http.disconnect":
if self.on_disconnect:
coro = self.on_disconnect()
if asyncio.iscoroutine(coro):
await coro
break

View File

@ -0,0 +1,29 @@
from fastapi import BackgroundTasks
from langflow.graph.utils import log_vertex_build
from langflow.services.deps import get_settings_service
class LimitVertexBuildBackgroundTasks(BackgroundTasks):
"""A subclass of FastAPI BackgroundTasks that limits the number of tasks added per vertex_id.
If more than max_vertex_builds_per_vertex tasks are added for a given vertex_id,
the oldest task is removed so that only the most recent remain.
This only applies to log_vertex_build tasks.
"""
def add_task(self, func, *args, **kwargs):
# Only apply limiting logic to log_vertex_build tasks
if func == log_vertex_build:
vertex_id = kwargs.get("vertex_id")
if vertex_id is not None:
# Filter tasks that are log_vertex_build calls with the same vertex_id
relevant_tasks = [
t for t in self.tasks if t.func == log_vertex_build and t.kwargs.get("vertex_id") == vertex_id
]
if len(relevant_tasks) >= get_settings_service().settings.max_vertex_builds_per_vertex:
# Remove the oldest task for this vertex_id
oldest_task = relevant_tasks[0]
self.tasks.remove(oldest_task)
super().add_task(func, *args, **kwargs)

View File

@ -14,6 +14,7 @@ from langflow.graph.graph.base import Graph
from langflow.services.auth.utils import get_current_active_user
from langflow.services.database.models import User
from langflow.services.database.models.flow import Flow
from langflow.services.database.models.message import MessageTable
from langflow.services.database.models.transactions.model import TransactionTable
from langflow.services.database.models.vertex_builds.model import VertexBuildTable
from langflow.services.deps import get_session, session_scope
@ -281,16 +282,16 @@ def parse_value(value: Any, input_type: str) -> Any:
async def cascade_delete_flow(session: AsyncSession, flow_id: uuid.UUID) -> None:
try:
await session.exec(delete(TransactionTable).where(TransactionTable.flow_id == flow_id))
await session.exec(delete(VertexBuildTable).where(VertexBuildTable.flow_id == flow_id))
# TODO: Verify if deleting messages is safe in terms of session id relevance
# If we delete messages directly, rather than setting flow_id to null,
# it might cause unexpected behaviors because the session id could still be
# used elsewhere to search for these messages.
# await session.exec(delete(MessageTable).where(MessageTable.flow_id == flow_id))
await session.exec(delete(MessageTable).where(MessageTable.flow_id == flow_id))
await session.exec(delete(TransactionTable).where(TransactionTable.flow_id == flow_id))
await session.exec(delete(VertexBuildTable).where(VertexBuildTable.flow_id == flow_id))
await session.exec(delete(Flow).where(Flow.id == flow_id))
except Exception as e:
msg = f"Unable to cascade delete flow: ${flow_id}"
msg = f"Unable to cascade delete flow: {flow_id}"
raise RuntimeError(msg, e) from e

View File

@ -1,26 +1,23 @@
from __future__ import annotations
import asyncio
import json
import time
import traceback
import typing
import uuid
from typing import TYPE_CHECKING, Annotated
from fastapi import APIRouter, BackgroundTasks, Body, HTTPException
from fastapi import APIRouter, BackgroundTasks, Body, Depends, HTTPException
from fastapi.responses import StreamingResponse
from loguru import logger
from sqlmodel import select
from starlette.background import BackgroundTask
from starlette.responses import ContentStream
from starlette.types import Receive
from langflow.api.build import (
get_flow_events_response,
start_flow_build,
)
from langflow.api.limited_background_tasks import LimitVertexBuildBackgroundTasks
from langflow.api.utils import (
CurrentActiveUser,
DbSession,
build_and_cache_graph_from_data,
build_graph_from_data,
build_graph_from_db,
format_elapsed_time,
format_exception_message,
@ -35,16 +32,21 @@ from langflow.api.v1.schemas import (
VertexBuildResponse,
VerticesOrderResponse,
)
from langflow.events.event_manager import EventManager, create_default_event_manager
from langflow.exceptions.component import ComponentBuildError
from langflow.graph.graph.base import Graph
from langflow.graph.utils import log_vertex_build
from langflow.schema.message import ErrorMessage
from langflow.schema.schema import OutputValue
from langflow.services.cache.utils import CacheMiss
from langflow.services.chat.service import ChatService
from langflow.services.database.models.flow.model import Flow
from langflow.services.deps import get_chat_service, get_session, get_telemetry_service, session_scope
from langflow.services.deps import (
get_chat_service,
get_queue_service,
get_session,
get_telemetry_service,
session_scope,
)
from langflow.services.job_queue.service import JobQueueService
from langflow.services.telemetry.schema import ComponentPayload, PlaygroundPayload
if TYPE_CHECKING:
@ -53,22 +55,6 @@ if TYPE_CHECKING:
router = APIRouter(tags=["Chat"])
async def try_running_celery_task(vertex, user_id):
# Try running the task in celery
# and set the task_id to the local vertex
# if it fails, run the task locally
try:
from langflow.worker import build_vertex
task = build_vertex.delay(vertex)
vertex.task_id = task.id
except Exception: # noqa: BLE001
logger.opt(exception=True).debug("Error running task in celery")
vertex.task_id = None
await vertex.build(user_id=user_id)
return vertex
@router.post("/build/{flow_id}/vertices", deprecated=True)
async def retrieve_vertices_order(
*,
@ -143,331 +129,52 @@ async def retrieve_vertices_order(
@router.post("/build/{flow_id}/flow")
async def build_flow(
*,
background_tasks: BackgroundTasks,
flow_id: uuid.UUID,
background_tasks: LimitVertexBuildBackgroundTasks,
inputs: Annotated[InputValueRequest | None, Body(embed=True)] = None,
data: Annotated[FlowDataRequest | None, Body(embed=True)] = None,
files: list[str] | None = None,
stop_component_id: str | None = None,
start_component_id: str | None = None,
log_builds: bool | None = True,
log_builds: bool = True,
current_user: CurrentActiveUser,
queue_service: Annotated[JobQueueService, Depends(get_queue_service)],
):
chat_service = get_chat_service()
telemetry_service = get_telemetry_service()
if not inputs:
inputs = InputValueRequest(session=str(flow_id))
"""Build and process a flow, returning a job ID for event polling."""
# First verify the flow exists
async with session_scope() as session:
flow = await session.get(Flow, flow_id)
if not flow:
raise HTTPException(status_code=404, detail=f"Flow with id {flow_id} not found")
async def build_graph_and_get_order() -> tuple[list[str], list[str], Graph]:
start_time = time.perf_counter()
components_count = 0
graph = None
try:
flow_id_str = str(flow_id)
# Create a fresh session for database operations
async with session_scope() as fresh_session:
graph = await create_graph(fresh_session, flow_id_str)
graph.validate_stream()
first_layer = sort_vertices(graph)
if inputs is not None and hasattr(inputs, "session") and inputs.session is not None:
graph.session_id = inputs.session
for vertex_id in first_layer:
graph.run_manager.add_to_vertices_being_run(vertex_id)
# Now vertices is a list of lists
# We need to get the id of each vertex
# and return the same structure but only with the ids
components_count = len(graph.vertices)
vertices_to_run = list(graph.vertices_to_run.union(get_top_level_vertices(graph, graph.vertices_to_run)))
await chat_service.set_cache(flow_id_str, graph)
await log_telemetry(start_time, components_count, success=True)
except Exception as exc:
await log_telemetry(start_time, components_count, success=False, error_message=str(exc))
if "stream or streaming set to True" in str(exc):
raise HTTPException(status_code=400, detail=str(exc)) from exc
logger.exception("Error checking build status")
raise HTTPException(status_code=500, detail=str(exc)) from exc
return first_layer, vertices_to_run, graph
async def log_telemetry(
start_time: float, components_count: int, *, success: bool, error_message: str | None = None
):
background_tasks.add_task(
telemetry_service.log_package_playground,
PlaygroundPayload(
playground_seconds=int(time.perf_counter() - start_time),
playground_component_count=components_count,
playground_success=success,
playground_error_message=str(error_message) if error_message else "",
),
)
async def create_graph(fresh_session, flow_id_str: str) -> Graph:
if not data:
return await build_graph_from_db(flow_id=flow_id, session=fresh_session, chat_service=chat_service)
result = await fresh_session.exec(select(Flow.name).where(Flow.id == flow_id))
flow_name = result.first()
return await build_graph_from_data(
flow_id=flow_id_str,
payload=data.model_dump(),
user_id=str(current_user.id),
flow_name=flow_name,
)
def sort_vertices(graph: Graph) -> list[str]:
try:
return graph.sort_vertices(stop_component_id, start_component_id)
except Exception: # noqa: BLE001
logger.exception("Error sorting vertices")
return graph.sort_vertices()
async def _build_vertex(vertex_id: str, graph: Graph, event_manager: EventManager) -> VertexBuildResponse:
flow_id_str = str(flow_id)
next_runnable_vertices = []
top_level_vertices = []
start_time = time.perf_counter()
error_message = None
try:
vertex = graph.get_vertex(vertex_id)
try:
lock = chat_service.async_cache_locks[flow_id_str]
vertex_build_result = await graph.build_vertex(
vertex_id=vertex_id,
user_id=str(current_user.id),
inputs_dict=inputs.model_dump() if inputs else {},
files=files,
get_cache=chat_service.get_cache,
set_cache=chat_service.set_cache,
event_manager=event_manager,
)
result_dict = vertex_build_result.result_dict
params = vertex_build_result.params
valid = vertex_build_result.valid
artifacts = vertex_build_result.artifacts
next_runnable_vertices = await graph.get_next_runnable_vertices(lock, vertex=vertex, cache=False)
top_level_vertices = graph.get_top_level_vertices(next_runnable_vertices)
result_data_response = ResultDataResponse.model_validate(result_dict, from_attributes=True)
except Exception as exc: # noqa: BLE001
if isinstance(exc, ComponentBuildError):
params = exc.message
tb = exc.formatted_traceback
else:
tb = traceback.format_exc()
logger.exception("Error building Component")
params = format_exception_message(exc)
message = {"errorMessage": params, "stackTrace": tb}
valid = False
error_message = params
output_label = vertex.outputs[0]["name"] if vertex.outputs else "output"
outputs = {output_label: OutputValue(message=message, type="error")}
result_data_response = ResultDataResponse(results={}, outputs=outputs)
artifacts = {}
background_tasks.add_task(graph.end_all_traces, error=exc)
result_data_response.message = artifacts
# Log the vertex build
if not vertex.will_stream and log_builds:
background_tasks.add_task(
log_vertex_build,
flow_id=flow_id_str,
vertex_id=vertex_id,
valid=valid,
params=params,
data=result_data_response,
artifacts=artifacts,
)
else:
await chat_service.set_cache(flow_id_str, graph)
timedelta = time.perf_counter() - start_time
duration = format_elapsed_time(timedelta)
result_data_response.duration = duration
result_data_response.timedelta = timedelta
vertex.add_build_time(timedelta)
inactivated_vertices = list(graph.inactivated_vertices)
graph.reset_inactivated_vertices()
graph.reset_activated_vertices()
# graph.stop_vertex tells us if the user asked
# to stop the build of the graph at a certain vertex
# if it is in next_vertices_ids, we need to remove other
# vertices from next_vertices_ids
if graph.stop_vertex and graph.stop_vertex in next_runnable_vertices:
next_runnable_vertices = [graph.stop_vertex]
if not graph.run_manager.vertices_being_run and not next_runnable_vertices:
background_tasks.add_task(graph.end_all_traces)
build_response = VertexBuildResponse(
inactivated_vertices=list(set(inactivated_vertices)),
next_vertices_ids=list(set(next_runnable_vertices)),
top_level_vertices=list(set(top_level_vertices)),
valid=valid,
params=params,
id=vertex.id,
data=result_data_response,
)
background_tasks.add_task(
telemetry_service.log_package_component,
ComponentPayload(
component_name=vertex_id.split("-")[0],
component_seconds=int(time.perf_counter() - start_time),
component_success=valid,
component_error_message=error_message,
),
)
except Exception as exc:
background_tasks.add_task(
telemetry_service.log_package_component,
ComponentPayload(
component_name=vertex_id.split("-")[0],
component_seconds=int(time.perf_counter() - start_time),
component_success=False,
component_error_message=str(exc),
),
)
logger.exception("Error building Component")
message = parse_exception(exc)
raise HTTPException(status_code=500, detail=message) from exc
return build_response
async def build_vertices(
vertex_id: str,
graph: Graph,
client_consumed_queue: asyncio.Queue,
event_manager: EventManager,
) -> None:
build_task = asyncio.create_task(_build_vertex(vertex_id, graph, event_manager))
try:
await build_task
vertex_build_response: VertexBuildResponse = build_task.result()
except asyncio.CancelledError as exc:
logger.exception(exc)
build_task.cancel()
return
# send built event or error event
try:
vertex_build_response_json = vertex_build_response.model_dump_json()
build_data = json.loads(vertex_build_response_json)
except Exception as exc:
msg = f"Error serializing vertex build response: {exc}"
raise ValueError(msg) from exc
event_manager.on_end_vertex(data={"build_data": build_data})
await client_consumed_queue.get()
if vertex_build_response.valid and vertex_build_response.next_vertices_ids:
tasks = []
for next_vertex_id in vertex_build_response.next_vertices_ids:
task = asyncio.create_task(build_vertices(next_vertex_id, graph, client_consumed_queue, event_manager))
tasks.append(task)
try:
await asyncio.gather(*tasks)
except asyncio.CancelledError:
for task in tasks:
task.cancel()
return
async def event_generator(event_manager: EventManager, client_consumed_queue: asyncio.Queue) -> None:
try:
ids, vertices_to_run, graph = await build_graph_and_get_order()
except Exception as e:
error_message = ErrorMessage(
flow_id=flow_id,
exception=e,
)
event_manager.on_error(data=error_message.data)
raise
event_manager.on_vertices_sorted(data={"ids": ids, "to_run": vertices_to_run})
await client_consumed_queue.get()
tasks = []
for vertex_id in ids:
task = asyncio.create_task(build_vertices(vertex_id, graph, client_consumed_queue, event_manager))
tasks.append(task)
try:
await asyncio.gather(*tasks)
except asyncio.CancelledError:
background_tasks.add_task(graph.end_all_traces)
for task in tasks:
task.cancel()
return
except Exception as e:
logger.error(f"Error building vertices: {e}")
custom_component = graph.get_vertex(vertex_id).custom_component
trace_name = getattr(custom_component, "trace_name", None)
error_message = ErrorMessage(
flow_id=flow_id,
exception=e,
session_id=graph.session_id,
trace_name=trace_name,
)
event_manager.on_error(data=error_message.data)
raise
event_manager.on_end(data={})
await event_manager.queue.put((None, None, time.time))
async def consume_and_yield(queue: asyncio.Queue, client_consumed_queue: asyncio.Queue) -> typing.AsyncGenerator:
while True:
event_id, value, put_time = await queue.get()
if value is None:
break
get_time = time.time()
yield value
get_time_yield = time.time()
client_consumed_queue.put_nowait(event_id)
logger.debug(
f"consumed event {event_id} "
f"(time in queue, {get_time - put_time:.4f}, "
f"client {get_time_yield - get_time:.4f})"
)
asyncio_queue: asyncio.Queue = asyncio.Queue()
asyncio_queue_client_consumed: asyncio.Queue = asyncio.Queue()
event_manager = create_default_event_manager(queue=asyncio_queue)
main_task = asyncio.create_task(event_generator(event_manager, asyncio_queue_client_consumed))
def on_disconnect() -> None:
logger.debug("Client disconnected, closing tasks")
main_task.cancel()
return DisconnectHandlerStreamingResponse(
consume_and_yield(asyncio_queue, asyncio_queue_client_consumed),
media_type="application/x-ndjson",
on_disconnect=on_disconnect,
job_id = await start_flow_build(
flow_id=flow_id,
background_tasks=background_tasks,
inputs=inputs,
data=data,
files=files,
stop_component_id=stop_component_id,
start_component_id=start_component_id,
log_builds=log_builds,
current_user=current_user,
queue_service=queue_service,
)
return {"job_id": job_id}
class DisconnectHandlerStreamingResponse(StreamingResponse):
def __init__(
self,
content: ContentStream,
status_code: int = 200,
headers: typing.Mapping[str, str] | None = None,
media_type: str | None = None,
background: BackgroundTask | None = None,
on_disconnect: typing.Callable | None = None,
):
super().__init__(content, status_code, headers, media_type, background)
self.on_disconnect = on_disconnect
async def listen_for_disconnect(self, receive: Receive) -> None:
while True:
message = await receive()
if message["type"] == "http.disconnect":
if self.on_disconnect:
coro = self.on_disconnect()
if asyncio.iscoroutine(coro):
await coro
break
@router.get("/build/{job_id}/events")
async def get_build_events(
job_id: str,
queue_service: Annotated[JobQueueService, Depends(get_queue_service)],
*,
stream: bool = True,
):
"""Get events for a specific build job."""
return await get_flow_events_response(
job_id=job_id,
queue_service=queue_service,
stream=stream,
)
@router.post("/build/{flow_id}/vertices/{vertex_id}", deprecated=True)

View File

@ -44,7 +44,7 @@ from langflow.services.database.models.flow import Flow
from langflow.services.database.models.flow.model import FlowRead
from langflow.services.database.models.flow.utils import get_all_webhook_components_in_flow
from langflow.services.database.models.user.model import User, UserRead
from langflow.services.deps import get_session_service, get_settings_service, get_task_service, get_telemetry_service
from langflow.services.deps import get_session_service, get_settings_service, get_telemetry_service
from langflow.services.settings.feature_flags import FEATURE_FLAGS
from langflow.services.telemetry.schema import RunPayload
from langflow.utils.version import get_version_info
@ -599,29 +599,16 @@ async def process() -> None:
)
@router.get("/task/{task_id}")
async def get_task_status(task_id: str) -> TaskStatusResponse:
task_service = get_task_service()
task = task_service.get_task(task_id)
result = None
if task is None:
raise HTTPException(status_code=404, detail="Task not found")
if task.ready():
result = task.result
# If result isinstance of Exception, can we get the traceback?
if isinstance(result, Exception):
logger.exception(task.traceback)
@router.get("/task/{_task_id}", deprecated=True)
async def get_task_status(_task_id: str) -> TaskStatusResponse:
"""Get the status of a task by ID (Deprecated).
if isinstance(result, dict) and "result" in result:
result = result["result"]
elif hasattr(result, "result"):
result = result.result
if task.status == "FAILURE":
result = str(task.result)
logger.error(f"Task {task_id} failed: {task.traceback}")
return TaskStatusResponse(status=task.status, result=result)
This endpoint is deprecated and will be removed in a future version.
"""
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="The /task endpoint is deprecated and will be removed in a future version. Please use /run instead.",
)
@router.post(

View File

@ -3,7 +3,6 @@ import base64
import json
import logging
import traceback
from contextlib import suppress
from contextvars import ContextVar
from typing import Annotated
from urllib.parse import quote, unquote, urlparse
@ -266,8 +265,9 @@ async def handle_call_tool(
return collected_results
finally:
progress_task.cancel()
with suppress(asyncio.CancelledError):
await progress_task
await asyncio.wait([progress_task])
if not progress_task.cancelled() and (exc := progress_task.exception()) is not None:
raise exc
except Exception as e:
msg = f"Error in async session: {e}"
logger.exception(msg)
@ -333,6 +333,7 @@ async def handle_sse(request: Request, current_user: Annotated[User, Depends(get
logger.info("Client disconnected from SSE connection")
except asyncio.CancelledError:
logger.info("SSE connection was cancelled")
raise
except Exception as e:
msg = f"Error in MCP: {e!s}"
logger.exception(msg)

View File

@ -35,6 +35,7 @@ async def get_vertex_builds(flow_id: Annotated[UUID, Query()], session: DbSessio
async def delete_vertex_builds(flow_id: Annotated[UUID, Query()], session: DbSession) -> None:
try:
await delete_vertex_builds_by_flow_id(session, flow_id)
await session.commit()
except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) from e

View File

@ -1,7 +1,7 @@
from datetime import datetime, timezone
from enum import Enum
from pathlib import Path
from typing import Any
from typing import Any, Literal
from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator, model_serializer
@ -376,3 +376,4 @@ class ConfigResponse(BaseModel):
auto_saving_interval: int
health_check_max_retries: int
max_file_size_upload: int
event_delivery: Literal["polling", "streaming"]

View File

@ -169,8 +169,9 @@ class LCAgentComponent(Component):
cast("SendMessageFunctionType", self.send_message),
)
except ExceptionWithMessageError as e:
msg_id = e.agent_message.id
await delete_message(id_=msg_id)
if hasattr(e, "agent_message") and hasattr(e.agent_message, "id"):
msg_id = e.agent_message.id
await delete_message(id_=msg_id)
await self._send_message_event(e.agent_message, category="remove_message")
logger.error(f"ExceptionWithMessageError: {e}")
raise

View File

@ -73,7 +73,7 @@ class HuggingFaceInferenceAPIEmbeddingsComponent(LCEmbeddingsModel):
def get_api_url(self) -> str:
if "huggingface" in self.inference_endpoint.lower():
return f"{self.inference_endpoint}{self.model_name}"
return f"{self.inference_endpoint}"
return self.inference_endpoint
@retry(stop=stop_after_attempt(3), wait=wait_fixed(2))

View File

@ -26,6 +26,7 @@ class FirecrawlCrawlApi(Component):
display_name="URL",
required=True,
info="The URL to scrape.",
tool_mode=True,
),
IntInput(
name="timeout",

View File

@ -30,6 +30,7 @@ class FirecrawlScrapeApi(Component):
display_name="URL",
required=True,
info="The URL to scrape.",
tool_mode=True,
),
IntInput(
name="timeout",

View File

@ -1,9 +1,18 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any
from loguru import logger
from langflow.custom import Component
from langflow.io import DataFrameInput, HandleInput, MultilineInput, Output, StrInput
from langflow.io import (
BoolInput,
DataFrameInput,
HandleInput,
MessageTextInput,
MultilineInput,
Output,
)
from langflow.schema import DataFrame
if TYPE_CHECKING:
@ -14,8 +23,8 @@ class BatchRunComponent(Component):
display_name = "Batch Run"
description = (
"Runs a language model over each row of a DataFrame's text column and returns a new "
"DataFrame with two columns: 'text_input' (the original text) and 'model_response' "
"containing the model's response."
"DataFrame with three columns: '**text_input**' (the original text), "
"'**model_response**' (the model's response),and '**batch_index**' (the processing order)."
)
icon = "List"
beta = True
@ -26,6 +35,7 @@ class BatchRunComponent(Component):
display_name="Language Model",
info="Connect the 'Language Model' output from your LLM component here.",
input_types=["LanguageModel"],
required=True,
),
MultilineInput(
name="system_message",
@ -37,12 +47,23 @@ class BatchRunComponent(Component):
name="df",
display_name="DataFrame",
info="The DataFrame whose column (specified by 'column_name') we'll treat as text messages.",
required=True,
),
StrInput(
MessageTextInput(
name="column_name",
display_name="Column Name",
info="The name of the DataFrame column to treat as text messages. Default='text'.",
value="text",
required=True,
advanced=True,
),
BoolInput(
name="enable_metadata",
display_name="Enable Metadata",
info="If True, add metadata to the output DataFrame.",
value=True,
required=False,
advanced=True,
),
]
@ -51,51 +72,123 @@ class BatchRunComponent(Component):
display_name="Batch Results",
name="batch_results",
method="run_batch",
info="A DataFrame with two columns: 'text_input' and 'model_response'.",
info="A DataFrame with columns: 'text_input', 'model_response', 'batch_index', and 'metadata'.",
),
]
async def run_batch(self) -> DataFrame:
"""For each row in df[column_name], combine that text with system_message, then invoke the model asynchronously.
def _create_base_row(self, text_input: str = "", model_response: str = "", batch_index: int = -1) -> dict[str, Any]:
"""Create a base row with optional metadata."""
return {
"text_input": text_input,
"model_response": model_response,
"batch_index": batch_index,
}
Returns a new DataFrame of the same length, with columns 'text_input' and 'model_response'.
def _add_metadata(
self, row: dict[str, Any], *, success: bool = True, system_msg: str = "", error: str | None = None
) -> None:
"""Add metadata to a row if enabled."""
if not self.enable_metadata:
return
if success:
row["metadata"] = {
"has_system_message": bool(system_msg),
"input_length": len(row["text_input"]),
"response_length": len(row["model_response"]),
"processing_status": "success",
}
else:
row["metadata"] = {
"error": error,
"processing_status": "failed",
}
async def run_batch(self) -> DataFrame:
"""Process each row in df[column_name] with the language model asynchronously.
Returns:
DataFrame: A new DataFrame containing:
- text_input: The original input text
- model_response: The model's response
- batch_index: The processing order
- metadata: Additional processing information
Raises:
ValueError: If the specified column is not found in the DataFrame
TypeError: If the model is not compatible or input types are wrong
"""
model: Runnable = self.model
system_msg = self.system_message or ""
df: DataFrame = self.df
col_name = self.column_name or "text"
# Validate inputs first
if not isinstance(df, DataFrame):
msg = f"Expected DataFrame input, got {type(df)}"
raise TypeError(msg)
if col_name not in df.columns:
msg = f"Column '{col_name}' not found in the DataFrame."
msg = f"Column '{col_name}' not found in the DataFrame. Available columns: {', '.join(df.columns)}"
raise ValueError(msg)
# Convert the specified column to a list of strings
user_texts = df[col_name].astype(str).tolist()
try:
# Convert the specified column to a list of strings
user_texts = df[col_name].astype(str).tolist()
total_rows = len(user_texts)
# Prepare the batch of conversations
conversations = [
[{"role": "system", "content": system_msg}, {"role": "user", "content": text}]
if system_msg
else [{"role": "user", "content": text}]
for text in user_texts
]
model = model.with_config(
{
"run_name": self.display_name,
"project_name": self.get_project_name(),
"callbacks": self.get_langchain_callbacks(),
}
)
logger.info(f"Processing {total_rows} rows with batch run")
responses = await model.abatch(conversations)
# Prepare the batch of conversations
conversations = [
[{"role": "system", "content": system_msg}, {"role": "user", "content": text}]
if system_msg
else [{"role": "user", "content": text}]
for text in user_texts
]
# Build the final data, each row has 'text_input' + 'model_response'
rows = []
for original_text, response in zip(user_texts, responses, strict=False):
resp_text = response.content if hasattr(response, "content") else str(response)
# Configure the model with project info and callbacks
model = model.with_config(
{
"run_name": self.display_name,
"project_name": self.get_project_name(),
"callbacks": self.get_langchain_callbacks(),
}
)
row = {"text_input": original_text, "model_response": resp_text}
rows.append(row)
# Process batches and track progress
responses_with_idx = [
(idx, response)
for idx, response in zip(
range(len(conversations)), await model.abatch(list(conversations)), strict=True
)
]
# Convert to a new DataFrame
return DataFrame(rows) # Langflow DataFrame from a list of dicts
# Sort by index to maintain order
responses_with_idx.sort(key=lambda x: x[0])
# Build the final data with enhanced metadata
rows: list[dict[str, Any]] = []
for idx, response in responses_with_idx:
resp_text = response.content if hasattr(response, "content") else str(response)
row = self._create_base_row(
text_input=user_texts[idx],
model_response=resp_text,
batch_index=idx,
)
self._add_metadata(row, success=True, system_msg=system_msg)
rows.append(row)
# Log progress
if (idx + 1) % max(1, total_rows // 10) == 0:
logger.info(f"Processed {idx + 1}/{total_rows} rows")
logger.info("Batch processing completed successfully")
return DataFrame(rows)
except (KeyError, AttributeError) as e:
# Handle data structure and attribute access errors
logger.error(f"Data processing error: {e!s}")
error_row = self._create_base_row()
self._add_metadata(error_row, success=False, error=str(e))
return DataFrame([error_row])

View File

@ -61,10 +61,10 @@ class MessageStoreComponent(Component):
self.memory.session_id = message.session_id
lc_message = message.to_lc_message()
await self.memory.aadd_messages([lc_message])
stored_message = await self.memory.aget_messages()
stored_message = [Message.from_lc_message(m) for m in stored_message]
stored_messages = await self.memory.aget_messages()
stored_messages = [Message.from_lc_message(m) for m in stored_messages]
if message.sender:
stored_message = [m for m in stored_message if m.sender == message.sender]
stored_messages = [m for m in stored_messages if m.sender == message.sender]
else:
await astore_message(message, flow_id=self.graph.flow_id)
stored_messages = await aget_messages(

View File

@ -0,0 +1,3 @@
from .olivya import OlivyaComponent
__all__ = ["OlivyaComponent"]

View File

@ -0,0 +1,116 @@
import json
import httpx
from loguru import logger
from langflow.custom import Component
from langflow.io import MessageTextInput, Output
from langflow.schema import Data
class OlivyaComponent(Component):
display_name = "Place Call"
description = "A component to create an outbound call request from Olivya's platform."
documentation: str = "http://docs.langflow.org/components/olivya"
icon = "Olivya"
name = "OlivyaComponent"
inputs = [
MessageTextInput(
name="api_key",
display_name="API Key",
info="Your API key for authentication",
value="",
required=True,
),
MessageTextInput(
name="from_number",
display_name="From Number",
info="The Agent's phone number",
value="",
required=True,
),
MessageTextInput(
name="to_number",
display_name="To Number",
info="The recipient's phone number",
value="",
required=True,
),
MessageTextInput(
name="first_message",
display_name="First Message",
info="The Agent's introductory message",
value="",
required=False,
tool_mode=True,
),
MessageTextInput(
name="system_prompt",
display_name="System Prompt",
info="The system prompt to guide the interaction",
value="",
required=False,
),
MessageTextInput(
name="conversation_history",
display_name="Conversation History",
info="The summary of the conversation",
value="",
required=False,
tool_mode=True,
),
]
outputs = [
Output(display_name="Output", name="output", method="build_output"),
]
async def build_output(self) -> Data:
try:
payload = {
"variables": {
"first_message": self.first_message.strip() if self.first_message else None,
"system_prompt": self.system_prompt.strip() if self.system_prompt else None,
"conversation_history": self.conversation_history.strip() if self.conversation_history else None,
},
"from_number": self.from_number.strip(),
"to_number": self.to_number.strip(),
}
headers = {
"Authorization": self.api_key.strip(),
"Content-Type": "application/json",
}
logger.info("Sending POST request with payload: %s", payload)
# Send the POST request with a timeout
async with httpx.AsyncClient() as client:
response = await client.post(
"https://phone.olivya.io/create_zap_call",
headers=headers,
json=payload,
timeout=10.0,
)
response.raise_for_status()
# Parse and return the successful response
response_data = response.json()
logger.info("Request successful: %s", response_data)
except httpx.HTTPStatusError as http_err:
logger.exception("HTTP error occurred")
response_data = {"error": f"HTTP error occurred: {http_err}", "response_text": response.text}
except httpx.RequestError as req_err:
logger.exception("Request failed")
response_data = {"error": f"Request failed: {req_err}"}
except json.JSONDecodeError as json_err:
logger.exception("Response parsing failed")
response_data = {"error": f"Response parsing failed: {json_err}", "raw_response": response.text}
except Exception as e: # noqa: BLE001
logger.exception("An unexpected error occurred")
response_data = {"error": f"An unexpected error occurred: {e!s}"}
# Return the response as part of the output
return Data(value=response_data)

View File

@ -0,0 +1,68 @@
from langflow.custom import Component
from langflow.io import DataInput, Output
from langflow.schema import Data, DataFrame
class DataToDataFrameComponent(Component):
display_name = "Data → DataFrame"
description = (
"Converts one or multiple Data objects into a DataFrame. "
"Each Data object corresponds to one row. Fields from `.data` become columns, "
"and the `.text` (if present) is placed in a 'text' column."
)
icon = "table"
name = "DataToDataFrame"
inputs = [
DataInput(
name="data_list",
display_name="Data or Data List",
info="One or multiple Data objects to transform into a DataFrame.",
is_list=True,
),
]
outputs = [
Output(
display_name="DataFrame",
name="dataframe",
method="build_dataframe",
info="A DataFrame built from each Data object's fields plus a 'text' column.",
),
]
def build_dataframe(self) -> DataFrame:
"""Builds a DataFrame from Data objects by combining their fields.
For each Data object:
- Merge item.data (dictionary) as columns
- If item.text is present, add 'text' column
Returns a DataFrame with one row per Data object.
"""
data_input = self.data_list
# If user passed a single Data, it might come in as a single object rather than a list
if not isinstance(data_input, list):
data_input = [data_input]
rows = []
for item in data_input:
if not isinstance(item, Data):
msg = f"Expected Data objects, got {type(item)} instead."
raise TypeError(msg)
# Start with a copy of item.data or an empty dict
row_dict = dict(item.data) if item.data else {}
# If the Data object has text, store it under 'text' col
text_val = item.get_text()
if text_val:
row_dict["text"] = text_val
rows.append(row_dict)
# Build a DataFrame from these row dictionaries
df_result = DataFrame(rows)
self.status = df_result # store in self.status for logs
return df_result

View File

@ -10,9 +10,18 @@ class ParseDataComponent(Component):
description = "Convert Data objects into Messages using any {field_name} from input data."
icon = "message-square"
name = "ParseData"
metadata = {
"legacy_name": "Parse Data",
}
inputs = [
DataInput(name="data", display_name="Data", info="The data to convert to text.", is_list=True, required=True),
DataInput(
name="data",
display_name="Data",
info="The data to convert to text.",
is_list=True,
required=True,
),
MultilineInput(
name="template",
display_name="Template",

View File

@ -0,0 +1,172 @@
import json
from collections.abc import AsyncIterator, Iterator
from pathlib import Path
import pandas as pd
from langflow.custom import Component
from langflow.io import (
DataFrameInput,
DataInput,
DropdownInput,
MessageInput,
Output,
StrInput,
)
from langflow.schema import Data, DataFrame, Message
class SaveToFileComponent(Component):
display_name = "Save to File"
description = "Save DataFrames, Data, or Messages to various file formats."
icon = "save"
name = "SaveToFile"
# File format options for different types
DATA_FORMAT_CHOICES = ["csv", "excel", "json", "markdown"]
MESSAGE_FORMAT_CHOICES = ["txt", "json", "markdown"]
inputs = [
DropdownInput(
name="input_type",
display_name="Input Type",
options=["DataFrame", "Data", "Message"],
info="Select the type of input to save.",
value="DataFrame",
real_time_refresh=True,
),
DataFrameInput(
name="df",
display_name="DataFrame",
info="The DataFrame to save.",
dynamic=True,
show=True,
),
DataInput(
name="data",
display_name="Data",
info="The Data object to save.",
dynamic=True,
show=False,
),
MessageInput(
name="message",
display_name="Message",
info="The Message to save.",
dynamic=True,
show=False,
),
DropdownInput(
name="file_format",
display_name="File Format",
options=DATA_FORMAT_CHOICES,
info="Select the file format to save the input.",
real_time_refresh=True,
),
StrInput(
name="file_path",
display_name="File Path (including filename)",
info="The full file path (including filename and extension).",
value="./output",
),
]
outputs = [
Output(
name="confirmation",
display_name="Confirmation",
method="save_to_file",
info="Confirmation message after saving the file.",
),
]
def update_build_config(self, build_config, field_value, field_name=None):
# Hide/show dynamic fields based on the selected input type
if field_name == "input_type":
build_config["df"]["show"] = field_value == "DataFrame"
build_config["data"]["show"] = field_value == "Data"
build_config["message"]["show"] = field_value == "Message"
if field_value in ["DataFrame", "Data"]:
build_config["file_format"]["options"] = self.DATA_FORMAT_CHOICES
elif field_value == "Message":
build_config["file_format"]["options"] = self.MESSAGE_FORMAT_CHOICES
return build_config
def save_to_file(self) -> str:
input_type = self.input_type
file_format = self.file_format
file_path = Path(self.file_path).expanduser()
# Ensure the directory exists
if not file_path.parent.exists():
file_path.parent.mkdir(parents=True, exist_ok=True)
if input_type == "DataFrame":
dataframe = self.df
return self._save_dataframe(dataframe, file_path, file_format)
if input_type == "Data":
data = self.data
return self._save_data(data, file_path, file_format)
if input_type == "Message":
message = self.message
return self._save_message(message, file_path, file_format)
error_msg = f"Unsupported input type: {input_type}"
raise ValueError(error_msg)
def _save_dataframe(self, dataframe: DataFrame, path: Path, fmt: str) -> str:
if fmt == "csv":
dataframe.to_csv(path, index=False)
elif fmt == "excel":
dataframe.to_excel(path, index=False, engine="openpyxl")
elif fmt == "json":
dataframe.to_json(path, orient="records", indent=2)
elif fmt == "markdown":
path.write_text(dataframe.to_markdown(index=False), encoding="utf-8")
else:
error_msg = f"Unsupported DataFrame format: {fmt}"
raise ValueError(error_msg)
return f"DataFrame saved successfully as '{path}'"
def _save_data(self, data: Data, path: Path, fmt: str) -> str:
if fmt == "csv":
pd.DataFrame(data.data).to_csv(path, index=False)
elif fmt == "excel":
pd.DataFrame(data.data).to_excel(path, index=False, engine="openpyxl")
elif fmt == "json":
path.write_text(json.dumps(data.data, indent=2), encoding="utf-8")
elif fmt == "markdown":
path.write_text(pd.DataFrame(data.data).to_markdown(index=False), encoding="utf-8")
else:
error_msg = f"Unsupported Data format: {fmt}"
raise ValueError(error_msg)
return f"Data saved successfully as '{path}'"
def _save_message(self, message: Message, path: Path, fmt: str) -> str:
if message.text is None:
content = ""
elif isinstance(message.text, AsyncIterator):
# AsyncIterator needs to be handled differently
error_msg = "AsyncIterator not supported"
raise ValueError(error_msg)
elif isinstance(message.text, Iterator):
# Convert iterator to string
content = " ".join(str(item) for item in message.text)
else:
content = str(message.text)
if fmt == "txt":
path.write_text(content, encoding="utf-8")
elif fmt == "json":
path.write_text(json.dumps({"message": content}, indent=2), encoding="utf-8")
elif fmt == "markdown":
path.write_text(f"**Message:**\n\n{content}", encoding="utf-8")
else:
error_msg = f"Unsupported Message format: {fmt}"
raise ValueError(error_msg)
return f"Message saved successfully as '{path}'"

View File

@ -1,8 +1,8 @@
import os
from collections import defaultdict
from dataclasses import dataclass, field
from dataclasses import asdict, dataclass, field
from astrapy import AstraDBAdmin, DataAPIClient, Database
from astrapy.info import CollectionDescriptor
from langchain_astradb import AstraDBVectorStore, CollectionVectorServiceOptions
from langflow.base.vectorstores.model import LCVectorStoreComponent, check_cached_vector_store
@ -36,22 +36,24 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
default_factory=lambda: {
"data": {
"node": {
"description": "Create a new database in Astra DB.",
"display_name": "Create New Database",
"name": "create_database",
"description": "",
"display_name": "Create new database",
"field_order": ["new_database_name", "cloud_provider", "region"],
"template": {
"new_database_name": StrInput(
name="new_database_name",
display_name="New Database Name",
display_name="Name",
info="Name of the new database to create in Astra DB.",
required=True,
),
"cloud_provider": DropdownInput(
name="cloud_provider",
display_name="Cloud Provider",
display_name="Cloud provider",
info="Cloud provider for the new database.",
options=["Amazon Web Services", "Google Cloud Platform", "Microsoft Azure"],
required=True,
real_time_refresh=True,
),
"region": DropdownInput(
name="region",
@ -73,8 +75,9 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
default_factory=lambda: {
"data": {
"node": {
"description": "Create a new collection in Astra DB.",
"display_name": "Create New Collection",
"name": "create_collection",
"description": "",
"display_name": "Create new collection",
"field_order": [
"new_collection_name",
"embedding_generation_provider",
@ -83,23 +86,31 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
"template": {
"new_collection_name": StrInput(
name="new_collection_name",
display_name="New Collection Name",
display_name="Name",
info="Name of the new collection to create in Astra DB.",
required=True,
),
"embedding_generation_provider": DropdownInput(
name="embedding_generation_provider",
display_name="Embedding Generation Provider",
display_name="Embedding generation method",
info="Provider to use for generating embeddings.",
options=[],
real_time_refresh=True,
required=True,
options=["Bring your own", "Nvidia"],
),
"embedding_generation_model": DropdownInput(
name="embedding_generation_model",
display_name="Embedding Generation Model",
display_name="Embedding model",
info="Model to use for generating embeddings.",
options=[],
required=True,
options=[],
),
"dimension": IntInput(
name="dimension",
display_name="Dimensions (Required only for `Bring your own`)",
info="Dimensions of the embeddings to generate.",
required=False,
value=1024,
),
},
},
@ -125,17 +136,18 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
real_time_refresh=True,
),
DropdownInput(
name="api_endpoint",
name="database_name",
display_name="Database",
info="The Database / API Endpoint for the Astra DB instance.",
info="The Database name for the Astra DB instance.",
required=True,
refresh_button=True,
real_time_refresh=True,
dialog_inputs=asdict(NewDatabaseInput()),
combobox=True,
),
StrInput(
name="d_api_endpoint",
display_name="Database API Endpoint",
name="api_endpoint",
display_name="Astra DB API Endpoint",
info="The API Endpoint for the Astra DB instance. Supercedes database selection.",
advanced=True,
),
@ -146,8 +158,9 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
required=True,
refresh_button=True,
real_time_refresh=True,
# dialog_inputs=asdict(NewCollectionInput()),
dialog_inputs=asdict(NewCollectionInput()),
combobox=True,
advanced=True,
),
StrInput(
name="keyspace",
@ -238,6 +251,7 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
@classmethod
def map_cloud_providers(cls):
# TODO: Programmatically fetch the regions for each cloud provider
return {
"Amazon Web Services": {
"id": "aws",
@ -254,54 +268,87 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
}
@classmethod
def create_database_api(
def get_vectorize_providers(cls, token: str, environment: str | None = None, api_endpoint: str | None = None):
try:
# Get the admin object
admin = AstraDBAdmin(token=token, environment=environment)
db_admin = admin.get_database_admin(api_endpoint=api_endpoint)
# Get the list of embedding providers
embedding_providers = db_admin.find_embedding_providers().as_dict()
vectorize_providers_mapping = {}
# Map the provider display name to the provider key and models
for provider_key, provider_data in embedding_providers["embeddingProviders"].items():
# Get the provider display name and models
display_name = provider_data["displayName"]
models = [model["name"] for model in provider_data["models"]]
# Build our mapping
vectorize_providers_mapping[display_name] = [provider_key, models]
# Sort the resulting dictionary
return defaultdict(list, dict(sorted(vectorize_providers_mapping.items())))
except Exception as e:
msg = f"Error fetching vectorize providers: {e}"
raise ValueError(msg) from e
@classmethod
async def create_database_api(
cls,
token: str,
new_database_name: str,
cloud_provider: str,
region: str,
token: str,
environment: str | None = None,
keyspace: str | None = None,
):
client = DataAPIClient(token=token)
client = DataAPIClient(token=token, environment=environment)
# Get the admin object
admin_client = client.get_admin(token=token)
# Call the create database function
return admin_client.create_database(
return await admin_client.async_create_database(
name=new_database_name,
cloud_provider=cloud_provider,
cloud_provider=cls.map_cloud_providers()[cloud_provider]["id"],
region=region,
keyspace=keyspace,
wait_until_active=False,
)
@classmethod
def create_collection_api(
async def create_collection_api(
cls,
token: str,
database_name: str,
new_collection_name: str,
token: str,
api_endpoint: str,
environment: str | None = None,
keyspace: str | None = None,
dimension: int | None = None,
embedding_generation_provider: str | None = None,
embedding_generation_model: str | None = None,
):
# Create the data API client
client = DataAPIClient(token=token)
api_endpoint = cls.get_api_endpoint_static(token=token, database_name=database_name)
# Get the database object
database = client.get_database(api_endpoint=api_endpoint, token=token)
database = client.get_async_database(api_endpoint=api_endpoint, token=token)
# Build vectorize options, if needed
vectorize_options = None
if not dimension:
vectorize_options = CollectionVectorServiceOptions(
provider=embedding_generation_provider,
provider=cls.get_vectorize_providers(
token=token, environment=environment, api_endpoint=api_endpoint
).get(embedding_generation_provider, [None, []])[0],
model_name=embedding_generation_model,
authentication=None,
parameters=None,
)
# Create the collection
return database.create_collection(
return await database.create_collection(
name=new_collection_name,
keyspace=keyspace,
dimension=dimension,
service=vectorize_options,
)
@ -325,16 +372,28 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
db_info_dict = {}
for db in db_list:
try:
# Get the API endpoint for the database
api_endpoint = f"https://{db.info.id}-{db.info.region}.apps.astra{env_string}.datastax.com"
db_info_dict[db.info.name] = {
"api_endpoint": api_endpoint,
"collections": len(
# Get the number of collections
try:
num_collections = len(
list(
client.get_database(
api_endpoint=api_endpoint, token=token, keyspace=db.info.keyspace
).list_collection_names(keyspace=db.info.keyspace)
)
),
)
except Exception: # noqa: BLE001
num_collections = 0
if db.status != "PENDING":
continue
# Add the database to the dictionary
db_info_dict[db.info.name] = {
"api_endpoint": api_endpoint,
"collections": num_collections,
"status": db.status if db.status != "ACTIVE" else None,
}
except Exception: # noqa: BLE001, S110
pass
@ -364,15 +423,20 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
if not database_name:
return None
# Otherwise, get the URL from the database list
return cls.get_database_list_static(token=token, environment=environment).get(database_name).get("api_endpoint")
# Grab the database object
db = cls.get_database_list_static(token=token, environment=environment).get(database_name)
if not db:
return None
def get_api_endpoint(self, *, api_endpoint: str | None = None):
# Otherwise, get the URL from the database list
return db.get("api_endpoint")
def get_api_endpoint(self):
return self.get_api_endpoint_static(
token=self.token,
environment=self.environment,
api_endpoint=api_endpoint or self.d_api_endpoint,
database_name=self.api_endpoint,
api_endpoint=self.api_endpoint,
database_name=self.database_name,
)
def get_keyspace(self):
@ -388,7 +452,7 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
client = DataAPIClient(token=self.token, environment=self.environment)
return client.get_database(
api_endpoint=self.get_api_endpoint(api_endpoint=api_endpoint),
api_endpoint=api_endpoint or self.get_api_endpoint(),
token=self.token,
keyspace=self.get_keyspace(),
)
@ -415,40 +479,15 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
return None
def get_vectorize_providers(self):
try:
self.log("Dynamically updating list of Vectorize providers.")
# Get the admin object
admin = AstraDBAdmin(token=self.token)
db_admin = admin.get_database_admin(api_endpoint=self.get_api_endpoint())
# Get the list of embedding providers
embedding_providers = db_admin.find_embedding_providers().as_dict()
vectorize_providers_mapping = {}
# Map the provider display name to the provider key and models
for provider_key, provider_data in embedding_providers["embeddingProviders"].items():
display_name = provider_data["displayName"]
models = [model["name"] for model in provider_data["models"]]
# TODO: https://astra.datastax.com/api/v2/graphql
vectorize_providers_mapping[display_name] = [provider_key, models]
# Sort the resulting dictionary
return defaultdict(list, dict(sorted(vectorize_providers_mapping.items())))
except Exception as e: # noqa: BLE001
self.log(f"Error fetching Vectorize providers: {e}")
return {}
def _initialize_database_options(self):
try:
return [
{
"name": name,
"status": info["status"],
"collections": info["collections"],
"api_endpoint": info["api_endpoint"],
"icon": "data",
}
for name, info in self.get_database_list().items()
]
@ -456,7 +495,35 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
msg = f"Error fetching database options: {e}"
raise ValueError(msg) from e
@classmethod
def get_provider_icon(cls, collection: CollectionDescriptor | None = None, provider_name: str | None = None) -> str:
# Get the provider name from the collection
provider_name = provider_name or (
collection.options.vector.service.provider
if collection and collection.options and collection.options.vector and collection.options.vector.service
else None
)
# If there is no provider, use the vector store icon
if not provider_name or provider_name == "bring your own":
return "vectorstores"
# Special case for certain models
# TODO: Add more icons
if provider_name == "nvidia":
return "NVIDIA"
if provider_name == "openai":
return "OpenAI"
# Title case on the provider for the icon if no special case
return provider_name.title()
def _initialize_collection_options(self, api_endpoint: str | None = None):
# Nothing to generate if we don't have an API endpoint yet
api_endpoint = api_endpoint or self.get_api_endpoint()
if not api_endpoint:
return []
# Retrieve the database object
database = self.get_database_object(api_endpoint=api_endpoint)
@ -471,7 +538,7 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
"provider": (
col.options.vector.service.provider if col.options.vector and col.options.vector.service else None
),
"icon": "",
"icon": self.get_provider_icon(collection=col),
"model": (
col.options.vector.service.model_name if col.options.vector and col.options.vector.service else None
),
@ -479,9 +546,53 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
for col in collection_list
]
def reset_provider_options(self, build_config: dict):
# Get the list of vectorize providers
vectorize_providers = self.get_vectorize_providers(
token=self.token,
environment=self.environment,
api_endpoint=build_config["api_endpoint"]["value"],
)
# If the collection is set, allow user to see embedding options
build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"][
"embedding_generation_provider"
]["options"] = ["Bring your own", "Nvidia", *[key for key in vectorize_providers if key != "Nvidia"]]
# For all not Bring your own or Nvidia providers, add metadata saying configure in Astra DB Portal
provider_options = build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"][
"embedding_generation_provider"
]["options"]
# Go over each possible provider and add metadata to configure in Astra DB Portal
for provider in provider_options:
# Skip Bring your own and Nvidia, automatically configured
if provider in ["Bring your own", "Nvidia"]:
build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"][
"embedding_generation_provider"
]["options_metadata"].append({"icon": self.get_provider_icon(provider_name=provider.lower())})
continue
# Add metadata to configure in Astra DB Portal
build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"][
"embedding_generation_provider"
]["options_metadata"].append({" ": "Configure in Astra DB Portal"})
# And allow the user to see the models based on a selected provider
embedding_provider = build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"][
"embedding_generation_provider"
]["value"]
# Set the options for the embedding model based on the provider
build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"][
"embedding_generation_model"
]["options"] = vectorize_providers.get(embedding_provider, [[], []])[1]
return build_config
def reset_collection_list(self, build_config: dict):
# Get the list of options we have based on the token provided
collection_options = self._initialize_collection_options()
collection_options = self._initialize_collection_options(api_endpoint=build_config["api_endpoint"]["value"])
# If we retrieved options based on the token, show the dropdown
build_config["collection_name"]["options"] = [col["name"] for col in collection_options]
@ -490,7 +601,11 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
]
# Reset the selected collection
build_config["collection_name"]["value"] = ""
if build_config["collection_name"]["value"] not in build_config["collection_name"]["options"]:
build_config["collection_name"]["value"] = ""
# If we have a database, collection name should not be advanced
build_config["collection_name"]["advanced"] = not build_config["database_name"]["value"]
return build_config
@ -499,84 +614,171 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
database_options = self._initialize_database_options()
# If we retrieved options based on the token, show the dropdown
build_config["api_endpoint"]["options"] = [db["name"] for db in database_options]
build_config["api_endpoint"]["options_metadata"] = [
build_config["database_name"]["options"] = [db["name"] for db in database_options]
build_config["database_name"]["options_metadata"] = [
{k: v for k, v in db.items() if k not in ["name"]} for db in database_options
]
# Reset the selected database
build_config["api_endpoint"]["value"] = ""
if build_config["database_name"]["value"] not in build_config["database_name"]["options"]:
build_config["database_name"]["value"] = ""
build_config["api_endpoint"]["value"] = ""
build_config["collection_name"]["advanced"] = True
# If we have a token, database name should not be advanced
build_config["database_name"]["advanced"] = not build_config["token"]["value"]
return build_config
def reset_build_config(self, build_config: dict):
# Reset the list of databases we have based on the token provided
build_config["api_endpoint"]["options"] = []
build_config["api_endpoint"]["options_metadata"] = []
build_config["database_name"]["options"] = []
build_config["database_name"]["options_metadata"] = []
build_config["database_name"]["value"] = ""
build_config["database_name"]["advanced"] = True
build_config["api_endpoint"]["value"] = ""
build_config["api_endpoint"]["name"] = "Database"
# Reset the list of collections and metadata associated
build_config["collection_name"]["options"] = []
build_config["collection_name"]["options_metadata"] = []
build_config["collection_name"]["value"] = ""
build_config["collection_name"]["advanced"] = True
return build_config
def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):
# When the component first executes, this is the update refresh call
first_run = field_name == "collection_name" and not field_value and not build_config["api_endpoint"]["options"]
async def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):
# Callback for database creation
if field_name == "database_name" and isinstance(field_value, dict) and "new_database_name" in field_value:
try:
await self.create_database_api(
new_database_name=field_value["new_database_name"],
token=self.token,
keyspace=self.get_keyspace(),
environment=self.environment,
cloud_provider=field_value["cloud_provider"],
region=field_value["region"],
)
except Exception as e:
msg = f"Error creating database: {e}"
raise ValueError(msg) from e
# If the token has not been provided, simply return
# Add the new database to the list of options
build_config["database_name"]["options"] = build_config["database_name"]["options"] + [
field_value["new_database_name"]
]
build_config["database_name"]["options_metadata"] = build_config["database_name"]["options_metadata"] + [
{"status": "PENDING"}
]
return self.reset_collection_list(build_config)
# This is the callback required to update the list of regions for a cloud provider
if field_name == "database_name" and isinstance(field_value, dict) and "new_database_name" not in field_value:
cloud_provider = field_value["cloud_provider"]
build_config["database_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"]["region"][
"options"
] = self.map_cloud_providers()[cloud_provider]["regions"]
return build_config
# Callback for the creation of collections
if field_name == "collection_name" and isinstance(field_value, dict) and "new_collection_name" in field_value:
try:
# Get the dimension if its a BYO provider
dimension = (
field_value["dimension"]
if field_value["embedding_generation_provider"] == "Bring your own"
else None
)
# Create the collection
await self.create_collection_api(
new_collection_name=field_value["new_collection_name"],
token=self.token,
api_endpoint=build_config["api_endpoint"]["value"],
environment=self.environment,
keyspace=self.get_keyspace(),
dimension=dimension,
embedding_generation_provider=field_value["embedding_generation_provider"],
embedding_generation_model=field_value["embedding_generation_model"],
)
except Exception as e:
msg = f"Error creating collection: {e}"
raise ValueError(msg) from e
# Add the new collection to the list of options
build_config["collection_name"]["value"] = field_value["new_collection_name"]
build_config["collection_name"]["options"].append(field_value["new_collection_name"])
# Get the provider and model for the new collection
generation_provider = field_value["embedding_generation_provider"]
provider = generation_provider if generation_provider != "Bring your own" else None
generation_model = field_value["embedding_generation_model"]
model = generation_model if generation_model else None
# Add the new collection to the list of options
icon = "NVIDIA" if provider == "Nvidia" else "vectorstores"
build_config["collection_name"]["options_metadata"] = build_config["collection_name"][
"options_metadata"
] + [{"records": 0, "provider": provider, "icon": icon, "model": model}]
return build_config
# Callback to update the model list based on the embedding provider
if (
field_name == "collection_name"
and isinstance(field_value, dict)
and "new_collection_name" not in field_value
):
return self.reset_provider_options(build_config)
# When the component first executes, this is the update refresh call
first_run = field_name == "collection_name" and not field_value and not build_config["database_name"]["options"]
# If the token has not been provided, simply return the empty build config
if not self.token:
return self.reset_build_config(build_config)
# If this is the first execution of the component, reset and build database list
if first_run or field_name in ["token", "environment"]:
# Reset the build config to ensure we are starting fresh
build_config = self.reset_build_config(build_config)
build_config = self.reset_database_list(build_config)
# Get list of regions for a given cloud provider
"""
cloud_provider = (
build_config["api_endpoint"]["dialog_inputs"]["fields"]["data"]["node"]["template"]["cloud_provider"][
"value"
]
or "Amazon Web Services"
)
build_config["api_endpoint"]["dialog_inputs"]["fields"]["data"]["node"]["template"]["region"][
"options"
] = self.map_cloud_providers()[cloud_provider]["regions"]
"""
return build_config
return self.reset_database_list(build_config)
# Refresh the collection name options
if field_name == "api_endpoint":
if field_name == "database_name" and not isinstance(field_value, dict):
# If missing, refresh the database options
if not build_config["api_endpoint"]["options"] or not field_value:
return self.update_build_config(build_config, field_value=self.token, field_name="token")
if field_value not in build_config["database_name"]["options"]:
build_config = await self.update_build_config(build_config, field_value=self.token, field_name="token")
build_config["database_name"]["value"] = ""
else:
# Find the position of the selected database to align with metadata
index_of_name = build_config["database_name"]["options"].index(field_value)
# Set the underlying api endpoint value of the database
if field_value in build_config["api_endpoint"]["options"]:
index_of_name = build_config["api_endpoint"]["options"].index(field_value)
build_config["d_api_endpoint"]["value"] = build_config["api_endpoint"]["options_metadata"][
# Initializing database condition
pending = build_config["database_name"]["options_metadata"][index_of_name]["status"] == "PENDING"
if pending:
return self.update_build_config(build_config, field_value=self.token, field_name="token")
# Set the API endpoint based on the selected database
build_config["api_endpoint"]["value"] = build_config["database_name"]["options_metadata"][
index_of_name
]["api_endpoint"]
else:
build_config["d_api_endpoint"]["value"] = ""
# Reset the provider options
build_config = self.reset_provider_options(build_config)
# Reset the list of collections we have based on the token provided
return self.reset_collection_list(build_config)
# Hide embedding model option if opriona_metadata provider is not null
if field_name == "collection_name" and field_value:
if field_name == "collection_name" and not isinstance(field_value, dict):
# Assume we will be autodetecting the collection:
build_config["autodetect_collection"]["value"] = True
# Reload the collection list
build_config = self.reset_collection_list(build_config)
# Set the options for collection name to be the field value if its a new collection
if field_value not in build_config["collection_name"]["options"]:
if field_value and field_value not in build_config["collection_name"]["options"]:
# Add the new collection to the list of options
build_config["collection_name"]["options"].append(field_value)
build_config["collection_name"]["options_metadata"].append(
@ -598,36 +800,8 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
build_config["embedding_model"]["advanced"] = False
build_config["embedding_choice"]["value"] = "Embedding Model"
# For the final step, get the list of vectorize providers
"""
vectorize_providers = self.get_vectorize_providers()
if not vectorize_providers:
return build_config
# Allow the user to see the embedding provider options
provider_options = build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"][
"embedding_generation_provider"
]["options"]
if not provider_options:
# If the collection is set, allow user to see embedding options
build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"][
"embedding_generation_provider"
]["options"] = ["Bring your own", "Nvidia", *[key for key in vectorize_providers if key != "Nvidia"]]
# And allow the user to see the models based on a selected provider
model_options = build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"][
"embedding_generation_model"
]["options"]
if not model_options:
embedding_provider = build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"][
"embedding_generation_provider"
]["value"]
build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"][
"embedding_generation_model"
]["options"] = vectorize_providers.get(embedding_provider, [[], []])[1]
"""
return build_config
@check_cached_vector_store
@ -654,11 +828,11 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
# Get Langflow version and platform information
__version__ = get_version_info()["version"]
langflow_prefix = ""
if os.getenv("AWS_EXECUTION_ENV") == "AWS_ECS_FARGATE": # TODO: More precise way of detecting
langflow_prefix = "ds-"
# if os.getenv("AWS_EXECUTION_ENV") == "AWS_ECS_FARGATE": # TODO: More precise way of detecting
# langflow_prefix = "ds-"
# Get the database object
database = self.get_database_object(api_endpoint=self.d_api_endpoint)
database = self.get_database_object()
autodetect = self.collection_name in database.list_collection_names() and self.autodetect_collection
# Bundle up the auto-detect parameters
@ -714,7 +888,7 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
if documents and self.deletion_field:
self.log(f"Deleting documents where {self.deletion_field}")
try:
database = self.get_database_object(api_endpoint=self.d_api_endpoint)
database = self.get_database_object()
collection = database.get_collection(self.collection_name, keyspace=database.keyspace)
delete_values = list({doc.metadata[self.deletion_field] for doc in documents})
self.log(f"Deleting documents where {self.deletion_field} matches {delete_values}.")

View File

@ -981,9 +981,6 @@ class Component(CustomComponent):
return {"repr": custom_repr, "raw": raw, "type": artifact_type}
def _process_raw_result(self, result):
"""Process the raw result of the component."""
if len(self.outputs) == 1:
return self.status or self.extract_data(result)
return self.extract_data(result)
def extract_data(self, result):

View File

@ -1,21 +1,26 @@
import asyncio
from __future__ import annotations
import inspect
import json
import time
import uuid
from functools import partial
from typing import Literal
from typing import TYPE_CHECKING, Literal
from fastapi.encoders import jsonable_encoder
from loguru import logger
from typing_extensions import Protocol
from langflow.schema.log import LoggableType
from langflow.schema.playground_events import create_event_by_type
if TYPE_CHECKING:
import asyncio
from langflow.schema.log import LoggableType
class EventCallback(Protocol):
def __call__(self, *, manager: "EventManager", event_type: str, data: LoggableType): ...
def __call__(self, *, manager: EventManager, event_type: str, data: LoggableType): ...
class PartialEventCallback(Protocol):

View File

@ -635,6 +635,15 @@ class Graph:
raise ValueError(msg)
return self._run_id
def set_tracing_session_id(self) -> None:
"""Sets the ID of the current session.
Args:
session_id (str): The session ID.
"""
if self.tracing_service:
self.tracing_service.set_session_id(self._session_id)
def set_run_id(self, run_id: uuid.UUID | None = None) -> None:
"""Sets the ID of the current run.
@ -647,6 +656,8 @@ class Graph:
self._run_id = str(run_id)
if self.tracing_service:
self.tracing_service.set_run_id(run_id)
if self._session_id and self.tracing_service is not None:
self.tracing_service.set_session_id(self.session_id)
def set_run_name(self) -> None:
# Given a flow name, flow_id

View File

@ -135,10 +135,12 @@ async def log_transaction(
flow_id=flow_id if isinstance(flow_id, UUID) else UUID(flow_id),
)
async with session_getter(get_db_service()) as session:
inserted = await crud_log_transaction(session, transaction)
logger.debug(f"Logged transaction: {inserted.id}")
with session.no_autoflush:
inserted = await crud_log_transaction(session, transaction)
if inserted:
logger.debug(f"Logged transaction: {inserted.id}")
except Exception: # noqa: BLE001
logger.exception("Error logging transaction")
logger.error("Error logging transaction")
async def log_vertex_build(

View File

@ -8,16 +8,12 @@
"dataType": "ChatInput",
"id": "ChatInput-jFwUm",
"name": "message",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "input_value",
"id": "OpenAIModel-OcXkl",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -34,16 +30,12 @@
"dataType": "Prompt",
"id": "Prompt-3SM2g",
"name": "prompt",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "system_message",
"id": "OpenAIModel-OcXkl",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -60,16 +52,12 @@
"dataType": "OpenAIModel",
"id": "OpenAIModel-OcXkl",
"name": "text_output",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "input_value",
"id": "ChatOutput-gDYiJ",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -87,9 +75,7 @@
"display_name": "Chat Input",
"id": "ChatInput-jFwUm",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -120,9 +106,7 @@
"name": "message",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -135,9 +119,7 @@
"display_name": "Background Color",
"dynamic": false,
"info": "The background color of the icon.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "background_color",
@ -156,9 +138,7 @@
"display_name": "Icon",
"dynamic": false,
"info": "The icon of the message.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "chat_icon",
@ -255,10 +235,7 @@
"dynamic": false,
"info": "Type of sender.",
"name": "sender",
"options": [
"Machine",
"User"
],
"options": ["Machine", "User"],
"placeholder": "",
"required": false,
"show": true,
@ -272,9 +249,7 @@
"display_name": "Sender Name",
"dynamic": false,
"info": "Name of the sender.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "sender_name",
@ -292,9 +267,7 @@
"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"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "session_id",
@ -329,9 +302,7 @@
"display_name": "Text Color",
"dynamic": false,
"info": "The text color of the name",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "text_color",
@ -373,9 +344,7 @@
"display_name": "Prompt",
"id": "Prompt-3SM2g",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {
@ -385,9 +354,7 @@
"display_name": "Prompt",
"documentation": "",
"edited": false,
"field_order": [
"template"
],
"field_order": ["template"],
"frozen": false,
"icon": "prompts",
"legacy": false,
@ -402,9 +369,7 @@
"name": "prompt",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -453,9 +418,7 @@
"display_name": "Tool Placeholder",
"dynamic": false,
"info": "A placeholder input for tool mode.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "tool_placeholder",
@ -570,9 +533,7 @@
"data": {
"id": "ChatOutput-gDYiJ",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -605,9 +566,7 @@
"name": "message",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -620,9 +579,7 @@
"display_name": "Background Color",
"dynamic": false,
"info": "The background color of the icon.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "background_color",
@ -642,9 +599,7 @@
"display_name": "Icon",
"dynamic": false,
"info": "The icon of the message.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "chat_icon",
@ -682,9 +637,7 @@
"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"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "data_template",
@ -704,9 +657,7 @@
"display_name": "Text",
"dynamic": false,
"info": "Message to be passed as output.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "input_value",
@ -727,10 +678,7 @@
"dynamic": false,
"info": "Type of sender.",
"name": "sender",
"options": [
"Machine",
"User"
],
"options": ["Machine", "User"],
"placeholder": "",
"required": false,
"show": true,
@ -746,9 +694,7 @@
"display_name": "Sender Name",
"dynamic": false,
"info": "Name of the sender.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "sender_name",
@ -768,9 +714,7 @@
"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"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "session_id",
@ -806,9 +750,7 @@
"display_name": "Text Color",
"dynamic": false,
"info": "The text color of the name",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "text_color",
@ -850,10 +792,7 @@
"data": {
"id": "OpenAIModel-OcXkl",
"node": {
"base_classes": [
"LanguageModel",
"Message"
],
"base_classes": ["LanguageModel", "Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -892,9 +831,7 @@
"required_inputs": [],
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
},
{
@ -903,14 +840,10 @@
"display_name": "Language Model",
"method": "build_model",
"name": "model_output",
"required_inputs": [
"api_key"
],
"required_inputs": ["api_key"],
"selected": "LanguageModel",
"tool_mode": true,
"types": [
"LanguageModel"
],
"types": ["LanguageModel"],
"value": "__UNDEFINED__"
}
],
@ -923,10 +856,8 @@
"display_name": "OpenAI API Key",
"dynamic": false,
"info": "The OpenAI API Key to use for the OpenAI model.",
"input_types": [
"Message"
],
"load_from_db": false,
"input_types": ["Message"],
"load_from_db": true,
"name": "api_key",
"password": true,
"placeholder": "",
@ -934,7 +865,7 @@
"show": true,
"title_case": false,
"type": "str",
"value": ""
"value": "OPENAI_API_KEY"
},
"code": {
"advanced": true,
@ -960,9 +891,7 @@
"display_name": "Input",
"dynamic": false,
"info": "",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1144,9 +1073,7 @@
"display_name": "System Message",
"dynamic": false,
"info": "System message to pass to the model.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1243,7 +1170,5 @@
"is_component": false,
"last_tested_version": "1.0.19.post2",
"name": "Basic Prompting",
"tags": [
"chatbots"
]
}
"tags": ["chatbots"]
}

View File

@ -9,17 +9,12 @@
"dataType": "ParseData",
"id": "ParseData-4Sckw",
"name": "text",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "references",
"id": "Prompt-65R68",
"inputTypes": [
"Message",
"Text"
],
"inputTypes": ["Message", "Text"],
"type": "str"
}
},
@ -37,17 +32,12 @@
"dataType": "TextInput",
"id": "TextInput-t88FI",
"name": "text",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "instructions",
"id": "Prompt-65R68",
"inputTypes": [
"Message",
"Text"
],
"inputTypes": ["Message", "Text"],
"type": "str"
}
},
@ -64,16 +54,12 @@
"dataType": "Prompt",
"id": "Prompt-65R68",
"name": "prompt",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "input_value",
"id": "OpenAIModel-MyAsQ",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -90,16 +76,12 @@
"dataType": "OpenAIModel",
"id": "OpenAIModel-MyAsQ",
"name": "text_output",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "input_value",
"id": "ChatOutput-BE4YI",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -116,16 +98,12 @@
"dataType": "URL",
"id": "URL-EPEnt",
"name": "data",
"output_types": [
"Data"
]
"output_types": ["Data"]
},
"targetHandle": {
"fieldName": "data",
"id": "ParseData-4Sckw",
"inputTypes": [
"Data"
],
"inputTypes": ["Data"],
"type": "other"
}
},
@ -143,9 +121,7 @@
"display_name": "Parse Data",
"id": "ParseData-4Sckw",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -153,16 +129,14 @@
"display_name": "Parse Data",
"documentation": "",
"edited": false,
"field_order": [
"data",
"template",
"sep"
],
"field_order": ["data", "template", "sep"],
"frozen": false,
"icon": "message-square",
"legacy": false,
"lf_version": "1.0.19.post2",
"metadata": {},
"metadata": {
"legacy_name": "Parse Data"
},
"output_types": [],
"outputs": [
{
@ -173,9 +147,7 @@
"name": "text",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
},
{
@ -186,9 +158,7 @@
"name": "data_list",
"selected": "Data",
"tool_mode": true,
"types": [
"Data"
],
"types": ["Data"],
"value": "__UNDEFINED__"
}
],
@ -211,16 +181,14 @@
"show": true,
"title_case": false,
"type": "code",
"value": "from langflow.custom import Component\nfrom langflow.helpers.data import data_to_text, data_to_text_list\nfrom langflow.io import DataInput, MultilineInput, Output, StrInput\nfrom langflow.schema import Data\nfrom langflow.schema.message import Message\n\n\nclass ParseDataComponent(Component):\n display_name = \"Data to Message\"\n description = \"Convert Data objects into Messages using any {field_name} from input data.\"\n icon = \"message-square\"\n name = \"ParseData\"\n\n inputs = [\n DataInput(name=\"data\", display_name=\"Data\", info=\"The data to convert to text.\", is_list=True, required=True),\n MultilineInput(\n name=\"template\",\n display_name=\"Template\",\n info=\"The template to use for formatting the data. \"\n \"It can contain the keys {text}, {data} or any other key in the Data.\",\n value=\"{text}\",\n required=True,\n ),\n StrInput(name=\"sep\", display_name=\"Separator\", advanced=True, value=\"\\n\"),\n ]\n\n outputs = [\n Output(\n display_name=\"Message\",\n name=\"text\",\n info=\"Data as a single Message, with each input Data separated by Separator\",\n method=\"parse_data\",\n ),\n Output(\n display_name=\"Data List\",\n name=\"data_list\",\n info=\"Data as a list of new Data, each having `text` formatted by Template\",\n method=\"parse_data_as_list\",\n ),\n ]\n\n def _clean_args(self) -> tuple[list[Data], str, str]:\n data = self.data if isinstance(self.data, list) else [self.data]\n template = self.template\n sep = self.sep\n return data, template, sep\n\n def parse_data(self) -> Message:\n data, template, sep = self._clean_args()\n result_string = data_to_text(template, data, sep)\n self.status = result_string\n return Message(text=result_string)\n\n def parse_data_as_list(self) -> list[Data]:\n data, template, _ = self._clean_args()\n text_list, data_list = data_to_text_list(template, data)\n for item, text in zip(data_list, text_list, strict=True):\n item.set_text(text)\n self.status = data_list\n return data_list\n"
"value": "from langflow.custom import Component\nfrom langflow.helpers.data import data_to_text, data_to_text_list\nfrom langflow.io import DataInput, MultilineInput, Output, StrInput\nfrom langflow.schema import Data\nfrom langflow.schema.message import Message\n\n\nclass ParseDataComponent(Component):\n display_name = \"Data to Message\"\n description = \"Convert Data objects into Messages using any {field_name} from input data.\"\n icon = \"message-square\"\n name = \"ParseData\"\n metadata = {\n \"legacy_name\": \"Parse Data\",\n }\n\n inputs = [\n DataInput(\n name=\"data\",\n display_name=\"Data\",\n info=\"The data to convert to text.\",\n is_list=True,\n required=True,\n ),\n MultilineInput(\n name=\"template\",\n display_name=\"Template\",\n info=\"The template to use for formatting the data. \"\n \"It can contain the keys {text}, {data} or any other key in the Data.\",\n value=\"{text}\",\n required=True,\n ),\n StrInput(name=\"sep\", display_name=\"Separator\", advanced=True, value=\"\\n\"),\n ]\n\n outputs = [\n Output(\n display_name=\"Message\",\n name=\"text\",\n info=\"Data as a single Message, with each input Data separated by Separator\",\n method=\"parse_data\",\n ),\n Output(\n display_name=\"Data List\",\n name=\"data_list\",\n info=\"Data as a list of new Data, each having `text` formatted by Template\",\n method=\"parse_data_as_list\",\n ),\n ]\n\n def _clean_args(self) -> tuple[list[Data], str, str]:\n data = self.data if isinstance(self.data, list) else [self.data]\n template = self.template\n sep = self.sep\n return data, template, sep\n\n def parse_data(self) -> Message:\n data, template, sep = self._clean_args()\n result_string = data_to_text(template, data, sep)\n self.status = result_string\n return Message(text=result_string)\n\n def parse_data_as_list(self) -> list[Data]:\n data, template, _ = self._clean_args()\n text_list, data_list = data_to_text_list(template, data)\n for item, text in zip(data_list, text_list, strict=True):\n item.set_text(text)\n self.status = data_list\n return data_list\n"
},
"data": {
"advanced": false,
"display_name": "Data",
"dynamic": false,
"info": "The data to convert to text.",
"input_types": [
"Data"
],
"input_types": ["Data"],
"list": true,
"name": "data",
"placeholder": "",
@ -253,9 +221,7 @@
"display_name": "Template",
"dynamic": false,
"info": "The template to use for formatting the data. It can contain the keys {text}, {data} or any other key in the Data.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"multiline": true,
@ -298,24 +264,17 @@
"display_name": "Prompt",
"id": "Prompt-65R68",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {
"template": [
"references",
"instructions"
]
"template": ["references", "instructions"]
},
"description": "Create a prompt template with dynamic variables.",
"display_name": "Prompt",
"documentation": "",
"edited": false,
"field_order": [
"template"
],
"field_order": ["template"],
"frozen": false,
"icon": "prompts",
"legacy": false,
@ -331,9 +290,7 @@
"name": "prompt",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -366,10 +323,7 @@
"fileTypes": [],
"file_path": "",
"info": "",
"input_types": [
"Message",
"Text"
],
"input_types": ["Message", "Text"],
"list": false,
"load_from_db": false,
"multiline": true,
@ -390,10 +344,7 @@
"fileTypes": [],
"file_path": "",
"info": "",
"input_types": [
"Message",
"Text"
],
"input_types": ["Message", "Text"],
"list": false,
"load_from_db": false,
"multiline": true,
@ -428,9 +379,7 @@
"display_name": "Tool Placeholder",
"dynamic": false,
"info": "A placeholder input for tool mode.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "tool_placeholder",
@ -473,9 +422,7 @@
"display_name": "Instructions",
"id": "TextInput-t88FI",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -483,9 +430,7 @@
"display_name": "Instructions",
"documentation": "",
"edited": false,
"field_order": [
"input_value"
],
"field_order": ["input_value"],
"frozen": false,
"icon": "type",
"legacy": false,
@ -501,9 +446,7 @@
"name": "text",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -534,9 +477,7 @@
"display_name": "Text",
"dynamic": false,
"info": "Text to be passed as input.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"multiline": true,
@ -579,9 +520,7 @@
"display_name": "Chat Output",
"id": "ChatOutput-BE4YI",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -612,9 +551,7 @@
"name": "message",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -627,9 +564,7 @@
"display_name": "Background Color",
"dynamic": false,
"info": "The background color of the icon.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "background_color",
@ -648,9 +583,7 @@
"display_name": "Icon",
"dynamic": false,
"info": "The icon of the message.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "chat_icon",
@ -686,9 +619,7 @@
"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"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "data_template",
@ -706,9 +637,7 @@
"display_name": "Text",
"dynamic": false,
"info": "Message to be passed as output.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "input_value",
@ -727,10 +656,7 @@
"dynamic": false,
"info": "Type of sender.",
"name": "sender",
"options": [
"Machine",
"User"
],
"options": ["Machine", "User"],
"placeholder": "",
"required": false,
"show": true,
@ -744,9 +670,7 @@
"display_name": "Sender Name",
"dynamic": false,
"info": "Name of the sender.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "sender_name",
@ -764,9 +688,7 @@
"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"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "session_id",
@ -801,9 +723,7 @@
"display_name": "Text Color",
"dynamic": false,
"info": "The text color of the name",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "text_color",
@ -952,10 +872,7 @@
"data": {
"id": "OpenAIModel-MyAsQ",
"node": {
"base_classes": [
"LanguageModel",
"Message"
],
"base_classes": ["LanguageModel", "Message"],
"beta": false,
"category": "models",
"conditional_paths": [],
@ -994,9 +911,7 @@
"required_inputs": [],
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
},
{
@ -1005,14 +920,10 @@
"display_name": "Language Model",
"method": "build_model",
"name": "model_output",
"required_inputs": [
"api_key"
],
"required_inputs": ["api_key"],
"selected": "LanguageModel",
"tool_mode": true,
"types": [
"LanguageModel"
],
"types": ["LanguageModel"],
"value": "__UNDEFINED__"
}
],
@ -1026,9 +937,7 @@
"display_name": "OpenAI API Key",
"dynamic": false,
"info": "The OpenAI API Key to use for the OpenAI model.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"load_from_db": true,
"name": "api_key",
"password": true,
@ -1037,7 +946,7 @@
"show": true,
"title_case": false,
"type": "str",
"value": ""
"value": "OPENAI_API_KEY"
},
"code": {
"advanced": true,
@ -1063,9 +972,7 @@
"display_name": "Input",
"dynamic": false,
"info": "",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1247,9 +1154,7 @@
"display_name": "System Message",
"dynamic": false,
"info": "System message to pass to the model.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1334,11 +1239,7 @@
"data": {
"id": "URL-EPEnt",
"node": {
"base_classes": [
"Data",
"DataFrame",
"Message"
],
"base_classes": ["Data", "DataFrame", "Message"],
"beta": false,
"category": "data",
"conditional_paths": [],
@ -1347,10 +1248,7 @@
"display_name": "URL",
"documentation": "",
"edited": false,
"field_order": [
"urls",
"format"
],
"field_order": ["urls", "format"],
"frozen": false,
"icon": "layout-template",
"key": "URL",
@ -1367,9 +1265,7 @@
"name": "data",
"selected": "Data",
"tool_mode": true,
"types": [
"Data"
],
"types": ["Data"],
"value": "__UNDEFINED__"
},
{
@ -1380,9 +1276,7 @@
"name": "text",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
},
{
@ -1393,9 +1287,7 @@
"name": "dataframe",
"selected": "DataFrame",
"tool_mode": true,
"types": [
"DataFrame"
],
"types": ["DataFrame"],
"value": "__UNDEFINED__"
}
],
@ -1430,10 +1322,7 @@
"dynamic": false,
"info": "Output Format. Use 'Text' to extract the text from the HTML or 'Raw HTML' for the raw HTML content.",
"name": "format",
"options": [
"Text",
"Raw HTML"
],
"options": ["Text", "Raw HTML"],
"options_metadata": [],
"placeholder": "",
"required": false,
@ -1450,9 +1339,7 @@
"display_name": "URLs",
"dynamic": false,
"info": "",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": true,
"list_add_label": "Add URL",
"load_from_db": false,
@ -1465,10 +1352,7 @@
"trace_as_input": true,
"trace_as_metadata": true,
"type": "str",
"value": [
"https://langflow.org/",
"https://docs.langflow.org/"
]
"value": ["https://langflow.org/", "https://docs.langflow.org/"]
}
},
"tool_mode": false
@ -1504,8 +1388,5 @@
"is_component": false,
"last_tested_version": "1.0.19.post2",
"name": "Blog Writer",
"tags": [
"chatbots",
"content-generation"
]
}
"tags": ["chatbots", "content-generation"]
}

View File

@ -204,9 +204,7 @@
"name": "message",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -490,9 +488,7 @@
"name": "messages",
"selected": "Data",
"tool_mode": true,
"types": [
"Data"
],
"types": ["Data"],
"value": "__UNDEFINED__"
},
{
@ -503,9 +499,7 @@
"name": "messages_text",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -722,9 +716,7 @@
"name": "prompt",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -950,9 +942,7 @@
"name": "message",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -1267,9 +1257,7 @@
"name": "data",
"selected": "Data",
"tool_mode": true,
"types": [
"Data"
],
"types": ["Data"],
"value": "__UNDEFINED__"
},
{
@ -1280,9 +1268,7 @@
"name": "text",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
},
{
@ -1293,9 +1279,7 @@
"name": "dataframe",
"selected": "DataFrame",
"tool_mode": true,
"types": [
"DataFrame"
],
"types": ["DataFrame"],
"value": "__UNDEFINED__"
}
],
@ -1412,9 +1396,7 @@
"name": "data",
"selected": "Data",
"tool_mode": true,
"types": [
"Data"
],
"types": ["Data"],
"value": "__UNDEFINED__"
},
{
@ -1425,9 +1407,7 @@
"name": "text",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
},
{
@ -1438,9 +1418,7 @@
"name": "dataframe",
"selected": "DataFrame",
"tool_mode": true,
"types": [
"DataFrame"
],
"types": ["DataFrame"],
"value": "__UNDEFINED__"
}
],
@ -1563,9 +1541,7 @@
"name": "data",
"selected": "Data",
"tool_mode": true,
"types": [
"Data"
],
"types": ["Data"],
"value": "__UNDEFINED__"
},
{
@ -1576,9 +1552,7 @@
"name": "text",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
},
{
@ -1589,9 +1563,7 @@
"name": "dataframe",
"selected": "DataFrame",
"tool_mode": true,
"types": [
"DataFrame"
],
"types": ["DataFrame"],
"value": "__UNDEFINED__"
}
],
@ -1722,9 +1694,7 @@
"required_inputs": [],
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
},
{
@ -1736,9 +1706,7 @@
"required_inputs": ["api_key"],
"selected": "LanguageModel",
"tool_mode": true,
"types": [
"LanguageModel"
],
"types": ["LanguageModel"],
"value": "__UNDEFINED__"
}
],
@ -1753,7 +1721,7 @@
"dynamic": false,
"info": "Your Anthropic API key.",
"input_types": ["Message"],
"load_from_db": false,
"load_from_db": true,
"name": "api_key",
"password": true,
"placeholder": "",
@ -1762,7 +1730,7 @@
"show": true,
"title_case": false,
"type": "str",
"value": ""
"value": "ANTHROPIC_API_KEY"
},
"base_url": {
"_input_type": "MessageTextInput",

View File

@ -8,16 +8,12 @@
"dataType": "File",
"id": "File-GwJQZ",
"name": "data",
"output_types": [
"Data"
]
"output_types": ["Data"]
},
"targetHandle": {
"fieldName": "data",
"id": "ParseData-BbvKb",
"inputTypes": [
"Data"
],
"inputTypes": ["Data"],
"type": "other"
}
},
@ -35,17 +31,12 @@
"dataType": "ParseData",
"id": "ParseData-BbvKb",
"name": "text",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "Document",
"id": "Prompt-yvZHT",
"inputTypes": [
"Message",
"Text"
],
"inputTypes": ["Message", "Text"],
"type": "str"
}
},
@ -63,16 +54,12 @@
"dataType": "ChatInput",
"id": "ChatInput-li477",
"name": "message",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "input_value",
"id": "OpenAIModel-atkmo",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -90,16 +77,12 @@
"dataType": "Prompt",
"id": "Prompt-yvZHT",
"name": "prompt",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "system_message",
"id": "OpenAIModel-atkmo",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -117,16 +100,12 @@
"dataType": "OpenAIModel",
"id": "OpenAIModel-atkmo",
"name": "text_output",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "input_value",
"id": "ChatOutput-8pgwS",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -145,9 +124,7 @@
"display_name": "Chat Input",
"id": "ChatInput-li477",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -178,9 +155,7 @@
"name": "message",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -193,9 +168,7 @@
"display_name": "Background Color",
"dynamic": false,
"info": "The background color of the icon.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "background_color",
@ -214,9 +187,7 @@
"display_name": "Icon",
"dynamic": false,
"info": "The icon of the message.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "chat_icon",
@ -313,10 +284,7 @@
"dynamic": false,
"info": "Type of sender.",
"name": "sender",
"options": [
"Machine",
"User"
],
"options": ["Machine", "User"],
"placeholder": "",
"required": false,
"show": true,
@ -330,9 +298,7 @@
"display_name": "Sender Name",
"dynamic": false,
"info": "Name of the sender.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "sender_name",
@ -350,9 +316,7 @@
"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"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "session_id",
@ -387,9 +351,7 @@
"display_name": "Text Color",
"dynamic": false,
"info": "The text color of the name",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "text_color",
@ -431,9 +393,7 @@
"display_name": "Chat Output",
"id": "ChatOutput-8pgwS",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -467,9 +427,7 @@
"name": "message",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -482,9 +440,7 @@
"display_name": "Background Color",
"dynamic": false,
"info": "The background color of the icon.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "background_color",
@ -504,9 +460,7 @@
"display_name": "Icon",
"dynamic": false,
"info": "The icon of the message.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "chat_icon",
@ -544,9 +498,7 @@
"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"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "data_template",
@ -566,9 +518,7 @@
"display_name": "Text",
"dynamic": false,
"info": "Message to be passed as output.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "input_value",
@ -589,10 +539,7 @@
"dynamic": false,
"info": "Type of sender.",
"name": "sender",
"options": [
"Machine",
"User"
],
"options": ["Machine", "User"],
"placeholder": "",
"required": false,
"show": true,
@ -608,9 +555,7 @@
"display_name": "Sender Name",
"dynamic": false,
"info": "Name of the sender.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "sender_name",
@ -630,9 +575,7 @@
"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"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "session_id",
@ -668,9 +611,7 @@
"display_name": "Text Color",
"dynamic": false,
"info": "The text color of the name",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "text_color",
@ -714,9 +655,7 @@
"display_name": "Parse Data",
"id": "ParseData-BbvKb",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -724,16 +663,14 @@
"display_name": "Parse Data",
"documentation": "",
"edited": false,
"field_order": [
"data",
"template",
"sep"
],
"field_order": ["data", "template", "sep"],
"frozen": false,
"icon": "message-square",
"legacy": false,
"lf_version": "1.0.19.post2",
"metadata": {},
"metadata": {
"legacy_name": "Parse Data"
},
"output_types": [],
"outputs": [
{
@ -744,9 +681,7 @@
"name": "text",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
},
{
@ -757,9 +692,7 @@
"name": "data_list",
"selected": "Data",
"tool_mode": true,
"types": [
"Data"
],
"types": ["Data"],
"value": "__UNDEFINED__"
}
],
@ -782,16 +715,14 @@
"show": true,
"title_case": false,
"type": "code",
"value": "from langflow.custom import Component\nfrom langflow.helpers.data import data_to_text, data_to_text_list\nfrom langflow.io import DataInput, MultilineInput, Output, StrInput\nfrom langflow.schema import Data\nfrom langflow.schema.message import Message\n\n\nclass ParseDataComponent(Component):\n display_name = \"Data to Message\"\n description = \"Convert Data objects into Messages using any {field_name} from input data.\"\n icon = \"message-square\"\n name = \"ParseData\"\n\n inputs = [\n DataInput(name=\"data\", display_name=\"Data\", info=\"The data to convert to text.\", is_list=True, required=True),\n MultilineInput(\n name=\"template\",\n display_name=\"Template\",\n info=\"The template to use for formatting the data. \"\n \"It can contain the keys {text}, {data} or any other key in the Data.\",\n value=\"{text}\",\n required=True,\n ),\n StrInput(name=\"sep\", display_name=\"Separator\", advanced=True, value=\"\\n\"),\n ]\n\n outputs = [\n Output(\n display_name=\"Message\",\n name=\"text\",\n info=\"Data as a single Message, with each input Data separated by Separator\",\n method=\"parse_data\",\n ),\n Output(\n display_name=\"Data List\",\n name=\"data_list\",\n info=\"Data as a list of new Data, each having `text` formatted by Template\",\n method=\"parse_data_as_list\",\n ),\n ]\n\n def _clean_args(self) -> tuple[list[Data], str, str]:\n data = self.data if isinstance(self.data, list) else [self.data]\n template = self.template\n sep = self.sep\n return data, template, sep\n\n def parse_data(self) -> Message:\n data, template, sep = self._clean_args()\n result_string = data_to_text(template, data, sep)\n self.status = result_string\n return Message(text=result_string)\n\n def parse_data_as_list(self) -> list[Data]:\n data, template, _ = self._clean_args()\n text_list, data_list = data_to_text_list(template, data)\n for item, text in zip(data_list, text_list, strict=True):\n item.set_text(text)\n self.status = data_list\n return data_list\n"
"value": "from langflow.custom import Component\nfrom langflow.helpers.data import data_to_text, data_to_text_list\nfrom langflow.io import DataInput, MultilineInput, Output, StrInput\nfrom langflow.schema import Data\nfrom langflow.schema.message import Message\n\n\nclass ParseDataComponent(Component):\n display_name = \"Data to Message\"\n description = \"Convert Data objects into Messages using any {field_name} from input data.\"\n icon = \"message-square\"\n name = \"ParseData\"\n metadata = {\n \"legacy_name\": \"Parse Data\",\n }\n\n inputs = [\n DataInput(\n name=\"data\",\n display_name=\"Data\",\n info=\"The data to convert to text.\",\n is_list=True,\n required=True,\n ),\n MultilineInput(\n name=\"template\",\n display_name=\"Template\",\n info=\"The template to use for formatting the data. \"\n \"It can contain the keys {text}, {data} or any other key in the Data.\",\n value=\"{text}\",\n required=True,\n ),\n StrInput(name=\"sep\", display_name=\"Separator\", advanced=True, value=\"\\n\"),\n ]\n\n outputs = [\n Output(\n display_name=\"Message\",\n name=\"text\",\n info=\"Data as a single Message, with each input Data separated by Separator\",\n method=\"parse_data\",\n ),\n Output(\n display_name=\"Data List\",\n name=\"data_list\",\n info=\"Data as a list of new Data, each having `text` formatted by Template\",\n method=\"parse_data_as_list\",\n ),\n ]\n\n def _clean_args(self) -> tuple[list[Data], str, str]:\n data = self.data if isinstance(self.data, list) else [self.data]\n template = self.template\n sep = self.sep\n return data, template, sep\n\n def parse_data(self) -> Message:\n data, template, sep = self._clean_args()\n result_string = data_to_text(template, data, sep)\n self.status = result_string\n return Message(text=result_string)\n\n def parse_data_as_list(self) -> list[Data]:\n data, template, _ = self._clean_args()\n text_list, data_list = data_to_text_list(template, data)\n for item, text in zip(data_list, text_list, strict=True):\n item.set_text(text)\n self.status = data_list\n return data_list\n"
},
"data": {
"advanced": false,
"display_name": "Data",
"dynamic": false,
"info": "The data to convert to text.",
"input_types": [
"Data"
],
"input_types": ["Data"],
"list": true,
"name": "data",
"placeholder": "",
@ -824,9 +755,7 @@
"display_name": "Template",
"dynamic": false,
"info": "The template to use for formatting the data. It can contain the keys {text}, {data} or any other key in the Data.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"multiline": true,
@ -934,9 +863,7 @@
"data": {
"id": "File-GwJQZ",
"node": {
"base_classes": [
"Data"
],
"base_classes": ["Data"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -965,9 +892,7 @@
"required_inputs": [],
"selected": "Data",
"tool_mode": true,
"types": [
"Data"
],
"types": ["Data"],
"value": "__UNDEFINED__"
}
],
@ -1030,10 +955,7 @@
"display_name": "Server File Path",
"dynamic": false,
"info": "Data object with a 'file_path' property pointing to server file or a Message object with a path to the file. Supercedes 'Path' but supports same file types.",
"input_types": [
"Data",
"Message"
],
"input_types": ["Data", "Message"],
"list": true,
"name": "file_path",
"placeholder": "",
@ -1180,24 +1102,18 @@
"display_name": "Prompt",
"id": "Prompt-yvZHT",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {
"template": [
"Document"
]
"template": ["Document"]
},
"description": "Create a prompt template with dynamic variables.",
"display_name": "Prompt",
"documentation": "",
"edited": false,
"error": null,
"field_order": [
"template"
],
"field_order": ["template"],
"frozen": false,
"full_path": null,
"icon": "prompts",
@ -1218,9 +1134,7 @@
"name": "prompt",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -1234,10 +1148,7 @@
"fileTypes": [],
"file_path": "",
"info": "",
"input_types": [
"Message",
"Text"
],
"input_types": ["Message", "Text"],
"list": false,
"load_from_db": false,
"multiline": true,
@ -1290,9 +1201,7 @@
"display_name": "Tool Placeholder",
"dynamic": false,
"info": "A placeholder input for tool mode.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "tool_placeholder",
@ -1334,10 +1243,7 @@
"data": {
"id": "OpenAIModel-atkmo",
"node": {
"base_classes": [
"LanguageModel",
"Message"
],
"base_classes": ["LanguageModel", "Message"],
"beta": false,
"category": "models",
"conditional_paths": [],
@ -1376,9 +1282,7 @@
"required_inputs": [],
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
},
{
@ -1387,14 +1291,10 @@
"display_name": "Language Model",
"method": "build_model",
"name": "model_output",
"required_inputs": [
"api_key"
],
"required_inputs": ["api_key"],
"selected": "LanguageModel",
"tool_mode": true,
"types": [
"LanguageModel"
],
"types": ["LanguageModel"],
"value": "__UNDEFINED__"
}
],
@ -1408,9 +1308,7 @@
"display_name": "OpenAI API Key",
"dynamic": false,
"info": "The OpenAI API Key to use for the OpenAI model.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"load_from_db": true,
"name": "api_key",
"password": true,
@ -1419,7 +1317,7 @@
"show": true,
"title_case": false,
"type": "str",
"value": ""
"value": "OPENAI_API_KEY"
},
"code": {
"advanced": true,
@ -1445,9 +1343,7 @@
"display_name": "Input",
"dynamic": false,
"info": "",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1629,9 +1525,7 @@
"display_name": "System Message",
"dynamic": false,
"info": "System message to pass to the model.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1727,9 +1621,5 @@
"is_component": false,
"last_tested_version": "1.0.19.post2",
"name": "Document Q&A",
"tags": [
"rag",
"q-a",
"openai"
]
}
"tags": ["rag", "q-a", "openai"]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -9,16 +9,12 @@
"dataType": "StructuredOutputComponent",
"id": "StructuredOutputComponent-XYoUc",
"name": "structured_output",
"output_types": [
"Data"
]
"output_types": ["Data"]
},
"targetHandle": {
"fieldName": "data",
"id": "ParseData-HzweJ",
"inputTypes": [
"Data"
],
"inputTypes": ["Data"],
"type": "other"
}
},
@ -37,16 +33,12 @@
"dataType": "ParseData",
"id": "ParseData-HzweJ",
"name": "text",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "input_value",
"id": "ChatOutput-xQxLm",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -64,16 +56,12 @@
"dataType": "ChatInput",
"id": "ChatInput-rAWlE",
"name": "message",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "input_value",
"id": "OpenAIModel-cqeNw",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -91,16 +79,12 @@
"dataType": "Prompt",
"id": "Prompt-AzK6t",
"name": "prompt",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "system_message",
"id": "OpenAIModel-cqeNw",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -118,16 +102,12 @@
"dataType": "OpenAIModel",
"id": "OpenAIModel-cqeNw",
"name": "text_output",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "input_value",
"id": "StructuredOutputComponent-XYoUc",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -145,16 +125,12 @@
"dataType": "OpenAIModel",
"id": "OpenAIModel-cqeNw",
"name": "model_output",
"output_types": [
"LanguageModel"
]
"output_types": ["LanguageModel"]
},
"targetHandle": {
"fieldName": "llm",
"id": "StructuredOutputComponent-XYoUc",
"inputTypes": [
"LanguageModel"
],
"inputTypes": ["LanguageModel"],
"type": "other"
}
},
@ -173,9 +149,7 @@
"display_name": "Chat Input",
"id": "ChatInput-rAWlE",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -209,9 +183,7 @@
"name": "message",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -224,9 +196,7 @@
"display_name": "Background Color",
"dynamic": false,
"info": "The background color of the icon.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "background_color",
@ -246,9 +216,7 @@
"display_name": "Icon",
"dynamic": false,
"info": "The icon of the message.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "chat_icon",
@ -351,10 +319,7 @@
"dynamic": false,
"info": "Type of sender.",
"name": "sender",
"options": [
"Machine",
"User"
],
"options": ["Machine", "User"],
"placeholder": "",
"required": false,
"show": true,
@ -370,9 +335,7 @@
"display_name": "Sender Name",
"dynamic": false,
"info": "Name of the sender.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "sender_name",
@ -392,9 +355,7 @@
"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"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "session_id",
@ -430,9 +391,7 @@
"display_name": "Text Color",
"dynamic": false,
"info": "The text color of the name",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "text_color",
@ -476,9 +435,7 @@
"display_name": "Chat Output",
"id": "ChatOutput-xQxLm",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -512,9 +469,7 @@
"name": "message",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -527,9 +482,7 @@
"display_name": "Background Color",
"dynamic": false,
"info": "The background color of the icon.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "background_color",
@ -549,9 +502,7 @@
"display_name": "Icon",
"dynamic": false,
"info": "The icon of the message.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "chat_icon",
@ -589,9 +540,7 @@
"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"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "data_template",
@ -611,9 +560,7 @@
"display_name": "Text",
"dynamic": false,
"info": "Message to be passed as output.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "input_value",
@ -634,10 +581,7 @@
"dynamic": false,
"info": "Type of sender.",
"name": "sender",
"options": [
"Machine",
"User"
],
"options": ["Machine", "User"],
"placeholder": "",
"required": false,
"show": true,
@ -653,9 +597,7 @@
"display_name": "Sender Name",
"dynamic": false,
"info": "Name of the sender.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "sender_name",
@ -675,9 +617,7 @@
"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"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "session_id",
@ -713,9 +653,7 @@
"display_name": "Text Color",
"dynamic": false,
"info": "The text color of the name",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "text_color",
@ -794,9 +732,7 @@
"display_name": "Structured Output",
"id": "StructuredOutputComponent-XYoUc",
"node": {
"base_classes": [
"Data"
],
"base_classes": ["Data"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -824,9 +760,7 @@
"method": "build_structured_output",
"name": "structured_output",
"selected": "Data",
"types": [
"Data"
],
"types": ["Data"],
"value": "__UNDEFINED__"
}
],
@ -857,9 +791,7 @@
"display_name": "Input message",
"dynamic": false,
"info": "",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "input_value",
@ -879,9 +811,7 @@
"display_name": "Language Model",
"dynamic": false,
"info": "The language model to use to generate the structured output.",
"input_types": [
"LanguageModel"
],
"input_types": ["LanguageModel"],
"list": false,
"name": "llm",
"placeholder": "",
@ -1025,9 +955,7 @@
"data": {
"id": "ParseData-HzweJ",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -1035,16 +963,14 @@
"display_name": "Parse Data",
"documentation": "",
"edited": false,
"field_order": [
"data",
"template",
"sep"
],
"field_order": ["data", "template", "sep"],
"frozen": false,
"icon": "message-square",
"legacy": false,
"lf_version": "1.0.19.post2",
"metadata": {},
"metadata": {
"legacy_name": "Parse Data"
},
"output_types": [],
"outputs": [
{
@ -1055,9 +981,7 @@
"name": "text",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
},
{
@ -1068,9 +992,7 @@
"name": "data_list",
"selected": "Data",
"tool_mode": true,
"types": [
"Data"
],
"types": ["Data"],
"value": "__UNDEFINED__"
}
],
@ -1093,7 +1015,7 @@
"show": true,
"title_case": false,
"type": "code",
"value": "from langflow.custom import Component\nfrom langflow.helpers.data import data_to_text, data_to_text_list\nfrom langflow.io import DataInput, MultilineInput, Output, StrInput\nfrom langflow.schema import Data\nfrom langflow.schema.message import Message\n\n\nclass ParseDataComponent(Component):\n display_name = \"Data to Message\"\n description = \"Convert Data objects into Messages using any {field_name} from input data.\"\n icon = \"message-square\"\n name = \"ParseData\"\n\n inputs = [\n DataInput(name=\"data\", display_name=\"Data\", info=\"The data to convert to text.\", is_list=True, required=True),\n MultilineInput(\n name=\"template\",\n display_name=\"Template\",\n info=\"The template to use for formatting the data. \"\n \"It can contain the keys {text}, {data} or any other key in the Data.\",\n value=\"{text}\",\n required=True,\n ),\n StrInput(name=\"sep\", display_name=\"Separator\", advanced=True, value=\"\\n\"),\n ]\n\n outputs = [\n Output(\n display_name=\"Message\",\n name=\"text\",\n info=\"Data as a single Message, with each input Data separated by Separator\",\n method=\"parse_data\",\n ),\n Output(\n display_name=\"Data List\",\n name=\"data_list\",\n info=\"Data as a list of new Data, each having `text` formatted by Template\",\n method=\"parse_data_as_list\",\n ),\n ]\n\n def _clean_args(self) -> tuple[list[Data], str, str]:\n data = self.data if isinstance(self.data, list) else [self.data]\n template = self.template\n sep = self.sep\n return data, template, sep\n\n def parse_data(self) -> Message:\n data, template, sep = self._clean_args()\n result_string = data_to_text(template, data, sep)\n self.status = result_string\n return Message(text=result_string)\n\n def parse_data_as_list(self) -> list[Data]:\n data, template, _ = self._clean_args()\n text_list, data_list = data_to_text_list(template, data)\n for item, text in zip(data_list, text_list, strict=True):\n item.set_text(text)\n self.status = data_list\n return data_list\n"
"value": "from langflow.custom import Component\nfrom langflow.helpers.data import data_to_text, data_to_text_list\nfrom langflow.io import DataInput, MultilineInput, Output, StrInput\nfrom langflow.schema import Data\nfrom langflow.schema.message import Message\n\n\nclass ParseDataComponent(Component):\n display_name = \"Data to Message\"\n description = \"Convert Data objects into Messages using any {field_name} from input data.\"\n icon = \"message-square\"\n name = \"ParseData\"\n metadata = {\n \"legacy_name\": \"Parse Data\",\n }\n\n inputs = [\n DataInput(\n name=\"data\",\n display_name=\"Data\",\n info=\"The data to convert to text.\",\n is_list=True,\n required=True,\n ),\n MultilineInput(\n name=\"template\",\n display_name=\"Template\",\n info=\"The template to use for formatting the data. \"\n \"It can contain the keys {text}, {data} or any other key in the Data.\",\n value=\"{text}\",\n required=True,\n ),\n StrInput(name=\"sep\", display_name=\"Separator\", advanced=True, value=\"\\n\"),\n ]\n\n outputs = [\n Output(\n display_name=\"Message\",\n name=\"text\",\n info=\"Data as a single Message, with each input Data separated by Separator\",\n method=\"parse_data\",\n ),\n Output(\n display_name=\"Data List\",\n name=\"data_list\",\n info=\"Data as a list of new Data, each having `text` formatted by Template\",\n method=\"parse_data_as_list\",\n ),\n ]\n\n def _clean_args(self) -> tuple[list[Data], str, str]:\n data = self.data if isinstance(self.data, list) else [self.data]\n template = self.template\n sep = self.sep\n return data, template, sep\n\n def parse_data(self) -> Message:\n data, template, sep = self._clean_args()\n result_string = data_to_text(template, data, sep)\n self.status = result_string\n return Message(text=result_string)\n\n def parse_data_as_list(self) -> list[Data]:\n data, template, _ = self._clean_args()\n text_list, data_list = data_to_text_list(template, data)\n for item, text in zip(data_list, text_list, strict=True):\n item.set_text(text)\n self.status = data_list\n return data_list\n"
},
"data": {
"_input_type": "DataInput",
@ -1101,9 +1023,7 @@
"display_name": "Data",
"dynamic": false,
"info": "The data to convert to text.",
"input_types": [
"Data"
],
"input_types": ["Data"],
"list": true,
"name": "data",
"placeholder": "",
@ -1139,9 +1059,7 @@
"display_name": "Template",
"dynamic": false,
"info": "The template to use for formatting the data. It can contain the keys {text}, {data} or any other key in the Data.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"multiline": true,
@ -1186,9 +1104,7 @@
"display_name": "Prompt",
"id": "Prompt-AzK6t",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {
@ -1198,9 +1114,7 @@
"display_name": "Prompt",
"documentation": "",
"edited": false,
"field_order": [
"template"
],
"field_order": ["template"],
"frozen": false,
"icon": "prompts",
"legacy": false,
@ -1216,9 +1130,7 @@
"name": "prompt",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -1267,9 +1179,7 @@
"display_name": "Tool Placeholder",
"dynamic": false,
"info": "A placeholder input for tool mode.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "tool_placeholder",
@ -1311,10 +1221,7 @@
"data": {
"id": "OpenAIModel-cqeNw",
"node": {
"base_classes": [
"LanguageModel",
"Message"
],
"base_classes": ["LanguageModel", "Message"],
"beta": false,
"category": "models",
"conditional_paths": [],
@ -1353,9 +1260,7 @@
"required_inputs": [],
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
},
{
@ -1364,14 +1269,10 @@
"display_name": "Language Model",
"method": "build_model",
"name": "model_output",
"required_inputs": [
"api_key"
],
"required_inputs": ["api_key"],
"selected": "LanguageModel",
"tool_mode": true,
"types": [
"LanguageModel"
],
"types": ["LanguageModel"],
"value": "__UNDEFINED__"
}
],
@ -1385,9 +1286,7 @@
"display_name": "OpenAI API Key",
"dynamic": false,
"info": "The OpenAI API Key to use for the OpenAI model.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"load_from_db": true,
"name": "api_key",
"password": true,
@ -1396,7 +1295,7 @@
"show": true,
"title_case": false,
"type": "str",
"value": ""
"value": "OPENAI_API_KEY"
},
"code": {
"advanced": true,
@ -1422,9 +1321,7 @@
"display_name": "Input",
"dynamic": false,
"info": "",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1606,9 +1503,7 @@
"display_name": "System Message",
"dynamic": false,
"info": "System message to pass to the model.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1704,7 +1599,5 @@
"is_component": false,
"last_tested_version": "1.0.19.post2",
"name": "Image Sentiment Analysis",
"tags": [
"classification"
]
}
"tags": ["classification"]
}

File diff suppressed because one or more lines are too long

View File

@ -9,16 +9,12 @@
"dataType": "ArXivComponent",
"id": "ArXivComponent-LChQN",
"name": "papers",
"output_types": [
"Data"
]
"output_types": ["Data"]
},
"targetHandle": {
"fieldName": "data",
"id": "LoopComponent-3vpc1",
"inputTypes": [
"Data"
],
"inputTypes": ["Data"],
"type": "other"
}
},
@ -36,16 +32,12 @@
"dataType": "LoopComponent",
"id": "LoopComponent-3vpc1",
"name": "item",
"output_types": [
"Data"
]
"output_types": ["Data"]
},
"targetHandle": {
"fieldName": "data",
"id": "ParseData-Pf12J",
"inputTypes": [
"Data"
],
"inputTypes": ["Data"],
"type": "other"
}
},
@ -63,16 +55,12 @@
"dataType": "ParseData",
"id": "ParseData-Pf12J",
"name": "text",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "input_value",
"id": "AnthropicModel-beO6B",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -90,16 +78,12 @@
"dataType": "AnthropicModel",
"id": "AnthropicModel-beO6B",
"name": "text_output",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "message",
"id": "MessagetoData-QRSBb",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -117,17 +101,13 @@
"dataType": "MessagetoData",
"id": "MessagetoData-QRSBb",
"name": "data",
"output_types": [
"Data"
]
"output_types": ["Data"]
},
"targetHandle": {
"dataType": "LoopComponent",
"id": "LoopComponent-3vpc1",
"name": "item",
"output_types": [
"Data"
]
"output_types": ["Data"]
}
},
"id": "xy-edge__MessagetoData-QRSBb{œdataTypeœ:œMessagetoDataœ,œidœ:œMessagetoData-QRSBbœ,œnameœ:œdataœ,œoutput_typesœ:[œDataœ]}-LoopComponent-3vpc1{œdataTypeœ:œLoopComponentœ,œidœ:œLoopComponent-3vpc1œ,œnameœ:œitemœ,œoutput_typesœ:[œDataœ]}",
@ -144,16 +124,12 @@
"dataType": "LoopComponent",
"id": "LoopComponent-3vpc1",
"name": "done",
"output_types": [
"Data"
]
"output_types": ["Data"]
},
"targetHandle": {
"fieldName": "data",
"id": "ParseData-igEkj",
"inputTypes": [
"Data"
],
"inputTypes": ["Data"],
"type": "other"
}
},
@ -171,16 +147,12 @@
"dataType": "ParseData",
"id": "ParseData-igEkj",
"name": "text",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "input_value",
"id": "ChatOutput-UZgon",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -198,16 +170,12 @@
"dataType": "ChatInput",
"id": "ChatInput-m10vc",
"name": "message",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "search_query",
"id": "ArXivComponent-LChQN",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -223,9 +191,7 @@
"data": {
"id": "ArXivComponent-LChQN",
"node": {
"base_classes": [
"Data"
],
"base_classes": ["Data"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -233,11 +199,7 @@
"display_name": "arXiv",
"documentation": "",
"edited": false,
"field_order": [
"search_query",
"search_type",
"max_results"
],
"field_order": ["search_query", "search_type", "max_results"],
"frozen": false,
"icon": "arXiv",
"legacy": false,
@ -254,9 +216,7 @@
"name": "papers",
"selected": "Data",
"tool_mode": true,
"types": [
"Data"
],
"types": ["Data"],
"value": "__UNDEFINED__"
}
],
@ -305,9 +265,7 @@
"display_name": "Search Query",
"dynamic": false,
"info": "The search query for arXiv papers (e.g., 'quantum computing')",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -331,13 +289,7 @@
"dynamic": false,
"info": "The field to search in",
"name": "search_type",
"options": [
"all",
"title",
"abstract",
"author",
"cat"
],
"options": ["all", "title", "abstract", "author", "cat"],
"options_metadata": [],
"placeholder": "",
"required": false,
@ -371,9 +323,7 @@
"data": {
"id": "LoopComponent-3vpc1",
"node": {
"base_classes": [
"Data"
],
"base_classes": ["Data"],
"beta": false,
"category": "logic",
"conditional_paths": [],
@ -382,9 +332,7 @@
"display_name": "Loop",
"documentation": "",
"edited": false,
"field_order": [
"data"
],
"field_order": ["data"],
"frozen": false,
"icon": "infinity",
"key": "LoopComponent",
@ -402,9 +350,7 @@
"name": "item",
"selected": "Data",
"tool_mode": true,
"types": [
"Data"
],
"types": ["Data"],
"value": "__UNDEFINED__"
},
{
@ -415,9 +361,7 @@
"name": "done",
"selected": "Data",
"tool_mode": true,
"types": [
"Data"
],
"types": ["Data"],
"value": "__UNDEFINED__"
}
],
@ -449,9 +393,7 @@
"display_name": "Data",
"dynamic": false,
"info": "The initial list of Data objects to iterate over.",
"input_types": [
"Data"
],
"input_types": ["Data"],
"list": false,
"list_add_label": "Add More",
"name": "data",
@ -488,10 +430,7 @@
"data": {
"id": "ParseData-Pf12J",
"node": {
"base_classes": [
"Data",
"Message"
],
"base_classes": ["Data", "Message"],
"beta": false,
"category": "processing",
"conditional_paths": [],
@ -500,17 +439,15 @@
"display_name": "Data to Message",
"documentation": "",
"edited": false,
"field_order": [
"data",
"template",
"sep"
],
"field_order": ["data", "template", "sep"],
"frozen": false,
"icon": "message-square",
"key": "ParseData",
"legacy": false,
"lf_version": "1.1.5",
"metadata": {},
"metadata": {
"legacy_name": "Parse Data"
},
"minimized": false,
"output_types": [],
"outputs": [
@ -522,9 +459,7 @@
"name": "text",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
},
{
@ -535,9 +470,7 @@
"name": "data_list",
"selected": "Data",
"tool_mode": true,
"types": [
"Data"
],
"types": ["Data"],
"value": "__UNDEFINED__"
}
],
@ -561,7 +494,7 @@
"show": true,
"title_case": false,
"type": "code",
"value": "from langflow.custom import Component\nfrom langflow.helpers.data import data_to_text, data_to_text_list\nfrom langflow.io import DataInput, MultilineInput, Output, StrInput\nfrom langflow.schema import Data\nfrom langflow.schema.message import Message\n\n\nclass ParseDataComponent(Component):\n display_name = \"Data to Message\"\n description = \"Convert Data objects into Messages using any {field_name} from input data.\"\n icon = \"message-square\"\n name = \"ParseData\"\n\n inputs = [\n DataInput(name=\"data\", display_name=\"Data\", info=\"The data to convert to text.\", is_list=True, required=True),\n MultilineInput(\n name=\"template\",\n display_name=\"Template\",\n info=\"The template to use for formatting the data. \"\n \"It can contain the keys {text}, {data} or any other key in the Data.\",\n value=\"{text}\",\n required=True,\n ),\n StrInput(name=\"sep\", display_name=\"Separator\", advanced=True, value=\"\\n\"),\n ]\n\n outputs = [\n Output(\n display_name=\"Message\",\n name=\"text\",\n info=\"Data as a single Message, with each input Data separated by Separator\",\n method=\"parse_data\",\n ),\n Output(\n display_name=\"Data List\",\n name=\"data_list\",\n info=\"Data as a list of new Data, each having `text` formatted by Template\",\n method=\"parse_data_as_list\",\n ),\n ]\n\n def _clean_args(self) -> tuple[list[Data], str, str]:\n data = self.data if isinstance(self.data, list) else [self.data]\n template = self.template\n sep = self.sep\n return data, template, sep\n\n def parse_data(self) -> Message:\n data, template, sep = self._clean_args()\n result_string = data_to_text(template, data, sep)\n self.status = result_string\n return Message(text=result_string)\n\n def parse_data_as_list(self) -> list[Data]:\n data, template, _ = self._clean_args()\n text_list, data_list = data_to_text_list(template, data)\n for item, text in zip(data_list, text_list, strict=True):\n item.set_text(text)\n self.status = data_list\n return data_list\n"
"value": "from langflow.custom import Component\nfrom langflow.helpers.data import data_to_text, data_to_text_list\nfrom langflow.io import DataInput, MultilineInput, Output, StrInput\nfrom langflow.schema import Data\nfrom langflow.schema.message import Message\n\n\nclass ParseDataComponent(Component):\n display_name = \"Data to Message\"\n description = \"Convert Data objects into Messages using any {field_name} from input data.\"\n icon = \"message-square\"\n name = \"ParseData\"\n metadata = {\n \"legacy_name\": \"Parse Data\",\n }\n\n inputs = [\n DataInput(\n name=\"data\",\n display_name=\"Data\",\n info=\"The data to convert to text.\",\n is_list=True,\n required=True,\n ),\n MultilineInput(\n name=\"template\",\n display_name=\"Template\",\n info=\"The template to use for formatting the data. \"\n \"It can contain the keys {text}, {data} or any other key in the Data.\",\n value=\"{text}\",\n required=True,\n ),\n StrInput(name=\"sep\", display_name=\"Separator\", advanced=True, value=\"\\n\"),\n ]\n\n outputs = [\n Output(\n display_name=\"Message\",\n name=\"text\",\n info=\"Data as a single Message, with each input Data separated by Separator\",\n method=\"parse_data\",\n ),\n Output(\n display_name=\"Data List\",\n name=\"data_list\",\n info=\"Data as a list of new Data, each having `text` formatted by Template\",\n method=\"parse_data_as_list\",\n ),\n ]\n\n def _clean_args(self) -> tuple[list[Data], str, str]:\n data = self.data if isinstance(self.data, list) else [self.data]\n template = self.template\n sep = self.sep\n return data, template, sep\n\n def parse_data(self) -> Message:\n data, template, sep = self._clean_args()\n result_string = data_to_text(template, data, sep)\n self.status = result_string\n return Message(text=result_string)\n\n def parse_data_as_list(self) -> list[Data]:\n data, template, _ = self._clean_args()\n text_list, data_list = data_to_text_list(template, data)\n for item, text in zip(data_list, text_list, strict=True):\n item.set_text(text)\n self.status = data_list\n return data_list\n"
},
"data": {
"_input_type": "DataInput",
@ -569,9 +502,7 @@
"display_name": "Data",
"dynamic": false,
"info": "The data to convert to text.",
"input_types": [
"Data"
],
"input_types": ["Data"],
"list": true,
"list_add_label": "Add More",
"name": "data",
@ -610,9 +541,7 @@
"display_name": "Template",
"dynamic": false,
"info": "The template to use for formatting the data. It can contain the keys {text}, {data} or any other key in the Data.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -651,10 +580,7 @@
"data": {
"id": "AnthropicModel-beO6B",
"node": {
"base_classes": [
"LanguageModel",
"Message"
],
"base_classes": ["LanguageModel", "Message"],
"beta": false,
"category": "models",
"conditional_paths": [],
@ -693,9 +619,7 @@
"required_inputs": [],
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
},
{
@ -704,14 +628,10 @@
"display_name": "Language Model",
"method": "build_model",
"name": "model_output",
"required_inputs": [
"api_key"
],
"required_inputs": ["api_key"],
"selected": "LanguageModel",
"tool_mode": true,
"types": [
"LanguageModel"
],
"types": ["LanguageModel"],
"value": "__UNDEFINED__"
}
],
@ -725,9 +645,7 @@
"display_name": "Anthropic API Key",
"dynamic": false,
"info": "Your Anthropic API key.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"load_from_db": true,
"name": "api_key",
"password": true,
@ -737,7 +655,7 @@
"show": true,
"title_case": false,
"type": "str",
"value": ""
"value": "ANTHROPIC_API_KEY"
},
"base_url": {
"_input_type": "MessageTextInput",
@ -745,9 +663,7 @@
"display_name": "Anthropic API URL",
"dynamic": false,
"info": "Endpoint of the Anthropic API. Defaults to 'https://api.anthropic.com' if not specified.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -787,9 +703,7 @@
"display_name": "Input",
"dynamic": false,
"info": "",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -858,9 +772,7 @@
"display_name": "Prefill",
"dynamic": false,
"info": "Prefill text to guide the model's response.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -899,9 +811,7 @@
"display_name": "System Message",
"dynamic": false,
"info": "System message to pass to the model.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -987,9 +897,7 @@
"data": {
"id": "MessagetoData-QRSBb",
"node": {
"base_classes": [
"Data"
],
"base_classes": ["Data"],
"beta": true,
"category": "processing",
"conditional_paths": [],
@ -998,9 +906,7 @@
"display_name": "Message to Data",
"documentation": "",
"edited": false,
"field_order": [
"message"
],
"field_order": ["message"],
"frozen": false,
"icon": "message-square-share",
"key": "MessagetoData",
@ -1018,9 +924,7 @@
"name": "data",
"selected": "Data",
"tool_mode": true,
"types": [
"Data"
],
"types": ["Data"],
"value": "__UNDEFINED__"
}
],
@ -1052,9 +956,7 @@
"display_name": "Message",
"dynamic": false,
"info": "The Message object to convert to a Data object",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1092,10 +994,7 @@
"data": {
"id": "ParseData-igEkj",
"node": {
"base_classes": [
"Data",
"Message"
],
"base_classes": ["Data", "Message"],
"beta": false,
"category": "processing",
"conditional_paths": [],
@ -1104,17 +1003,15 @@
"display_name": "Data to Message",
"documentation": "",
"edited": false,
"field_order": [
"data",
"template",
"sep"
],
"field_order": ["data", "template", "sep"],
"frozen": false,
"icon": "message-square",
"key": "ParseData",
"legacy": false,
"lf_version": "1.1.5",
"metadata": {},
"metadata": {
"legacy_name": "Parse Data"
},
"minimized": false,
"output_types": [],
"outputs": [
@ -1126,9 +1023,7 @@
"name": "text",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
},
{
@ -1139,9 +1034,7 @@
"name": "data_list",
"selected": "Data",
"tool_mode": true,
"types": [
"Data"
],
"types": ["Data"],
"value": "__UNDEFINED__"
}
],
@ -1165,7 +1058,7 @@
"show": true,
"title_case": false,
"type": "code",
"value": "from langflow.custom import Component\nfrom langflow.helpers.data import data_to_text, data_to_text_list\nfrom langflow.io import DataInput, MultilineInput, Output, StrInput\nfrom langflow.schema import Data\nfrom langflow.schema.message import Message\n\n\nclass ParseDataComponent(Component):\n display_name = \"Data to Message\"\n description = \"Convert Data objects into Messages using any {field_name} from input data.\"\n icon = \"message-square\"\n name = \"ParseData\"\n\n inputs = [\n DataInput(name=\"data\", display_name=\"Data\", info=\"The data to convert to text.\", is_list=True, required=True),\n MultilineInput(\n name=\"template\",\n display_name=\"Template\",\n info=\"The template to use for formatting the data. \"\n \"It can contain the keys {text}, {data} or any other key in the Data.\",\n value=\"{text}\",\n required=True,\n ),\n StrInput(name=\"sep\", display_name=\"Separator\", advanced=True, value=\"\\n\"),\n ]\n\n outputs = [\n Output(\n display_name=\"Message\",\n name=\"text\",\n info=\"Data as a single Message, with each input Data separated by Separator\",\n method=\"parse_data\",\n ),\n Output(\n display_name=\"Data List\",\n name=\"data_list\",\n info=\"Data as a list of new Data, each having `text` formatted by Template\",\n method=\"parse_data_as_list\",\n ),\n ]\n\n def _clean_args(self) -> tuple[list[Data], str, str]:\n data = self.data if isinstance(self.data, list) else [self.data]\n template = self.template\n sep = self.sep\n return data, template, sep\n\n def parse_data(self) -> Message:\n data, template, sep = self._clean_args()\n result_string = data_to_text(template, data, sep)\n self.status = result_string\n return Message(text=result_string)\n\n def parse_data_as_list(self) -> list[Data]:\n data, template, _ = self._clean_args()\n text_list, data_list = data_to_text_list(template, data)\n for item, text in zip(data_list, text_list, strict=True):\n item.set_text(text)\n self.status = data_list\n return data_list\n"
"value": "from langflow.custom import Component\nfrom langflow.helpers.data import data_to_text, data_to_text_list\nfrom langflow.io import DataInput, MultilineInput, Output, StrInput\nfrom langflow.schema import Data\nfrom langflow.schema.message import Message\n\n\nclass ParseDataComponent(Component):\n display_name = \"Data to Message\"\n description = \"Convert Data objects into Messages using any {field_name} from input data.\"\n icon = \"message-square\"\n name = \"ParseData\"\n metadata = {\n \"legacy_name\": \"Parse Data\",\n }\n\n inputs = [\n DataInput(\n name=\"data\",\n display_name=\"Data\",\n info=\"The data to convert to text.\",\n is_list=True,\n required=True,\n ),\n MultilineInput(\n name=\"template\",\n display_name=\"Template\",\n info=\"The template to use for formatting the data. \"\n \"It can contain the keys {text}, {data} or any other key in the Data.\",\n value=\"{text}\",\n required=True,\n ),\n StrInput(name=\"sep\", display_name=\"Separator\", advanced=True, value=\"\\n\"),\n ]\n\n outputs = [\n Output(\n display_name=\"Message\",\n name=\"text\",\n info=\"Data as a single Message, with each input Data separated by Separator\",\n method=\"parse_data\",\n ),\n Output(\n display_name=\"Data List\",\n name=\"data_list\",\n info=\"Data as a list of new Data, each having `text` formatted by Template\",\n method=\"parse_data_as_list\",\n ),\n ]\n\n def _clean_args(self) -> tuple[list[Data], str, str]:\n data = self.data if isinstance(self.data, list) else [self.data]\n template = self.template\n sep = self.sep\n return data, template, sep\n\n def parse_data(self) -> Message:\n data, template, sep = self._clean_args()\n result_string = data_to_text(template, data, sep)\n self.status = result_string\n return Message(text=result_string)\n\n def parse_data_as_list(self) -> list[Data]:\n data, template, _ = self._clean_args()\n text_list, data_list = data_to_text_list(template, data)\n for item, text in zip(data_list, text_list, strict=True):\n item.set_text(text)\n self.status = data_list\n return data_list\n"
},
"data": {
"_input_type": "DataInput",
@ -1173,9 +1066,7 @@
"display_name": "Data",
"dynamic": false,
"info": "The data to convert to text.",
"input_types": [
"Data"
],
"input_types": ["Data"],
"list": true,
"list_add_label": "Add More",
"name": "data",
@ -1214,9 +1105,7 @@
"display_name": "Template",
"dynamic": false,
"info": "The template to use for formatting the data. It can contain the keys {text}, {data} or any other key in the Data.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1255,9 +1144,7 @@
"data": {
"id": "ChatOutput-UZgon",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -1292,9 +1179,7 @@
"name": "message",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -1307,9 +1192,7 @@
"display_name": "Background Color",
"dynamic": false,
"info": "The background color of the icon.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1330,9 +1213,7 @@
"display_name": "Icon",
"dynamic": false,
"info": "The icon of the message.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1371,9 +1252,7 @@
"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"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1394,9 +1273,7 @@
"display_name": "Text",
"dynamic": false,
"info": "Message to be passed as output.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1420,10 +1297,7 @@
"dynamic": false,
"info": "Type of sender.",
"name": "sender",
"options": [
"Machine",
"User"
],
"options": ["Machine", "User"],
"options_metadata": [],
"placeholder": "",
"required": false,
@ -1440,9 +1314,7 @@
"display_name": "Sender Name",
"dynamic": false,
"info": "Name of the sender.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1463,9 +1335,7 @@
"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"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1504,9 +1374,7 @@
"display_name": "Text Color",
"dynamic": false,
"info": "The text color of the name",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1544,9 +1412,7 @@
"data": {
"id": "ChatInput-m10vc",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -1581,9 +1447,7 @@
"name": "message",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -1596,9 +1460,7 @@
"display_name": "Background Color",
"dynamic": false,
"info": "The background color of the icon.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1619,9 +1481,7 @@
"display_name": "Icon",
"dynamic": false,
"info": "The icon of the message.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1728,10 +1588,7 @@
"dynamic": false,
"info": "Type of sender.",
"name": "sender",
"options": [
"Machine",
"User"
],
"options": ["Machine", "User"],
"options_metadata": [],
"placeholder": "",
"required": false,
@ -1748,9 +1605,7 @@
"display_name": "Sender Name",
"dynamic": false,
"info": "Name of the sender.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1771,9 +1626,7 @@
"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"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1812,9 +1665,7 @@
"display_name": "Text Color",
"dynamic": false,
"info": "The text color of the name",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1919,8 +1770,5 @@
"is_component": false,
"last_tested_version": "1.1.5",
"name": "Research Translation Loop",
"tags": [
"chatbots",
"content-generation"
]
}
"tags": ["chatbots", "content-generation"]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -8,17 +8,12 @@
"dataType": "Memory",
"id": "Memory-gWJrq",
"name": "messages_text",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "memory",
"id": "Prompt-yhdMP",
"inputTypes": [
"Message",
"Text"
],
"inputTypes": ["Message", "Text"],
"type": "str"
}
},
@ -36,16 +31,12 @@
"dataType": "ChatInput",
"id": "ChatInput-PEO9d",
"name": "message",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "input_value",
"id": "OpenAIModel-63o3Q",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -63,16 +54,12 @@
"dataType": "Prompt",
"id": "Prompt-yhdMP",
"name": "prompt",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "system_message",
"id": "OpenAIModel-63o3Q",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -90,16 +77,12 @@
"dataType": "OpenAIModel",
"id": "OpenAIModel-63o3Q",
"name": "text_output",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "input_value",
"id": "ChatOutput-BIXzI",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -116,9 +99,7 @@
"data": {
"id": "ChatInput-PEO9d",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -152,9 +133,7 @@
"name": "message",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -167,9 +146,7 @@
"display_name": "Background Color",
"dynamic": false,
"info": "The background color of the icon.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "background_color",
@ -189,9 +166,7 @@
"display_name": "Icon",
"dynamic": false,
"info": "The icon of the message.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "chat_icon",
@ -294,10 +269,7 @@
"dynamic": false,
"info": "Type of sender.",
"name": "sender",
"options": [
"Machine",
"User"
],
"options": ["Machine", "User"],
"placeholder": "",
"required": false,
"show": true,
@ -313,9 +285,7 @@
"display_name": "Sender Name",
"dynamic": false,
"info": "Name of the sender.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "sender_name",
@ -335,9 +305,7 @@
"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"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "session_id",
@ -373,9 +341,7 @@
"display_name": "Text Color",
"dynamic": false,
"info": "The text color of the name",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "text_color",
@ -419,9 +385,7 @@
"display_name": "Chat Output",
"id": "ChatOutput-BIXzI",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -455,9 +419,7 @@
"name": "message",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -470,9 +432,7 @@
"display_name": "Background Color",
"dynamic": false,
"info": "The background color of the icon.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "background_color",
@ -492,9 +452,7 @@
"display_name": "Icon",
"dynamic": false,
"info": "The icon of the message.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "chat_icon",
@ -532,9 +490,7 @@
"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"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "data_template",
@ -554,9 +510,7 @@
"display_name": "Text",
"dynamic": false,
"info": "Message to be passed as output.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "input_value",
@ -577,10 +531,7 @@
"dynamic": false,
"info": "Type of sender.",
"name": "sender",
"options": [
"Machine",
"User"
],
"options": ["Machine", "User"],
"placeholder": "",
"required": false,
"show": true,
@ -596,9 +547,7 @@
"display_name": "Sender Name",
"dynamic": false,
"info": "Name of the sender.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "sender_name",
@ -618,9 +567,7 @@
"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"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "session_id",
@ -656,9 +603,7 @@
"display_name": "Text Color",
"dynamic": false,
"info": "The text color of the name",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "text_color",
@ -767,10 +712,7 @@
"data": {
"id": "Memory-gWJrq",
"node": {
"base_classes": [
"Data",
"Message"
],
"base_classes": ["Data", "Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -802,9 +744,7 @@
"name": "messages",
"selected": "Data",
"tool_mode": true,
"types": [
"Data"
],
"types": ["Data"],
"value": "__UNDEFINED__"
},
{
@ -815,9 +755,7 @@
"name": "messages_text",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -848,9 +786,7 @@
"display_name": "External Memory",
"dynamic": false,
"info": "Retrieve messages from an external memory. If empty, it will use the Langflow tables.",
"input_types": [
"Memory"
],
"input_types": ["Memory"],
"list": false,
"name": "memory",
"placeholder": "",
@ -885,10 +821,7 @@
"dynamic": false,
"info": "Order of the messages.",
"name": "order",
"options": [
"Ascending",
"Descending"
],
"options": ["Ascending", "Descending"],
"placeholder": "",
"required": false,
"show": true,
@ -906,11 +839,7 @@
"dynamic": false,
"info": "Filter by sender type.",
"name": "sender",
"options": [
"Machine",
"User",
"Machine and User"
],
"options": ["Machine", "User", "Machine and User"],
"placeholder": "",
"required": false,
"show": true,
@ -926,9 +855,7 @@
"display_name": "Sender Name",
"dynamic": false,
"info": "Filter by sender name.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "sender_name",
@ -948,9 +875,7 @@
"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"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "session_id",
@ -970,9 +895,7 @@
"display_name": "Template",
"dynamic": false,
"info": "The template to use for formatting the data. It can contain the keys {text}, {sender} or any other key in the message data.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"multiline": true,
@ -1015,24 +938,18 @@
"data": {
"id": "Prompt-yhdMP",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {
"template": [
"memory"
]
"template": ["memory"]
},
"description": "Create a prompt template with dynamic variables.",
"display_name": "Prompt",
"documentation": "",
"edited": false,
"error": null,
"field_order": [
"template"
],
"field_order": ["template"],
"frozen": false,
"full_path": null,
"icon": "prompts",
@ -1053,9 +970,7 @@
"name": "prompt",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -1088,10 +1003,7 @@
"fileTypes": [],
"file_path": "",
"info": "",
"input_types": [
"Message",
"Text"
],
"input_types": ["Message", "Text"],
"list": false,
"load_from_db": false,
"multiline": true,
@ -1126,9 +1038,7 @@
"display_name": "Tool Placeholder",
"dynamic": false,
"info": "A placeholder input for tool mode.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "tool_placeholder",
@ -1170,10 +1080,7 @@
"data": {
"id": "OpenAIModel-63o3Q",
"node": {
"base_classes": [
"LanguageModel",
"Message"
],
"base_classes": ["LanguageModel", "Message"],
"beta": false,
"category": "models",
"conditional_paths": [],
@ -1212,9 +1119,7 @@
"required_inputs": [],
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
},
{
@ -1223,14 +1128,10 @@
"display_name": "Language Model",
"method": "build_model",
"name": "model_output",
"required_inputs": [
"api_key"
],
"required_inputs": ["api_key"],
"selected": "LanguageModel",
"tool_mode": true,
"types": [
"LanguageModel"
],
"types": ["LanguageModel"],
"value": "__UNDEFINED__"
}
],
@ -1244,9 +1145,7 @@
"display_name": "OpenAI API Key",
"dynamic": false,
"info": "The OpenAI API Key to use for the OpenAI model.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"load_from_db": true,
"name": "api_key",
"password": true,
@ -1255,7 +1154,7 @@
"show": true,
"title_case": false,
"type": "str",
"value": ""
"value": "OPENAI_API_KEY"
},
"code": {
"advanced": true,
@ -1281,9 +1180,7 @@
"display_name": "Input",
"dynamic": false,
"info": "",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1465,9 +1362,7 @@
"display_name": "System Message",
"dynamic": false,
"info": "System message to pass to the model.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1563,9 +1458,5 @@
"is_component": false,
"last_tested_version": "1.0.19.post2",
"name": "Memory Chatbot",
"tags": [
"chatbots",
"openai",
"assistants"
]
}
"tags": ["chatbots", "openai", "assistants"]
}

File diff suppressed because one or more lines are too long

View File

@ -9,16 +9,12 @@
"dataType": "Prompt",
"id": "Prompt-ysecC",
"name": "prompt",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "input_value",
"id": "StructuredOutput-TArXO",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -36,16 +32,12 @@
"dataType": "StructuredOutput",
"id": "StructuredOutput-TArXO",
"name": "structured_output",
"output_types": [
"Data"
]
"output_types": ["Data"]
},
"targetHandle": {
"fieldName": "data",
"id": "ParseData-x7Rgx",
"inputTypes": [
"Data"
],
"inputTypes": ["Data"],
"type": "other"
}
},
@ -63,16 +55,12 @@
"dataType": "ParseData",
"id": "ParseData-sGhWo",
"name": "text",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "resume",
"id": "Prompt-ysecC",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -90,16 +78,12 @@
"dataType": "AnthropicModel",
"id": "AnthropicModel-sHFTc",
"name": "model_output",
"output_types": [
"LanguageModel"
]
"output_types": ["LanguageModel"]
},
"targetHandle": {
"fieldName": "llm",
"id": "StructuredOutput-TArXO",
"inputTypes": [
"LanguageModel"
],
"inputTypes": ["LanguageModel"],
"type": "other"
}
},
@ -117,16 +101,12 @@
"dataType": "ParseData",
"id": "ParseData-x7Rgx",
"name": "text",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "input_value",
"id": "AnthropicModel-IrjAe",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -144,16 +124,12 @@
"dataType": "TextInput",
"id": "TextInput-CPTOR",
"name": "text",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "system_message",
"id": "AnthropicModel-IrjAe",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -171,16 +147,12 @@
"dataType": "AnthropicModel",
"id": "AnthropicModel-IrjAe",
"name": "text_output",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "input_value",
"id": "ChatOutput-d1tmJ",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -197,16 +169,12 @@
"dataType": "File",
"id": "File-JgIx7",
"name": "data",
"output_types": [
"Data"
]
"output_types": ["Data"]
},
"targetHandle": {
"fieldName": "data",
"id": "ParseData-sGhWo",
"inputTypes": [
"Data"
],
"inputTypes": ["Data"],
"type": "other"
}
},
@ -222,25 +190,18 @@
"data": {
"id": "Prompt-ysecC",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {
"template": [
"resume"
]
"template": ["resume"]
},
"description": "Create a prompt template with dynamic variables.",
"display_name": "Prompt",
"documentation": "",
"edited": false,
"error": null,
"field_order": [
"template",
"tool_placeholder"
],
"field_order": ["template", "tool_placeholder"],
"frozen": false,
"full_path": null,
"icon": "prompts",
@ -262,9 +223,7 @@
"name": "prompt",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -297,9 +256,7 @@
"fileTypes": [],
"file_path": "",
"info": "",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"multiline": true,
@ -335,9 +292,7 @@
"display_name": "Tool Placeholder",
"dynamic": false,
"info": "A placeholder input for tool mode.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -375,10 +330,7 @@
"data": {
"id": "ParseData-sGhWo",
"node": {
"base_classes": [
"Data",
"Message"
],
"base_classes": ["Data", "Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -386,16 +338,14 @@
"display_name": "Data to Message",
"documentation": "",
"edited": false,
"field_order": [
"data",
"template",
"sep"
],
"field_order": ["data", "template", "sep"],
"frozen": false,
"icon": "message-square",
"legacy": false,
"lf_version": "1.1.4.post1",
"metadata": {},
"metadata": {
"legacy_name": "Parse Data"
},
"minimized": false,
"output_types": [],
"outputs": [
@ -407,9 +357,7 @@
"name": "text",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
},
{
@ -420,9 +368,7 @@
"name": "data_list",
"selected": "Data",
"tool_mode": true,
"types": [
"Data"
],
"types": ["Data"],
"value": "__UNDEFINED__"
}
],
@ -445,7 +391,7 @@
"show": true,
"title_case": false,
"type": "code",
"value": "from langflow.custom import Component\nfrom langflow.helpers.data import data_to_text, data_to_text_list\nfrom langflow.io import DataInput, MultilineInput, Output, StrInput\nfrom langflow.schema import Data\nfrom langflow.schema.message import Message\n\n\nclass ParseDataComponent(Component):\n display_name = \"Data to Message\"\n description = \"Convert Data objects into Messages using any {field_name} from input data.\"\n icon = \"message-square\"\n name = \"ParseData\"\n\n inputs = [\n DataInput(name=\"data\", display_name=\"Data\", info=\"The data to convert to text.\", is_list=True, required=True),\n MultilineInput(\n name=\"template\",\n display_name=\"Template\",\n info=\"The template to use for formatting the data. \"\n \"It can contain the keys {text}, {data} or any other key in the Data.\",\n value=\"{text}\",\n required=True,\n ),\n StrInput(name=\"sep\", display_name=\"Separator\", advanced=True, value=\"\\n\"),\n ]\n\n outputs = [\n Output(\n display_name=\"Message\",\n name=\"text\",\n info=\"Data as a single Message, with each input Data separated by Separator\",\n method=\"parse_data\",\n ),\n Output(\n display_name=\"Data List\",\n name=\"data_list\",\n info=\"Data as a list of new Data, each having `text` formatted by Template\",\n method=\"parse_data_as_list\",\n ),\n ]\n\n def _clean_args(self) -> tuple[list[Data], str, str]:\n data = self.data if isinstance(self.data, list) else [self.data]\n template = self.template\n sep = self.sep\n return data, template, sep\n\n def parse_data(self) -> Message:\n data, template, sep = self._clean_args()\n result_string = data_to_text(template, data, sep)\n self.status = result_string\n return Message(text=result_string)\n\n def parse_data_as_list(self) -> list[Data]:\n data, template, _ = self._clean_args()\n text_list, data_list = data_to_text_list(template, data)\n for item, text in zip(data_list, text_list, strict=True):\n item.set_text(text)\n self.status = data_list\n return data_list\n"
"value": "from langflow.custom import Component\nfrom langflow.helpers.data import data_to_text, data_to_text_list\nfrom langflow.io import DataInput, MultilineInput, Output, StrInput\nfrom langflow.schema import Data\nfrom langflow.schema.message import Message\n\n\nclass ParseDataComponent(Component):\n display_name = \"Data to Message\"\n description = \"Convert Data objects into Messages using any {field_name} from input data.\"\n icon = \"message-square\"\n name = \"ParseData\"\n metadata = {\n \"legacy_name\": \"Parse Data\",\n }\n\n inputs = [\n DataInput(\n name=\"data\",\n display_name=\"Data\",\n info=\"The data to convert to text.\",\n is_list=True,\n required=True,\n ),\n MultilineInput(\n name=\"template\",\n display_name=\"Template\",\n info=\"The template to use for formatting the data. \"\n \"It can contain the keys {text}, {data} or any other key in the Data.\",\n value=\"{text}\",\n required=True,\n ),\n StrInput(name=\"sep\", display_name=\"Separator\", advanced=True, value=\"\\n\"),\n ]\n\n outputs = [\n Output(\n display_name=\"Message\",\n name=\"text\",\n info=\"Data as a single Message, with each input Data separated by Separator\",\n method=\"parse_data\",\n ),\n Output(\n display_name=\"Data List\",\n name=\"data_list\",\n info=\"Data as a list of new Data, each having `text` formatted by Template\",\n method=\"parse_data_as_list\",\n ),\n ]\n\n def _clean_args(self) -> tuple[list[Data], str, str]:\n data = self.data if isinstance(self.data, list) else [self.data]\n template = self.template\n sep = self.sep\n return data, template, sep\n\n def parse_data(self) -> Message:\n data, template, sep = self._clean_args()\n result_string = data_to_text(template, data, sep)\n self.status = result_string\n return Message(text=result_string)\n\n def parse_data_as_list(self) -> list[Data]:\n data, template, _ = self._clean_args()\n text_list, data_list = data_to_text_list(template, data)\n for item, text in zip(data_list, text_list, strict=True):\n item.set_text(text)\n self.status = data_list\n return data_list\n"
},
"data": {
"_input_type": "DataInput",
@ -453,9 +399,7 @@
"display_name": "Data",
"dynamic": false,
"info": "The data to convert to text.",
"input_types": [
"Data"
],
"input_types": ["Data"],
"list": true,
"list_add_label": "Add More",
"name": "data",
@ -494,9 +438,7 @@
"display_name": "Template",
"dynamic": false,
"info": "The template to use for formatting the data. It can contain the keys {text}, {data} or any other key in the Data.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -535,9 +477,7 @@
"data": {
"id": "StructuredOutput-TArXO",
"node": {
"base_classes": [
"Data"
],
"base_classes": ["Data"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -569,9 +509,7 @@
"name": "structured_output",
"selected": "Data",
"tool_mode": true,
"types": [
"Data"
],
"types": ["Data"],
"value": "__UNDEFINED__"
}
],
@ -602,9 +540,7 @@
"display_name": "Input Message",
"dynamic": false,
"info": "The input message to the language model.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -625,9 +561,7 @@
"display_name": "Language Model",
"dynamic": false,
"info": "The language model to use to generate the structured output.",
"input_types": [
"LanguageModel"
],
"input_types": ["LanguageModel"],
"list": false,
"list_add_label": "Add More",
"name": "llm",
@ -846,10 +780,7 @@
"data": {
"id": "ParseData-x7Rgx",
"node": {
"base_classes": [
"Data",
"Message"
],
"base_classes": ["Data", "Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -857,16 +788,14 @@
"display_name": "Data to Message",
"documentation": "",
"edited": false,
"field_order": [
"data",
"template",
"sep"
],
"field_order": ["data", "template", "sep"],
"frozen": false,
"icon": "message-square",
"legacy": false,
"lf_version": "1.1.4.post1",
"metadata": {},
"metadata": {
"legacy_name": "Parse Data"
},
"minimized": false,
"output_types": [],
"outputs": [
@ -878,9 +807,7 @@
"name": "text",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
},
{
@ -891,9 +818,7 @@
"name": "data_list",
"selected": "Data",
"tool_mode": true,
"types": [
"Data"
],
"types": ["Data"],
"value": "__UNDEFINED__"
}
],
@ -916,7 +841,7 @@
"show": true,
"title_case": false,
"type": "code",
"value": "from langflow.custom import Component\nfrom langflow.helpers.data import data_to_text, data_to_text_list\nfrom langflow.io import DataInput, MultilineInput, Output, StrInput\nfrom langflow.schema import Data\nfrom langflow.schema.message import Message\n\n\nclass ParseDataComponent(Component):\n display_name = \"Data to Message\"\n description = \"Convert Data objects into Messages using any {field_name} from input data.\"\n icon = \"message-square\"\n name = \"ParseData\"\n\n inputs = [\n DataInput(name=\"data\", display_name=\"Data\", info=\"The data to convert to text.\", is_list=True, required=True),\n MultilineInput(\n name=\"template\",\n display_name=\"Template\",\n info=\"The template to use for formatting the data. \"\n \"It can contain the keys {text}, {data} or any other key in the Data.\",\n value=\"{text}\",\n required=True,\n ),\n StrInput(name=\"sep\", display_name=\"Separator\", advanced=True, value=\"\\n\"),\n ]\n\n outputs = [\n Output(\n display_name=\"Message\",\n name=\"text\",\n info=\"Data as a single Message, with each input Data separated by Separator\",\n method=\"parse_data\",\n ),\n Output(\n display_name=\"Data List\",\n name=\"data_list\",\n info=\"Data as a list of new Data, each having `text` formatted by Template\",\n method=\"parse_data_as_list\",\n ),\n ]\n\n def _clean_args(self) -> tuple[list[Data], str, str]:\n data = self.data if isinstance(self.data, list) else [self.data]\n template = self.template\n sep = self.sep\n return data, template, sep\n\n def parse_data(self) -> Message:\n data, template, sep = self._clean_args()\n result_string = data_to_text(template, data, sep)\n self.status = result_string\n return Message(text=result_string)\n\n def parse_data_as_list(self) -> list[Data]:\n data, template, _ = self._clean_args()\n text_list, data_list = data_to_text_list(template, data)\n for item, text in zip(data_list, text_list, strict=True):\n item.set_text(text)\n self.status = data_list\n return data_list\n"
"value": "from langflow.custom import Component\nfrom langflow.helpers.data import data_to_text, data_to_text_list\nfrom langflow.io import DataInput, MultilineInput, Output, StrInput\nfrom langflow.schema import Data\nfrom langflow.schema.message import Message\n\n\nclass ParseDataComponent(Component):\n display_name = \"Data to Message\"\n description = \"Convert Data objects into Messages using any {field_name} from input data.\"\n icon = \"message-square\"\n name = \"ParseData\"\n metadata = {\n \"legacy_name\": \"Parse Data\",\n }\n\n inputs = [\n DataInput(\n name=\"data\",\n display_name=\"Data\",\n info=\"The data to convert to text.\",\n is_list=True,\n required=True,\n ),\n MultilineInput(\n name=\"template\",\n display_name=\"Template\",\n info=\"The template to use for formatting the data. \"\n \"It can contain the keys {text}, {data} or any other key in the Data.\",\n value=\"{text}\",\n required=True,\n ),\n StrInput(name=\"sep\", display_name=\"Separator\", advanced=True, value=\"\\n\"),\n ]\n\n outputs = [\n Output(\n display_name=\"Message\",\n name=\"text\",\n info=\"Data as a single Message, with each input Data separated by Separator\",\n method=\"parse_data\",\n ),\n Output(\n display_name=\"Data List\",\n name=\"data_list\",\n info=\"Data as a list of new Data, each having `text` formatted by Template\",\n method=\"parse_data_as_list\",\n ),\n ]\n\n def _clean_args(self) -> tuple[list[Data], str, str]:\n data = self.data if isinstance(self.data, list) else [self.data]\n template = self.template\n sep = self.sep\n return data, template, sep\n\n def parse_data(self) -> Message:\n data, template, sep = self._clean_args()\n result_string = data_to_text(template, data, sep)\n self.status = result_string\n return Message(text=result_string)\n\n def parse_data_as_list(self) -> list[Data]:\n data, template, _ = self._clean_args()\n text_list, data_list = data_to_text_list(template, data)\n for item, text in zip(data_list, text_list, strict=True):\n item.set_text(text)\n self.status = data_list\n return data_list\n"
},
"data": {
"_input_type": "DataInput",
@ -924,9 +849,7 @@
"display_name": "Data",
"dynamic": false,
"info": "The data to convert to text.",
"input_types": [
"Data"
],
"input_types": ["Data"],
"list": true,
"list_add_label": "Add More",
"name": "data",
@ -965,9 +888,7 @@
"display_name": "Template",
"dynamic": false,
"info": "The template to use for formatting the data. It can contain the keys {text}, {data} or any other key in the Data.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1006,9 +927,7 @@
"data": {
"id": "TextInput-CPTOR",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"category": "inputs",
"conditional_paths": [],
@ -1017,9 +936,7 @@
"display_name": "Text Input",
"documentation": "",
"edited": false,
"field_order": [
"input_value"
],
"field_order": ["input_value"],
"frozen": false,
"icon": "type",
"key": "TextInput",
@ -1037,9 +954,7 @@
"name": "text",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -1071,9 +986,7 @@
"display_name": "Text",
"dynamic": false,
"info": "Text to be passed as input.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1112,10 +1025,7 @@
"data": {
"id": "AnthropicModel-sHFTc",
"node": {
"base_classes": [
"LanguageModel",
"Message"
],
"base_classes": ["LanguageModel", "Message"],
"beta": false,
"category": "models",
"conditional_paths": [],
@ -1154,9 +1064,7 @@
"required_inputs": [],
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
},
{
@ -1165,14 +1073,10 @@
"display_name": "Language Model",
"method": "build_model",
"name": "model_output",
"required_inputs": [
"api_key"
],
"required_inputs": ["api_key"],
"selected": "LanguageModel",
"tool_mode": true,
"types": [
"LanguageModel"
],
"types": ["LanguageModel"],
"value": "__UNDEFINED__"
}
],
@ -1186,10 +1090,8 @@
"display_name": "Anthropic API Key",
"dynamic": false,
"info": "Your Anthropic API key.",
"input_types": [
"Message"
],
"load_from_db": false,
"input_types": ["Message"],
"load_from_db": true,
"name": "api_key",
"password": true,
"placeholder": "",
@ -1198,7 +1100,7 @@
"show": true,
"title_case": false,
"type": "str",
"value": ""
"value": "ANTHROPIC_API_KEY"
},
"base_url": {
"_input_type": "MessageTextInput",
@ -1206,9 +1108,7 @@
"display_name": "Anthropic API URL",
"dynamic": false,
"info": "Endpoint of the Anthropic API. Defaults to 'https://api.anthropic.com' if not specified.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1248,9 +1148,7 @@
"display_name": "Input",
"dynamic": false,
"info": "",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1319,9 +1217,7 @@
"display_name": "Prefill",
"dynamic": false,
"info": "Prefill text to guide the model's response.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1360,9 +1256,7 @@
"display_name": "System Message",
"dynamic": false,
"info": "System message to pass to the model.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1448,10 +1342,7 @@
"data": {
"id": "AnthropicModel-IrjAe",
"node": {
"base_classes": [
"LanguageModel",
"Message"
],
"base_classes": ["LanguageModel", "Message"],
"beta": false,
"category": "models",
"conditional_paths": [],
@ -1490,9 +1381,7 @@
"required_inputs": [],
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
},
{
@ -1501,14 +1390,10 @@
"display_name": "Language Model",
"method": "build_model",
"name": "model_output",
"required_inputs": [
"api_key"
],
"required_inputs": ["api_key"],
"selected": "LanguageModel",
"tool_mode": true,
"types": [
"LanguageModel"
],
"types": ["LanguageModel"],
"value": "__UNDEFINED__"
}
],
@ -1522,10 +1407,8 @@
"display_name": "Anthropic API Key",
"dynamic": false,
"info": "Your Anthropic API key.",
"input_types": [
"Message"
],
"load_from_db": false,
"input_types": ["Message"],
"load_from_db": true,
"name": "api_key",
"password": true,
"placeholder": "",
@ -1534,7 +1417,7 @@
"show": true,
"title_case": false,
"type": "str",
"value": ""
"value": "ANTHROPIC_API_KEY"
},
"base_url": {
"_input_type": "MessageTextInput",
@ -1542,9 +1425,7 @@
"display_name": "Anthropic API URL",
"dynamic": false,
"info": "Endpoint of the Anthropic API. Defaults to 'https://api.anthropic.com' if not specified.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1584,9 +1465,7 @@
"display_name": "Input",
"dynamic": false,
"info": "",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1655,9 +1534,7 @@
"display_name": "Prefill",
"dynamic": false,
"info": "Prefill text to guide the model's response.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1696,9 +1573,7 @@
"display_name": "System Message",
"dynamic": false,
"info": "System message to pass to the model.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1784,9 +1659,7 @@
"data": {
"id": "ChatOutput-d1tmJ",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"category": "outputs",
"conditional_paths": [],
@ -1823,9 +1696,7 @@
"name": "message",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -1839,9 +1710,7 @@
"display_name": "Background Color",
"dynamic": false,
"info": "The background color of the icon.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1862,9 +1731,7 @@
"display_name": "Icon",
"dynamic": false,
"info": "The icon of the message.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1903,9 +1770,7 @@
"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"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1926,9 +1791,7 @@
"display_name": "Text",
"dynamic": false,
"info": "Message to be passed as output.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1952,10 +1815,7 @@
"dynamic": false,
"info": "Type of sender.",
"name": "sender",
"options": [
"Machine",
"User"
],
"options": ["Machine", "User"],
"options_metadata": [],
"placeholder": "",
"required": false,
@ -1972,9 +1832,7 @@
"display_name": "Sender Name",
"dynamic": false,
"info": "Name of the sender.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1995,9 +1853,7 @@
"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"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -2036,9 +1892,7 @@
"display_name": "Text Color",
"dynamic": false,
"info": "The text color of the name",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -2076,9 +1930,7 @@
"data": {
"id": "File-JgIx7",
"node": {
"base_classes": [
"Data"
],
"base_classes": ["Data"],
"beta": false,
"category": "data",
"conditional_paths": [],
@ -2114,9 +1966,7 @@
"required_inputs": [],
"selected": "Data",
"tool_mode": true,
"types": [
"Data"
],
"types": ["Data"],
"value": "__UNDEFINED__"
}
],
@ -2184,10 +2034,7 @@
"display_name": "Server File Path",
"dynamic": false,
"info": "Data object with a 'file_path' property pointing to server file or a Message object with a path to the file. Supercedes 'Path' but supports same file types.",
"input_types": [
"Data",
"Message"
],
"input_types": ["Data", "Message"],
"list": true,
"list_add_label": "Add More",
"name": "file_path",
@ -2491,8 +2338,5 @@
"is_component": false,
"last_tested_version": "1.1.4",
"name": "Portfolio Website Code Generator",
"tags": [
"chatbots",
"coding"
]
}
"tags": ["chatbots", "coding"]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -8,16 +8,12 @@
"dataType": "Prompt",
"id": "Prompt-a6SIY",
"name": "prompt",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "system_message",
"id": "OpenAIModel-brPVM",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -35,16 +31,12 @@
"dataType": "Prompt",
"id": "Prompt-jkCpO",
"name": "prompt",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "input_value",
"id": "OpenAIModel-brPVM",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -62,16 +54,12 @@
"dataType": "OpenAIModel",
"id": "OpenAIModel-brPVM",
"name": "text_output",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "input_value",
"id": "ChatOutput-oE2ic",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -90,9 +78,7 @@
"display_name": "Prompt",
"id": "Prompt-jkCpO",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {
@ -109,9 +95,7 @@
"display_name": "Prompt",
"documentation": "",
"edited": false,
"field_order": [
"template"
],
"field_order": ["template"],
"frozen": false,
"icon": "prompts",
"legacy": false,
@ -127,9 +111,7 @@
"name": "prompt",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -162,10 +144,7 @@
"fileTypes": [],
"file_path": "",
"info": "",
"input_types": [
"Message",
"Text"
],
"input_types": ["Message", "Text"],
"list": false,
"load_from_db": false,
"multiline": true,
@ -185,10 +164,7 @@
"fileTypes": [],
"file_path": "",
"info": "",
"input_types": [
"Message",
"Text"
],
"input_types": ["Message", "Text"],
"list": false,
"load_from_db": false,
"multiline": true,
@ -208,10 +184,7 @@
"fileTypes": [],
"file_path": "",
"info": "",
"input_types": [
"Message",
"Text"
],
"input_types": ["Message", "Text"],
"list": false,
"load_from_db": false,
"multiline": true,
@ -231,10 +204,7 @@
"fileTypes": [],
"file_path": "",
"info": "",
"input_types": [
"Message",
"Text"
],
"input_types": ["Message", "Text"],
"list": false,
"load_from_db": false,
"multiline": true,
@ -254,10 +224,7 @@
"fileTypes": [],
"file_path": "",
"info": "",
"input_types": [
"Message",
"Text"
],
"input_types": ["Message", "Text"],
"list": false,
"load_from_db": false,
"multiline": true,
@ -277,10 +244,7 @@
"fileTypes": [],
"file_path": "",
"info": "",
"input_types": [
"Message",
"Text"
],
"input_types": ["Message", "Text"],
"list": false,
"load_from_db": false,
"multiline": true,
@ -316,9 +280,7 @@
"display_name": "Tool Placeholder",
"dynamic": false,
"info": "A placeholder input for tool mode.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "tool_placeholder",
@ -397,9 +359,7 @@
"display_name": "Prompt",
"id": "Prompt-a6SIY",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {
@ -409,9 +369,7 @@
"display_name": "Prompt",
"documentation": "",
"edited": false,
"field_order": [
"template"
],
"field_order": ["template"],
"frozen": false,
"icon": "prompts",
"legacy": false,
@ -427,9 +385,7 @@
"name": "prompt",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -478,9 +434,7 @@
"display_name": "Tool Placeholder",
"dynamic": false,
"info": "A placeholder input for tool mode.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "tool_placeholder",
@ -524,9 +478,7 @@
"display_name": "Chat Output",
"id": "ChatOutput-oE2ic",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -560,9 +512,7 @@
"name": "message",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -575,9 +525,7 @@
"display_name": "Background Color",
"dynamic": false,
"info": "The background color of the icon.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "background_color",
@ -597,9 +545,7 @@
"display_name": "Icon",
"dynamic": false,
"info": "The icon of the message.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "chat_icon",
@ -637,9 +583,7 @@
"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"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "data_template",
@ -659,9 +603,7 @@
"display_name": "Text",
"dynamic": false,
"info": "Message to be passed as output.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "input_value",
@ -682,10 +624,7 @@
"dynamic": false,
"info": "Type of sender.",
"name": "sender",
"options": [
"Machine",
"User"
],
"options": ["Machine", "User"],
"placeholder": "",
"required": false,
"show": true,
@ -701,9 +640,7 @@
"display_name": "Sender Name",
"dynamic": false,
"info": "Name of the sender.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "sender_name",
@ -723,9 +660,7 @@
"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"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "session_id",
@ -761,9 +696,7 @@
"display_name": "Text Color",
"dynamic": false,
"info": "The text color of the name",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "text_color",
@ -835,10 +768,7 @@
"data": {
"id": "OpenAIModel-brPVM",
"node": {
"base_classes": [
"LanguageModel",
"Message"
],
"base_classes": ["LanguageModel", "Message"],
"beta": false,
"category": "models",
"conditional_paths": [],
@ -877,9 +807,7 @@
"required_inputs": [],
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
},
{
@ -888,14 +816,10 @@
"display_name": "Language Model",
"method": "build_model",
"name": "model_output",
"required_inputs": [
"api_key"
],
"required_inputs": ["api_key"],
"selected": "LanguageModel",
"tool_mode": true,
"types": [
"LanguageModel"
],
"types": ["LanguageModel"],
"value": "__UNDEFINED__"
}
],
@ -909,9 +833,7 @@
"display_name": "OpenAI API Key",
"dynamic": false,
"info": "The OpenAI API Key to use for the OpenAI model.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"load_from_db": true,
"name": "api_key",
"password": true,
@ -920,7 +842,7 @@
"show": true,
"title_case": false,
"type": "str",
"value": ""
"value": "OPENAI_API_KEY"
},
"code": {
"advanced": true,
@ -946,9 +868,7 @@
"display_name": "Input",
"dynamic": false,
"info": "",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1130,9 +1050,7 @@
"display_name": "System Message",
"dynamic": false,
"info": "System message to pass to the model.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1228,8 +1146,5 @@
"is_component": false,
"last_tested_version": "1.0.19.post2",
"name": "SEO Keyword Generator",
"tags": [
"chatbots",
"assistants"
]
}
"tags": ["chatbots", "assistants"]
}

View File

@ -109,9 +109,7 @@
"name": "prompt",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -338,9 +336,7 @@
"name": "message",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -654,9 +650,7 @@
"name": "response",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -745,7 +739,7 @@
"show": true,
"title_case": false,
"type": "str",
"value": ""
"value": "OPENAI_API_KEY"
},
"code": {
"advanced": true,

File diff suppressed because one or more lines are too long

View File

@ -152,9 +152,7 @@
"name": "response",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -243,7 +241,7 @@
"show": true,
"title_case": false,
"type": "str",
"value": ""
"value": "OPENAI_API_KEY"
},
"code": {
"advanced": true,
@ -720,9 +718,7 @@
"name": "message",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -1002,9 +998,7 @@
"name": "message",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],

View File

@ -199,9 +199,7 @@
"name": "message",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -479,9 +477,7 @@
"name": "message",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -760,9 +756,7 @@
"name": "response",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -851,7 +845,7 @@
"show": true,
"title_case": false,
"type": "str",
"value": ""
"value": "OPENAI_API_KEY"
},
"code": {
"advanced": true,
@ -1353,9 +1347,7 @@
"name": "response",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -1444,7 +1436,7 @@
"show": true,
"title_case": false,
"type": "str",
"value": ""
"value": "OPENAI_API_KEY"
},
"code": {
"advanced": true,
@ -1946,9 +1938,7 @@
"name": "response",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -2037,7 +2027,7 @@
"show": true,
"title_case": false,
"type": "str",
"value": ""
"value": "OPENAI_API_KEY"
},
"code": {
"advanced": true,
@ -3101,7 +3091,7 @@
"dynamic": false,
"info": "",
"input_types": ["Message"],
"load_from_db": false,
"load_from_db": true,
"name": "api_key",
"password": true,
"placeholder": "",
@ -3109,7 +3099,7 @@
"show": true,
"title_case": false,
"type": "str",
"value": ""
"value": "SEARCHAPI_API_KEY"
},
"code": {
"advanced": true,

View File

@ -9,17 +9,12 @@
"dataType": "TextInput",
"id": "TextInput-eClq5",
"name": "text",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "CONTENT_GUIDELINES",
"id": "Prompt-AWZtN",
"inputTypes": [
"Message",
"Text"
],
"inputTypes": ["Message", "Text"],
"type": "str"
}
},
@ -38,17 +33,12 @@
"dataType": "TextInput",
"id": "TextInput-IpoG7",
"name": "text",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "OUTPUT_FORMAT",
"id": "Prompt-AWZtN",
"inputTypes": [
"Message",
"Text"
],
"inputTypes": ["Message", "Text"],
"type": "str"
}
},
@ -67,17 +57,12 @@
"dataType": "TextInput",
"id": "TextInput-npraC",
"name": "text",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "OUTPUT_LANGUAGE",
"id": "Prompt-AWZtN",
"inputTypes": [
"Message",
"Text"
],
"inputTypes": ["Message", "Text"],
"type": "str"
}
},
@ -96,17 +81,12 @@
"dataType": "TextInput",
"id": "TextInput-EZaR7",
"name": "text",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "PROFILE_DETAILS",
"id": "Prompt-AWZtN",
"inputTypes": [
"Message",
"Text"
],
"inputTypes": ["Message", "Text"],
"type": "str"
}
},
@ -125,17 +105,12 @@
"dataType": "TextInput",
"id": "TextInput-fKGcs",
"name": "text",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "PROFILE_TYPE",
"id": "Prompt-AWZtN",
"inputTypes": [
"Message",
"Text"
],
"inputTypes": ["Message", "Text"],
"type": "str"
}
},
@ -154,17 +129,12 @@
"dataType": "TextInput",
"id": "TextInput-92vEK",
"name": "text",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "TONE_AND_STYLE",
"id": "Prompt-AWZtN",
"inputTypes": [
"Message",
"Text"
],
"inputTypes": ["Message", "Text"],
"type": "str"
}
},
@ -182,16 +152,12 @@
"dataType": "ChatInput",
"id": "ChatInput-ECcN8",
"name": "message",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "input_value",
"id": "OpenAIModel-p0R9m",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -209,16 +175,12 @@
"dataType": "Prompt",
"id": "Prompt-AWZtN",
"name": "prompt",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "system_message",
"id": "OpenAIModel-p0R9m",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -236,16 +198,12 @@
"dataType": "OpenAIModel",
"id": "OpenAIModel-p0R9m",
"name": "text_output",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "input_value",
"id": "ChatOutput-0jDYx",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -262,9 +220,7 @@
"data": {
"id": "ChatInput-ECcN8",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -298,9 +254,7 @@
"name": "message",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -313,9 +267,7 @@
"display_name": "Background Color",
"dynamic": false,
"info": "The background color of the icon.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "background_color",
@ -334,9 +286,7 @@
"display_name": "Icon",
"dynamic": false,
"info": "The icon of the message.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "chat_icon",
@ -437,10 +387,7 @@
"dynamic": false,
"info": "Type of sender.",
"name": "sender",
"options": [
"Machine",
"User"
],
"options": ["Machine", "User"],
"placeholder": "",
"required": false,
"show": true,
@ -455,9 +402,7 @@
"display_name": "Sender Name",
"dynamic": false,
"info": "Name of the sender.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "sender_name",
@ -476,9 +421,7 @@
"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"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "session_id",
@ -513,9 +456,7 @@
"display_name": "Text Color",
"dynamic": false,
"info": "The text color of the name",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "text_color",
@ -555,9 +496,7 @@
"data": {
"id": "TextInput-eClq5",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -565,9 +504,7 @@
"display_name": "Content Guidelines",
"documentation": "",
"edited": false,
"field_order": [
"input_value"
],
"field_order": ["input_value"],
"frozen": false,
"icon": "type",
"legacy": false,
@ -583,9 +520,7 @@
"name": "text",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -616,9 +551,7 @@
"display_name": "Text",
"dynamic": false,
"info": "Text to be passed as input.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"multiline": true,
@ -661,9 +594,7 @@
"display_name": "Chat Output",
"id": "ChatOutput-0jDYx",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -697,9 +628,7 @@
"name": "message",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -712,9 +641,7 @@
"display_name": "Background Color",
"dynamic": false,
"info": "The background color of the icon.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "background_color",
@ -734,9 +661,7 @@
"display_name": "Icon",
"dynamic": false,
"info": "The icon of the message.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "chat_icon",
@ -774,9 +699,7 @@
"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"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "data_template",
@ -796,9 +719,7 @@
"display_name": "Text",
"dynamic": false,
"info": "Message to be passed as output.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "input_value",
@ -819,10 +740,7 @@
"dynamic": false,
"info": "Type of sender.",
"name": "sender",
"options": [
"Machine",
"User"
],
"options": ["Machine", "User"],
"placeholder": "",
"required": false,
"show": true,
@ -838,9 +756,7 @@
"display_name": "Sender Name",
"dynamic": false,
"info": "Name of the sender.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "sender_name",
@ -860,9 +776,7 @@
"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"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "session_id",
@ -898,9 +812,7 @@
"display_name": "Text Color",
"dynamic": false,
"info": "The text color of the name",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "text_color",
@ -942,9 +854,7 @@
"data": {
"id": "TextInput-IpoG7",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -952,9 +862,7 @@
"display_name": "Output Format",
"documentation": "",
"edited": false,
"field_order": [
"input_value"
],
"field_order": ["input_value"],
"frozen": false,
"icon": "type",
"legacy": false,
@ -970,9 +878,7 @@
"name": "text",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -1003,9 +909,7 @@
"display_name": "Text",
"dynamic": false,
"info": "Text to be passed as input.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"multiline": true,
@ -1046,9 +950,7 @@
"data": {
"id": "TextInput-npraC",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -1056,9 +958,7 @@
"display_name": "Output Language",
"documentation": "",
"edited": false,
"field_order": [
"input_value"
],
"field_order": ["input_value"],
"frozen": false,
"icon": "type",
"legacy": false,
@ -1074,9 +974,7 @@
"name": "text",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -1107,9 +1005,7 @@
"display_name": "Text",
"dynamic": false,
"info": "Text to be passed as input.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"multiline": true,
@ -1150,9 +1046,7 @@
"data": {
"id": "TextInput-EZaR7",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -1160,9 +1054,7 @@
"display_name": "Profile Details",
"documentation": "",
"edited": false,
"field_order": [
"input_value"
],
"field_order": ["input_value"],
"frozen": false,
"icon": "type",
"legacy": false,
@ -1178,9 +1070,7 @@
"name": "text",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -1211,9 +1101,7 @@
"display_name": "Text",
"dynamic": false,
"info": "Text to be passed as input.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"multiline": true,
@ -1254,9 +1142,7 @@
"data": {
"id": "TextInput-92vEK",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -1264,9 +1150,7 @@
"display_name": "Tone And Style",
"documentation": "",
"edited": false,
"field_order": [
"input_value"
],
"field_order": ["input_value"],
"frozen": false,
"icon": "type",
"legacy": false,
@ -1282,9 +1166,7 @@
"name": "text",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -1315,9 +1197,7 @@
"display_name": "Text",
"dynamic": false,
"info": "Text to be passed as input.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"multiline": true,
@ -1358,9 +1238,7 @@
"data": {
"id": "TextInput-fKGcs",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -1368,9 +1246,7 @@
"display_name": "Profile Type",
"documentation": "",
"edited": false,
"field_order": [
"input_value"
],
"field_order": ["input_value"],
"frozen": false,
"icon": "type",
"legacy": false,
@ -1386,9 +1262,7 @@
"name": "text",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -1419,9 +1293,7 @@
"display_name": "Text",
"dynamic": false,
"info": "Text to be passed as input.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"multiline": true,
@ -1501,9 +1373,7 @@
"display_name": "Prompt",
"id": "Prompt-AWZtN",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {
@ -1520,9 +1390,7 @@
"display_name": "Prompt",
"documentation": "",
"edited": false,
"field_order": [
"template"
],
"field_order": ["template"],
"frozen": false,
"icon": "prompts",
"legacy": false,
@ -1538,9 +1406,7 @@
"name": "prompt",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -1554,10 +1420,7 @@
"fileTypes": [],
"file_path": "",
"info": "",
"input_types": [
"Message",
"Text"
],
"input_types": ["Message", "Text"],
"list": false,
"load_from_db": false,
"multiline": true,
@ -1577,10 +1440,7 @@
"fileTypes": [],
"file_path": "",
"info": "",
"input_types": [
"Message",
"Text"
],
"input_types": ["Message", "Text"],
"list": false,
"load_from_db": false,
"multiline": true,
@ -1600,10 +1460,7 @@
"fileTypes": [],
"file_path": "",
"info": "",
"input_types": [
"Message",
"Text"
],
"input_types": ["Message", "Text"],
"list": false,
"load_from_db": false,
"multiline": true,
@ -1623,10 +1480,7 @@
"fileTypes": [],
"file_path": "",
"info": "",
"input_types": [
"Message",
"Text"
],
"input_types": ["Message", "Text"],
"list": false,
"load_from_db": false,
"multiline": true,
@ -1646,10 +1500,7 @@
"fileTypes": [],
"file_path": "",
"info": "",
"input_types": [
"Message",
"Text"
],
"input_types": ["Message", "Text"],
"list": false,
"load_from_db": false,
"multiline": true,
@ -1669,10 +1520,7 @@
"fileTypes": [],
"file_path": "",
"info": "",
"input_types": [
"Message",
"Text"
],
"input_types": ["Message", "Text"],
"list": false,
"load_from_db": false,
"multiline": true,
@ -1727,9 +1575,7 @@
"display_name": "Tool Placeholder",
"dynamic": false,
"info": "A placeholder input for tool mode.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"name": "tool_placeholder",
@ -1771,10 +1617,7 @@
"data": {
"id": "OpenAIModel-p0R9m",
"node": {
"base_classes": [
"LanguageModel",
"Message"
],
"base_classes": ["LanguageModel", "Message"],
"beta": false,
"category": "models",
"conditional_paths": [],
@ -1813,9 +1656,7 @@
"required_inputs": [],
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
},
{
@ -1824,14 +1665,10 @@
"display_name": "Language Model",
"method": "build_model",
"name": "model_output",
"required_inputs": [
"api_key"
],
"required_inputs": ["api_key"],
"selected": "LanguageModel",
"tool_mode": true,
"types": [
"LanguageModel"
],
"types": ["LanguageModel"],
"value": "__UNDEFINED__"
}
],
@ -1845,9 +1682,7 @@
"display_name": "OpenAI API Key",
"dynamic": false,
"info": "The OpenAI API Key to use for the OpenAI model.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"load_from_db": true,
"name": "api_key",
"password": true,
@ -1856,7 +1691,7 @@
"show": true,
"title_case": false,
"type": "str",
"value": ""
"value": "OPENAI_API_KEY"
},
"code": {
"advanced": true,
@ -1882,9 +1717,7 @@
"display_name": "Input",
"dynamic": false,
"info": "",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -2066,9 +1899,7 @@
"display_name": "System Message",
"dynamic": false,
"info": "System message to pass to the model.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -2164,8 +1995,5 @@
"is_component": false,
"last_tested_version": "1.0.19.post2",
"name": "Twitter Thread Generator",
"tags": [
"chatbots",
"content-generation"
]
}
"tags": ["chatbots", "content-generation"]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -37,20 +37,34 @@ class TableInput(BaseInputMixin, MetadataTraceMixin, TableMixin, ListableInputMi
@field_validator("value")
@classmethod
def validate_value(cls, v: Any, _info):
# Check if value is a list of dicts
# Convert single dict or Data instance into a list.
if isinstance(v, dict | Data):
v = [v]
# Automatically convert DataFrame into a list of dictionaries.
if isinstance(v, DataFrame):
v = v.to_dict(orient="records")
# Verify the value is now a list.
if not isinstance(v, list):
msg = f"TableInput value must be a list of dictionaries or Data. Value '{v}' is not a list."
raise ValueError(msg) # noqa: TRY004
for item in v:
msg = (
"The table input must be a list of rows. You provided a "
f"{type(v).__name__}, which cannot be converted to table format. "
"Please provide your data as either:\n"
"- A list of dictionaries (each dict is a row)\n"
"- A pandas DataFrame\n"
"- A single dictionary (will become a one-row table)\n"
"- A Data object (Langflow's internal data structure)\n"
)
raise ValueError(msg) # noqa: TRY004 Pydantic only catches ValueError or AssertionError
# Ensure each item in the list is either a dict or a Data instance.
for i, item in enumerate(v):
if not isinstance(item, dict | Data):
msg = (
"TableInput value must be a list of dictionaries or Data. "
f"Item '{item}' is not a dictionary or Data."
f"Row {i + 1} in your table has an invalid format. Each row must be either:\n"
"- A dictionary containing column name/value pairs\n"
"- A Data object (Langflow's internal data structure for passing data between components)\n"
f"Instead, got a {type(item).__name__}. Please check the format of your input data."
)
raise ValueError(msg) # noqa: TRY004
raise ValueError(msg) # noqa: TRY004 Pydantic only catches ValueError or AssertionError
return v

View File

@ -34,7 +34,7 @@ from langflow.interface.components import get_and_cache_all_types_dict
from langflow.interface.utils import setup_llm_caching
from langflow.logging.logger import configure
from langflow.middleware import ContentSizeLimitMiddleware
from langflow.services.deps import get_settings_service, get_telemetry_service
from langflow.services.deps import get_queue_service, get_settings_service, get_telemetry_service
from langflow.services.utils import initialize_services, teardown_services
if TYPE_CHECKING:
@ -43,6 +43,7 @@ if TYPE_CHECKING:
# Ignore Pydantic deprecation warnings from Langchain
warnings.filterwarnings("ignore", category=PydanticDeprecatedSince20)
_tasks: list[asyncio.Task] = []
MAX_PORT = 65535
@ -127,6 +128,9 @@ def get_lifespan(*, fix_migration=False, version=None):
await create_or_update_starter_projects(all_types_dict)
telemetry_service.start()
await load_flows_from_directory()
queue_service = get_queue_service()
if not queue_service.is_started(): # Start if not already started
queue_service.start()
yield
except Exception as exc:

View File

@ -221,8 +221,8 @@ def _serialize_dispatcher(obj: Any, max_length: int | None, max_items: int | Non
def serialize(
obj: Any,
max_length: int | None = MAX_TEXT_LENGTH,
max_items: int | None = MAX_ITEMS_LENGTH,
max_length: int | None = None,
max_items: int | None = None,
*,
to_str: bool = False,
) -> Any:
@ -275,7 +275,9 @@ def serialize(
def serialize_or_str(
obj: Any, max_length: int | None = MAX_TEXT_LENGTH, max_items: int | None = MAX_ITEMS_LENGTH
obj: Any,
max_length: int | None = MAX_TEXT_LENGTH,
max_items: int | None = MAX_ITEMS_LENGTH,
) -> Any:
"""Calls serialize() and if it fails, returns a string representation of the object.

View File

@ -375,13 +375,29 @@ def encrypt_api_key(api_key: str, settings_service: SettingsService):
def decrypt_api_key(encrypted_api_key: str, settings_service: SettingsService):
"""Decrypt the provided encrypted API key using Fernet decryption.
This function first attempts to decrypt the API key by encoding it,
assuming it is a properly encoded string. If that fails, it logs a detailed
debug message including the exception information and retries decryption
using the original string input.
Args:
encrypted_api_key (str): The encrypted API key.
settings_service (SettingsService): Service providing authentication settings.
Returns:
str: The decrypted API key, or an empty string if decryption cannot be performed.
"""
fernet = get_fernet(settings_service)
decrypted_key = ""
# Two-way decryption
if isinstance(encrypted_api_key, str):
try:
decrypted_key = fernet.decrypt(encrypted_api_key.encode()).decode()
except Exception: # noqa: BLE001
logger.debug("Failed to decrypt API key")
decrypted_key = fernet.decrypt(encrypted_api_key).decode()
return decrypted_key
return fernet.decrypt(encrypted_api_key.encode()).decode()
except Exception as primary_exception: # noqa: BLE001
logger.debug(
"Decryption using UTF-8 encoded API key failed. Error: %s. "
"Retrying decryption using the raw string input.",
primary_exception,
)
return fernet.decrypt(encrypted_api_key).decode()
return ""

View File

@ -1,5 +1,6 @@
from uuid import UUID
from loguru import logger
from sqlmodel import col, delete, select
from sqlmodel.ext.asyncio.session import AsyncSession
@ -25,7 +26,7 @@ async def get_transactions_by_flow_id(
return list(transactions)
async def log_transaction(db: AsyncSession, transaction: TransactionBase) -> TransactionTable:
async def log_transaction(db: AsyncSession, transaction: TransactionBase) -> TransactionTable | None:
"""Log a transaction and maintain a maximum number of transactions in the database.
This function logs a new transaction into the database and ensures that the number of transactions
@ -42,6 +43,9 @@ async def log_transaction(db: AsyncSession, transaction: TransactionBase) -> Tra
Raises:
IntegrityError: If there is a database integrity error
"""
if not transaction.flow_id:
logger.debug("Transaction flow_id is None")
return None
table = TransactionTable(**transaction.model_dump())
try:
@ -63,7 +67,6 @@ async def log_transaction(db: AsyncSession, transaction: TransactionBase) -> Tra
db.add(table)
await db.exec(delete_older)
await db.commit()
await db.refresh(table)
except Exception:
await db.rollback()

View File

@ -5,7 +5,8 @@ from uuid import UUID, uuid4
from pydantic import field_serializer, field_validator
from sqlmodel import JSON, Column, Field, Relationship, SQLModel
from langflow.services.database.utils import truncate_json
from langflow.serialization.constants import MAX_ITEMS_LENGTH, MAX_TEXT_LENGTH
from langflow.serialization.serialization import serialize
if TYPE_CHECKING:
from langflow.services.database.models.flow.model import Flow
@ -36,11 +37,11 @@ class TransactionBase(SQLModel):
@field_serializer("inputs")
def serialize_inputs(self, data) -> dict:
return truncate_json(data)
return serialize(data, max_length=MAX_TEXT_LENGTH, max_items=MAX_ITEMS_LENGTH)
@field_serializer("outputs")
def serialize_outputs(self, data) -> dict:
return truncate_json(data)
return serialize(data, max_length=MAX_TEXT_LENGTH, max_items=MAX_ITEMS_LENGTH)
class TransactionTable(TransactionBase, table=True): # type: ignore[call-arg]

View File

@ -143,4 +143,3 @@ async def delete_vertex_builds_by_flow_id(db: AsyncSession, flow_id: UUID) -> No
"""
stmt = delete(VertexBuildTable).where(VertexBuildTable.flow_id == flow_id)
await db.exec(stmt)
await db.commit()

View File

@ -6,13 +6,12 @@ from pydantic import BaseModel, field_serializer, field_validator
from sqlalchemy import Text
from sqlmodel import JSON, Column, Field, Relationship, SQLModel
from langflow.services.database.utils import truncate_json
from langflow.serialization.constants import MAX_ITEMS_LENGTH, MAX_TEXT_LENGTH
from langflow.serialization.serialization import serialize
if TYPE_CHECKING:
from langflow.services.database.models.flow.model import Flow
from langflow.utils.util_strings import truncate_long_strings
class VertexBuildBase(SQLModel):
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
@ -45,15 +44,15 @@ class VertexBuildBase(SQLModel):
@field_serializer("data")
def serialize_data(self, data) -> dict:
return truncate_json(data)
return serialize(data, max_length=MAX_TEXT_LENGTH, max_items=MAX_ITEMS_LENGTH)
@field_serializer("artifacts")
def serialize_artifacts(self, data) -> dict:
return truncate_json(data)
return serialize(data, max_length=MAX_TEXT_LENGTH, max_items=MAX_ITEMS_LENGTH)
@field_serializer("params")
def serialize_params(self, data) -> str:
return truncate_long_strings(data)
return serialize(data, max_length=MAX_TEXT_LENGTH, max_items=MAX_ITEMS_LENGTH)
class VertexBuildTable(VertexBuildBase, table=True): # type: ignore[call-arg]

View File

@ -1,6 +1,5 @@
from __future__ import annotations
import json
from contextlib import asynccontextmanager
from dataclasses import dataclass
from typing import TYPE_CHECKING
@ -10,54 +9,10 @@ from loguru import logger
from sqlmodel import text
from sqlmodel.ext.asyncio.session import AsyncSession
from langflow.serialization import constants
if TYPE_CHECKING:
from langflow.services.database.service import DatabaseService
def truncate_json(data, *, max_size: int = constants.MAX_TEXT_LENGTH):
def calculate_size(data):
return len(json.dumps(data))
def shrink_to_size(data, remaining_size):
if isinstance(data, dict):
truncated = {}
for key, value in data.items():
key_size = len(json.dumps(key))
if remaining_size - key_size <= 0:
break
truncated[key] = shrink_to_size(value, remaining_size - key_size)
remaining_size -= len(json.dumps({key: value})) - key_size
return truncated
if isinstance(data, list):
truncated = []
for item in data:
if remaining_size <= len('""'):
break
truncated.append(shrink_to_size(item, remaining_size))
remaining_size -= len(json.dumps(item)) + 1
return truncated
if isinstance(data, str):
max_string_length = max(remaining_size - 2, 0)
return data[:max_string_length] + "" if max_string_length > 0 else ""
return data
try:
json.dumps(data)
is_serialized = True
except Exception: # noqa: BLE001
is_serialized = False
if calculate_size(data) <= max_size or not is_serialized:
return data
return shrink_to_size(data, max_size)
async def initialize_database(*, fix_migration: bool = False) -> None:
logger.debug("Initializing database")
from langflow.services.deps import get_db_service

View File

@ -15,6 +15,7 @@ if TYPE_CHECKING:
from langflow.services.cache.service import AsyncBaseCacheService, CacheService
from langflow.services.chat.service import ChatService
from langflow.services.database.service import DatabaseService
from langflow.services.job_queue.service import JobQueueService
from langflow.services.session.service import SessionService
from langflow.services.settings.service import SettingsService
from langflow.services.socket.service import SocketIOService
@ -239,3 +240,10 @@ def get_store_service() -> StoreService:
StoreService: The StoreService instance.
"""
return get_service(ServiceType.STORE_SERVICE)
def get_queue_service() -> JobQueueService:
"""Retrieves the QueueService instance from the service manager."""
from langflow.services.job_queue.factory import JobQueueServiceFactory
return get_service(ServiceType.JOB_QUEUE_SERVICE, JobQueueServiceFactory())

View File

@ -0,0 +1,11 @@
from langflow.services.base import Service
from langflow.services.factory import ServiceFactory
from langflow.services.job_queue.service import JobQueueService
class JobQueueServiceFactory(ServiceFactory):
def __init__(self):
super().__init__(JobQueueService)
def create(self) -> Service:
return JobQueueService()

View File

@ -0,0 +1,263 @@
from __future__ import annotations
import asyncio
from loguru import logger
from langflow.events.event_manager import EventManager, create_default_event_manager
from langflow.services.base import Service
class JobQueueService(Service):
"""Asynchronous service for managing job-specific queues and their associated tasks.
This service allows clients to:
- Create dedicated asyncio queues for individual jobs.
- Associate each queue with an EventManager, enabling event-driven handling.
- Launch and manage asynchronous tasks that process these job queues.
- Safely clean up resources by cancelling active tasks and emptying queues.
- Automatically perform periodic cleanup of inactive or completed job queues.
Attributes:
name (str): Unique identifier for the service.
_queues (dict[str, tuple[asyncio.Queue, EventManager, asyncio.Task | None]]):
Dictionary mapping job IDs to a tuple containing:
* The job's asyncio.Queue instance.
* The associated EventManager instance.
* The asyncio.Task processing the job (if any).
_cleanup_task (asyncio.Task | None): Background task for periodic cleanup.
_closed (bool): Flag indicating whether the service is currently active.
Example:
service = JobQueueService()
await service.start()
queue, event_manager = service.create_queue("job123")
service.start_job("job123", some_async_coroutine())
# Retrieve and use the queue data as needed
data = service.get_queue_data("job123")
await service.cleanup_job("job123")
await service.stop()
"""
name = "job_queue_service"
def __init__(self) -> None:
"""Initialize the JobQueueService.
Sets up the internal registry for job queues, initializes the cleanup task, and sets the service state
to active.
"""
self._queues: dict[str, tuple[asyncio.Queue, EventManager, asyncio.Task | None]] = {}
self._cleanup_task: asyncio.Task | None = None
self._closed = False
self.ready = False
def is_started(self) -> bool:
"""Check if the JobQueueService has started.
Returns:
bool: True if the service has started, False otherwise.
"""
return self._cleanup_task is not None
def set_ready(self) -> None:
if not self.is_started():
self.start()
super().set_ready()
def start(self) -> None:
"""Start the JobQueueService and begin the periodic cleanup routine.
This method marks the service as active and launches a background task that
periodically checks and cleans up job queues whose tasks have been completed or cancelled.
"""
self._closed = False
self._cleanup_task = asyncio.create_task(self._periodic_cleanup())
logger.debug("JobQueueService started: periodic cleanup task initiated.")
async def stop(self) -> None:
"""Gracefully stop the JobQueueService by terminating background operations and cleaning up all resources.
This coroutine performs the following steps:
1. Marks the service as closed, preventing further job queue creation.
2. Cancels the background periodic cleanup task and awaits its termination.
3. Iterates over all registered job queues to clean up their resources—cancelling active tasks and
clearing queued items.
"""
self._closed = True
if self._cleanup_task:
self._cleanup_task.cancel()
await asyncio.wait([self._cleanup_task])
if not self._cleanup_task.cancelled():
exc = self._cleanup_task.exception()
if exc is not None:
raise exc
# Clean up each registered job queue.
for job_id in list(self._queues.keys()):
await self.cleanup_job(job_id)
logger.info("JobQueueService stopped: all job queues have been cleaned up.")
async def teardown(self) -> None:
await self.stop()
def create_queue(self, job_id: str) -> tuple[asyncio.Queue, EventManager]:
"""Create and register a new queue along with its corresponding event manager for a job.
Args:
job_id (str): Unique identifier for the job.
Returns:
tuple[asyncio.Queue, EventManager]: A tuple containing:
- The asyncio.Queue instance for handling the job's tasks or messages.
- The EventManager instance for event handling tied to the queue.
"""
if job_id in self._queues:
msg = f"Queue for job_id {job_id} already exists"
logger.error(msg)
raise ValueError(msg)
if self._closed:
msg = "Queue service is closed"
logger.error(msg)
raise RuntimeError(msg)
main_queue: asyncio.Queue = asyncio.Queue()
event_manager = create_default_event_manager(main_queue)
# Register the queue without an active task.
self._queues[job_id] = (main_queue, event_manager, None)
logger.debug(f"Queue and event manager successfully created for job_id {job_id}")
return main_queue, event_manager
def start_job(self, job_id: str, task_coro) -> None:
"""Start an asynchronous task for a given job, replacing any existing active task.
The method performs the following:
- Verifies the presence of a registered queue for the job.
- Cancels any currently running task associated with the job.
- Launches a new asynchronous task using the provided coroutine.
- Updates the internal registry with the new task.
Args:
job_id (str): Unique identifier for the job.
task_coro: A coroutine representing the job's asynchronous task.
"""
if job_id not in self._queues:
msg = f"No queue found for job_id {job_id}"
logger.error(msg)
raise ValueError(msg)
if self._closed:
msg = "Queue service is closed"
logger.error(msg)
raise RuntimeError(msg)
main_queue, event_manager, existing_task = self._queues[job_id]
if existing_task and not existing_task.done():
logger.debug(f"Existing task for job_id {job_id} detected; cancelling it.")
existing_task.cancel()
# Initiate the new asynchronous task.
task = asyncio.create_task(task_coro)
self._queues[job_id] = (main_queue, event_manager, task)
logger.debug(f"New task started for job_id {job_id}")
def get_queue_data(self, job_id: str) -> tuple[asyncio.Queue, EventManager, asyncio.Task | None]:
"""Retrieve the complete data structure associated with a job's queue.
Args:
job_id (str): Unique identifier for the job.
Returns:
tuple[asyncio.Queue, EventManager, asyncio.Task | None]:
A tuple containing the job's main queue, its linked event manager, and the associated task (if any).
"""
if job_id not in self._queues:
msg = f"No queue found for job_id {job_id}"
logger.error(msg)
raise ValueError(msg)
if self._closed:
msg = "Queue service is closed"
logger.error(msg)
raise RuntimeError(msg)
return self._queues[job_id]
async def cleanup_job(self, job_id: str) -> None:
"""Clean up and release resources for a specific job.
The cleanup process includes:
1. Verifying if the job's queue is registered.
2. Cancelling the running task (if active) and awaiting its termination.
3. Clearing all items from the job's queue.
4. Removing the job's entry from the internal registry.
Args:
job_id (str): Unique identifier for the job to be cleaned up.
"""
if job_id not in self._queues:
logger.debug(f"No queue found for job_id {job_id} during cleanup.")
return
logger.info(f"Commencing cleanup for job_id {job_id}")
main_queue, event_manager, task = self._queues[job_id]
# Cancel the associated task if it is still running.
if task and not task.done():
logger.debug(f"Cancelling active task for job_id {job_id}")
task.cancel()
await asyncio.wait([task])
# Log any exceptions that occurred during the task's execution.
if exc := task.exception():
logger.error(f"Error in task for job_id {job_id}: {exc}")
logger.debug(f"Task cancellation complete for job_id {job_id}")
# Clear the queue since we just cancelled the task or it has completed
items_cleared = 0
while not main_queue.empty():
try:
main_queue.get_nowait()
items_cleared += 1
except asyncio.QueueEmpty:
break
logger.debug(f"Removed {items_cleared} items from queue for job_id {job_id}")
# Remove the job entry from the registry
self._queues.pop(job_id, None)
logger.info(f"Cleanup successful for job_id {job_id}: resources have been released.")
async def _periodic_cleanup(self) -> None:
"""Execute a periodic task that cleans up completed or cancelled job queues.
This internal coroutine continuously:
- Sleeps for a fixed interval (60 seconds).
- Initiates the cleanup of job queues by calling _cleanup_old_queues.
- Monitors and logs any exceptions during the cleanup cycle.
The loop terminates when the service is marked as closed.
"""
while not self._closed:
try:
await asyncio.sleep(60) # Sleep for 60 seconds before next cleanup attempt.
await self._cleanup_old_queues()
except asyncio.CancelledError:
logger.debug("Periodic cleanup task received cancellation signal.")
raise
except Exception as exc: # noqa: BLE001
logger.error(f"Exception encountered during periodic cleanup: {exc}")
async def _cleanup_old_queues(self) -> None:
"""Scan all registered job queues and clean up those with inactive tasks.
For each job:
- Check whether the associated task is either complete or cancelled.
- If so, execute the cleanup_job method to release the job's resources.
"""
for job_id in list(self._queues.keys()):
_, _, task = self._queues[job_id]
if task and task.done():
logger.debug(f"Job queue for job_id {job_id} marked for cleanup.")
await self.cleanup_job(job_id)

View File

@ -19,3 +19,4 @@ class ServiceType(str, Enum):
STATE_SERVICE = "state_service"
TRACING_SERVICE = "tracing_service"
TELEMETRY_SERVICE = "telemetry_service"
JOB_QUEUE_SERVICE = "job_queue_service"

View File

@ -76,14 +76,13 @@ class Settings(BaseSettings):
`postgresql+psycopg` respectively)."""
database_connection_retry: bool = False
"""If True, Langflow will retry to connect to the database if it fails."""
pool_size: int = 10
"""DEPRECATED: Use db_connection_settings['pool_size'] instead.
The number of connections to keep open in the connection pool. If not provided, the default is 10."""
max_overflow: int = 20
"""DEPRECATED: Use db_connection_settings['max_overflow'] instead.
The number of connections to allow that can be opened beyond the pool size.
If not provided, the default is 20."""
db_connect_timeout: int = 20
pool_size: int = 20
"""The number of connections to keep open in the connection pool.
For high load scenarios, this should be increased based on expected concurrent users."""
max_overflow: int = 30
"""The number of connections to allow that can be opened beyond the pool size.
Should be 2x the pool_size for optimal performance under load."""
db_connect_timeout: int = 30
"""The number of seconds to wait before giving up on a lock to released or establishing a connection to the
database."""
@ -92,12 +91,27 @@ class Settings(BaseSettings):
"""SQLite pragmas to use when connecting to the database."""
db_connection_settings: dict | None = {
"pool_size": 10,
"max_overflow": 20,
"pool_timeout": 30,
"pool_pre_ping": True,
"pool_size": 20, # Match the pool_size above
"max_overflow": 30, # Match the max_overflow above
"pool_timeout": 30, # Seconds to wait for a connection from pool
"pool_pre_ping": True, # Check connection validity before using
"pool_recycle": 1800, # Recycle connections after 30 minutes
"echo": False, # Set to True for debugging only
}
"""Common database connection settings."""
"""Database connection settings optimized for high load scenarios.
Note: These settings are most effective with PostgreSQL. For SQLite:
- Reduce pool_size and max_overflow if experiencing lock contention
- SQLite has limited concurrent write capability even with WAL mode
- Best for read-heavy or moderate write workloads
Settings:
- pool_size: Number of connections to maintain (increase for higher concurrency)
- max_overflow: Additional connections allowed beyond pool_size
- pool_timeout: Seconds to wait for an available connection
- pool_pre_ping: Validates connections before use to prevent stale connections
- pool_recycle: Seconds before connections are recycled (prevents timeouts)
- echo: Enable SQL query logging (development only)
"""
# cache configuration
cache_type: Literal["async", "redis", "memory", "disk"] = "async"
@ -205,6 +219,9 @@ class Settings(BaseSettings):
mcp_server_enable_progress_notifications: bool = False
"""If set to False, Langflow will not send progress notifications in the MCP server."""
event_delivery: Literal["polling", "streaming"] = "streaming"
"""How to deliver build events to the frontend. Can be 'polling' or 'streaming'."""
@field_validator("dev")
@classmethod
def set_dev(cls, value):

View File

@ -1,19 +1,24 @@
from __future__ import annotations
import traceback
from collections.abc import Callable
from typing import Any
from typing import TYPE_CHECKING, Any
import anyio
from loguru import logger
from langflow.services.task.backends.base import TaskBackend
if TYPE_CHECKING:
from collections.abc import Callable
from types import TracebackType
class AnyIOTaskResult:
def __init__(self, scope) -> None:
self._scope = scope
def __init__(self) -> None:
self._status = "PENDING"
self._result = None
self._exception: Exception | None = None
self._traceback: TracebackType | None = None
self.cancel_scope: anyio.CancelScope | None = None
@property
def status(self) -> str:
@ -34,9 +39,11 @@ class AnyIOTaskResult:
def ready(self) -> bool:
return self._status == "DONE"
async def run(self, func, *args, **kwargs) -> None:
async def run(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> None:
try:
self._result = await func(*args, **kwargs)
async with anyio.CancelScope() as scope:
self.cancel_scope = scope
self._result = await func(*args, **kwargs)
except Exception as e: # noqa: BLE001
self._exception = e
self._traceback = e.__traceback__
@ -45,36 +52,66 @@ class AnyIOTaskResult:
class AnyIOBackend(TaskBackend):
"""Backend for handling asynchronous tasks using AnyIO."""
name = "anyio"
def __init__(self) -> None:
"""Initialize the AnyIO backend with an empty task dictionary."""
self.tasks: dict[str, AnyIOTaskResult] = {}
self._run_tasks: list[anyio.TaskGroup] = []
async def launch_task(
self, task_func: Callable[..., Any], *args: Any, **kwargs: Any
) -> tuple[str | None, AnyIOTaskResult | None]:
) -> tuple[str, AnyIOTaskResult]:
"""Launch a new task in an asynchronous manner.
Parameters:
Args:
task_func: The asynchronous function to run.
*args: Positional arguments to pass to task_func.
**kwargs: Keyword arguments to pass to task_func.
Returns:
A tuple containing a unique task ID and the task result object.
"""
async with anyio.create_task_group() as tg:
try:
task_result = AnyIOTaskResult(tg)
tg.start_soon(task_result.run, task_func, *args, **kwargs)
except Exception: # noqa: BLE001
logger.exception("An error occurred while launching the task")
return None, None
tuple[str, AnyIOTaskResult]: A tuple containing the task ID and task result object.
Raises:
RuntimeError: If task creation fails.
"""
try:
task_result = AnyIOTaskResult()
# Create task ID before starting the task
task_id = str(id(task_result))
self.tasks[task_id] = task_result
logger.info(f"Task {task_id} started.")
return task_id, task_result
def get_task(self, task_id: str) -> Any:
# Start the task in the background using TaskGroup
async with anyio.create_task_group() as tg:
tg.start_soon(task_result.run, task_func, *args, **kwargs)
self._run_tasks.append(tg)
except Exception as e:
msg = f"Failed to launch task: {e!s}"
raise RuntimeError(msg) from e
return task_id, task_result
def get_task(self, task_id: str) -> AnyIOTaskResult | None:
"""Retrieve a task by its ID.
Args:
task_id: The unique identifier of the task.
Returns:
AnyIOTaskResult | None: The task result object if found, None otherwise.
"""
return self.tasks.get(task_id)
async def cleanup_task(self, task_id: str) -> None:
"""Clean up a completed task and its resources.
Args:
task_id: The unique identifier of the task to clean up.
"""
if task := self.tasks.get(task_id):
if task.cancel_scope:
task.cancel_scope.cancel()
self.tasks.pop(task_id, None)

View File

@ -3,45 +3,20 @@ from __future__ import annotations
from collections.abc import Callable, Coroutine
from typing import TYPE_CHECKING, Any
from loguru import logger
from langflow.services.base import Service
from langflow.services.task.backends.anyio import AnyIOBackend
from langflow.services.task.utils import get_celery_worker_status
if TYPE_CHECKING:
from langflow.services.settings.service import SettingsService
from langflow.services.task.backends.base import TaskBackend
def check_celery_availability():
try:
from langflow.worker import celery_app
status = get_celery_worker_status(celery_app)
logger.debug(f"Celery status: {status}")
except Exception: # noqa: BLE001
logger.opt(exception=True).debug("Celery not available")
status = {"availability": None}
return status
class TaskService(Service):
name = "task_service"
def __init__(self, settings_service: SettingsService):
self.settings_service = settings_service
try:
if self.settings_service.settings.celery_enabled:
status = check_celery_availability()
use_celery = status.get("availability") is not None
else:
use_celery = False
except ImportError:
use_celery = False
self.use_celery = use_celery
self.use_celery = False
self.backend = self.get_backend()
@property
@ -49,12 +24,6 @@ class TaskService(Service):
return self.backend.name
def get_backend(self) -> TaskBackend:
if self.use_celery:
from langflow.services.task.backends.celery import CeleryBackend
logger.debug("Using Celery backend")
return CeleryBackend()
logger.debug("Using AnyIO backend")
return AnyIOBackend()
# In your TaskService class
@ -64,24 +33,8 @@ class TaskService(Service):
*args: Any,
**kwargs: Any,
) -> Any:
if not self.use_celery:
return None, await task_func(*args, **kwargs)
if not hasattr(task_func, "apply"):
msg = f"Task function {task_func} does not have an apply method"
raise ValueError(msg)
task = task_func.apply(args=args, kwargs=kwargs)
result = task.get()
# if result is coroutine
if isinstance(result, Coroutine):
result = await result
return task.id, result
return await task_func(*args, **kwargs)
async def launch_task(self, task_func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
logger.debug(f"Launching task {task_func} with args {args} and kwargs {kwargs}")
logger.debug(f"Using backend {self.backend}")
task = self.backend.launch_task(task_func, *args, **kwargs)
return await task if isinstance(task, Coroutine) else task
def get_task(self, task_id: str) -> Any:
return self.backend.get_task(task_id)

View File

@ -21,6 +21,7 @@ class VersionPayload(BaseModel):
auto_login: bool = Field(serialization_alias="autoLogin")
cache_type: str = Field(serialization_alias="cacheType")
backend_only: bool = Field(serialization_alias="backendOnly")
desktop: bool = False
class PlaygroundPayload(BaseModel):

View File

@ -90,6 +90,10 @@ class TelemetryService(Service):
return
await self.telemetry_queue.put(payload)
def _get_langflow_desktop(self) -> bool:
# Coerce to bool, could be 1, 0, True, False, "1", "0", "True", "False"
return str(os.getenv("LANGFLOW_DESKTOP", "False")).lower() in ("1", "true")
async def log_package_version(self) -> None:
python_version = ".".join(platform.python_version().split(".")[:2])
version_info = get_version_info()
@ -104,6 +108,7 @@ class TelemetryService(Service):
backend_only=self.settings_service.settings.backend_only,
arch=self.architecture,
auto_login=self.settings_service.auth_settings.AUTO_LOGIN,
desktop=self._get_langflow_desktop(),
)
await self._queue_event((self.send_telemetry_data, payload, None))

View File

@ -39,7 +39,9 @@ class ArizePhoenixTracer(BaseTracer):
chat_input_value: str
chat_output_value: str
def __init__(self, trace_name: str, trace_type: str, project_name: str, trace_id: UUID):
def __init__(
self, trace_name: str, trace_type: str, project_name: str, trace_id: UUID, session_id: str | None = None
):
"""Initializes the ArizePhoenixTracer instance and sets up a root span."""
self.trace_name = trace_name
self.trace_type = trace_type
@ -49,6 +51,7 @@ class ArizePhoenixTracer(BaseTracer):
self.flow_id = trace_name.split(" - ")[-1]
self.chat_input_value = ""
self.chat_output_value = ""
self.session_id = session_id
try:
self._ready = self.setup_arize_phoenix()
@ -63,7 +66,7 @@ class ArizePhoenixTracer(BaseTracer):
name=self.flow_id,
start_time=self._get_current_timestamp(),
)
self.root_span.set_attribute(SpanAttributes.SESSION_ID, self.flow_id)
self.root_span.set_attribute(SpanAttributes.SESSION_ID, self.session_id or self.flow_id)
self.root_span.set_attribute(SpanAttributes.OPENINFERENCE_SPAN_KIND, self.trace_type)
self.root_span.set_attribute("langflow.project.name", self.project_name)
self.root_span.set_attribute("langflow.flow.name", self.flow_name)

View File

@ -65,6 +65,7 @@ class TracingService(Service):
self.worker_task: asyncio.Task | None = None
self.end_trace_tasks: set[asyncio.Task] = set()
self.deactivated = self.settings_service.settings.deactivate_tracing
self.session_id: str | None = None
async def log_worker(self) -> None:
while self.running or not self.logs_queue.empty():
@ -163,6 +164,7 @@ class TracingService(Service):
trace_type="chain",
project_name=self.project_name,
trace_id=self.run_id,
session_id=self.session_id,
)
def set_run_name(self, name: str) -> None:
@ -241,7 +243,7 @@ class TracingService(Service):
trace_id,
trace_name,
trace_type,
self._cleanup_inputs(inputs),
inputs,
metadata,
component._vertex,
)
@ -286,3 +288,7 @@ class TracingService(Service):
if langchain_callback:
callbacks.append(langchain_callback)
return callbacks
def set_session_id(self, session_id: str) -> None:
"""Set the session ID for tracing."""
self.session_id = session_id

View File

@ -170,7 +170,7 @@ dev-dependencies = [
module-root = "langflow"
tests-root = "../tests/unit"
test-framework = "pytest"
ignore-paths = []
ignore-paths = ["src/backend/base/langflow/components/"]
formatter-cmds = ["ruff check --exit-zero --fix $file", "ruff format $file"]
#disable plugins that might interfere with runtime measurement
pytest-cmd = "pytest -p no:profiling -p no:sugar -p no:xdist -p no:cov -p no:split"

File diff suppressed because it is too large Load Diff

View File

@ -135,11 +135,12 @@ def get_text():
async def delete_transactions_by_flow_id(db: AsyncSession, flow_id: UUID):
if not flow_id:
return
stmt = select(TransactionTable).where(TransactionTable.flow_id == flow_id)
transactions = await db.exec(stmt)
for transaction in transactions:
await db.delete(transaction)
await db.commit()
async def _delete_transactions_and_vertex_builds(session, flows: list[Flow]):
@ -147,8 +148,14 @@ async def _delete_transactions_and_vertex_builds(session, flows: list[Flow]):
for flow_id in flow_ids:
if not flow_id:
continue
await delete_vertex_builds_by_flow_id(session, flow_id)
await delete_transactions_by_flow_id(session, flow_id)
try:
await delete_vertex_builds_by_flow_id(session, flow_id)
except Exception as e: # noqa: BLE001
logger.debug(f"Error deleting vertex builds for flow {flow_id}: {e}")
try:
await delete_transactions_by_flow_id(session, flow_id)
except Exception as e: # noqa: BLE001
logger.debug(f"Error deleting transactions for flow {flow_id}: {e}")
@pytest.fixture
@ -433,12 +440,21 @@ async def active_user(client): # noqa: ARG001
yield user
# Clean up
# Now cleanup transactions, vertex_build
async with db_manager.with_session() as session:
user = await session.get(User, user.id, options=[selectinload(User.flows)])
await _delete_transactions_and_vertex_builds(session, user.flows)
await session.delete(user)
try:
async with db_manager.with_session() as session:
user = await session.get(User, user.id, options=[selectinload(User.flows)])
await _delete_transactions_and_vertex_builds(session, user.flows)
await session.commit()
except Exception as e: # noqa: BLE001
logger.exception(f"Error deleting transactions and vertex builds for user: {e}")
await session.commit()
try:
async with db_manager.with_session() as session:
user = await session.get(User, user.id)
await session.delete(user)
await session.commit()
except Exception as e: # noqa: BLE001
logger.exception(f"Error deleting user: {e}")
@pytest.fixture

View File

@ -1,125 +1,125 @@
import random
import os
import time
from pathlib import Path
from http import HTTPStatus
import httpx
import orjson
from locust import FastHttpUser, between, task
from rich import print # noqa: A004
from locust import FastHttpUser, between, events, task
class NameTest(FastHttpUser):
wait_time = between(1, 5)
@events.quitting.add_listener
def _(environment, **_kwargs):
"""Print stats at test end for analysis."""
if environment.stats.total.fail_ratio > 0.01:
environment.process_exit_code = 1
environment.runner.quit()
with Path("names.txt").open(encoding="utf-8") as file:
names = [line.strip() for line in file]
headers: dict = {}
class FlowRunUser(FastHttpUser):
"""FlowRunUser simulates users sending requests to the Langflow run endpoint.
def poll_task(self, task_id, sleep_time=1):
while True:
with self.rest(
"GET",
f"/task/{task_id}",
name="task_status",
headers=self.headers,
) as response:
status = response.js.get("status")
print(f"Poll Response: {response.js}")
if status == "SUCCESS":
return response.js.get("result")
if status in {"FAILURE", "REVOKED"}:
msg = f"Task failed with status: {status}"
raise ValueError(msg)
time.sleep(sleep_time)
Designed for high-load testing with proper wait times and connection handling.
Uses FastHttpUser for better performance with keep-alive connections and connection pooling.
def process(self, name, flow_id, payload):
task_id = None
print(f"Processing {payload}")
with self.rest(
"POST",
f"/process/{flow_id}",
json=payload,
name="process",
headers=self.headers,
) as response:
print(response.js)
if response.status_code != 200:
response.failure("Process call failed")
msg = "Process call failed"
raise ValueError(msg)
task_id = response.js.get("id")
session_id = response.js.get("session_id")
assert task_id, "Inner Task ID not found"
Environment Variables:
- LANGFLOW_HOST: Base URL for the Langflow server (default: http://localhost:7860)
- FLOW_ID: UUID or endpoint name of the flow to test (default: 62c21279-f7ca-43e2-b5e3-326ac573db04)
- API_KEY: API key for authentication, sent as header 'x-api-key' (Required)
- MIN_WAIT: Minimum wait time between requests in ms (default: 2000)
- MAX_WAIT: Maximum wait time between requests in ms (default: 5000)
- REQUEST_TIMEOUT: Timeout for each request in seconds (default: 30.0)
"""
assert task_id, "Task ID not found"
result = self.poll_task(task_id)
print(f"Result for {name}: {result}")
abstract = False # This user class can be instantiated
connection_timeout = float(os.getenv("REQUEST_TIMEOUT", "30.0")) # Configurable timeout
network_timeout = float(os.getenv("REQUEST_TIMEOUT", "30.0"))
return result, session_id
# Dynamic wait time based on environment variables or defaults
# Increased default minimum wait to reduce database pressure
wait_time = between(
float(os.getenv("MIN_WAIT", "2000")) / 1000,
float(os.getenv("MAX_WAIT", "5000")) / 1000,
)
@task
def send_name_and_check(self):
name = random.choice(self.names) # noqa: S311
# Use the host provided by environment variable or default
host = os.getenv("LANGFLOW_HOST", "http://localhost:7860")
payload1 = {
"inputs": {"text": f"Hello, My name is {name}"},
"sync": False,
}
_result1, session_id = self.process(name, self.flow_id, payload1)
# Flow ID from environment variable or default example UUID
flow_id = os.getenv("FLOW_ID")
payload2 = {
"inputs": {"text": "What is my name? Please, answer like this: Your name is <name>"},
"session_id": session_id,
"sync": False,
}
result2, session_id = self.process(name, self.flow_id, payload2)
assert f"Your name is {name}" in str(result2), "Name not found in response"
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self._last_response: dict | None = None
self._consecutive_failures = 0
def on_start(self):
print("Starting")
login_data = {"username": "superuser", "password": "superuser"}
response = httpx.post(f"{self.host}/login", data=login_data)
print(response.json())
"""Setup and validate required configurations."""
if not os.getenv("API_KEY"):
msg = "API_KEY environment variable is required for load testing"
raise ValueError(msg)
tokens = response.json()
print(tokens)
a_token = tokens["access_token"]
logged_in_headers = {"Authorization": f"Bearer {a_token}"}
print("Logged in")
json_flow = (Path(__file__).parent.parent / "data" / "BasicChatwithPromptandHistory.json").read_text(
encoding="utf-8"
)
flow = orjson.loads(json_flow)
data = flow["data"]
# Create test data
flow = {"name": "Flow 1", "description": "description", "data": data}
print("Creating flow")
# Make request to endpoint
response = httpx.post(
f"{self.host}/flows/",
json=flow,
headers=logged_in_headers,
)
self.flow_id = response.json()["id"]
print(f"Flow ID: {self.flow_id}")
# Test connection and auth before starting
with self.client.get("/health", catch_response=True) as response:
if response.status_code != HTTPStatus.OK:
msg = f"Initial health check failed: {response.status_code}"
raise ConnectionError(msg)
# read all users
response = httpx.get(
f"{self.host}/users/",
headers=logged_in_headers,
)
print(response.json())
user_id = next(
(user["id"] for user in response.json()["users"] if user["username"] == "superuser"),
None,
)
# Create api key
response = httpx.post(
f"{self.host}/api_key/",
json={"user_id": user_id},
headers=logged_in_headers,
)
print(response.json())
self.headers["x-api-key"] = response.json()["api_key"]
def log_error(self, name: str, exc: Exception, response_time: float):
"""Helper method to log errors in a format Locust expects.
Args:
name: The name/endpoint of the request
exc: The exception that occurred
response_time: The response time in milliseconds
"""
# Log error in stats
self.environment.stats.log_error("ERROR", name, str(exc))
# Log request with error
self.environment.stats.log_request("ERROR", name, response_time, 0)
@task(1)
def run_flow_endpoint(self):
"""Sends a POST request to the run endpoint using a realistic payload.
Includes basic error handling.
"""
if not self.flow_id:
msg = "FLOW_ID environment variable is required for load testing"
raise ValueError(msg)
endpoint = f"/api/v1/run/{self.flow_id}?stream=false"
# Realistic payload that exercises the system
payload = {
"input_value": (
"Hey, Could you check https://docs.langflow.org for me? Later, could you calculate 1390 / 192 ?"
),
"output_type": "chat",
"input_type": "chat",
"tweaks": {},
}
headers = {
"Content-Type": "application/json",
"x-api-key": os.getenv("API_KEY"),
"Accept": "application/json",
}
start_time = time.time()
try:
with self.client.post(
endpoint, json=payload, headers=headers, catch_response=True, timeout=self.connection_timeout
) as response:
response_time = (time.time() - start_time) * 1000
if response.status_code == HTTPStatus.OK:
try:
self._last_response = response.json()
except ValueError as e:
response.failure("Invalid JSON response")
self.log_error(endpoint, e, response_time)
else:
error_text = response.text or "No response text"
error_msg = f"Unexpected status code: {response.status_code}, Response: {error_text[:200]}"
response.failure(error_msg)
self.log_error(endpoint, Exception(error_msg), response_time)
except Exception as e: # noqa: BLE001
response_time = (time.time() - start_time) * 1000
self.log_error(endpoint, e, response_time)
response.failure(f"Error: {e}")

View File

@ -35,17 +35,21 @@ def test_component_tool():
@pytest.mark.api_key_required
def test_component_tool_with_api_key():
@pytest.mark.usefixtures("client")
async def test_component_tool_with_api_key():
chat_output = ChatOutput()
openai_llm = OpenAIModelComponent()
openai_llm.set(api_key=os.environ["OPENAI_API_KEY"])
tool_calling_agent = ToolCallingAgentComponent()
tools = await chat_output.to_toolkit()
tool_calling_agent.set(
llm=openai_llm.build_model, tools=[chat_output], input_value="Which tools are available? Please tell its name."
llm=openai_llm.build_model,
tools=tools,
input_value="Which tools are available? Please tell its name.",
)
g = Graph(start=tool_calling_agent, end=tool_calling_agent)
assert g is not None
results = list(g.start())
results = [result async for result in g.async_start()]
assert len(results) == 4
assert "message_response" in tool_calling_agent._outputs_map["response"].value.get_text()

View File

@ -0,0 +1,75 @@
import json
from typing import Any
from uuid import UUID
from httpx import AsyncClient, codes
async def create_flow(client: AsyncClient, flow_data: str, headers: dict[str, str]) -> UUID:
"""Create a flow and return its ID."""
response = await client.post("api/v1/flows/", json=json.loads(flow_data), headers=headers)
assert response.status_code == codes.CREATED
return UUID(response.json()["id"])
async def build_flow(
client: AsyncClient, flow_id: UUID, headers: dict[str, str], json: dict[str, Any] | None = None
) -> dict[str, Any]:
"""Start a flow build and return the job_id."""
if json is None:
json = {}
response = await client.post(f"api/v1/build/{flow_id}/flow", json=json, headers=headers)
assert response.status_code == codes.OK
return response.json()
async def get_build_events(client: AsyncClient, job_id: str, headers: dict[str, str]):
"""Get events for a build job."""
return await client.get(f"api/v1/build/{job_id}/events", headers=headers)
async def consume_and_assert_stream(response, job_id):
"""Consume the event stream and assert the expected event structure."""
count = 0
lines = []
async for line in response.aiter_lines():
# Skip empty lines (ndjson uses double newlines)
if not line:
continue
lines.append(line)
parsed = json.loads(line)
if "job_id" in parsed:
assert parsed["job_id"] == job_id
continue
if count == 0:
# First event should be vertices_sorted
assert parsed["event"] == "vertices_sorted", (
"Invalid first event. Expected 'vertices_sorted'. Full event stream:\n" + "\n".join(lines)
)
ids = parsed["data"]["ids"]
ids.sort()
assert ids == ["ChatInput-CIGht"], "Invalid ids in first event. Full event stream:\n" + "\n".join(lines)
to_run = parsed["data"]["to_run"]
to_run.sort()
assert to_run == ["ChatInput-CIGht", "ChatOutput-QA7ej", "Memory-amN4Z", "Prompt-iWbCC"], (
"Invalid to_run list in first event. Full event stream:\n" + "\n".join(lines)
)
elif count > 0 and count < 5:
# Next events should be end_vertex events
assert parsed["event"] == "end_vertex", (
f"Invalid event at position {count}. Expected 'end_vertex'. Full event stream:\n" + "\n".join(lines)
)
assert parsed["data"]["build_data"] is not None, (
f"Missing build_data at position {count}. Full event stream:\n" + "\n".join(lines)
)
elif count == 5:
# Final event should be end
assert parsed["event"] == "end", "Invalid final event. Expected 'end'. Full event stream:\n" + "\n".join(
lines
)
else:
raise ValueError(f"Unexpected event at position {count}. Full event stream:\n" + "\n".join(lines))
count += 1

View File

@ -78,9 +78,8 @@ class TestAgentComponent(ComponentTestBaseWithoutClient):
assert all(provider in updated_config["agent_llm"]["options"] for provider in MODEL_PROVIDERS_DICT)
assert "Anthropic" in updated_config["agent_llm"]["options"]
assert updated_config["agent_llm"]["input_types"] == []
assert any("sonnet" in option.lower() for option in updated_config["model_name"]["options"]), (
f"Options: {updated_config['model_name']['options']}"
)
options = updated_config["model_name"]["options"]
assert any("sonnet" in option.lower() for option in options), f"Options: {options}"
# Test updating build config for Custom
updated_config = await component.update_build_config(build_config, "Custom", "agent_llm")
@ -113,6 +112,7 @@ async def test_agent_component_with_calculator():
model_name="gpt-4o",
llm_type="OpenAI",
temperature=temperature,
_session_id=str(uuid4()),
)
response = await agent.message_response()

View File

@ -19,7 +19,7 @@ class TestURLComponent(ComponentTestBaseWithoutClient):
def default_kwargs(self):
"""Return the default kwargs for the component."""
return {
"urls": ["https://example.com"],
"urls": ["https://google.com"],
"format": "Text",
}

View File

@ -21,6 +21,7 @@ class TestBatchRunComponent(ComponentTestBaseWithoutClient):
"model": MockLanguageModel(),
"df": DataFrame({"text": ["Hello"]}),
"column_name": "text",
"enable_metadata": True,
}
@pytest.fixture
@ -33,7 +34,11 @@ class TestBatchRunComponent(ComponentTestBaseWithoutClient):
test_df = DataFrame({"text": ["Hello", "World", "Test"]})
component = BatchRunComponent(
model=MockLanguageModel(), system_message="You are a helpful assistant", df=test_df, column_name="text"
model=MockLanguageModel(),
system_message="You are a helpful assistant",
df=test_df,
column_name="text",
enable_metadata=True,
)
# Run the batch process
@ -43,46 +48,188 @@ class TestBatchRunComponent(ComponentTestBaseWithoutClient):
assert isinstance(result, DataFrame)
assert "text_input" in result.columns
assert "model_response" in result.columns
assert "metadata" in result.columns
assert len(result) == 3
assert all(isinstance(resp, str) for resp in result["model_response"])
# Convert DataFrame to list of dicts for easier testing
result_dicts = result.to_dict("records")
# Verify metadata
assert all(row["metadata"]["has_system_message"] for row in result_dicts)
assert all(row["metadata"]["processing_status"] == "success" for row in result_dicts)
async def test_batch_run_without_system_message(self):
async def test_batch_run_without_metadata(self):
test_df = DataFrame({"text": ["Hello", "World"]})
component = BatchRunComponent(model=MockLanguageModel(), df=test_df, column_name="text")
component = BatchRunComponent(
model=MockLanguageModel(),
df=test_df,
column_name="text",
enable_metadata=False,
)
result = await component.run_batch()
assert isinstance(result, DataFrame)
assert len(result) == 2
assert "metadata" not in result.columns
assert all(isinstance(resp, str) for resp in result["model_response"])
async def test_batch_run_error_with_metadata(self):
component = BatchRunComponent(
model=MockLanguageModel(),
df="not_a_dataframe", # This will cause a TypeError
column_name="text",
enable_metadata=True,
)
with pytest.raises(TypeError, match=re.escape("Expected DataFrame input, got <class 'str'>")):
await component.run_batch()
async def test_batch_run_error_without_metadata(self):
component = BatchRunComponent(
model=MockLanguageModel(),
df="not_a_dataframe", # This will cause a TypeError
column_name="text",
enable_metadata=False,
)
with pytest.raises(TypeError, match=re.escape("Expected DataFrame input, got <class 'str'>")):
await component.run_batch()
async def test_operational_error_with_metadata(self):
# Create a mock model that raises an AttributeError during processing
class ErrorModel:
def with_config(self, *_, **__):
return self
async def abatch(self, *_):
msg = "Mock error during batch processing"
raise AttributeError(msg)
component = BatchRunComponent(
model=ErrorModel(),
df=DataFrame({"text": ["test1", "test2"]}),
column_name="text",
enable_metadata=True,
)
result = await component.run_batch()
assert isinstance(result, DataFrame)
assert len(result) == 1 # Component returns a single error row
error_row = result.iloc[0]
# Verify error metadata
assert error_row["metadata"]["processing_status"] == "failed"
assert "Mock error during batch processing" in error_row["metadata"]["error"]
# Verify base row structure
assert error_row["text_input"] == ""
assert error_row["model_response"] == ""
assert error_row["batch_index"] == -1
async def test_operational_error_without_metadata(self):
# Create a mock model that raises an AttributeError during processing
class ErrorModel:
def with_config(self, *_, **__):
return self
async def abatch(self, *_):
msg = "Mock error during batch processing"
raise AttributeError(msg)
component = BatchRunComponent(
model=ErrorModel(),
df=DataFrame({"text": ["test1", "test2"]}),
column_name="text",
enable_metadata=False,
)
result = await component.run_batch()
assert isinstance(result, DataFrame)
assert len(result) == 1 # Component returns a single error row
error_row = result.iloc[0]
# Verify no metadata
assert "metadata" not in error_row
# Verify base row structure
assert error_row["text_input"] == ""
assert error_row["model_response"] == ""
assert error_row["batch_index"] == -1
def test_create_base_row(self):
component = BatchRunComponent()
row = component._create_base_row(text_input="test_input", model_response="test_response", batch_index=1)
assert row == {
"text_input": "test_input",
"model_response": "test_response",
"batch_index": 1,
}
def test_add_metadata_success(self):
component = BatchRunComponent(enable_metadata=True)
row = component._create_base_row(text_input="test_input", model_response="test_response", batch_index=1)
component._add_metadata(row, success=True, system_msg="test_system")
assert "metadata" in row
assert row["metadata"]["has_system_message"] is True
assert row["metadata"]["processing_status"] == "success"
assert row["metadata"]["input_length"] == len("test_input")
assert row["metadata"]["response_length"] == len("test_response")
def test_add_metadata_failure(self):
component = BatchRunComponent(enable_metadata=True)
row = component._create_base_row()
component._add_metadata(row, success=False, error="test_error")
assert "metadata" in row
assert row["metadata"]["processing_status"] == "failed"
assert row["metadata"]["error"] == "test_error"
def test_metadata_disabled(self):
component = BatchRunComponent(enable_metadata=False)
row = component._create_base_row(text_input="test")
component._add_metadata(row, success=True)
assert "metadata" not in row
async def test_invalid_column_name(self):
component = BatchRunComponent(
model=MockLanguageModel(), df=DataFrame({"text": ["Hello"]}), column_name="nonexistent_column"
model=MockLanguageModel(),
df=DataFrame({"text": ["Hello"]}),
column_name="nonexistent_column",
enable_metadata=True,
)
with pytest.raises(ValueError, match=re.escape("Column 'nonexistent_column' not found in the DataFrame.")):
with pytest.raises(
ValueError,
match=re.escape("Column 'nonexistent_column' not found in the DataFrame. Available columns: text"),
):
await component.run_batch()
async def test_empty_dataframe(self):
component = BatchRunComponent(model=MockLanguageModel(), df=DataFrame({"text": []}), column_name="text")
component = BatchRunComponent(
model=MockLanguageModel(),
df=DataFrame({"text": []}),
column_name="text",
enable_metadata=True,
)
result = await component.run_batch()
assert isinstance(result, DataFrame)
assert len(result) == 0
async def test_non_string_column_conversion(self):
test_df = DataFrame(
{
"text": [123, 456, 789] # Numeric values
}
)
test_df = DataFrame({"text": [123, 456, 789]}) # Numeric values
component = BatchRunComponent(model=MockLanguageModel(), df=test_df, column_name="text")
component = BatchRunComponent(
model=MockLanguageModel(),
df=test_df,
column_name="text",
enable_metadata=True,
)
result = await component.run_batch()
assert isinstance(result, DataFrame)
assert all(isinstance(text, str) for text in result["text_input"])
assert all(str(num) in text for num, text in zip(test_df["text"], result["text_input"], strict=False))
result_dicts = result.to_dict("records")
assert all(row["metadata"]["processing_status"] == "success" for row in result_dicts)

View File

@ -1,14 +1,15 @@
from uuid import UUID
import orjson
import pytest
from httpx import AsyncClient
from langflow.components.logic.loop import LoopComponent
from langflow.memory import aget_messages
from langflow.schema.data import Data
from langflow.services.database.models.flow import FlowCreate
from orjson import orjson
from tests.base import ComponentTestBaseWithClient
from tests.unit.build_utils import build_flow, get_build_events
TEXT = (
"lorem ipsum dolor sit amet lorem ipsum dolor sit amet lorem ipsum dolor sit amet. "
@ -62,15 +63,25 @@ class TestLoopComponentWithAPI(ComponentTestBaseWithClient):
assert len(messages[1].text) > 0
async def test_build_flow_loop(self, client, json_loop_test, logged_in_headers):
# TODO: Add a test for the loop where the loop component gets updated even the component in json
"""Test building a flow with a loop component."""
# Create the flow
flow_id = await self._create_flow(client, json_loop_test, logged_in_headers)
async with client.stream("POST", f"api/v1/build/{flow_id}/flow", json={}, headers=logged_in_headers) as r:
async for line in r.aiter_lines():
# httpx split by \n, but ndjson sends two \n for each line
if line:
# Process the line if needed
pass
# Start the build and get job_id
build_response = await build_flow(client, flow_id, logged_in_headers)
job_id = build_response["job_id"]
assert job_id is not None
# Get the events stream
events_response = await get_build_events(client, job_id, logged_in_headers)
assert events_response.status_code == 200
# Process the events stream
async for line in events_response.aiter_lines():
if not line: # Skip empty lines
continue
# Process events if needed
# We could add specific assertions here for loop-related events
await self.check_messages(flow_id)

View File

@ -0,0 +1,127 @@
import pytest
from langflow.components.processing.data_to_dataframe import DataToDataFrameComponent
from langflow.schema import Data, DataFrame
from tests.base import ComponentTestBaseWithoutClient
class TestDataToDataFrameComponent(ComponentTestBaseWithoutClient):
@pytest.fixture
def component_class(self):
"""Return the component class to test."""
return DataToDataFrameComponent
@pytest.fixture
def default_kwargs(self):
"""Return the default kwargs for the component."""
return {
"data_list": [
Data(text="Row 1", data={"field1": "value1", "field2": 1}),
Data(text="Row 2", data={"field1": "value2", "field2": 2}),
]
}
@pytest.fixture
def file_names_mapping(self):
"""Return the file names mapping for different versions."""
# This is a new component, so we return an empty list
return []
def test_basic_setup(self, component_class, default_kwargs):
"""Test basic component initialization."""
component = component_class()
component.set_attributes(default_kwargs)
assert component.data_list == default_kwargs["data_list"]
def test_build_dataframe_basic(self, component_class, default_kwargs):
"""Test basic DataFrame construction."""
component = component_class()
component.set_attributes(default_kwargs)
result_df = component.build_dataframe()
assert isinstance(result_df, DataFrame)
assert len(result_df) == 2
assert list(result_df.columns) == ["field1", "field2", "text"]
assert result_df["text"].tolist() == ["Row 1", "Row 2"]
assert result_df["field1"].tolist() == ["value1", "value2"]
assert result_df["field2"].tolist() == [1, 2]
def test_single_data_input(self, component_class):
"""Test handling single Data object input."""
single_data = Data(text="Single Row", data={"field1": "value"})
component = component_class()
component.set_attributes({"data_list": single_data})
result_df = component.build_dataframe()
assert len(result_df) == 1
assert result_df["text"].iloc[0] == "Single Row"
assert result_df["field1"].iloc[0] == "value"
def test_empty_data_list(self, component_class):
"""Test behavior with empty data list."""
component = component_class()
component.set_attributes({"data_list": []})
result_df = component.build_dataframe()
assert len(result_df) == 0
def test_data_without_text(self, component_class):
"""Test handling Data objects without text field."""
data_without_text = [Data(data={"field1": "value1"}), Data(data={"field1": "value2"})]
component = component_class()
component.set_attributes({"data_list": data_without_text})
result_df = component.build_dataframe()
assert len(result_df) == 2
assert "text" not in result_df.columns
assert result_df["field1"].tolist() == ["value1", "value2"]
def test_data_without_data_dict(self, component_class):
"""Test handling Data objects without data dictionary."""
data_without_dict = [Data(text="Text 1"), Data(text="Text 2")]
component = component_class()
component.set_attributes({"data_list": data_without_dict})
result_df = component.build_dataframe()
assert len(result_df) == 2
assert list(result_df.columns) == ["text"]
assert result_df["text"].tolist() == ["Text 1", "Text 2"]
def test_mixed_data_fields(self, component_class):
"""Test handling Data objects with different fields."""
mixed_data = [
Data(text="Row 1", data={"field1": "value1", "field2": 1}),
Data(text="Row 2", data={"field1": "value2", "field3": "extra"}),
]
component = component_class()
component.set_attributes({"data_list": mixed_data})
result_df = component.build_dataframe()
assert len(result_df) == 2
assert set(result_df.columns) == {"field1", "field2", "field3", "text"}
assert result_df["field1"].tolist() == ["value1", "value2"]
assert result_df["field2"].iloc[1] != result_df["field2"].iloc[1] # Check for NaN using inequality
assert result_df["field3"].iloc[0] != result_df["field3"].iloc[0] # Check for NaN using inequality
def test_invalid_input_type(self, component_class):
"""Test error handling for invalid input types."""
invalid_data = [{"not": "a Data object"}]
component = component_class()
component.set_attributes({"data_list": invalid_data})
with pytest.raises(TypeError) as exc_info:
component.build_dataframe()
assert "Expected Data objects" in str(exc_info.value)
def test_status_update(self, component_class, default_kwargs):
"""Test that status is properly updated."""
component = component_class()
component.set_attributes(default_kwargs)
result = component.build_dataframe()
assert component.status is result # Status should be set to the DataFrame

View File

@ -0,0 +1,165 @@
import json
from pathlib import Path
from unittest.mock import MagicMock, patch
import pandas as pd
import pytest
from langflow.components.processing.save_to_file import SaveToFileComponent
from langflow.schema import Data, Message
from tests.base import ComponentTestBaseWithoutClient
class TestSaveToFileComponent(ComponentTestBaseWithoutClient):
@pytest.fixture(autouse=True)
def setup_and_teardown(self):
"""Setup and teardown for each test."""
# Setup
test_files = [
"./test_output.csv",
"./test_output.xlsx",
"./test_output.json",
"./test_output.md",
"./test_output.txt",
]
# Teardown
yield
# Delete test files after each test
for file_path in test_files:
path = Path(file_path)
if path.exists():
path.unlink()
@pytest.fixture
def component_class(self):
"""Return the component class to test."""
return SaveToFileComponent
@pytest.fixture
def default_kwargs(self):
"""Return the default kwargs for the component."""
sample_df = pd.DataFrame([{"col1": 1, "col2": "a"}, {"col1": 2, "col2": "b"}])
return {"input_type": "DataFrame", "df": sample_df, "file_format": "csv", "file_path": "./test_output.csv"}
@pytest.fixture
def file_names_mapping(self):
"""Return the file names mapping for different versions."""
return [] # New component
def test_basic_setup(self, component_class, default_kwargs):
"""Test basic component initialization."""
component = component_class()
component.set_attributes(default_kwargs)
assert component.input_type == "DataFrame"
assert component.file_format == "csv"
assert component.file_path == "./test_output.csv"
def test_update_build_config_dataframe(self, component_class):
"""Test build config update for DataFrame input type."""
component = component_class()
build_config = {
"df": {"show": False},
"data": {"show": False},
"message": {"show": False},
"file_format": {"options": []},
}
updated_config = component.update_build_config(build_config, "DataFrame", "input_type")
assert updated_config["df"]["show"] is True
assert updated_config["data"]["show"] is False
assert updated_config["message"]["show"] is False
assert set(updated_config["file_format"]["options"]) == set(component.DATA_FORMAT_CHOICES)
def test_save_message(self, component_class):
"""Test saving Message to different formats."""
test_cases = [
("txt", "Test message"),
("json", json.dumps({"message": "Test message"}, indent=2)),
("markdown", "**Message:**\n\nTest message"),
]
for fmt, expected_content in test_cases:
mock_file = MagicMock()
mock_parent = MagicMock()
mock_parent.exists.return_value = True
mock_file.parent = mock_parent
mock_file.expanduser.return_value = mock_file
# Mock Path at the module level where it's imported
with patch("langflow.components.processing.save_to_file.Path") as mock_path:
mock_path.return_value = mock_file
component = component_class()
component.set_attributes(
{
"input_type": "Message",
"message": Message(text="Test message"),
"file_format": fmt,
"file_path": f"./test_output.{fmt}",
}
)
result = component.save_to_file()
mock_file.write_text.assert_called_once_with(expected_content, encoding="utf-8")
assert "saved successfully" in result
def test_save_data(self, component_class):
"""Test saving Data object to JSON."""
test_data = {"col1": ["value1"], "col2": ["value2"]}
mock_file = MagicMock()
mock_parent = MagicMock()
mock_parent.exists.return_value = True
mock_file.parent = mock_parent
mock_file.expanduser.return_value = mock_file
with patch("langflow.components.processing.save_to_file.Path") as mock_path:
mock_path.return_value = mock_file
component = component_class()
component.set_attributes(
{
"input_type": "Data",
"data": Data(data=test_data),
"file_format": "json",
"file_path": "./test_output.json",
}
)
result = component.save_to_file()
expected_json = json.dumps(test_data, indent=2)
mock_file.write_text.assert_called_once_with(expected_json, encoding="utf-8")
assert "saved successfully" in result
def test_directory_creation(self, component_class, default_kwargs):
"""Test directory creation if it doesn't exist."""
mock_file = MagicMock()
mock_parent = MagicMock()
mock_parent.exists.return_value = False
mock_file.parent = mock_parent
mock_file.expanduser.return_value = mock_file
with patch("langflow.components.processing.save_to_file.Path") as mock_path:
mock_path.return_value = mock_file
with patch.object(pd.DataFrame, "to_csv") as mock_to_csv:
component = component_class()
component.set_attributes(default_kwargs)
result = component.save_to_file()
mock_parent.mkdir.assert_called_once_with(parents=True, exist_ok=True)
assert mock_to_csv.called
assert "saved successfully" in result
def test_invalid_input_type(self, default_kwargs):
"""Test handling of invalid input type."""
component = SaveToFileComponent()
invalid_kwargs = default_kwargs.copy() # Create a copy to modify
invalid_kwargs["input_type"] = "InvalidType"
component.set_attributes(invalid_kwargs)
with pytest.raises(ValueError, match="Unsupported input type"):
component.save_to_file()

View File

@ -1,110 +0,0 @@
import json
import pytest
from langflow.services.database.utils import truncate_json
@pytest.fixture
def small_json():
return [
{"name": "Cole Ramos", "email": "egestas.fusce.aliquet@google.couk"},
{"name": "Chancellor Torres", "email": "lorem.eu@hotmail.com"},
{"name": "Deanna Lyons", "email": "neque.venenatis.lacus@outlook.couk"},
{"name": "Ruby O'connor", "email": "lectus.justo.eu@hotmail.couk"},
{"name": "Iona Dorsey", "email": "rutrum@yahoo.org"},
]
@pytest.fixture
def large_json():
return [
{
"name": "Nash Briggs",
"phone": "1-827-252-5669",
"email": "magna.ut@icloud.edu",
"address": "847-2983 Vel Rd.",
"list": 5,
"country": "South Korea",
"region": "Gilgit Baltistan",
"postalZip": "6088-8521",
"text": "ipsum. Curabitur consequat, lectus sit amet luctus vulputate, nisi sem",
"alphanumeric": "OJG47QKX4DO",
"currency": "$46.88",
"numberrange": 6,
},
{
"name": "Keefe Cooley",
"phone": "(164) 954-5395",
"email": "congue.turpis.in@protonmail.ca",
"address": "Ap #674-3382 Egestas. St.",
"list": 3,
"country": "Spain",
"region": "Antioquia",
"postalZip": "42452",
"text": "nisl. Nulla eu neque pellentesque massa lobortis ultrices. Vivamus rhoncus.",
"alphanumeric": "FIE81ZDK2RI",
"currency": "$37.74",
"numberrange": 3,
},
{
"name": "Randall Booth",
"phone": "(762) 778-9833",
"email": "a@icloud.edu",
"address": "Ap #116-8418 Nec Ave",
"list": 9,
"country": "Norway",
"region": "Prince Edward Island",
"postalZip": "39155",
"text": "tempor arcu. Vestibulum ut eros non enim commodo hendrerit. Donec",
"alphanumeric": "GMF33SGB4XD",
"currency": "$87.24",
"numberrange": 0,
},
{
"name": "Aurora Mooney",
"phone": "(626) 435-3885",
"email": "morbi.sit.amet@icloud.org",
"address": "837-8038 Duis Rd.",
"list": 15,
"country": "United States",
"region": "West Sulawesi",
"postalZip": "84466-29328",
"text": "metus eu erat semper rutrum. Fusce dolor quam, elementum at,",
"alphanumeric": "CVK31QJA8GZ",
"currency": "$85.97",
"numberrange": 1,
},
{
"name": "Irma Snider",
"phone": "1-682-186-4584",
"email": "senectus.et@hotmail.org",
"address": "718-8593 Mauris. Avenue",
"list": 13,
"country": "Italy",
"region": "East Region",
"postalZip": "47178",
"text": "Cras convallis convallis dolor. Quisque tincidunt pede ac urna. Ut",
"alphanumeric": "KXR03TWX8QA",
"currency": "$65.54",
"numberrange": 3,
},
]
def test_truncate_json__small_case(small_json):
max_size = 400
result = truncate_json(small_json, max_size=max_size)
assert len(str(small_json)) < max_size, "small_json must be smaller than max_size"
assert result == small_json, "small_json should not be truncated"
def test_truncate_json__large_case(large_json):
max_size = 1000
result = truncate_json(large_json, max_size=max_size)
assert len(str(large_json)) > max_size, "large_json must be larger than max_size"
assert len(str(result)) < len(str(large_json)), "result must be smaller than large_json"
assert json.dumps(result), "result must be a valid JSON object"

View File

@ -1,42 +1,63 @@
import json
import asyncio
import uuid
from uuid import UUID
import pytest
from httpx import codes
from langflow.memory import aget_messages
from langflow.services.database.models.flow import FlowCreate, FlowUpdate
from orjson import orjson
from langflow.services.database.models.flow import FlowUpdate
from tests.unit.build_utils import build_flow, consume_and_assert_stream, create_flow, get_build_events
@pytest.mark.benchmark
async def test_build_flow(client, json_memory_chatbot_no_llm, logged_in_headers):
flow_id = await _create_flow(client, json_memory_chatbot_no_llm, logged_in_headers)
"""Test the build flow endpoint with the new two-step process."""
# First create the flow
flow_id = await create_flow(client, json_memory_chatbot_no_llm, logged_in_headers)
async with client.stream("POST", f"api/v1/build/{flow_id}/flow", json={}, headers=logged_in_headers) as r:
await consume_and_assert_stream(r)
# Start the build and get job_id
build_response = await build_flow(client, flow_id, logged_in_headers)
job_id = build_response["job_id"]
assert job_id is not None
await check_messages(flow_id)
# Get the events stream
events_response = await get_build_events(client, job_id, logged_in_headers)
assert events_response.status_code == codes.OK
# Consume and verify the events
await consume_and_assert_stream(events_response, job_id)
@pytest.mark.benchmark
async def test_build_flow_from_request_data(client, json_memory_chatbot_no_llm, logged_in_headers):
flow_id = await _create_flow(client, json_memory_chatbot_no_llm, logged_in_headers)
response = await client.get("api/v1/flows/" + str(flow_id), headers=logged_in_headers)
"""Test building a flow from request data."""
flow_id = await create_flow(client, json_memory_chatbot_no_llm, logged_in_headers)
response = await client.get(f"api/v1/flows/{flow_id}", headers=logged_in_headers)
flow_data = response.json()
async with client.stream(
"POST", f"api/v1/build/{flow_id}/flow", json={"data": flow_data["data"]}, headers=logged_in_headers
) as r:
await consume_and_assert_stream(r)
# Start the build and get job_id
build_response = await build_flow(client, flow_id, logged_in_headers, json={"data": flow_data["data"]})
job_id = build_response["job_id"]
# Get the events stream
events_response = await get_build_events(client, job_id, logged_in_headers)
assert events_response.status_code == codes.OK
# Consume and verify the events
await consume_and_assert_stream(events_response, job_id)
await check_messages(flow_id)
async def test_build_flow_with_frozen_path(client, json_memory_chatbot_no_llm, logged_in_headers):
flow_id = await _create_flow(client, json_memory_chatbot_no_llm, logged_in_headers)
"""Test building a flow with a frozen path."""
flow_id = await create_flow(client, json_memory_chatbot_no_llm, logged_in_headers)
response = await client.get("api/v1/flows/" + str(flow_id), headers=logged_in_headers)
response = await client.get(f"api/v1/flows/{flow_id}", headers=logged_in_headers)
flow_data = response.json()
flow_data["data"]["nodes"][0]["data"]["node"]["frozen"] = True
# Update the flow with frozen path
response = await client.patch(
f"api/v1/flows/{flow_id}",
json=FlowUpdate(name="Flow", description="description", data=flow_data["data"]).model_dump(),
@ -44,151 +65,131 @@ async def test_build_flow_with_frozen_path(client, json_memory_chatbot_no_llm, l
)
response.raise_for_status()
async with client.stream("POST", f"api/v1/build/{flow_id}/flow", json={}, headers=logged_in_headers) as r:
await consume_and_assert_stream(r)
# Start the build and get job_id
build_response = await build_flow(client, flow_id, logged_in_headers)
job_id = build_response["job_id"]
# Get the events stream
events_response = await get_build_events(client, job_id, logged_in_headers)
assert events_response.status_code == codes.OK
# Consume and verify the events
await consume_and_assert_stream(events_response, job_id)
await check_messages(flow_id)
async def check_messages(flow_id):
messages = await aget_messages(flow_id=UUID(flow_id), order="ASC")
if isinstance(flow_id, str):
flow_id = UUID(flow_id)
messages = await aget_messages(flow_id=flow_id, order="ASC")
flow_id_str = str(flow_id)
assert len(messages) == 2
assert messages[0].session_id == flow_id
assert messages[0].session_id == flow_id_str
assert messages[0].sender == "User"
assert messages[0].sender_name == "User"
assert messages[0].text == ""
assert messages[1].session_id == flow_id
assert messages[1].session_id == flow_id_str
assert messages[1].sender == "Machine"
assert messages[1].sender_name == "AI"
async def consume_and_assert_stream(r):
count = 0
async for line in r.aiter_lines():
# httpx split by \n, but ndjson sends two \n for each line
if not line:
continue
parsed = json.loads(line)
if count == 0:
assert parsed["event"] == "vertices_sorted"
ids = parsed["data"]["ids"]
ids.sort()
assert ids == ["ChatInput-CIGht"]
to_run = parsed["data"]["to_run"]
to_run.sort()
assert to_run == ["ChatInput-CIGht", "ChatOutput-QA7ej", "Memory-amN4Z", "Prompt-iWbCC"]
elif count > 0 and count < 5:
assert parsed["event"] == "end_vertex"
assert parsed["data"]["build_data"] is not None
elif count == 5:
assert parsed["event"] == "end"
else:
msg = f"Unexpected line: {line}"
raise ValueError(msg)
count += 1
@pytest.mark.benchmark
async def test_build_flow_invalid_job_id(client, logged_in_headers):
"""Test getting events for an invalid job ID."""
invalid_job_id = str(uuid.uuid4())
response = await get_build_events(client, invalid_job_id, logged_in_headers)
assert response.status_code == codes.NOT_FOUND
assert "No queue found for job_id" in response.json()["detail"]
async def _create_flow(client, json_memory_chatbot_no_llm, logged_in_headers):
vector_store = orjson.loads(json_memory_chatbot_no_llm)
data = vector_store["data"]
vector_store = FlowCreate(name="Flow", description="description", data=data, endpoint_name="f")
response = await client.post("api/v1/flows/", json=vector_store.model_dump(), headers=logged_in_headers)
response.raise_for_status()
return response.json()["id"]
@pytest.mark.benchmark
async def test_build_flow_invalid_flow_id(client, logged_in_headers):
"""Test starting a build with an invalid flow ID."""
invalid_flow_id = uuid.uuid4()
response = await client.post(f"api/v1/build/{invalid_flow_id}/flow", json={}, headers=logged_in_headers)
assert response.status_code == codes.NOT_FOUND
# TODO: Fix this test
# async def test_multiple_runs_with_no_payload_generate_max_vertex_builds(
# client, json_memory_chatbot_no_llm, logged_in_headers
# ):
# """Test that multiple builds of a flow generate the correct number of vertex builds."""
# # Create the initial flow
# flow_id = await _create_flow(client, json_memory_chatbot_no_llm, logged_in_headers)
@pytest.mark.benchmark
async def test_build_flow_start_only(client, json_memory_chatbot_no_llm, logged_in_headers):
"""Test only the build flow start endpoint."""
# First create the flow
flow_id = await create_flow(client, json_memory_chatbot_no_llm, logged_in_headers)
# # Get the flow data to count nodes before making requests
# response = await client.get(f"api/v1/flows/{flow_id}", headers=logged_in_headers)
# flow_data = response.json()
# num_nodes = len(flow_data["data"]["nodes"])
# max_vertex_builds = get_settings_service().settings.max_vertex_builds_per_vertex
# Start the build and get job_id
build_response = await build_flow(client, flow_id, logged_in_headers)
# logger.debug(f"Starting test with {num_nodes} nodes, max_vertex_builds={max_vertex_builds}")
# Assert response structure
assert "job_id" in build_response
assert isinstance(build_response["job_id"], str)
# Verify it's a valid UUID
assert uuid.UUID(build_response["job_id"])
# # Make multiple build requests - ensure we exceed max_vertex_builds significantly
# num_requests = max_vertex_builds * 3 # Triple the max to ensure rotation
# for i in range(num_requests):
# # Generate a random session ID for each request
# session_id = session_id_generator()
# payload = {"inputs": {"session": session_id, "type": "chat", "input_value": f"Test message {i + 1}"}}
# async with client.stream("POST", f"api/v1/build/{flow_id}/flow",
# json=payload, headers=logged_in_headers) as r:
# await consume_and_assert_stream(r)
@pytest.mark.benchmark
async def test_build_flow_start_with_inputs(client, json_memory_chatbot_no_llm, logged_in_headers):
"""Test the build flow start endpoint with input data."""
flow_id = await create_flow(client, json_memory_chatbot_no_llm, logged_in_headers)
# # Add a small delay between requests to ensure proper ordering
# await asyncio.sleep(0.1)
# Start build with some input data
test_inputs = {"inputs": {"session": "test_session", "input_value": "test message"}}
# # Track builds after each request
# async with session_scope() as session:
# builds = await get_vertex_builds_by_flow_id(db=session, flow_id=flow_id)
# by_vertex = {}
# for build in builds:
# build_dict = build.model_dump()
# vertex_id = build_dict.get("id")
# by_vertex.setdefault(vertex_id, []).append(build_dict)
build_response = await build_flow(client, flow_id, logged_in_headers, json=test_inputs)
# # Log state of each vertex with more details
# for vertex_id, vertex_builds in by_vertex.items():
# vertex_builds.sort(key=lambda x: x.get("timestamp"))
# logger.debug(
# f"Request {i + 1} (session={session_id}) - Vertex {vertex_id}: {len(vertex_builds)} builds "
# f"(max allowed: {max_vertex_builds}), "
# f"build_ids: {[b.get('build_id') for b in vertex_builds]}"
# )
assert "job_id" in build_response
assert isinstance(build_response["job_id"], str)
assert uuid.UUID(build_response["job_id"])
# # Wait a bit before final verification to ensure all DB operations complete
# await asyncio.sleep(0.5)
# # Final verification with detailed logging
# async with session_scope() as session:
# vertex_builds = await get_vertex_builds_by_flow_id(db=session, flow_id=flow_id)
# assert len(vertex_builds) > 0, "No vertex builds found"
@pytest.mark.benchmark
async def test_build_flow_polling(client, json_memory_chatbot_no_llm, logged_in_headers):
"""Test the build flow endpoint with polling (non-streaming)."""
# First create the flow
flow_id = await create_flow(client, json_memory_chatbot_no_llm, logged_in_headers)
# builds_by_vertex = {}
# for build in vertex_builds:
# build_dict = build.model_dump()
# vertex_id = build_dict.get("id")
# builds_by_vertex.setdefault(vertex_id, []).append(build_dict)
# Start the build and get job_id
build_response = await build_flow(client, flow_id, logged_in_headers)
job_id = build_response["job_id"]
assert job_id is not None
# # Log detailed final state
# logger.debug(f"\nFinal state after {num_requests} requests:")
# for vertex_id, builds in builds_by_vertex.items():
# builds.sort(key=lambda x: x.get("timestamp"))
# logger.debug(
# f"Vertex {vertex_id}: {len(builds)} builds "
# f"(oldest: {builds[0].get('timestamp')}, "
# f"newest: {builds[-1].get('timestamp')}), "
# f"build_ids: {[b.get('build_id') for b in builds]}"
# )
# Create a response object that mimics a streaming response but uses polling
class PollingResponse:
def __init__(self, client, job_id, headers):
self.client = client
self.job_id = job_id
self.headers = headers
self.status_code = codes.OK
# # Log individual build details for debugging
# for build in builds:
# logger.debug(
# f" - Build {build.get('build_id')}: timestamp={build.get('timestamp')}, "
# f"valid={build.get('valid')}"
# )
async def aiter_lines(self):
try:
sleeps = 0
max_sleeps = 100
while True:
response = await self.client.get(
f"api/v1/build/{self.job_id}/events?stream=false", headers=self.headers
)
assert response.status_code == codes.OK
data = response.json()
# # Verify each vertex has correct number of builds
# for vertex_id, vertex_builds_list in builds_by_vertex.items():
# assert len(vertex_builds_list) == max_vertex_builds, (
# f"Vertex {vertex_id} has {len(vertex_builds_list)} builds, expected {max_vertex_builds}"
# )
if data["event"] is None:
# No event available, add delay to prevent tight polling
await asyncio.sleep(0.1)
sleeps += 1
continue
# # Verify total number of builds
# total_builds = len(vertex_builds)
# expected_total = max_vertex_builds * num_nodes
# assert total_builds == expected_total, (
# f"Total builds ({total_builds}) doesn't match expected "
# f"({max_vertex_builds} builds/vertex * {num_nodes} nodes = {expected_total})"
# )
# assert all(vertex_build.get("valid") for vertex_build in vertex_builds)
yield data["event"]
# If this was the end event, stop polling
if '"end"' in data["event"]:
break
if sleeps > max_sleeps:
msg = "Build event polling timed out."
raise TimeoutError(msg)
except asyncio.TimeoutError as e:
msg = "Build event polling timed out."
raise TimeoutError(msg) from e
polling_response = PollingResponse(client, job_id, logged_in_headers)
# Use the same consume_and_assert_stream function to verify the events
await consume_and_assert_stream(polling_response, job_id)

View File

@ -46,7 +46,7 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"dompurify": "^3.1.5",
"dompurify": "^3.2.4",
"dotenv": "^16.4.5",
"elkjs": "^0.9.3",
"emoji-regex": "^10.3.0",
@ -7348,9 +7348,10 @@
}
},
"node_modules/dompurify": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.3.tgz",
"integrity": "sha512-U1U5Hzc2MO0oW3DF+G9qYN0aT7atAou4AgI0XjWz061nyBPbdxkfdhfy5uMgGn6+oLFCfn44ZGbdDqCzVmlOWA==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz",
"integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}

View File

@ -41,7 +41,7 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"dompurify": "^3.1.5",
"dompurify": "^3.2.4",
"dotenv": "^16.4.5",
"elkjs": "^0.9.3",
"emoji-regex": "^10.3.0",

View File

@ -66,8 +66,6 @@ body {
}
.jv-indent {
overflow-y: auto !important;
max-height: 310px !important;
border-radius: 10px;
}

View File

@ -21,8 +21,8 @@ export function DefaultEdge({
const targetHandleObject = scapeJSONParse(targetHandleId!);
const sourceXNew =
(sourceNode?.position.x ?? 0) + (sourceNode?.measured?.width ?? 0);
const targetXNew = targetNode?.position.x ?? 0;
(sourceNode?.position.x ?? 0) + (sourceNode?.measured?.width ?? 0) + 7;
const targetXNew = (targetNode?.position.x ?? 0) - 7;
const distance = 200 + 0.1 * ((sourceXNew - targetXNew) / 2);
@ -42,15 +42,18 @@ export function DefaultEdge({
200 * (1 - zeroOnNegative) +
0.3 * Math.abs(sourceY - targetY) * zeroOnNegative;
const edgePathLoop = `M ${sourceXNew} ${sourceY} C ${sourceXNew + distance} ${sourceY + sourceDistanceY}, ${targetXNew - distance} ${targetY + distanceY}, ${targetXNew} ${targetY}`;
const targetYNew = targetY + 1;
const sourceYNew = sourceY + 1;
const edgePathLoop = `M ${sourceXNew} ${sourceYNew} C ${sourceXNew + distance} ${sourceYNew + sourceDistanceY}, ${targetXNew - distance} ${targetYNew + distanceY}, ${targetXNew} ${targetYNew}`;
const [edgePath] = getBezierPath({
sourceX: sourceXNew,
sourceY,
sourceY: sourceYNew,
sourcePosition: Position.Right,
targetPosition: Position.Left,
targetX: targetXNew,
targetY,
targetY: targetYNew,
});
return (

Some files were not shown because too many files have changed in this diff Show More