Skip to content

Commit d52beb1

Browse files
committed
feat: auto-create PromptConfig for customPrompt + filtered extraction example
## Bug Fix - Auto-create PromptConfig when customPrompt is used without promptConfigId - Fixes FK constraint violation error during ingestion ## New Example - Add filtered-extraction-demo.ts demonstrating selective extraction - Extract only TEXT, QUESTION, LIST, TABLE with customPrompt - Context enrichment only for TEXT chunks (cost optimization) ## Documentation - Add "Custom Prompt / Filtered Extraction" section to README.md - Update examples/README.md with new demo instructions ## Code Quality (previous commits) - Add GeminiAPIError, PDFProcessingError, ContentPolicyError - Replace console.log/warn with structured logger - Fix any type casts in gemini.service.ts and ingestion.engine.ts All tests pass (59/59), TypeScript check clean.
1 parent 0a439a9 commit d52beb1

4 files changed

Lines changed: 135 additions & 4 deletions

File tree

README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,56 @@ const rag = new ContextRAG({
276276

277277
---
278278

279+
## 🎯 Custom Prompt / Filtered Extraction
280+
281+
Extract only specific content types without going through the Discovery flow:
282+
283+
```typescript
284+
// Extract ONLY specific types with custom prompt
285+
const result = await rag.ingest({
286+
file: './book.pdf',
287+
customPrompt: `
288+
Extract ONLY these content types:
289+
- TEXT: Normal paragraphs
290+
- QUESTION: Multiple choice questions
291+
- LIST: Bulleted or numbered lists
292+
- TABLE: Data tables
293+
294+
SKIP these types:
295+
- HEADING, CODE, QUOTE, IMAGE_REF
296+
`,
297+
// Context enrichment only for TEXT chunks (cost optimization)
298+
// Configure via ragEnhancement.skipChunkTypes
299+
});
300+
```
301+
302+
### Configuration for Selective Context Enrichment
303+
304+
```typescript
305+
const rag = new ContextRAG({
306+
prisma,
307+
geminiApiKey: process.env.GEMINI_API_KEY!,
308+
309+
ragEnhancement: {
310+
approach: 'anthropic_contextual',
311+
strategy: 'llm',
312+
// Only TEXT chunks get context enrichment
313+
// Other types (TABLE, LIST, QUESTION) are extracted but not enriched
314+
skipChunkTypes: ['HEADING', 'IMAGE_REF', 'TABLE', 'CODE', 'QUOTE', 'MIXED', 'QUESTION', 'LIST'],
315+
},
316+
});
317+
318+
// PromptConfig is auto-created when using customPrompt
319+
await rag.ingest({
320+
file: './document.pdf',
321+
customPrompt: 'Your custom extraction instructions...',
322+
});
323+
```
324+
325+
> **Note:** When using `customPrompt` without `promptConfigId`, the system automatically creates a PromptConfig for you.
326+
327+
---
328+
279329
## ⚙️ Configuration
280330

281331
```typescript

examples/README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,28 @@
4242
cp /path/to/your/document.pdf examples/test.pdf
4343
```
4444

45-
2. Run the demo:
45+
2. Run the full demo:
4646
```bash
4747
npx tsx examples/demo.ts
4848
```
4949

50+
3. **NEW:** Run the filtered extraction demo:
51+
```bash
52+
npx tsx examples/filtered-extraction-demo.ts
53+
```
54+
55+
### Filtered Extraction Demo
56+
57+
The `filtered-extraction-demo.ts` demonstrates:
58+
- **Custom Prompt:** Extract only TEXT, QUESTION, LIST, TABLE (skip HEADING, CODE, etc.)
59+
- **Selective Context:** Context enrichment only for TEXT chunks (cost optimization)
60+
- **Search Filtering:** Query only specific chunk types
61+
62+
This is useful when you want to:
63+
- Extract specific content types from a document
64+
- Reduce context generation costs by skipping non-essential chunks
65+
- Focus RAG search on particular content categories
66+
5067
## Expected Output
5168

5269
```

examples/filtered-extraction-demo.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ async function main() {
5252
const rag = new ContextRAG({
5353
prisma,
5454
geminiApiKey: process.env.GEMINI_API_KEY!,
55-
model: 'gemini-2.5-flash',
55+
model: 'gemini-3-pro-preview',
5656
generationConfig: {
5757
temperature: 0.2,
5858
maxOutputTokens: 16384,
@@ -71,8 +71,8 @@ async function main() {
7171
},
7272

7373
batchConfig: {
74-
pagesPerBatch: 10,
75-
maxConcurrency: 2,
74+
pagesPerBatch: 30,
75+
maxConcurrency: 3,
7676
},
7777
logging: {
7878
level: 'info',

src/engines/ingestion.engine.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,75 @@ export class IngestionEngine {
135135
.split('\n')
136136
.map(line => line.trim())
137137
.filter(line => line.length > 0);
138+
139+
// Auto-create PromptConfig if customPrompt is provided but no promptConfigId
140+
if (!promptConfigId) {
141+
const customDocType = options.documentType ?? 'CustomPrompt';
142+
const existingConfig = await this.promptConfigRepo.getDefault(customDocType);
143+
144+
if (existingConfig) {
145+
// Use existing config
146+
promptConfigId = existingConfig.id;
147+
this.logger.debug('Using existing PromptConfig for customPrompt', {
148+
promptConfigId,
149+
documentType: customDocType
150+
});
151+
} else {
152+
// Create new PromptConfig for custom prompt
153+
const newConfig = await this.promptConfigRepo.create({
154+
documentType: customDocType,
155+
name: `Custom Extraction - ${new Date().toISOString().slice(0, 10)}`,
156+
systemPrompt: options.customPrompt,
157+
chunkStrategy: {
158+
maxTokens: 800,
159+
splitBy: 'semantic',
160+
preserveTables: true,
161+
preserveLists: true,
162+
},
163+
setAsDefault: true,
164+
changeLog: 'Auto-created from customPrompt',
165+
});
166+
promptConfigId = newConfig.id;
167+
this.logger.info('Created new PromptConfig for customPrompt', {
168+
promptConfigId,
169+
documentType: customDocType
170+
});
171+
}
172+
}
138173
} else if (documentInstructions.length === 0) {
139174
documentInstructions = DEFAULT_DOCUMENT_INSTRUCTIONS
140175
.split('\n')
141176
.map(line => line.replace(/^-\s*/, '').trim())
142177
.filter(line => line.length > 0);
178+
179+
// Create default PromptConfig if none exists
180+
if (!promptConfigId) {
181+
const defaultDocType = options.documentType ?? 'General';
182+
const existingConfig = await this.promptConfigRepo.getDefault(defaultDocType);
183+
184+
if (existingConfig) {
185+
promptConfigId = existingConfig.id;
186+
} else {
187+
const newConfig = await this.promptConfigRepo.create({
188+
documentType: defaultDocType,
189+
name: 'Default Extraction',
190+
systemPrompt: DEFAULT_DOCUMENT_INSTRUCTIONS,
191+
chunkStrategy: {
192+
maxTokens: 800,
193+
splitBy: 'semantic',
194+
preserveTables: true,
195+
preserveLists: true,
196+
},
197+
setAsDefault: true,
198+
changeLog: 'Auto-created as system default',
199+
});
200+
promptConfigId = newConfig.id;
201+
this.logger.info('Created default PromptConfig', {
202+
promptConfigId,
203+
documentType: defaultDocType
204+
});
205+
}
206+
}
143207
}
144208

145209
// Create batches

0 commit comments

Comments
 (0)