From 0a5f25f32b3aba5e378e8ea3c10da80f4510a565 Mon Sep 17 00:00:00 2001 From: Adam Aghili Date: Tue, 26 May 2026 00:00:12 -0400 Subject: [PATCH] chore: port Remove the diskcache dependency port of https://github.com/langflow-ai/langflow/pull/12953 --- docs/docs/Develop/memory.mdx | 105 ++++++++------- pyproject.toml | 1 - .../base/langflow/services/cache/disk.py | 95 ------------- .../base/langflow/services/cache/factory.py | 6 - .../langflow/services/flow_events/factory.py | 11 +- .../langflow/services/flow_events/service.py | 114 ++++++++++++---- src/backend/base/pyproject.toml | 5 - src/backend/base/uv.lock | 123 ++--------------- .../tests/unit/test_flow_events_service.py | 34 ++++- src/lfx/src/lfx/services/settings/base.py | 6 +- uv.lock | 125 ++---------------- 11 files changed, 202 insertions(+), 423 deletions(-) delete mode 100644 src/backend/base/langflow/services/cache/disk.py diff --git a/docs/docs/Develop/memory.mdx b/docs/docs/Develop/memory.mdx index 49743931f1..65e92b5153 100644 --- a/docs/docs/Develop/memory.mdx +++ b/docs/docs/Develop/memory.mdx @@ -14,26 +14,26 @@ Langflow's default storage option is a [SQLite](https://www.sqlite.org/) databas The default storage path depends on your operating system and installation method: - **Langflow Desktop**: - - **macOS**: `/Users//.langflow/data/database.db` - - **Windows**: `C:\Users\\AppData\Roaming\com.LangflowDesktop\data\database.db` + - **macOS**: `/Users//.langflow/data/database.db` + - **Windows**: `C:\Users\\AppData\Roaming\com.LangflowDesktop\data\database.db` - **Langflow OSS** - - **macOS/Windows/Linux/WSL with `uv pip install`**: `/lib/python3.12/site-packages/langflow/langflow.db` (Python version can vary. Database isn't shared between virtual environments because it is tied to the venv path.) - - **macOS/Windows/Linux/WSL with `git clone`**: `/src/backend/base/langflow/langflow.db` + - **macOS/Windows/Linux/WSL with `uv pip install`**: `/lib/python3.12/site-packages/langflow/langflow.db` (Python version can vary. Database isn't shared between virtual environments because it is tied to the venv path.) + - **macOS/Windows/Linux/WSL with `git clone`**: `/src/backend/base/langflow/langflow.db` Langflow offers a few alternatives to the default database path: -* **Config directory**: Set `LANGFLOW_SAVE_DB_IN_CONFIG_DIR=True` to store the database in your Langflow config directory as set in [`LANGFLOW_CONFIG_DIR`](/logging). +- **Config directory**: Set `LANGFLOW_SAVE_DB_IN_CONFIG_DIR=True` to store the database in your Langflow config directory as set in [`LANGFLOW_CONFIG_DIR`](/logging). -* **External PostgreSQL database**: You can use an external PostgreSQL database for all of your Langflow storage. -For more information, see [Configure external memory](#configure-external-memory) +- **External PostgreSQL database**: You can use an external PostgreSQL database for all of your Langflow storage. + For more information, see [Configure external memory](#configure-external-memory) - External storage can be useful if you want to preserve the data after uninstalling Langflow or to share the same database between multiple virtual environments. + External storage can be useful if you want to preserve the data after uninstalling Langflow or to share the same database between multiple virtual environments. -* **Separate chat memory**: You can selectively use external storage for chat memory only, separate from other Langflow storage. -For more information, see [Store chat memory](#store-chat-memory). +- **Separate chat memory**: You can selectively use external storage for chat memory only, separate from other Langflow storage. + For more information, see [Store chat memory](#store-chat-memory). -* **No database**: To disable all database operations and run a no-op session, set `LANGFLOW_USE_NOOP_DATABASE=True` in your [Langflow environment variables](/environment-variables). -This is useful for testing when you don't want to persist any data. +- **No database**: To disable all database operations and run a no-op session, set `LANGFLOW_USE_NOOP_DATABASE=True` in your [Langflow environment variables](/environment-variables). + This is useful for testing when you don't want to persist any data. ## Langflow database tables @@ -78,31 +78,30 @@ LANGFLOW_DATABASE_URL=postgresql://user:password@localhost:5432/langflow To fine-tune your database connection pool and timeout settings, you can set the following additional environment variables: -* `LANGFLOW_DATABASE_CONNECTION_RETRY`: Whether to retry lost connections to your Langflow database. If `true`, Langflow tries to connect to the database again if the connection fails. Default: `false`. +- `LANGFLOW_DATABASE_CONNECTION_RETRY`: Whether to retry lost connections to your Langflow database. If `true`, Langflow tries to connect to the database again if the connection fails. Default: `false`. -* `LANGFLOW_DB_CONNECT_TIMEOUT`: The number of seconds to wait before giving up on a lock to be released or establishing a connection to the database. This may be separate from the `pool_timeout` in `LANGFLOW_DB_CONNECTION_SETTINGS`. Default: 30. +- `LANGFLOW_DB_CONNECT_TIMEOUT`: The number of seconds to wait before giving up on a lock to be released or establishing a connection to the database. This may be separate from the `pool_timeout` in `LANGFLOW_DB_CONNECTION_SETTINGS`. Default: 30. -* `LANGFLOW_MIGRATION_LOCK_NAMESPACE`: Optional namespace identifier for PostgreSQL advisory lock during migrations. If not provided, a hash of the database URL will be used. Useful when multiple Langflow instances share the same database and need coordinated migration locking +- `LANGFLOW_MIGRATION_LOCK_NAMESPACE`: Optional namespace identifier for PostgreSQL advisory lock during migrations. If not provided, a hash of the database URL will be used. Useful when multiple Langflow instances share the same database and need coordinated migration locking -* `LANGFLOW_DB_CONNECTION_SETTINGS`: A JSON dictionary containing the following database connection pool settings: +- `LANGFLOW_DB_CONNECTION_SETTINGS`: A JSON dictionary containing the following database connection pool settings: + - `pool_size`: The base number of connections to keep open in the connection pool. Default: 20. + - `max_overflow`: Maximum number of connections that can be created in excess of `pool_size` if needed. Default: 30. + - `pool_timeout`: Number of seconds to wait for a connection from the pool before timing out. Default: 30. + - `pool_pre_ping`: If `true`, the pool tests connections for liveness upon each checkout. Default: `true`. + - `pool_recycle`: Number of seconds after which a connection is automatically recycled. Default: 1800 (30 minutes). + - `echo`: If `true`, SQL queries are logged for debugging purposes. Default: `false`. - - `pool_size`: The base number of connections to keep open in the connection pool. Default: 20. - - `max_overflow`: Maximum number of connections that can be created in excess of `pool_size` if needed. Default: 30. - - `pool_timeout`: Number of seconds to wait for a connection from the pool before timing out. Default: 30. - - `pool_pre_ping`: If `true`, the pool tests connections for liveness upon each checkout. Default: `true`. - - `pool_recycle`: Number of seconds after which a connection is automatically recycled. Default: 1800 (30 minutes). - - `echo`: If `true`, SQL queries are logged for debugging purposes. Default: `false`. + For example: - For example: + ```text + LANGFLOW_DB_CONNECTION_SETTINGS='{"pool_size": 20, "max_overflow": 30, "pool_timeout": 30, "pool_pre_ping": true, "pool_recycle": 1800, "echo": false}' + ``` - ```text - LANGFLOW_DB_CONNECTION_SETTINGS='{"pool_size": 20, "max_overflow": 30, "pool_timeout": 30, "pool_pre_ping": true, "pool_recycle": 1800, "echo": false}' - ``` + Don't use the deprecated environment variables `LANGFLOW_DB_POOL_SIZE` or `LANGFLOW_DB_MAX_OVERFLOW`. + Instead, use `pool_size` and `max_overflow` in `LANGFLOW_DB_CONNECTION_SETTINGS`. - Don't use the deprecated environment variables `LANGFLOW_DB_POOL_SIZE` or `LANGFLOW_DB_MAX_OVERFLOW`. - Instead, use `pool_size` and `max_overflow` in `LANGFLOW_DB_CONNECTION_SETTINGS`. - -* `LANGFLOW_MIGRATION_LOCK_NAMESPACE`: Optional namespace for PostgreSQL advisory locks used during database migrations. This is useful when running multiple Langflow instances that share the same PostgreSQL database. Each instance should use a unique namespace to avoid conflicts. If not set, Langflow uses a default namespace. This setting only applies when using PostgreSQL as your database backend. +- `LANGFLOW_MIGRATION_LOCK_NAMESPACE`: Optional namespace for PostgreSQL advisory locks used during database migrations. This is useful when running multiple Langflow instances that share the same PostgreSQL database. Each instance should use a unique namespace to avoid conflicts. If not set, Langflow uses a default namespace. This setting only applies when using PostgreSQL as your database backend. ## Configure cache memory @@ -117,15 +116,15 @@ Langflow officially supports only the default asynchronous, in-memory cache, whi Other cache options, such as Redis, are experimental and can change without notice. If you want to use a non-default cache setting, you can use the following environment variables: -| Variable | Type | Default | Description | -|----------|------|---------|-------------| -| `LANGFLOW_CACHE_TYPE` | String | `async` | Set the cache type for Langflow's internal caching system. Possible values: `async`, `redis`, `memory`, `disk`. If you set the type to `redis`, then you must also set the `LANGFLOW_REDIS_*` environment variables. | -| `LANGFLOW_LANGCHAIN_CACHE` | String | `InMemoryCache` | Set the cache storage type for the LangChain caching system (a Langflow dependency), either `InMemoryCache` or `SQLiteCache`. | -| `LANGFLOW_REDIS_HOST` | String | `localhost` | Redis server hostname if `LANGFLOW_CACHE_TYPE=redis`. | -| `LANGFLOW_REDIS_PORT` | Integer | `6379` | Redis server port if `LANGFLOW_CACHE_TYPE=redis`. | -| `LANGFLOW_REDIS_DB` | Integer | `0` | Redis database number if `LANGFLOW_CACHE_TYPE=redis`. | -| `LANGFLOW_REDIS_CACHE_EXPIRE` | Integer | `3600` | Cache expiration time in seconds if `LANGFLOW_CACHE_TYPE=redis`. | -| `LANGFLOW_REDIS_PASSWORD` | String | Not set | Optional password for Redis authentication if `LANGFLOW_CACHE_TYPE=redis`. | +| Variable | Type | Default | Description | +| ----------------------------- | ------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `LANGFLOW_CACHE_TYPE` | String | `async` | Set the cache type for Langflow's internal caching system. Possible values: `async`, `redis`, `memory`. If you set the type to `redis`, then you must also set the `LANGFLOW_REDIS_*` environment variables. The `disk` backend was removed in 1.9.4 — switch to `async` for in-memory or `redis` for cross-worker. | +| `LANGFLOW_LANGCHAIN_CACHE` | String | `InMemoryCache` | Set the cache storage type for the LangChain caching system (a Langflow dependency), either `InMemoryCache` or `SQLiteCache`. | +| `LANGFLOW_REDIS_HOST` | String | `localhost` | Redis server hostname if `LANGFLOW_CACHE_TYPE=redis`. | +| `LANGFLOW_REDIS_PORT` | Integer | `6379` | Redis server port if `LANGFLOW_CACHE_TYPE=redis`. | +| `LANGFLOW_REDIS_DB` | Integer | `0` | Redis database number if `LANGFLOW_CACHE_TYPE=redis`. | +| `LANGFLOW_REDIS_CACHE_EXPIRE` | Integer | `3600` | Cache expiration time in seconds if `LANGFLOW_CACHE_TYPE=redis`. | +| `LANGFLOW_REDIS_PASSWORD` | String | Not set | Optional password for Redis authentication if `LANGFLOW_CACHE_TYPE=redis`. | ## Store chat memory @@ -161,27 +160,27 @@ For example, if you use user IDs as session IDs, then each user's chat history i Where and how chat memory is stored depends on the components used in your flow: -* **Agent component**: This component has built-in chat memory that is enabled by default. -This memory allows the agent to retrieve and reference messages from previous conversations associated with the same session ID. -All messages are stored in [Langflow storage](#storage-options-and-paths), and the component provides minimal memory configuration options, such as the number of messages to retrieve. +- **Agent component**: This component has built-in chat memory that is enabled by default. + This memory allows the agent to retrieve and reference messages from previous conversations associated with the same session ID. + All messages are stored in [Langflow storage](#storage-options-and-paths), and the component provides minimal memory configuration options, such as the number of messages to retrieve. - The **Agent** component's built-in chat memory is sufficient for most use cases. + The **Agent** component's built-in chat memory is sufficient for most use cases. - If you want to use external chat memory storage, retrieve memories outside the context of a chat, or use chat memory with a language model component (not an agent), you must use the **Message History** component (with or without a third-party chat memory component). + If you want to use external chat memory storage, retrieve memories outside the context of a chat, or use chat memory with a language model component (not an agent), you must use the **Message History** component (with or without a third-party chat memory component). -* **Message History component**: By default, this component stores and retrieves memories from Langflow storage, unless you attach a third-party chat memory component. It provides a few more options for sorting and filtering memories, although most of these options are also built-in to the **Agent** component as configurable or fixed parameters. +- **Message History component**: By default, this component stores and retrieves memories from Langflow storage, unless you attach a third-party chat memory component. It provides a few more options for sorting and filtering memories, although most of these options are also built-in to the **Agent** component as configurable or fixed parameters. - You can use the **Message History** component with or without a language model or agent. - For example, if you need to retrieve data from memories outside of chat, you can use the **Message History** component to fetch that data directly from your chat memory database without feeding it into a chat. + You can use the **Message History** component with or without a language model or agent. + For example, if you need to retrieve data from memories outside of chat, you can use the **Message History** component to fetch that data directly from your chat memory database without feeding it into a chat. -* **Third-party chat memory components**: Use one of these components only if you need to store or retrieve chat memories from a dedicated external chat memory database. -Typically, this is necessary only if you have specific storage needs that aren't met by Langflow storage. -For example, if you want to manage chat memory data by directly working with the database, or if you want to use a different database than the default Langflow storage. +- **Third-party chat memory components**: Use one of these components only if you need to store or retrieve chat memories from a dedicated external chat memory database. + Typically, this is necessary only if you have specific storage needs that aren't met by Langflow storage. + For example, if you want to manage chat memory data by directly working with the database, or if you want to use a different database than the default Langflow storage. For more information and examples, see [**Message History** component](/message-history) and [Agent memory](/agents#agent-memory). ## See also -* [Langflow file management](/concepts-file-management) -* [Langflow logs](/logging) -* [Langflow environment variables](/environment-variables) \ No newline at end of file +- [Langflow file management](/concepts-file-management) +- [Langflow logs](/logging) +- [Langflow environment variables](/environment-variables) diff --git a/pyproject.toml b/pyproject.toml index ae8a2d8fa8..b7631342db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -115,7 +115,6 @@ cassio = [ "cassio>=0.1.7" ] local = [ - "llama-cpp-python~=0.2.0", "sentence-transformers>=2.3.1", "ctransformers>=0.2.10" ] diff --git a/src/backend/base/langflow/services/cache/disk.py b/src/backend/base/langflow/services/cache/disk.py deleted file mode 100644 index 04f9bc9c2c..0000000000 --- a/src/backend/base/langflow/services/cache/disk.py +++ /dev/null @@ -1,95 +0,0 @@ -import asyncio -import pickle -import time -from typing import Generic - -from diskcache import Cache -from lfx.log.logger import logger -from lfx.services.cache.utils import CACHE_MISS - -from langflow.services.cache.base import AsyncBaseCacheService, AsyncLockType - - -class AsyncDiskCache(AsyncBaseCacheService, Generic[AsyncLockType]): - def __init__(self, cache_dir, max_size=None, expiration_time=3600) -> None: - self.cache = Cache(cache_dir) - # Let's clear the cache for now to maintain a similar - # behavior as the in-memory cache - # Later we should implement endpoints for the frontend to grab - # output logs from the cache - if len(self.cache) > 0: - self.cache.clear() - self.lock = asyncio.Lock() - self.max_size = max_size - self.expiration_time = expiration_time - - async def get(self, key, lock: asyncio.Lock | None = None): - if not lock: - async with self.lock: - return await asyncio.to_thread(self._get, key) - else: - return await asyncio.to_thread(self._get, key) - - def _get(self, key): - item = self.cache.get(key, default=None) - if item: - if time.time() - item["time"] < self.expiration_time: - self.cache.touch(key) # Refresh the expiry time - return pickle.loads(item["value"]) if isinstance(item["value"], bytes) else item["value"] - logger.info(f"Cache item for key '{key}' has expired and will be deleted.") - self.cache.delete(key) # Log before deleting the expired item - return CACHE_MISS - - async def set(self, key, value, lock: asyncio.Lock | None = None) -> None: - if not lock: - async with self.lock: - await self._set(key, value) - else: - await self._set(key, value) - - async def _set(self, key, value) -> None: - if self.max_size and len(self.cache) >= self.max_size: - await asyncio.to_thread(self.cache.cull) - item = {"value": pickle.dumps(value) if not isinstance(value, str | bytes) else value, "time": time.time()} - await asyncio.to_thread(self.cache.set, key, item) - - async def delete(self, key, lock: asyncio.Lock | None = None) -> None: - if not lock: - async with self.lock: - await self._delete(key) - else: - await self._delete(key) - - async def _delete(self, key) -> None: - await asyncio.to_thread(self.cache.delete, key) - - async def clear(self, lock: asyncio.Lock | None = None) -> None: - if not lock: - async with self.lock: - await self._clear() - else: - await self._clear() - - async def _clear(self) -> None: - await asyncio.to_thread(self.cache.clear) - - async def upsert(self, key, value, lock: asyncio.Lock | None = None) -> None: - if not lock: - async with self.lock: - await self._upsert(key, value) - else: - await self._upsert(key, value) - - async def _upsert(self, key, value) -> None: - existing_value = await asyncio.to_thread(self._get, key) - if existing_value is not CACHE_MISS and isinstance(existing_value, dict) and isinstance(value, dict): - existing_value.update(value) - value = existing_value - await self.set(key, value) - - async def contains(self, key) -> bool: - return await asyncio.to_thread(self.cache.__contains__, key) - - async def teardown(self) -> None: - # Clean up the cache directory - self.cache.clear(retry=True) diff --git a/src/backend/base/langflow/services/cache/factory.py b/src/backend/base/langflow/services/cache/factory.py index b0f08c15e6..8ce6eaba74 100644 --- a/src/backend/base/langflow/services/cache/factory.py +++ b/src/backend/base/langflow/services/cache/factory.py @@ -5,7 +5,6 @@ from typing import TYPE_CHECKING from lfx.log.logger import logger from typing_extensions import override -from langflow.services.cache.disk import AsyncDiskCache from langflow.services.cache.service import AsyncInMemoryCache, CacheService, RedisCache, ThreadingInMemoryCache from langflow.services.factory import ServiceFactory @@ -36,9 +35,4 @@ class CacheServiceFactory(ServiceFactory): return ThreadingInMemoryCache(expiration_time=settings_service.settings.cache_expire) if settings_service.settings.cache_type == "async": return AsyncInMemoryCache(expiration_time=settings_service.settings.cache_expire) - if settings_service.settings.cache_type == "disk": - return AsyncDiskCache( - cache_dir=settings_service.settings.config_dir, - expiration_time=settings_service.settings.cache_expire, - ) return None diff --git a/src/backend/base/langflow/services/flow_events/factory.py b/src/backend/base/langflow/services/flow_events/factory.py index af6db6fda8..1bdc6916da 100644 --- a/src/backend/base/langflow/services/flow_events/factory.py +++ b/src/backend/base/langflow/services/flow_events/factory.py @@ -1,13 +1,20 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + from typing_extensions import override from langflow.services.factory import ServiceFactory from langflow.services.flow_events.service import FlowEventsService +if TYPE_CHECKING: + from lfx.services.settings.service import SettingsService + class FlowEventsServiceFactory(ServiceFactory): def __init__(self) -> None: super().__init__(FlowEventsService) @override - def create(self): - return FlowEventsService() + def create(self, settings_service: SettingsService): + return FlowEventsService(cache_dir=settings_service.settings.cache_dir) diff --git a/src/backend/base/langflow/services/flow_events/service.py b/src/backend/base/langflow/services/flow_events/service.py index ec4498d363..60594d5bea 100644 --- a/src/backend/base/langflow/services/flow_events/service.py +++ b/src/backend/base/langflow/services/flow_events/service.py @@ -1,14 +1,13 @@ from __future__ import annotations -import json +import sqlite3 import tempfile +import threading import time -from dataclasses import asdict, dataclass +from dataclasses import dataclass from pathlib import Path from typing import Literal, get_args -from diskcache import Cache - from langflow.services.base import Service FLOW_EVENT_TYPES = Literal[ @@ -29,12 +28,25 @@ class FlowEvent: summary: str = "" -class FlowEventsService(Service): - """Disk-backed event queue keyed by flow_id. +_SCHEMA = """ +CREATE TABLE IF NOT EXISTS flow_events ( + flow_id TEXT NOT NULL, + ts REAL NOT NULL, + type TEXT NOT NULL, + summary TEXT NOT NULL DEFAULT '', + expires_at REAL NOT NULL +); +CREATE INDEX IF NOT EXISTS idx_flow_events_flow_ts ON flow_events(flow_id, ts); +CREATE INDEX IF NOT EXISTS idx_flow_events_expires ON flow_events(expires_at); +""" - Uses diskcache for cross-worker visibility (multiple uvicorn/gunicorn workers - share the same SQLite-backed cache directory). TTL-based cleanup is handled - by diskcache's built-in expiry. + +class FlowEventsService(Service): + """SQLite-backed event queue keyed by flow_id. + + Uses Python's stdlib sqlite3 in WAL mode for cross-worker visibility (multiple + uvicorn/gunicorn workers share the same on-disk database file). TTL-based + cleanup is performed lazily on each write and read. Limitations: - Events are ephemeral: lost on disk cleanup or container restart. @@ -49,28 +61,67 @@ class FlowEventsService(Service): SETTLE_TIMEOUT: float = 10.0 MAX_EVENTS_PER_FLOW: int = 1000 + _VALID_EVENT_TYPES: frozenset[str] = frozenset(get_args(FLOW_EVENT_TYPES)) + def __init__(self, cache_dir: str | Path | None = None) -> None: if cache_dir is None: cache_dir = Path(tempfile.gettempdir()) / "langflow_flow_events" - self._cache = Cache(str(cache_dir)) - - _VALID_EVENT_TYPES: frozenset[str] = frozenset(get_args(FLOW_EVENT_TYPES)) + cache_dir = Path(cache_dir) + cache_dir.mkdir(parents=True, exist_ok=True) + self._db_path = cache_dir / "flow_events.sqlite" + # isolation_level=None puts pysqlite in autocommit mode so we manage + # transactions explicitly via BEGIN/COMMIT. + self._conn = sqlite3.connect( + str(self._db_path), + isolation_level=None, + check_same_thread=False, + timeout=5.0, + ) + # Serialize use of the single connection across asyncio tasks / FastAPI threads + # in this worker. WAL handles cross-worker concurrency. + self._lock = threading.Lock() + with self._lock: + self._conn.execute("PRAGMA journal_mode=WAL") + self._conn.execute("PRAGMA synchronous=NORMAL") + self._conn.execute("PRAGMA busy_timeout=5000") + self._conn.executescript(_SCHEMA) def append(self, flow_id: str, event_type: str, summary: str = "") -> FlowEvent: if event_type not in self._VALID_EVENT_TYPES: msg = f"Invalid event type: {event_type!r}. Must be one of {sorted(self._VALID_EVENT_TYPES)}" raise ValueError(msg) - event = FlowEvent(type=event_type, timestamp=time.time(), summary=summary) - key = f"flow_events:{flow_id}" + now = time.time() + event = FlowEvent(type=event_type, timestamp=now, summary=summary) + expires_at = now + self.TTL_SECONDS - with self._cache.transact(): - raw = self._cache.get(key, default=None) - events: list[dict] = json.loads(raw) if raw else [] - events.append(asdict(event)) - # Trim to max size - if len(events) > self.MAX_EVENTS_PER_FLOW: - events = events[-self.MAX_EVENTS_PER_FLOW :] - self._cache.set(key, json.dumps(events), expire=self.TTL_SECONDS) + with self._lock: + self._conn.execute("BEGIN IMMEDIATE") + try: + # Opportunistic TTL cleanup across all flows. + self._conn.execute("DELETE FROM flow_events WHERE expires_at < ?", (now,)) + self._conn.execute( + "INSERT INTO flow_events (flow_id, ts, type, summary, expires_at) VALUES (?, ?, ?, ?, ?)", + (flow_id, now, event_type, summary, expires_at), + ) + # Bound per-flow size: keep only the most recent MAX_EVENTS_PER_FLOW rows. + # Order by (ts, rowid) so events appended in the same microsecond have a + # stable, insertion-aware ordering when picking which rows to drop. + self._conn.execute( + """ + DELETE FROM flow_events + WHERE rowid IN ( + SELECT rowid FROM flow_events + WHERE flow_id = ? + ORDER BY ts DESC, rowid DESC + LIMIT -1 OFFSET ? + ) + """, + (flow_id, self.MAX_EVENTS_PER_FLOW), + ) + self._conn.execute("COMMIT") + except Exception: + self._conn.execute("ROLLBACK") + raise return event @@ -82,9 +133,19 @@ class FlowEventsService(Service): - A flow_settled event exists after `since`, OR - The most recent event is older than SETTLE_TIMEOUT seconds. """ - key = f"flow_events:{flow_id}" - raw = self._cache.get(key, default=None) - all_events = [FlowEvent(**e) for e in json.loads(raw)] if raw else [] + now = time.time() + with self._lock: + rows = self._conn.execute( + """ + SELECT type, ts, summary + FROM flow_events + WHERE flow_id = ? AND expires_at >= ? + ORDER BY ts ASC, rowid ASC + """, + (flow_id, now), + ).fetchall() + + all_events = [FlowEvent(type=r[0], timestamp=r[1], summary=r[2]) for r in rows] after = [e for e in all_events if e.timestamp > since] @@ -100,4 +161,5 @@ class FlowEventsService(Service): return after, settled async def teardown(self) -> None: - self._cache.close() + with self._lock: + self._conn.close() diff --git a/src/backend/base/pyproject.toml b/src/backend/base/pyproject.toml index 1f806ee5e0..4221e07afb 100644 --- a/src/backend/base/pyproject.toml +++ b/src/backend/base/pyproject.toml @@ -77,7 +77,6 @@ dependencies = [ "filelock>=3.20.1,<4.0.0", "grandalf>=0.8.0,<1.0.0", "spider-client>=0.0.27,<1.0.0", - "diskcache>=5.6.3,<6.0.0", "clickhouse-connect==0.7.19", "assemblyai>=0.33.0,<1.0.0", "fastapi-pagination>=0.13.1,<1.0.0", @@ -182,7 +181,6 @@ postgresql = [ ] local = [ - "llama-cpp-python>=0.2.0", "sentence-transformers>=2.0.0", "ctransformers>=0.2" ] @@ -234,7 +232,6 @@ sambanova = ["langchain-sambanova~=1.0.0"] groq = ["langchain-groq~=1.1.1"] # Individual local LLM providers -llama-cpp = ["llama-cpp-python~=0.2.0"] sentence-transformers = ["sentence-transformers>=2.3.1"] ctransformers = ["ctransformers>=0.2.10"] @@ -252,7 +249,6 @@ pytube = ["pytube==15.0.0"] # Individual agent frameworks # crewai is commented out due to httpx version conflict # crewai = ["crewai>=0.126.0"] -dspy = ["dspy-ai==2.5.41"] smolagents = ["smolagents>=1.8.0"] # Individual tools @@ -410,7 +406,6 @@ complete = [ "langflow-base[docx]", "langflow-base[pytube]", # "langflow-base[crewai]", # commented out due to httpx version conflict - "langflow-base[dspy]", "langflow-base[smolagents]", "langflow-base[beautifulsoup]", "langflow-base[serpapi]", diff --git a/src/backend/base/uv.lock b/src/backend/base/uv.lock index ab6966d260..4400162f5f 100644 --- a/src/backend/base/uv.lock +++ b/src/backend/base/uv.lock @@ -718,14 +718,16 @@ wheels = [ [[package]] name = "asyncer" -version = "0.0.8" +version = "0.0.17" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, + { name = "sniffio" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ff/67/7ea59c3e69eaeee42e7fc91a5be67ca5849c8979acac2b920249760c6af2/asyncer-0.0.8.tar.gz", hash = "sha256:a589d980f57e20efb07ed91d0dbe67f1d2fd343e7142c66d3a099f05c620739c", size = 18217, upload-time = "2024-08-24T23:15:36.449Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d2/4c/62b6044679e08788322bbd0dee5b487a6f7f60bb4e2bd45617ff0d94d1e3/asyncer-0.0.17.tar.gz", hash = "sha256:8a41e185e7ec2ecd583c269d72907a0f9f832e744b6c7474aeb21e349c4becf4", size = 19516, upload-time = "2026-02-21T16:35:54.068Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/04/15b6ca6b7842eda2748bda0a0af73f2d054e9344320f8bba01f994294bcb/asyncer-0.0.8-py3-none-any.whl", hash = "sha256:5920d48fc99c8f8f0f1576e1882f5022885589c5fcbc46ce4224ec3e53776eeb", size = 9209, upload-time = "2024-08-24T23:15:35.317Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c5/b72735a095b4b3170b34150e89314a40dd0450d0eb6746b331cb664479d1/asyncer-0.0.17-py3-none-any.whl", hash = "sha256:b0055950e094fb84fd8d21611c7e7b6f5715ddcb57c522c058f64c20badd1438", size = 9252, upload-time = "2026-02-21T16:35:55.022Z" }, ] [[package]] @@ -2761,15 +2763,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" }, ] -[[package]] -name = "diskcache" -version = "5.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" }, -] - [[package]] name = "distlib" version = "0.4.0" @@ -2978,48 +2971,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, ] -[[package]] -name = "dspy" -version = "3.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "asyncer" }, - { name = "cachetools" }, - { name = "cloudpickle" }, - { name = "diskcache" }, - { name = "gepa" }, - { name = "json-repair" }, - { name = "litellm" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "openai" }, - { name = "optuna" }, - { name = "orjson" }, - { name = "pydantic" }, - { name = "regex" }, - { name = "requests" }, - { name = "tenacity" }, - { name = "tqdm" }, - { name = "xxhash" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d9/bd/339e1d949aa24e50b6edca8574cf8afbff005a1dfadc2d5b82a0003fa7da/dspy-3.1.0.tar.gz", hash = "sha256:3c1660cb687411f064509cd7ac47ed0231761f50ed92823cbbb59b3af2885702", size = 242611, upload-time = "2026-01-06T18:50:18.655Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/34/64e900657cad3c4df8a417fccbc4370ca851864edbd0f468210f1b57d084/dspy-3.1.0-py3-none-any.whl", hash = "sha256:b9f4d42cad9ac32b13fdf085dd3246c8ee8339c759cb291c5e7b7f9f5fb5dd72", size = 291317, upload-time = "2026-01-06T18:50:17.559Z" }, -] - -[[package]] -name = "dspy-ai" -version = "2.5.41" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "dspy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d6/25/94659c581e69fe11237294aeadf55785abec09361aa2d3a63bd0abc39d82/dspy-ai-2.5.41.tar.gz", hash = "sha256:a2804434eb3354d6e41f59a448938886914fed0c502eb42503569776a3f2f002", size = 258174, upload-time = "2024-11-29T03:17:24.301Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/50/f715b62acfe29718c3575dde16eb35e3ab6a5642652b13fb229bd758c040/dspy_ai-2.5.41-py3-none-any.whl", hash = "sha256:4f366a6b610ee93fd04b92b0dff2f78b24e23ee74c71d20bcf9fedcbf0d7aa97", size = 339737, upload-time = "2024-11-29T03:17:22.8Z" }, -] - [[package]] name = "duckdb" version = "1.5.3" @@ -4178,15 +4129,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/af/90/3bc780df088d439714af8295196a4332a26559ae66fd99865e36f92efa9e/geomet-1.1.0-py3-none-any.whl", hash = "sha256:4372fe4e286a34acc6f2e9308284850bd8c4aa5bc12065e2abbd4995900db12f", size = 31522, upload-time = "2023-11-14T15:43:35.305Z" }, ] -[[package]] -name = "gepa" -version = "0.0.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/be/9a4c31c65c4910e73bd4a316df99347681bce4f2ef6d10877cc1ed8d57da/gepa-0.0.22.tar.gz", hash = "sha256:0b13a644efd2af52186e456eaad2ae28fcf88c6f796a9c999910c587863d4315", size = 116337, upload-time = "2025-11-10T21:39:27.044Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/0e/d41272b28f2163f5bf840b508cbaf4a0bc01eaeaa6e5d447558d1050eb3b/gepa-0.0.22-py3-none-any.whl", hash = "sha256:ff57ee0442399a5cc62d01411935476bc80cfa8f1c318bed82e3db4c2c834665", size = 119666, upload-time = "2025-11-10T21:39:26.028Z" }, -] - [[package]] name = "gitdb" version = "4.0.12" @@ -6635,7 +6577,6 @@ dependencies = [ { name = "clickhouse-connect" }, { name = "cryptography" }, { name = "defusedxml" }, - { name = "diskcache" }, { name = "docstring-parser" }, { name = "duckdb" }, { name = "dynaconf" }, @@ -6750,7 +6691,6 @@ all = [ { name = "ddgs" }, { name = "docling", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, { name = "docling-core" }, - { name = "dspy-ai" }, { name = "easyocr", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, { name = "elasticsearch" }, { name = "faiss-cpu", version = "1.9.0.post1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, @@ -6920,7 +6860,6 @@ complete = [ { name = "ddgs" }, { name = "docling", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, { name = "docling-core" }, - { name = "dspy-ai" }, { name = "easyocr", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, { name = "elasticsearch" }, { name = "faiss-cpu", version = "1.9.0.post1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, @@ -7038,9 +6977,6 @@ docling = [ docx = [ { name = "python-docx" }, ] -dspy = [ - { name = "dspy-ai" }, -] duckduckgo = [ { name = "ddgs" }, ] @@ -7132,12 +7068,8 @@ lark = [ litellm = [ { name = "litellm" }, ] -llama-cpp = [ - { name = "llama-cpp-python" }, -] local = [ { name = "ctransformers" }, - { name = "llama-cpp-python" }, { name = "sentence-transformers" }, ] markdown = [ @@ -7361,11 +7293,9 @@ requires-dist = [ { name = "datasets", marker = "extra == 'datasets'", specifier = ">2.14.7,<5.0.0" }, { name = "ddgs", marker = "extra == 'duckduckgo'", specifier = ">=9.0.0" }, { name = "defusedxml", specifier = ">=0.7.1,<1.0.0" }, - { name = "diskcache", specifier = ">=5.6.3,<6.0.0" }, { name = "docling", marker = "(platform_machine != 'x86_64' and extra == 'docling') or (sys_platform != 'darwin' and extra == 'docling')", specifier = ">=2.36.1,<3.0.0" }, { name = "docling-core", marker = "extra == 'docling'", specifier = ">=2.77.0,<3.0.0" }, { name = "docstring-parser", specifier = ">=0.16,<1.0.0" }, - { name = "dspy-ai", marker = "extra == 'dspy'", specifier = "==2.5.41" }, { name = "duckdb", specifier = ">=1.0.0,<2.0.0" }, { name = "dynaconf", specifier = ">=3.2.13,<4.0.0" }, { name = "easyocr", marker = "(platform_machine != 'x86_64' and extra == 'easyocr') or (sys_platform != 'darwin' and extra == 'easyocr')", specifier = ">=1.7.2,<2.0.0" }, @@ -7466,7 +7396,6 @@ requires-dist = [ { name = "langflow-base", extras = ["datasets"], marker = "extra == 'complete'" }, { name = "langflow-base", extras = ["docling"], marker = "extra == 'complete'" }, { name = "langflow-base", extras = ["docx"], marker = "extra == 'complete'" }, - { name = "langflow-base", extras = ["dspy"], marker = "extra == 'complete'" }, { name = "langflow-base", extras = ["duckduckgo"], marker = "extra == 'complete'" }, { name = "langflow-base", extras = ["easyocr"], marker = "extra == 'complete'" }, { name = "langflow-base", extras = ["elasticsearch"], marker = "extra == 'complete'" }, @@ -7550,8 +7479,6 @@ requires-dist = [ { name = "lark", marker = "extra == 'lark'", specifier = "==1.2.2" }, { name = "lfx", editable = "../../lfx" }, { name = "litellm", marker = "extra == 'litellm'", specifier = ">=1.83.0" }, - { name = "llama-cpp-python", marker = "extra == 'llama-cpp'", specifier = "~=0.2.0" }, - { name = "llama-cpp-python", marker = "extra == 'local'", specifier = ">=0.2.0" }, { name = "loguru", specifier = ">=0.7.1,<1.0.0" }, { name = "markdown", marker = "extra == 'markdown'", specifier = ">=3.8.0" }, { name = "markupsafe", marker = "extra == 'markup'", specifier = "==3.0.2" }, @@ -7644,7 +7571,7 @@ requires-dist = [ { name = "youtube-transcript-api", marker = "extra == 'youtube'", specifier = ">=1.0.0,<2.0.0" }, { name = "zep-python", marker = "extra == 'zep'", specifier = "==2.0.2" }, ] -provides-extras = ["audio", "postgresql", "local", "couchbase", "cassandra", "clickhouse", "mongodb", "supabase", "redis", "elasticsearch", "faiss", "qdrant", "qdrant-vectors", "weaviate", "chroma", "upstash", "pinecone", "milvus", "mongodb-vectors", "astradb", "openai", "anthropic", "cohere", "google", "ollama", "nvidia", "mistral", "sambanova", "groq", "llama-cpp", "sentence-transformers", "ctransformers", "langfuse", "langwatch", "langsmith", "arize", "pypdf", "docx", "pytube", "dspy", "smolagents", "beautifulsoup", "serpapi", "google-api", "wikipedia", "fake-useragent", "duckduckgo", "yfinance", "wolframalpha", "pyarrow", "fastavro", "gitpython", "nltk", "lark", "huggingface", "metaphor", "metal", "markup", "aws", "numexpr", "qianfan", "pgvector", "litellm", "zep", "youtube", "markdown", "kubernetes", "json-repair", "composio", "ragstack", "opensearch", "atlassian", "mem0", "needle", "sseclient", "openinference", "mcp", "ag2", "scrapegraph", "apify", "spider", "altk", "toolguard", "langchain-huggingface", "langchain-unstructured", "graph-retriever", "langchain-mcp-adapters", "opik", "traceloop", "openlayer", "docling", "easyocr", "cleanlab", "twelvelabs", "jigsawstack", "assemblyai", "datasets", "fastparquet", "vlmrun", "cuga", "mlx", "fastmcp", "aioboto3", "astrapy", "gassist", "ibm-watsonx-clients", "complete", "all"] +provides-extras = ["audio", "postgresql", "local", "couchbase", "cassandra", "clickhouse", "mongodb", "supabase", "redis", "elasticsearch", "faiss", "qdrant", "qdrant-vectors", "weaviate", "chroma", "upstash", "pinecone", "milvus", "mongodb-vectors", "astradb", "openai", "anthropic", "cohere", "google", "ollama", "nvidia", "mistral", "sambanova", "groq", "sentence-transformers", "ctransformers", "langfuse", "langwatch", "langsmith", "arize", "pypdf", "docx", "pytube", "smolagents", "beautifulsoup", "serpapi", "google-api", "wikipedia", "fake-useragent", "duckduckgo", "yfinance", "wolframalpha", "pyarrow", "fastavro", "gitpython", "nltk", "lark", "huggingface", "metaphor", "metal", "markup", "aws", "numexpr", "qianfan", "pgvector", "litellm", "zep", "youtube", "markdown", "kubernetes", "json-repair", "composio", "ragstack", "opensearch", "atlassian", "mem0", "needle", "sseclient", "openinference", "mcp", "ag2", "scrapegraph", "apify", "spider", "altk", "toolguard", "langchain-huggingface", "langchain-unstructured", "graph-retriever", "langchain-mcp-adapters", "opik", "traceloop", "openlayer", "docling", "easyocr", "cleanlab", "twelvelabs", "jigsawstack", "assemblyai", "datasets", "fastparquet", "vlmrun", "cuga", "mlx", "fastmcp", "aioboto3", "astrapy", "gassist", "ibm-watsonx-clients", "complete", "all"] [package.metadata.requires-dev] dev = [ @@ -8093,7 +8020,7 @@ wheels = [ [[package]] name = "litellm" -version = "1.86.0" +version = "1.86.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -8109,24 +8036,11 @@ dependencies = [ { name = "tiktoken" }, { name = "tokenizers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/a7/26b8b04e4fcff26b60200ffe7458a255552ae51014468188f5db45674eb2/litellm-1.86.0.tar.gz", hash = "sha256:eccab86e0820b60b3f9484b233fb8d818b97afb19d5b4fa08d0d045621350ba4", size = 15379195, upload-time = "2026-05-24T02:42:10.865Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/7c/a94c15e1146f76a6e0dfe0394f824cdfeb9dadb988c6f698a35f6c1b8d4f/litellm-1.86.1.tar.gz", hash = "sha256:616100384073f2ddec1a5391b34c806c7c99e6af4511741434809caa46075e13", size = 15379863, upload-time = "2026-05-26T03:51:58.624Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/0b/9a044c061a69e801de042e962c34f5bc2e094810e28b49ce0b3bedee9327/litellm-1.86.0-py3-none-any.whl", hash = "sha256:9d8171ca1a17705b7c7a6fdce8cfc07bbf641284b46c1b6047f83a779159990c", size = 17011225, upload-time = "2026-05-24T02:42:00.629Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/e6e874857f45ff47762c6c29ec3dad9ea4757300e034400642cab90e4982/litellm-1.86.1-py3-none-any.whl", hash = "sha256:54e52372d326c642e9cc76d39afffb9b22387cbff3694c5e2758c9f860bca2e9", size = 17012257, upload-time = "2026-05-26T03:51:49.991Z" }, ] -[[package]] -name = "llama-cpp-python" -version = "0.2.90" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "diskcache" }, - { name = "jinja2" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1f/19/89836022affc1bf470e2485e28872b489254a66fe587155edba731a07112/llama_cpp_python-0.2.90.tar.gz", hash = "sha256:419b041c62dbdb9f7e67883a6ef2f247d583d08417058776be0bff05b4ec9e3d", size = 63762953, upload-time = "2024-08-29T07:00:35.267Z" } - [[package]] name = "llm-sandbox" version = "0.3.39" @@ -10694,25 +10608,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/72/58dcedaca964398d64824b0d85311e89031cf13ec100ee0440c65d87e5cc/opik-2.0.47-py3-none-any.whl", hash = "sha256:aa1a1aadbff5c804355a6645e55b78739fe44d6d055d74676495a42d756c4451", size = 1623856, upload-time = "2026-05-25T04:44:03.994Z" }, ] -[[package]] -name = "optuna" -version = "4.8.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "alembic" }, - { name = "colorlog" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "packaging" }, - { name = "pyyaml" }, - { name = "sqlalchemy" }, - { name = "tqdm" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bf/9b/62f120fb2ecbc4338bee70c5a3671c8e561714f3aa1a046b897ff142050e/optuna-4.8.0.tar.gz", hash = "sha256:6f7043e9f8ecb5e607af86a7eb00fb5ec2be26c3b08c201209a73d36aff37a38", size = 482603, upload-time = "2026-03-16T04:59:58.659Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/24/7c731839566d30dc70556d9824ef17692d896c15e3df627bce8c16f753e1/optuna-4.8.0-py3-none-any.whl", hash = "sha256:c57a7682679c36bfc9bca0da430698179e513874074b71bebedb0334964ab930", size = 419456, upload-time = "2026-03-16T04:59:56.977Z" }, -] - [[package]] name = "orjson" version = "3.11.9" diff --git a/src/backend/tests/unit/test_flow_events_service.py b/src/backend/tests/unit/test_flow_events_service.py index 8341d5eb34..c1b2f4e3ad 100644 --- a/src/backend/tests/unit/test_flow_events_service.py +++ b/src/backend/tests/unit/test_flow_events_service.py @@ -1,8 +1,15 @@ +import multiprocessing import time from langflow.services.flow_events.service import FlowEventsService +def _worker_append(cache_dir, flow_id, event_type, summary): + """Top-level helper used by multiprocessing tests; must be picklable.""" + svc = FlowEventsService(cache_dir=cache_dir) + svc.append(flow_id, event_type, summary) + + def test_append_and_get_events(tmp_path): svc = FlowEventsService(cache_dir=tmp_path / "cache") svc.append("flow-1", "component_added", "Added OpenAI") @@ -60,8 +67,7 @@ def test_ttl_cleanup(tmp_path): time.sleep(0.15) - # Force re-append to reset TTL for the key, then read - # Actually just read -- the key should have expired via diskcache expire + # The row should be filtered out by the expires_at >= now() predicate in get_since. events, _ = svc.get_since("flow-1", 0.0) assert len(events) == 0 @@ -202,3 +208,27 @@ def test_cross_worker_ttl_expiry(tmp_path): worker_b = FlowEventsService(cache_dir=shared_dir) events, _ = worker_b.get_since("flow-1", 0.0) assert len(events) == 0 + + +def test_multi_process_visibility(tmp_path): + """A separate OS process can write events that this process reads. + + Proves the WAL-mode sqlite backend gives the same cross-worker contract that + the prior diskcache-based implementation did under multi-uvicorn/gunicorn. + """ + shared_dir = tmp_path / "shared_cache" + + # Use spawn so the child does not inherit the parent's sqlite handle. + ctx = multiprocessing.get_context("spawn") + proc = ctx.Process( + target=_worker_append, + args=(str(shared_dir), "flow-mp", "component_added", "from-other-process"), + ) + proc.start() + proc.join(timeout=10) + assert proc.exitcode == 0, "Child process failed to append event" + + reader = FlowEventsService(cache_dir=shared_dir) + events, _ = reader.get_since("flow-mp", 0.0) + assert len(events) == 1 + assert events[0].summary == "from-other-process" diff --git a/src/lfx/src/lfx/services/settings/base.py b/src/lfx/src/lfx/services/settings/base.py index ede8aa4b8d..a6e4afa0b7 100644 --- a/src/lfx/src/lfx/services/settings/base.py +++ b/src/lfx/src/lfx/services/settings/base.py @@ -180,10 +180,10 @@ class Settings(BaseSettings): Controlled by LANGFLOW_USE_NOOP_DATABASE env variable.""" # cache configuration - cache_type: Literal["async", "redis", "memory", "disk"] = "async" - """The cache type can be 'async' or 'redis'.""" + cache_type: Literal["async", "redis", "memory"] = "async" + """The cache backend: 'async' (default in-memory), 'memory' (sync in-memory), or 'redis'.""" cache_expire: int = 3600 - """The cache expire in seconds.""" + """Directory used by FlowEventsService for cross-worker event storage. Defaults to a temp dir if not set.""" variable_store: str = "db" """The store can be 'db' or 'kubernetes'.""" diff --git a/uv.lock b/uv.lock index ff1f351f64..cddc10ad64 100644 --- a/uv.lock +++ b/uv.lock @@ -746,14 +746,16 @@ wheels = [ [[package]] name = "asyncer" -version = "0.0.8" +version = "0.0.17" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, + { name = "sniffio" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ff/67/7ea59c3e69eaeee42e7fc91a5be67ca5849c8979acac2b920249760c6af2/asyncer-0.0.8.tar.gz", hash = "sha256:a589d980f57e20efb07ed91d0dbe67f1d2fd343e7142c66d3a099f05c620739c", size = 18217, upload-time = "2024-08-24T23:15:36.449Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d2/4c/62b6044679e08788322bbd0dee5b487a6f7f60bb4e2bd45617ff0d94d1e3/asyncer-0.0.17.tar.gz", hash = "sha256:8a41e185e7ec2ecd583c269d72907a0f9f832e744b6c7474aeb21e349c4becf4", size = 19516, upload-time = "2026-02-21T16:35:54.068Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/04/15b6ca6b7842eda2748bda0a0af73f2d054e9344320f8bba01f994294bcb/asyncer-0.0.8-py3-none-any.whl", hash = "sha256:5920d48fc99c8f8f0f1576e1882f5022885589c5fcbc46ce4224ec3e53776eeb", size = 9209, upload-time = "2024-08-24T23:15:35.317Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c5/b72735a095b4b3170b34150e89314a40dd0450d0eb6746b331cb664479d1/asyncer-0.0.17-py3-none-any.whl", hash = "sha256:b0055950e094fb84fd8d21611c7e7b6f5715ddcb57c522c058f64c20badd1438", size = 9252, upload-time = "2026-02-21T16:35:55.022Z" }, ] [[package]] @@ -2851,15 +2853,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" }, ] -[[package]] -name = "diskcache" -version = "5.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" }, -] - [[package]] name = "distlib" version = "0.4.0" @@ -3072,48 +3065,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, ] -[[package]] -name = "dspy" -version = "3.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "asyncer" }, - { name = "cachetools" }, - { name = "cloudpickle" }, - { name = "diskcache" }, - { name = "gepa" }, - { name = "json-repair" }, - { name = "litellm" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "openai" }, - { name = "optuna" }, - { name = "orjson" }, - { name = "pydantic" }, - { name = "regex" }, - { name = "requests" }, - { name = "tenacity" }, - { name = "tqdm" }, - { name = "xxhash" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d9/bd/339e1d949aa24e50b6edca8574cf8afbff005a1dfadc2d5b82a0003fa7da/dspy-3.1.0.tar.gz", hash = "sha256:3c1660cb687411f064509cd7ac47ed0231761f50ed92823cbbb59b3af2885702", size = 242611, upload-time = "2026-01-06T18:50:18.655Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/34/64e900657cad3c4df8a417fccbc4370ca851864edbd0f468210f1b57d084/dspy-3.1.0-py3-none-any.whl", hash = "sha256:b9f4d42cad9ac32b13fdf085dd3246c8ee8339c759cb291c5e7b7f9f5fb5dd72", size = 291317, upload-time = "2026-01-06T18:50:17.559Z" }, -] - -[[package]] -name = "dspy-ai" -version = "2.5.41" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "dspy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d6/25/94659c581e69fe11237294aeadf55785abec09361aa2d3a63bd0abc39d82/dspy-ai-2.5.41.tar.gz", hash = "sha256:a2804434eb3354d6e41f59a448938886914fed0c502eb42503569776a3f2f002", size = 258174, upload-time = "2024-11-29T03:17:24.301Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/50/f715b62acfe29718c3575dde16eb35e3ab6a5642652b13fb229bd758c040/dspy_ai-2.5.41-py3-none-any.whl", hash = "sha256:4f366a6b610ee93fd04b92b0dff2f78b24e23ee74c71d20bcf9fedcbf0d7aa97", size = 339737, upload-time = "2024-11-29T03:17:22.8Z" }, -] - [[package]] name = "duckdb" version = "1.5.3" @@ -4320,15 +4271,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/af/90/3bc780df088d439714af8295196a4332a26559ae66fd99865e36f92efa9e/geomet-1.1.0-py3-none-any.whl", hash = "sha256:4372fe4e286a34acc6f2e9308284850bd8c4aa5bc12065e2abbd4995900db12f", size = 31522, upload-time = "2023-11-14T15:43:35.305Z" }, ] -[[package]] -name = "gepa" -version = "0.0.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/be/9a4c31c65c4910e73bd4a316df99347681bce4f2ef6d10877cc1ed8d57da/gepa-0.0.22.tar.gz", hash = "sha256:0b13a644efd2af52186e456eaad2ae28fcf88c6f796a9c999910c587863d4315", size = 116337, upload-time = "2025-11-10T21:39:27.044Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/0e/d41272b28f2163f5bf840b508cbaf4a0bc01eaeaa6e5d447558d1050eb3b/gepa-0.0.22-py3-none-any.whl", hash = "sha256:ff57ee0442399a5cc62d01411935476bc80cfa8f1c318bed82e3db4c2c834665", size = 119666, upload-time = "2025-11-10T21:39:26.028Z" }, -] - [[package]] name = "gevent" version = "25.9.1" @@ -7002,7 +6944,6 @@ docling = [ ] local = [ { name = "ctransformers" }, - { name = "llama-cpp-python" }, { name = "sentence-transformers" }, ] nv-ingest = [ @@ -7063,7 +7004,6 @@ requires-dist = [ { name = "ctransformers", marker = "extra == 'local'", specifier = ">=0.2.10" }, { name = "langchain-docling", marker = "extra == 'docling'", specifier = ">=1.1.0" }, { name = "langflow-base", extras = ["complete"], editable = "src/backend/base" }, - { name = "llama-cpp-python", marker = "extra == 'local'", specifier = "~=0.2.0" }, { name = "nv-ingest-api", marker = "python_full_version >= '3.12' and extra == 'nv-ingest'", specifier = ">=26.1.0,<27.0.0" }, { name = "nv-ingest-client", marker = "python_full_version >= '3.12' and extra == 'nv-ingest'", specifier = ">=26.1.0,<27.0.0" }, { name = "ocrmac", marker = "sys_platform == 'darwin' and extra == 'docling'", specifier = ">=1.0.0" }, @@ -7137,7 +7077,6 @@ dependencies = [ { name = "clickhouse-connect" }, { name = "cryptography" }, { name = "defusedxml" }, - { name = "diskcache" }, { name = "docstring-parser" }, { name = "duckdb" }, { name = "dynaconf" }, @@ -7252,7 +7191,6 @@ all = [ { name = "ddgs" }, { name = "docling", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, { name = "docling-core" }, - { name = "dspy-ai" }, { name = "easyocr", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, { name = "elasticsearch" }, { name = "faiss-cpu", version = "1.9.0.post1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, @@ -7422,7 +7360,6 @@ complete = [ { name = "ddgs" }, { name = "docling", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, { name = "docling-core" }, - { name = "dspy-ai" }, { name = "easyocr", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, { name = "elasticsearch" }, { name = "faiss-cpu", version = "1.9.0.post1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, @@ -7540,9 +7477,6 @@ docling = [ docx = [ { name = "python-docx" }, ] -dspy = [ - { name = "dspy-ai" }, -] duckduckgo = [ { name = "ddgs" }, ] @@ -7634,12 +7568,8 @@ lark = [ litellm = [ { name = "litellm" }, ] -llama-cpp = [ - { name = "llama-cpp-python" }, -] local = [ { name = "ctransformers" }, - { name = "llama-cpp-python" }, { name = "sentence-transformers" }, ] markdown = [ @@ -7863,11 +7793,9 @@ requires-dist = [ { name = "datasets", marker = "extra == 'datasets'", specifier = ">2.14.7,<5.0.0" }, { name = "ddgs", marker = "extra == 'duckduckgo'", specifier = ">=9.0.0" }, { name = "defusedxml", specifier = ">=0.7.1,<1.0.0" }, - { name = "diskcache", specifier = ">=5.6.3,<6.0.0" }, { name = "docling", marker = "(platform_machine != 'x86_64' and extra == 'docling') or (sys_platform != 'darwin' and extra == 'docling')", specifier = ">=2.36.1,<3.0.0" }, { name = "docling-core", marker = "extra == 'docling'", specifier = ">=2.77.0,<3.0.0" }, { name = "docstring-parser", specifier = ">=0.16,<1.0.0" }, - { name = "dspy-ai", marker = "extra == 'dspy'", specifier = "==2.5.41" }, { name = "duckdb", specifier = ">=1.0.0,<2.0.0" }, { name = "dynaconf", specifier = ">=3.2.13,<4.0.0" }, { name = "easyocr", marker = "(platform_machine != 'x86_64' and extra == 'easyocr') or (sys_platform != 'darwin' and extra == 'easyocr')", specifier = ">=1.7.2,<2.0.0" }, @@ -7968,7 +7896,6 @@ requires-dist = [ { name = "langflow-base", extras = ["datasets"], marker = "extra == 'complete'", editable = "src/backend/base" }, { name = "langflow-base", extras = ["docling"], marker = "extra == 'complete'", editable = "src/backend/base" }, { name = "langflow-base", extras = ["docx"], marker = "extra == 'complete'", editable = "src/backend/base" }, - { name = "langflow-base", extras = ["dspy"], marker = "extra == 'complete'", editable = "src/backend/base" }, { name = "langflow-base", extras = ["duckduckgo"], marker = "extra == 'complete'", editable = "src/backend/base" }, { name = "langflow-base", extras = ["easyocr"], marker = "extra == 'complete'", editable = "src/backend/base" }, { name = "langflow-base", extras = ["elasticsearch"], marker = "extra == 'complete'", editable = "src/backend/base" }, @@ -8052,8 +7979,6 @@ requires-dist = [ { name = "lark", marker = "extra == 'lark'", specifier = "==1.2.2" }, { name = "lfx", editable = "src/lfx" }, { name = "litellm", marker = "extra == 'litellm'", specifier = ">=1.83.0" }, - { name = "llama-cpp-python", marker = "extra == 'llama-cpp'", specifier = "~=0.2.0" }, - { name = "llama-cpp-python", marker = "extra == 'local'", specifier = ">=0.2.0" }, { name = "loguru", specifier = ">=0.7.1,<1.0.0" }, { name = "markdown", marker = "extra == 'markdown'", specifier = ">=3.8.0" }, { name = "markupsafe", marker = "extra == 'markup'", specifier = "==3.0.2" }, @@ -8146,7 +8071,7 @@ requires-dist = [ { name = "youtube-transcript-api", marker = "extra == 'youtube'", specifier = ">=1.0.0,<2.0.0" }, { name = "zep-python", marker = "extra == 'zep'", specifier = "==2.0.2" }, ] -provides-extras = ["audio", "postgresql", "local", "couchbase", "cassandra", "clickhouse", "mongodb", "supabase", "redis", "elasticsearch", "faiss", "qdrant", "qdrant-vectors", "weaviate", "chroma", "upstash", "pinecone", "milvus", "mongodb-vectors", "astradb", "openai", "anthropic", "cohere", "google", "ollama", "nvidia", "mistral", "sambanova", "groq", "llama-cpp", "sentence-transformers", "ctransformers", "langfuse", "langwatch", "langsmith", "arize", "pypdf", "docx", "pytube", "dspy", "smolagents", "beautifulsoup", "serpapi", "google-api", "wikipedia", "fake-useragent", "duckduckgo", "yfinance", "wolframalpha", "pyarrow", "fastavro", "gitpython", "nltk", "lark", "huggingface", "metaphor", "metal", "markup", "aws", "numexpr", "qianfan", "pgvector", "litellm", "zep", "youtube", "markdown", "kubernetes", "json-repair", "composio", "ragstack", "opensearch", "atlassian", "mem0", "needle", "sseclient", "openinference", "mcp", "ag2", "scrapegraph", "apify", "spider", "altk", "toolguard", "langchain-huggingface", "langchain-unstructured", "graph-retriever", "langchain-mcp-adapters", "opik", "traceloop", "openlayer", "docling", "easyocr", "cleanlab", "twelvelabs", "jigsawstack", "assemblyai", "datasets", "fastparquet", "vlmrun", "cuga", "mlx", "fastmcp", "aioboto3", "astrapy", "gassist", "ibm-watsonx-clients", "complete", "all"] +provides-extras = ["audio", "postgresql", "local", "couchbase", "cassandra", "clickhouse", "mongodb", "supabase", "redis", "elasticsearch", "faiss", "qdrant", "qdrant-vectors", "weaviate", "chroma", "upstash", "pinecone", "milvus", "mongodb-vectors", "astradb", "openai", "anthropic", "cohere", "google", "ollama", "nvidia", "mistral", "sambanova", "groq", "sentence-transformers", "ctransformers", "langfuse", "langwatch", "langsmith", "arize", "pypdf", "docx", "pytube", "smolagents", "beautifulsoup", "serpapi", "google-api", "wikipedia", "fake-useragent", "duckduckgo", "yfinance", "wolframalpha", "pyarrow", "fastavro", "gitpython", "nltk", "lark", "huggingface", "metaphor", "metal", "markup", "aws", "numexpr", "qianfan", "pgvector", "litellm", "zep", "youtube", "markdown", "kubernetes", "json-repair", "composio", "ragstack", "opensearch", "atlassian", "mem0", "needle", "sseclient", "openinference", "mcp", "ag2", "scrapegraph", "apify", "spider", "altk", "toolguard", "langchain-huggingface", "langchain-unstructured", "graph-retriever", "langchain-mcp-adapters", "opik", "traceloop", "openlayer", "docling", "easyocr", "cleanlab", "twelvelabs", "jigsawstack", "assemblyai", "datasets", "fastparquet", "vlmrun", "cuga", "mlx", "fastmcp", "aioboto3", "astrapy", "gassist", "ibm-watsonx-clients", "complete", "all"] [package.metadata.requires-dev] dev = [ @@ -8650,7 +8575,7 @@ wheels = [ [[package]] name = "litellm" -version = "1.86.0" +version = "1.86.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -8666,24 +8591,11 @@ dependencies = [ { name = "tiktoken" }, { name = "tokenizers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/a7/26b8b04e4fcff26b60200ffe7458a255552ae51014468188f5db45674eb2/litellm-1.86.0.tar.gz", hash = "sha256:eccab86e0820b60b3f9484b233fb8d818b97afb19d5b4fa08d0d045621350ba4", size = 15379195, upload-time = "2026-05-24T02:42:10.865Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/7c/a94c15e1146f76a6e0dfe0394f824cdfeb9dadb988c6f698a35f6c1b8d4f/litellm-1.86.1.tar.gz", hash = "sha256:616100384073f2ddec1a5391b34c806c7c99e6af4511741434809caa46075e13", size = 15379863, upload-time = "2026-05-26T03:51:58.624Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/0b/9a044c061a69e801de042e962c34f5bc2e094810e28b49ce0b3bedee9327/litellm-1.86.0-py3-none-any.whl", hash = "sha256:9d8171ca1a17705b7c7a6fdce8cfc07bbf641284b46c1b6047f83a779159990c", size = 17011225, upload-time = "2026-05-24T02:42:00.629Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/e6e874857f45ff47762c6c29ec3dad9ea4757300e034400642cab90e4982/litellm-1.86.1-py3-none-any.whl", hash = "sha256:54e52372d326c642e9cc76d39afffb9b22387cbff3694c5e2758c9f860bca2e9", size = 17012257, upload-time = "2026-05-26T03:51:49.991Z" }, ] -[[package]] -name = "llama-cpp-python" -version = "0.2.90" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "diskcache" }, - { name = "jinja2" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1f/19/89836022affc1bf470e2485e28872b489254a66fe587155edba731a07112/llama_cpp_python-0.2.90.tar.gz", hash = "sha256:419b041c62dbdb9f7e67883a6ef2f247d583d08417058776be0bff05b4ec9e3d", size = 63762953, upload-time = "2024-08-29T07:00:35.267Z" } - [[package]] name = "llm-sandbox" version = "0.3.39" @@ -11267,25 +11179,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/72/58dcedaca964398d64824b0d85311e89031cf13ec100ee0440c65d87e5cc/opik-2.0.47-py3-none-any.whl", hash = "sha256:aa1a1aadbff5c804355a6645e55b78739fe44d6d055d74676495a42d756c4451", size = 1623856, upload-time = "2026-05-25T04:44:03.994Z" }, ] -[[package]] -name = "optuna" -version = "4.8.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "alembic" }, - { name = "colorlog" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "packaging" }, - { name = "pyyaml" }, - { name = "sqlalchemy" }, - { name = "tqdm" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bf/9b/62f120fb2ecbc4338bee70c5a3671c8e561714f3aa1a046b897ff142050e/optuna-4.8.0.tar.gz", hash = "sha256:6f7043e9f8ecb5e607af86a7eb00fb5ec2be26c3b08c201209a73d36aff37a38", size = 482603, upload-time = "2026-03-16T04:59:58.659Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/24/7c731839566d30dc70556d9824ef17692d896c15e3df627bce8c16f753e1/optuna-4.8.0-py3-none-any.whl", hash = "sha256:c57a7682679c36bfc9bca0da430698179e513874074b71bebedb0334964ab930", size = 419456, upload-time = "2026-03-16T04:59:56.977Z" }, -] - [[package]] name = "orjson" version = "3.11.9"