Skip to content
Merged
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
125 changes: 125 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,131 @@ System.out.println(match.getDistance()); // Output: 0.273891836405

> Learn more about [semantic routing](https://redis.github.io/redis-vl-java/redisvl/current/semantic-router.html).

## 🧪 Experimental: VCR Test System

RedisVL includes an experimental VCR (Video Cassette Recorder) test system for recording and replaying LLM/embedding API calls. This enables:

- **Deterministic tests** - Replay recorded responses for consistent results
- **Cost reduction** - Avoid repeated API calls during test runs
- **Speed improvement** - Local Redis playback is faster than API calls
- **Offline testing** - Run tests without network access or API keys

### Quick Start with JUnit 5

The simplest way to use VCR is with the declarative annotations:

```java
import com.redis.vl.test.vcr.VCRMode;
import com.redis.vl.test.vcr.VCRModel;
import com.redis.vl.test.vcr.VCRTest;

@VCRTest(mode = VCRMode.PLAYBACK_OR_RECORD)
class MyLLMTest {

// Models are automatically wrapped by VCR
@VCRModel(modelName = "text-embedding-3-small")
private EmbeddingModel embeddingModel = createEmbeddingModel();

@VCRModel
private ChatLanguageModel chatModel = createChatModel();

@Test
void testEmbedding() {
// First run: Records API response to Redis
// Subsequent runs: Replays from Redis cassette
Response<Embedding> response = embeddingModel.embed("What is Redis?");
assertNotNull(response.content());
}

@Test
void testChat() {
String response = chatModel.generate("Explain Redis in one sentence.");
assertNotNull(response);
}
}
```

### VCR Modes

| Mode | Description | API Key Required |
|------|-------------|------------------|
| `PLAYBACK` | Only use recorded cassettes. Fails if missing. | No |
| `PLAYBACK_OR_RECORD` | Use cassette if available, record if not. | Only for first run |
| `RECORD` | Always call real API and record response. | Yes |
| `OFF` | Bypass VCR, always call real API. | Yes |

### Environment Variable Override

Override the VCR mode at runtime without changing code:

```bash
# Record new cassettes
VCR_MODE=RECORD OPENAI_API_KEY=your-key ./gradlew test

# Playback only (CI/CD, no API key needed)
VCR_MODE=PLAYBACK ./gradlew test
```

### LangChain4J Integration

```java
import com.redis.vl.test.vcr.VCREmbeddingModel;
import com.redis.vl.test.vcr.VCRChatModel;
import com.redis.vl.test.vcr.VCRMode;

// Wrap any LangChain4J EmbeddingModel
VCREmbeddingModel vcrEmbedding = new VCREmbeddingModel(openAiEmbeddingModel);
vcrEmbedding.setMode(VCRMode.PLAYBACK_OR_RECORD);
Response<Embedding> response = vcrEmbedding.embed("What is Redis?");

// Wrap any LangChain4J ChatLanguageModel
VCRChatModel vcrChat = new VCRChatModel(openAiChatModel);
vcrChat.setMode(VCRMode.PLAYBACK_OR_RECORD);
String response = vcrChat.generate("What is Redis?");
```

### Spring AI Integration

```java
import com.redis.vl.test.vcr.VCRSpringAIEmbeddingModel;
import com.redis.vl.test.vcr.VCRSpringAIChatModel;
import com.redis.vl.test.vcr.VCRMode;

// Wrap any Spring AI EmbeddingModel
VCRSpringAIEmbeddingModel vcrEmbedding = new VCRSpringAIEmbeddingModel(openAiEmbeddingModel);
vcrEmbedding.setMode(VCRMode.PLAYBACK_OR_RECORD);
EmbeddingResponse response = vcrEmbedding.embedForResponse(List.of("What is Redis?"));

// Wrap any Spring AI ChatModel
VCRSpringAIChatModel vcrChat = new VCRSpringAIChatModel(openAiChatModel);
vcrChat.setMode(VCRMode.PLAYBACK_OR_RECORD);
String response = vcrChat.call("What is Redis?");
```

### How It Works

1. **Container Management**: VCR starts a Redis Stack container with persistence
2. **Model Wrapping**: `@VCRModel` fields are wrapped with VCR proxies
3. **Cassette Storage**: Responses stored as Redis JSON documents
4. **Persistence**: Data saved to `src/test/resources/vcr-data/` via Redis AOF/RDB
5. **Playback**: Subsequent runs load cassettes from persistent storage

### Demo Projects

Complete working examples are available:

- **[LangChain4J VCR Demo](demos/langchain4j-vcr/)** - LangChain4J embedding and chat models
- **[Spring AI VCR Demo](demos/spring-ai-vcr/)** - Spring AI embedding and chat models

Run the demos without an API key (uses pre-recorded cassettes):

```bash
./gradlew :demos:langchain4j-vcr:test
./gradlew :demos:spring-ai-vcr:test
```

> Learn more about [VCR testing](https://redis.github.io/redis-vl-java/redisvl/current/vcr-testing.html).

## 🚀 Why RedisVL?

In the age of GenAI, **vector databases** and **LLMs** are transforming information retrieval systems. With emerging and popular frameworks like [LangChain4J](https://github.com/langchain4j/langchain4j) and [Spring AI](https://spring.io/projects/spring-ai), innovation is rapid. Yet, many organizations face the challenge of delivering AI solutions **quickly** and at **scale**.
Expand Down
3 changes: 3 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ allprojects {
maven {
url = uri("https://repo.spring.io/milestone")
}
maven {
url = uri("https://repo.spring.io/snapshot")
}
}
}

Expand Down
31 changes: 31 additions & 0 deletions core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ plugins {
java
`java-library`
`maven-publish`
id("io.spring.dependency-management") version "1.1.7"
}

description = "RedisVL - Vector Library for Java"
Expand Down Expand Up @@ -65,6 +66,16 @@ dependencies {
// Cohere Java SDK for reranking
compileOnly("com.cohere:cohere-java:1.8.1")

// VCR Test Utilities (optional - users include what they need for testing)
// JUnit 5 for extension development
compileOnly("org.junit.jupiter:junit-jupiter-api:5.10.2")
// Testcontainers for Redis persistence
compileOnly("org.testcontainers:testcontainers:1.19.7")
compileOnly("org.testcontainers:junit-jupiter:1.19.7")
// ByteBuddy for method interception (future LLM interceptor)
compileOnly("net.bytebuddy:byte-buddy:1.14.12")
compileOnly("net.bytebuddy:byte-buddy-agent:1.14.12")

// Test dependencies for LangChain4J (include in tests to verify integration)
testImplementation("dev.langchain4j:langchain4j:0.36.2")
testImplementation("dev.langchain4j:langchain4j-open-ai:0.36.2")
Expand All @@ -73,12 +84,32 @@ dependencies {
testImplementation("dev.langchain4j:langchain4j-embeddings-all-minilm-l6-v2:0.36.2")
testImplementation("dev.langchain4j:langchain4j-hugging-face:0.36.2")

// Spring AI for VCR testing (optional - users include what they need)
// Version managed by spring-ai-bom (spring-ai-model contains EmbeddingModel interface)
compileOnly("org.springframework.ai:spring-ai-model")
testImplementation("org.springframework.ai:spring-ai-model")

// Cohere for integration tests
testImplementation("com.cohere:cohere-java:1.8.1")

// Additional test dependencies
testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
testImplementation("org.mockito:mockito-core:5.11.0")

// VCR test dependencies (to test VCR functionality)
testImplementation("org.testcontainers:testcontainers:1.19.7")
testImplementation("org.testcontainers:junit-jupiter:1.19.7")
testImplementation("net.bytebuddy:byte-buddy:1.14.12")
testImplementation("net.bytebuddy:byte-buddy-agent:1.14.12")
}

// Spring AI 1.1.0 - BOM for dependency management
val springAiVersion = "1.1.0"

dependencyManagement {
imports {
mavenBom("org.springframework.ai:spring-ai-bom:$springAiVersion")
}
}

// Configure test execution
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.redis.vl.test.vcr;

/**
* Exception thrown when a VCR cassette is not found during playback mode.
*
* <p>This exception indicates that the test expected to find a recorded cassette but none was
* available. To fix this, run the test in RECORD or PLAYBACK_OR_RECORD mode first.
*/
public class VCRCassetteMissingException extends RuntimeException {

private static final long serialVersionUID = 1L;

private final String cassetteKey;
private final String testId;

/**
* Creates a new exception.
*
* @param cassetteKey the key that was not found
* @param testId the test identifier
*/
public VCRCassetteMissingException(String cassetteKey, String testId) {
super(
String.format(
"VCR cassette not found for test '%s'%nCassette key: %s%n"
+ "Run with VCRMode.RECORD or VCRMode.PLAYBACK_OR_RECORD to record this interaction",
testId, cassetteKey));
this.cassetteKey = cassetteKey;
this.testId = testId;
}

/**
* Gets the cassette key that was not found.
*
* @return the cassette key
*/
public String getCassetteKey() {
return cassetteKey;
}

/**
* Gets the test identifier.
*
* @return the test ID
*/
public String getTestId() {
return testId;
}
}
Loading