Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/auto-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ concurrency:
cancel-in-progress: false

env:
PYTHON_VERSION: "3.11"
UV_VERSION: "0.12.3"
PYTHON_VERSION: "3.12"
UV_VERSION: "0.13.1"
AUTO_VERSION: "11.3.6"
RELEASE_BOT_NAME: "applied-ai-releases[bot]"
RELEASE_BOT_EMAIL: "applied-ai-releases[bot]@users.noreply.github.com"
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
- main

env:
UV_VERSION: "0.12.3"
UV_VERSION: "0.13.1"

jobs:
lock:
Expand Down Expand Up @@ -38,11 +38,11 @@ jobs:
strategy:
matrix:
python-version:
- "3.10"
- "3.11"
- "3.12"
- "3.13"
- "3.14"
- "3.15"

steps:
- name: Check out repository
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test-fork-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ permissions:
pull-requests: read

env:
PYTHON_VERSION: "3.11"
UV_VERSION: "0.12.3"
PYTHON_VERSION: "3.12"
UV_VERSION: "0.13.1"

jobs:
setup:
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ on:
workflow_dispatch:

env:
PYTHON_VERSION: "3.11"
UV_VERSION: "0.12.3"
PYTHON_VERSION: "3.12"
UV_VERSION: "0.13.1"

jobs:
service-tests:
Expand Down Expand Up @@ -83,7 +83,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"]
redis-py-version: ["6.x", "7.x", "8.x"]
# `redis:latest` floats, so a failure on it is not reproducible from a
# SHA; it is advisory below. Pinned tags carry the blocking coverage.
Expand Down
2 changes: 1 addition & 1 deletion .readthedocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ version: 2
build:
os: ubuntu-24.04
tools:
python: "3.11"
python: "3.12"

jobs:
# 1️⃣ Install uv once before we create the virtual-env
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Here's how to get started with your code contribution:

### Prerequisites

- **Python**: RedisVL supports Python 3.9 and above
- **Python**: RedisVL supports Python 3.11 through 3.15
- **Docker**: Required for running Redis and integration tests
- **UV**: Modern Python package manager for fast dependency management

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Perfect for building **RAG pipelines** with real-time retrieval, **AI agents** w

## Installation

Install `redisvl` into your Python (>=3.10) environment using `pip`:
Install `redisvl` into your Python (>=3.11) environment using `pip`:

```bash
pip install redisvl
Expand Down
2 changes: 1 addition & 1 deletion docs/user_guide/13_langcache_semantic_cache.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"\n",
"Before you begin, ensure you have:\n",
"- Installed RedisVL with the LangCache extra: `pip install redisvl[langcache]`\n",
"- Python 3.10+ (same as RedisVL)\n",
"- Python 3.11+ (same as RedisVL)\n",
"- A LangCache service with a **cache ID** and **API key**. You can set up a LangCache service in Redis Cloud [here](https://cloud.redis.io/#/)\n",
"- Optionally: **attributes** configured on your LangCache cache if you plan to pass `metadata` / `attributes` from RedisVL\n",
"\n",
Expand Down
3 changes: 2 additions & 1 deletion docs/user_guide/how_to_guides/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ For the higher-level design, see {doc}`/concepts/mcp`.

RedisVL MCP assumes all of the following are already true:

- you have Python 3.10 or newer
- you have Python 3.11 or newer
- you have Redis with Search capabilities available
- the Redis index already exists
- you know which text field and vector field the server should use
Expand Down Expand Up @@ -193,6 +193,7 @@ indexes:
- `runtime.default_embed_text_field` is only required when the server should generate embeddings during upsert
- `vectorizer` is required for query embedding and server-side embedding, but optional for fulltext-only configs
- `runtime.max_result_window` caps deep paging by limiting the maximum `offset + limit`
- `runtime.startup_timeout_seconds` bounds three startup steps for the binding: connecting to Redis together with the first `INFO`, which share one deadline, then reading the index, then building the vectorizer
- `schema_overrides` is only for patching incomplete field attrs discovered from Redis

### Fulltext-Only Config
Expand Down
5 changes: 4 additions & 1 deletion docs/user_guide/how_to_guides/migrate-indexes.md
Original file line number Diff line number Diff line change
Expand Up @@ -887,7 +887,10 @@ backup is written *before* the index is dropped, so a crash at any point
leaves a complete backup on disk. In multi-worker mode, dump and quantize
are fused (each worker reads, backs up, and converts its shard in one pass
*after* the index drop). A crash during this fused phase may leave partial
backup shards. Re-running detects and resumes from partial state.
backup shards. Re-running detects and resumes from partial state. With
`--async` (`AsyncMigrationExecutor`), the first worker to fail cancels the
others; the sync executor lets them finish their shards. Either way, no shard
is still being written when the migration reports the failure.
```

