Skip to content

Commit c760fd8

Browse files
committed
feat(demo): add LangChain4J VCR demo project
Adds a complete demo project showing VCR usage with LangChain4J: - LangChain4JVCRDemoTest: demonstrates embedding and chat model recording - Pre-recorded cassettes in src/test/resources/vcr-data/ - README with usage instructions and best practices Demo tests: - Single and batch text embedding - Chat completion with various prompts - Combined RAG-style workflow (embed + generate) Run without API key using pre-recorded cassettes: ./gradlew :demos:langchain4j-vcr:test
1 parent 18a5811 commit c760fd8

File tree

7 files changed

+1746
-0
lines changed

7 files changed

+1746
-0
lines changed

demos/langchain4j-vcr/README.md

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
# LangChain4J VCR Demo
2+
3+
This demo shows how to use the VCR (Video Cassette Recorder) test system with LangChain4J models. VCR records LLM/embedding API responses to Redis and replays them in subsequent test runs, enabling fast, deterministic, and cost-effective testing.
4+
5+
## Features
6+
7+
- Record and replay LangChain4J `EmbeddingModel` responses
8+
- Record and replay LangChain4J `ChatLanguageModel` responses
9+
- Declarative `@VCRTest` and `@VCRModel` annotations
10+
- Automatic model wrapping via JUnit 5 extension
11+
- Redis-backed persistence with automatic test isolation
12+
13+
## Quick Start
14+
15+
### 1. Annotate Your Test Class
16+
17+
```java
18+
import com.redis.vl.test.vcr.VCRMode;
19+
import com.redis.vl.test.vcr.VCRModel;
20+
import com.redis.vl.test.vcr.VCRTest;
21+
22+
@VCRTest(mode = VCRMode.PLAYBACK_OR_RECORD)
23+
class MyLangChain4JTest {
24+
25+
@VCRModel(modelName = "text-embedding-3-small")
26+
private EmbeddingModel embeddingModel = createEmbeddingModel();
27+
28+
@VCRModel
29+
private ChatLanguageModel chatModel = createChatModel();
30+
31+
// Models must be initialized at field declaration time,
32+
// not in @BeforeEach (VCR wrapping happens before @BeforeEach)
33+
}
34+
```
35+
36+
### 2. Use Models Normally
37+
38+
```java
39+
@Test
40+
void shouldEmbedText() {
41+
// First run: calls real API and records response
42+
// Subsequent runs: replays from Redis cassette
43+
Response<Embedding> response = embeddingModel.embed("What is Redis?");
44+
45+
assertNotNull(response.content());
46+
}
47+
48+
@Test
49+
void shouldGenerateResponse() {
50+
String response = chatModel.generate("Explain Redis in one sentence.");
51+
52+
assertNotNull(response);
53+
}
54+
```
55+
56+
## VCR Modes
57+
58+
| Mode | Description | API Key Required |
59+
|------|-------------|------------------|
60+
| `PLAYBACK` | Only use recorded cassettes. Fails if cassette missing. | No |
61+
| `PLAYBACK_OR_RECORD` | Use cassette if available, record if not. | Only for first run |
62+
| `RECORD` | Always call real API and record response. | Yes |
63+
| `OFF` | Bypass VCR, always call real API. | Yes |
64+
65+
### Setting Mode via Environment Variable
66+
67+
Override the annotation mode at runtime without changing code:
68+
69+
```bash
70+
# Record new cassettes
71+
VCR_MODE=RECORD ./gradlew :demos:langchain4j-vcr:test
72+
73+
# Playback only (CI/CD, no API key needed)
74+
VCR_MODE=PLAYBACK ./gradlew :demos:langchain4j-vcr:test
75+
76+
# Default behavior from annotation
77+
./gradlew :demos:langchain4j-vcr:test
78+
```
79+
80+
## Running the Demo
81+
82+
### With Pre-recorded Cassettes (No API Key)
83+
84+
The demo includes pre-recorded cassettes in `src/test/resources/vcr-data/`. Run tests without an API key:
85+
86+
```bash
87+
./gradlew :demos:langchain4j-vcr:test
88+
```
89+
90+
### Recording New Cassettes
91+
92+
To record fresh cassettes, set your OpenAI API key:
93+
94+
```bash
95+
OPENAI_API_KEY=your-key VCR_MODE=RECORD ./gradlew :demos:langchain4j-vcr:test
96+
```
97+
98+
## How It Works
99+
100+
1. **Test Setup**: `@VCRTest` annotation triggers the VCR JUnit 5 extension
101+
2. **Container Start**: A Redis Stack container is started with persistence enabled
102+
3. **Model Wrapping**: Fields annotated with `@VCRModel` are wrapped with VCR proxies
103+
4. **Recording**: When a model is called, VCR checks for existing cassette:
104+
- **Cache hit**: Returns recorded response
105+
- **Cache miss**: Calls real API, stores response as cassette
106+
5. **Persistence**: Cassettes are saved to `vcr-data/` directory via Redis persistence
107+
6. **Cleanup**: Container stops, data persists for next run
108+
109+
## Cassette Storage
110+
111+
Cassettes are stored in Redis JSON format with keys like:
112+
113+
```
114+
vcr:embedding:MyTest.testMethod:0001
115+
vcr:chat:MyTest.testMethod:0001
116+
```
117+
118+
Data persists to `src/test/resources/vcr-data/` via Redis AOF/RDB.
119+
120+
## Test Structure
121+
122+
```
123+
demos/langchain4j-vcr/
124+
├── src/test/java/
125+
│ └── com/redis/vl/demo/vcr/
126+
│ └── LangChain4JVCRDemoTest.java
127+
└── src/test/resources/
128+
└── vcr-data/ # Persisted cassettes
129+
├── appendonly.aof
130+
└── dump.rdb
131+
```
132+
133+
## Configuration Options
134+
135+
### @VCRTest Annotation
136+
137+
| Parameter | Default | Description |
138+
|-----------|---------|-------------|
139+
| `mode` | `PLAYBACK_OR_RECORD` | VCR operating mode |
140+
| `dataDir` | `src/test/resources/vcr-data` | Cassette storage directory |
141+
| `redisImage` | `redis/redis-stack:latest` | Redis Docker image |
142+
143+
### @VCRModel Annotation
144+
145+
| Parameter | Default | Description |
146+
|-----------|---------|-------------|
147+
| `modelName` | `""` | Optional model identifier for logging |
148+
149+
## Best Practices
150+
151+
1. **Initialize models at field declaration** - Not in `@BeforeEach`
152+
2. **Use dummy API key in PLAYBACK mode** - VCR will use cached responses
153+
3. **Commit cassettes to version control** - Enables reproducible tests
154+
4. **Use specific test names** - Cassette keys include test class and method names
155+
5. **Re-record periodically** - API responses may change over time
156+
157+
## Troubleshooting
158+
159+
### Tests fail with "Cassette missing"
160+
161+
- Ensure cassettes exist in `src/test/resources/vcr-data/`
162+
- Run once with `VCR_MODE=RECORD` and API key to generate cassettes
163+
164+
### API key required error
165+
166+
- In `PLAYBACK` mode, use a dummy key: `"vcr-playback-mode"`
167+
- VCR won't call the real API when cassettes exist
168+
169+
### Tests pass but call real API
170+
171+
- Verify models are initialized at field declaration, not `@BeforeEach`
172+
- Check that `@VCRModel` annotation is present on model fields
173+
174+
## See Also
175+
176+
- [Spring AI VCR Demo](../spring-ai-vcr/README.md)
177+
- [VCR Test System Documentation](../../README.md#-experimental-vcr-test-system)
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
plugins {
2+
java
3+
}
4+
5+
group = "com.redis.vl.demo"
6+
version = "0.12.0"
7+
8+
java {
9+
sourceCompatibility = JavaVersion.VERSION_21
10+
targetCompatibility = JavaVersion.VERSION_21
11+
toolchain {
12+
languageVersion = JavaLanguageVersion.of(21)
13+
}
14+
}
15+
16+
repositories {
17+
mavenCentral()
18+
}
19+
20+
dependencies {
21+
// RedisVL Core (includes VCR support)
22+
implementation(project(":core"))
23+
24+
// SpotBugs annotations
25+
compileOnly("com.github.spotbugs:spotbugs-annotations:4.8.3")
26+
27+
// LangChain4J
28+
implementation("dev.langchain4j:langchain4j:0.36.2")
29+
implementation("dev.langchain4j:langchain4j-open-ai:0.36.2")
30+
31+
// Redis
32+
implementation("redis.clients:jedis:5.2.0")
33+
34+
// Logging
35+
implementation("org.slf4j:slf4j-api:2.0.16")
36+
runtimeOnly("ch.qos.logback:logback-classic:1.5.15")
37+
38+
// Testing
39+
testImplementation("org.junit.jupiter:junit-jupiter:5.11.4")
40+
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
41+
testCompileOnly("com.github.spotbugs:spotbugs-annotations:4.8.3")
42+
43+
// TestContainers for integration tests
44+
testImplementation("org.testcontainers:testcontainers:1.19.3")
45+
testImplementation("org.testcontainers:junit-jupiter:1.19.3")
46+
}
47+
48+
tasks.withType<JavaCompile> {
49+
options.encoding = "UTF-8"
50+
options.compilerArgs.addAll(listOf(
51+
"-parameters",
52+
"-Xlint:all",
53+
"-Xlint:-processing"
54+
))
55+
}
56+
57+
tasks.withType<Test> {
58+
useJUnitPlatform()
59+
testLogging {
60+
events("passed", "skipped", "failed")
61+
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
62+
}
63+
// Pass environment variables to tests
64+
environment("OPENAI_API_KEY", System.getenv("OPENAI_API_KEY") ?: "")
65+
}

0 commit comments

Comments
 (0)