### Rollback
Expand Down
8 changes: 6 additions & 2 deletions docs/user_guide/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ There are a few ways to install RedisVL. The easiest way is to use pip.

## Install RedisVL with Pip

Install `redisvl` into your Python (>=3.10) environment using `pip`:
Install `redisvl` into your Python (>=3.11) environment using `pip`:

```bash
$ pip install -U redisvl
Expand All @@ -32,7 +32,7 @@ $ pip install redisvl[vertexai] # Google Vertex AI embeddings (legacy
$ pip install redisvl[bedrock] # AWS Bedrock embeddings

# Other optional features
$ pip install redisvl[mcp] # RedisVL MCP server support (Python 3.10+)
$ pip install redisvl[mcp] # RedisVL MCP server support (Python 3.11+)
$ pip install redisvl[langcache] # LangCache managed service integration
$ pip install redisvl[sql-redis] # SQL query support
```
Expand All @@ -55,6 +55,10 @@ To install **all** optional dependencies at once:
$ pip install redisvl[all]
```

```{note}
The `voyageai` package declares support for Python below 3.15, and `sql-redis` for Python below 3.14. On newer versions, `pip` refuses those extras, and `all` with them, until their maintainers lift the limit. `uv` installs them regardless.
```

## Install RedisVL from Source

To install RedisVL from source, clone the repository and install the package using `pip`:
Expand Down
20 changes: 16 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ name = "redisvl"
version = "0.29.0"
description = "Python client library and CLI for using Redis as a vector database"
authors = [{ name = "Redis Inc.", email = "applied.ai@redis.com" }]
requires-python = ">=3.10,<3.15"
requires-python = ">=3.11,<3.16"
readme = "README.md"
license = "MIT"
keywords = [
Expand All @@ -15,11 +15,11 @@ keywords = [
"vector-search",
]
classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3.15",
"License :: OSI Approved :: MIT License",
]
dependencies = [
Expand All @@ -45,6 +45,10 @@ mcp = [
# Supporting that revision is its own piece of work.
"fastmcp>=2.0.0,<4",
"pydantic-settings>=2.0,<3",
# fastmcp pulls in beartype transitively. Releases before 0.23 import
# typing.no_type_check_decorator, which Python 3.15 removed, so fastmcp
# fails to import and the MCP server falls back to its stub.
"beartype>=0.23.1",
]
mistralai = ["mistralai>=1.0.0,<2"]
openai = ["openai>=1.1.0"]
Expand Down Expand Up @@ -115,7 +119,7 @@ build-backend = "hatchling.build"

[dependency-groups]
dev = [
"black>=25.1.0,<27",
"black>=26.5.0,<27",
"isort>=5.6.4,<6",
"pylint>=3.1.0,<4",
"pytest>=8.1.1,<9",
Expand Down Expand Up @@ -151,7 +155,7 @@ default-groups = [
]

[tool.black]
target-version = ['py310', 'py311', 'py312', 'py313', 'py314']
target-version = ['py311', 'py312', 'py313', 'py314', 'py315']
exclude = '''
(
| \.egg
Expand All @@ -171,6 +175,14 @@ exclude = '''
[tool.pytest.ini_options]
log_cli = true
asyncio_mode = "auto"
# pytest-asyncio 0.23 calls asyncio APIs that Python 3.14+ deprecates, once
# per collected object. On 3.15 that is millions of warnings per run, which
# slows the suite roughly tenfold. Scoped to pytest_asyncio so the same
# deprecations raised from redisvl still surface. Drop this once the
# pytest-asyncio<0.24 pin is lifted.
filterwarnings = [
"ignore:'asyncio\\.(iscoroutinefunction|get_event_loop_policy|set_event_loop_policy)' is deprecated:DeprecationWarning:pytest_asyncio",
]

[tool.mypy]
warn_unused_configs = true
Expand Down
4 changes: 2 additions & 2 deletions redisvl/extensions/message_history/schema.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import warnings
from enum import Enum
from enum import StrEnum
from typing import Any
from uuid import uuid4

Expand All @@ -20,7 +20,7 @@
from redisvl.utils.utils import current_timestamp


class ChatRole(str, Enum):
class ChatRole(StrEnum):
"""Enumeration of valid roles for a chat message."""

USER = "user"
Expand Down
8 changes: 4 additions & 4 deletions redisvl/extensions/router/semantic.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from pathlib import Path
from typing import Any, NoReturn
from typing import Any, NoReturn, Self

import redis.commands.search.reducers as reducers
import yaml
Expand Down Expand Up @@ -163,7 +163,7 @@ def from_existing(
redis_client: SyncRedisClient | None = None,
redis_url: str = "redis://localhost:6379",
**kwargs,
) -> "SemanticRouter":
) -> Self:
"""Return SemanticRouter instance from existing index.

Reads the stored route config with ``JSON.GET``, so unlike
Expand Down Expand Up @@ -913,7 +913,7 @@ def from_dict(
cls,
data: dict[str, Any],
**kwargs,
) -> "SemanticRouter":
) -> Self:
"""Create a SemanticRouter from a dictionary.

Args:
Expand Down Expand Up @@ -991,7 +991,7 @@ def from_yaml(
cls,
file_path: str,
**kwargs,
) -> "SemanticRouter":
) -> Self:
"""Create a SemanticRouter from a YAML file.

Args:
Expand Down
24 changes: 12 additions & 12 deletions redisvl/mcp/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import re
from copy import deepcopy
from pathlib import Path
from typing import Any, Literal
from typing import Any, Literal, Self

import yaml
from pydantic import BaseModel, ConfigDict, Field, model_validator
Expand Down Expand Up @@ -73,7 +73,7 @@ class MCPRuntimeConfig(BaseModel):
max_concurrency: int = 16

@model_validator(mode="after")
def _validate_limits(self) -> "MCPRuntimeConfig":
def _validate_limits(self) -> Self:
"""Validate runtime bounds during config load."""
if self.default_limit <= 0:
raise ValueError("runtime.default_limit must be greater than 0")
Expand Down Expand Up @@ -148,7 +148,7 @@ class MCPAuthConfig(BaseModel):
authorization_claim: str = Field(default="scp", min_length=1)

@model_validator(mode="after")
def _validate_jwt(self) -> "MCPAuthConfig":
def _validate_jwt(self) -> Self:
"""Enforce the JWT key-source, issuer, and audience requirements."""
jwt_fields_set = any(
(
Expand Down Expand Up @@ -231,7 +231,7 @@ class MCPServerConfig(BaseModel):
)

@model_validator(mode="after")
def _validate_builtin_tools(self) -> "MCPServerConfig":
def _validate_builtin_tools(self) -> Self:
"""Reject disable/enable entries that do not name a built-in tool."""
unknown = sorted(set(self.builtin_tools) - builtin_tool_names())
if unknown:
Expand Down Expand Up @@ -259,7 +259,7 @@ class MCPIndexSearchConfig(BaseModel):
params: dict[str, Any] = Field(default_factory=dict)

@model_validator(mode="after")
def _validate_params(self) -> "MCPIndexSearchConfig":
def _validate_params(self) -> Self:
"""Reject params that do not belong to the configured search mode."""
allowed_params = {
"vector": {
Expand Down Expand Up @@ -402,7 +402,7 @@ def requires_startup_vectorizer(self) -> bool:
return self.uses_query_embedding or self.supports_server_side_embedding

@model_validator(mode="after")
def _validate_capability_requirements(self) -> "MCPIndexBindingConfig":
def _validate_capability_requirements(self) -> Self:
"""Require only the config fields needed by enabled capabilities."""
if self.uses_text_search and self.runtime.text_field_name is None:
raise ValueError(
Expand Down Expand Up @@ -620,7 +620,7 @@ class MCPProfileParamConfig(BaseModel):
max: int | None = None

@model_validator(mode="after")
def _validate_max(self) -> "MCPProfileParamConfig":
def _validate_max(self) -> Self:
"""Reject a non-positive cap."""
if self.max is not None and self.max <= 0:
raise ValueError("custom_tools params max must be greater than 0")
Expand Down Expand Up @@ -665,7 +665,7 @@ class MCPProfileInjectConfig(BaseModel):
required: Literal[True] = True

@model_validator(mode="after")
def _validate_inject(self) -> "MCPProfileInjectConfig":
def _validate_inject(self) -> Self:
"""Reject a blank field or claim name, which min_length lets through."""
if not self.field.strip():
raise ValueError("custom_tools lock.inject field must not be blank")
Expand All @@ -684,7 +684,7 @@ class MCPProfileLockConfig(BaseModel):
inject: list[MCPProfileInjectConfig] | None = None

@model_validator(mode="after")
def _validate_lock(self) -> "MCPProfileLockConfig":
def _validate_lock(self) -> Self:
"""Reject an empty or blank projection.

The annotations already give the rest: pydantic enforces ``list[str]``
Expand Down Expand Up @@ -757,7 +757,7 @@ class MCPCustomToolConfig(BaseModel):
params: dict[str, MCPProfileParamConfig] = Field(default_factory=dict)

@model_validator(mode="after")
def _validate_profile(self) -> "MCPCustomToolConfig":
def _validate_profile(self) -> Self:
"""Validate naming, parameter policy, and lock/expose coherence."""
if not self.description.strip():
# min_length alone lets whitespace through, which would publish a
Expand Down Expand Up @@ -853,7 +853,7 @@ class MCPConfig(BaseModel):
custom_tools: list[MCPCustomToolConfig] = Field(default_factory=list)

@model_validator(mode="after")
def _validate_bindings(self) -> "MCPConfig":
def _validate_bindings(self) -> Self:
"""Require at least one binding and reject blank logical ids."""
if not self.indexes:
raise ValueError("indexes must contain at least one configured binding")
Expand All @@ -864,7 +864,7 @@ def _validate_bindings(self) -> "MCPConfig":
return self

@model_validator(mode="after")
def _validate_custom_tools(self) -> "MCPConfig":
def _validate_custom_tools(self) -> Self:
"""Validate custom tool names and index pinning against the bindings.

Field-level checks that need the inspected schema (locked filter fields,
Expand Down
4 changes: 2 additions & 2 deletions redisvl/mcp/errors.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import asyncio
from enum import Enum
from enum import StrEnum
from typing import Any

from pydantic import ValidationError
Expand All @@ -8,7 +8,7 @@
from redisvl.exceptions import RedisSearchError


class MCPErrorCode(str, Enum):
class MCPErrorCode(StrEnum):
"""Stable internal error codes exposed by the MCP framework."""

INVALID_REQUEST = "invalid_request"
Expand Down
Loading
Loading