What You’ll Build
A script that:- Searches your indexed documents in a vault
- Analyzes the results with an LLM
- Returns structured insights with source citations
Architecture
Prerequisites
- Case.dev API key (get one here)
- A vault with ingested documents (we’ll set one up if you don’t have one)
Step 1: Set Up Your Vault
If you already have a vault with indexed documents, skip to Step 2.Create a vault
curl -X POST https://api.case.dev/vault \
-H "Authorization: Bearer $CASEDEV_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Legal Research Vault"}'
casedev vault create --name "Legal Research Vault"
import Casedev from 'casedev';
const client = new Casedev({ apiKey: process.env.CASEDEV_API_KEY });
const vault = await client.vault.create({
name: 'Legal Research Vault'
});
console.log(`Vault ID: ${vault.id}`);
import os
import casedev
client = casedev.Casedev(api_key=os.environ['CASEDEV_API_KEY'])
vault = client.vault.create(name='Legal Research Vault')
print(f'Vault ID: {vault.id}')
vault, _ := client.Vault.New(ctx, casedev.VaultNewParams{
Name: casedev.F("Legal Research Vault"),
})
fmt.Println(vault.ID)
id — you’ll need it.
Upload a document
# Get upload URL
UPLOAD_RESPONSE=$(curl -s -X POST "https://api.case.dev/vault/$VAULT_ID/upload" \
-H "Authorization: Bearer $CASEDEV_API_KEY" \
-H "Content-Type: application/json" \
-d '{"filename": "contract.pdf", "contentType": "application/pdf"}')
UPLOAD_URL=$(echo $UPLOAD_RESPONSE | jq -r '.uploadUrl')
OBJECT_ID=$(echo $UPLOAD_RESPONSE | jq -r '.objectId')
# Upload your file
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: application/pdf" \
--data-binary @contract.pdf
casedev vault upload \
--id $VAULT_ID \
--filename "document.pdf" \
--content-type "application/pdf"
// Get upload URL
const upload = await client.vault.upload(vault.id, {
filename: 'contract.pdf',
contentType: 'application/pdf'
});
// Upload the file directly to S3
await fetch(upload.uploadUrl, {
method: 'PUT',
headers: { 'Content-Type': 'application/pdf' },
body: fs.readFileSync('contract.pdf')
});
console.log(`Object ID: ${upload.objectId}`);
import requests
# Get upload URL
upload = client.vault.upload(vault.id,
filename='contract.pdf',
content_type='application/pdf'
)
# Upload the file directly to S3
with open('contract.pdf', 'rb') as f:
requests.put(upload.upload_url, data=f,
headers={'Content-Type': 'application/pdf'})
print(f'Object ID: {upload.object_id}')
upload, _ := client.Vault.Upload(ctx, vaultID, casedev.VaultUploadParams{
Filename: casedev.F("document.pdf"),
ContentType: casedev.F("application/pdf"),
})
// PUT file to upload.UploadURL via net/http
fmt.Println(upload.ObjectID)
Ingest (index) the document
curl -X POST "https://api.case.dev/vault/$VAULT_ID/ingest/$OBJECT_ID" \
-H "Authorization: Bearer $CASEDEV_API_KEY"
# Check status (poll until completed)
curl "https://api.case.dev/vault/$VAULT_ID/objects/$OBJECT_ID" \
-H "Authorization: Bearer $CASEDEV_API_KEY" | jq '.ingestionStatus'
casedev vault ingest --id $VAULT_ID --object-id $OBJECT_ID
await client.vault.ingest(vault.id, upload.objectId);
// Poll until complete
let obj = await client.vault.objects.retrieve(vault.id, upload.objectId);
while (obj.ingestionStatus === 'processing') {
await new Promise(r => setTimeout(r, 5000));
obj = await client.vault.objects.retrieve(vault.id, upload.objectId);
}
console.log(`Ingestion: ${obj.ingestionStatus}`);
import time
client.vault.ingest(upload.object_id, id=vault.id)
# Poll until complete
obj = client.vault.objects.retrieve(vault.id, upload.object_id)
while obj.ingestion_status == 'processing':
time.sleep(5)
obj = client.vault.objects.retrieve(vault.id, upload.object_id)
print(f'Ingestion: {obj.ingestion_status}')
result, _ := client.Vault.Ingest(ctx, objectID, casedev.VaultIngestParams{
ID: casedev.F(vaultID),
})
fmt.Println(result.Status)
Ingestion is async. Wait for
ingestionStatus: "completed" before searching. For production, use webhooks instead of polling.Step 2: Search Your Documents
Query your vault with a natural language question:curl -X POST "https://api.case.dev/vault/$VAULT_ID/search" \
-H "Authorization: Bearer $CASEDEV_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "What are the key terms of this agreement?",
"method": "hybrid",
"limit": 10
}'
casedev vault search \
--id $VAULT_ID \
--query "What are the key terms of this agreement?"
const searchResults = await client.vault.search(vaultId, {
query: 'What are the key terms of this agreement?',
method: 'hybrid',
limit: 10
});
console.log(`Found ${searchResults.chunks.length} relevant passages`);
search_results = client.vault.search(vault_id,
query='What are the key terms of this agreement?',
method='hybrid',
top_k=10
)
print(f'Found {len(search_results.chunks)} relevant passages')
results, _ := client.Vault.Search(ctx, vaultID, casedev.VaultSearchParams{
Query: casedev.F("What are the key terms of this agreement?"),
Method: casedev.F(casedev.VaultSearchParamsMethodHybrid),
})
for _, chunk := range results.Chunks {
fmt.Println(chunk.Text)
}
Response
{
"chunks": [
{
"text": "The Parties agree to the following terms...",
"object_id": "obj_xyz789",
"hybridScore": 0.89,
"vectorScore": 0.92,
"bm25Score": 0.78
}
],
"sources": [
{ "id": "obj_xyz789", "filename": "contract.pdf" }
]
}
Step 3: Analyze with an LLM
Pass the search results to an LLM for structured analysis:casedev llm:v1:chat create-completion \
--model anthropic/claude-sonnet-4.5 \
--message '{role: system, content: "You are a legal document analyst. Cite specific passages to support your analysis."}' \
--message '{role: user, content: "<document excerpts>\n\nQuestion: What are the key terms of this agreement?"}' \
--temperature 0.3
const chunks = searchResults.chunks.map(c => c.text).join('\n\n');
const sources = searchResults.sources.map(s => s.filename).join(', ');
const analysis = await client.llm.v1.chat.createCompletion({
model: 'anthropic/claude-sonnet-4.5',
messages: [
{
role: 'system',
content: 'You are a legal document analyst. Analyze the provided document excerpts and answer the user\'s question. Always cite specific passages to support your analysis.'
},
{
role: 'user',
content: `## Document Excerpts\n\n${chunks}\n\n## Sources\n${sources}\n\n## Question\nWhat are the key terms of this agreement?`
}
],
temperature: 0.3
});
console.log(analysis.choices[0].message.content);
chunks = '\n\n'.join(c.text for c in search_results.chunks)
sources = ', '.join(s.filename for s in search_results.sources)
analysis = client.llm.v1.chat.create_completion(
model='anthropic/claude-sonnet-4.5',
messages=[
{
'role': 'system',
'content': 'You are a legal document analyst. Analyze the provided document excerpts and answer the user\'s question. Always cite specific passages to support your analysis.'
},
{
'role': 'user',
'content': f'## Document Excerpts\n\n{chunks}\n\n## Sources\n{sources}\n\n## Question\nWhat are the key terms of this agreement?'
}
],
temperature=0.3
)
print(analysis.choices[0].message.content)
resp, _ := client.Llm.V1.Chat.NewCompletion(ctx, casedev.LlmV1ChatNewCompletionParams{
Model: casedev.F("anthropic/claude-sonnet-4.5"),
Messages: casedev.F([]casedev.LlmV1ChatNewCompletionParamsMessage{
{
Role: casedev.F(casedev.LlmV1ChatNewCompletionParamsMessagesRoleSystem),
Content: casedev.F("You are a legal document analyst. Analyze the provided document excerpts and answer the user's question. Always cite specific passages to support your analysis."),
},
{
Role: casedev.F(casedev.LlmV1ChatNewCompletionParamsMessagesRoleUser),
Content: casedev.F("## Document Excerpts\n\n" + chunks + "\n\n## Sources\n" + sources + "\n\n## Question\nWhat are the key terms of this agreement?"),
},
}),
Temperature: casedev.F(float64(0.3)),
})
fmt.Println(resp.Choices[0].Message.Content)
Response
{
"choices": [{
"message": {
"content": "Based on the contract excerpts, the key terms include:\n\n1. **Payment Terms**: Section 3.2 states that payment is due within 30 days...\n\n2. **Termination**: Either party may terminate with 90 days written notice (Section 7.1)...\n\n3. **Liability Cap**: Liability is limited to the total fees paid in the preceding 12 months (Section 9.3)..."
}
}],
"usage": {
"prompt_tokens": 1245,
"completion_tokens": 387,
"total_tokens": 1632,
"cost": 0.004896
}
}
Complete Example
Putting it all together — a reusable function that searches and analyzes:# 1. Search vault for relevant documents
casedev vault search --id $VAULT_ID \
--query "What are the indemnification clauses?" \
--method hybrid --limit 10
# 2. Analyze with LLM
casedev llm:v1:chat create-completion \
--model anthropic/claude-sonnet-4.5 \
--message '{role: system, content: "You are a senior legal analyst. Answer using only the provided excerpts."}' \
--message '{role: user, content: "<document excerpts from search>\n\nQuestion: What are the indemnification clauses?"}' \
--temperature 0.3
import Casedev from 'casedev';
const client = new Casedev({ apiKey: process.env.CASEDEV_API_KEY });
async function analyzeDocuments(vaultId: string, query: string) {
// 1. Search
const searchResults = await client.vault.search(vaultId, {
query,
method: 'hybrid',
limit: 10
});
const chunks = searchResults.chunks.map(c => c.text).join('\n\n');
const sources = searchResults.sources.map(s => s.filename).join(', ');
// 2. Analyze
const analysis = await client.llm.v1.chat.createCompletion({
model: 'anthropic/claude-sonnet-4.5',
messages: [
{
role: 'system',
content: `You are a senior legal analyst. Provide comprehensive analysis with:
1. Executive Summary
2. Key Findings (cite specific passages)
3. Supporting Evidence
4. Recommendations`
},
{
role: 'user',
content: `## Document Excerpts\n\n${chunks}\n\n## Sources\n${sources}\n\n## Question\n${query}`
}
],
temperature: 0.3
});
return {
answer: analysis.choices[0].message.content,
sources: searchResults.sources,
usage: analysis.usage
};
}
// Run it
const result = await analyzeDocuments('vault_abc123', 'What are the indemnification clauses?');
console.log(result.answer);
import os
import casedev
client = casedev.Casedev(api_key=os.environ['CASEDEV_API_KEY'])
def analyze_documents(vault_id: str, query: str) -> dict:
# 1. Search
search_results = client.vault.search(vault_id,
query=query,
method='hybrid',
top_k=10
)
chunks = '\n\n'.join(c.text for c in search_results.chunks)
sources = ', '.join(s.filename for s in search_results.sources)
# 2. Analyze
analysis = client.llm.v1.chat.create_completion(
model='anthropic/claude-sonnet-4.5',
messages=[
{
'role': 'system',
'content': '''You are a senior legal analyst. Provide comprehensive analysis with:
1. Executive Summary
2. Key Findings (cite specific passages)
3. Supporting Evidence
4. Recommendations'''
},
{
'role': 'user',
'content': f'## Document Excerpts\n\n{chunks}\n\n## Sources\n{sources}\n\n## Question\n{query}'
}
],
temperature=0.3
)
return {
'answer': analysis.choices[0].message.content,
'sources': search_results.sources,
'usage': analysis.usage
}
# Run it
result = analyze_documents('vault_abc123', 'What are the indemnification clauses?')
print(result['answer'])
// 1. Search vault
searchResults, _ := client.Vault.Search(ctx, vaultID, casedev.VaultSearchParams{
Query: casedev.F(query),
Method: casedev.F(casedev.VaultSearchParamsMethodHybrid),
TopK: casedev.F(int64(10)),
})
// Build context from chunks
var chunks string
for _, c := range searchResults.Chunks {
chunks += c.Text + "\n\n"
}
var sources string
for _, s := range searchResults.Sources {
sources += s.Filename + ", "
}
// 2. Analyze with LLM
analysis, _ := client.Llm.V1.Chat.NewCompletion(ctx, casedev.LlmV1ChatNewCompletionParams{
Model: casedev.F("anthropic/claude-sonnet-4.5"),
Messages: casedev.F([]casedev.LlmV1ChatNewCompletionParamsMessage{
{
Role: casedev.F(casedev.LlmV1ChatNewCompletionParamsMessagesRoleSystem),
Content: casedev.F("You are a senior legal analyst. Answer questions using only the provided document excerpts. Cite sources."),
},
{
Role: casedev.F(casedev.LlmV1ChatNewCompletionParamsMessagesRoleUser),
Content: casedev.F("## Document Excerpts\n\n" + chunks + "\n\n## Question\n" + query),
},
}),
Temperature: casedev.F(float64(0.3)),
})
fmt.Println(analysis.Choices[0].Message.Content)
Extending the Analyzer
Add Entity Extraction
Run a second LLM pass to extract structured entities:casedev llm:v1:chat create-completion \
--model openai/gpt-4o \
--message '{role: system, content: "Extract named entities as JSON: {people: [], organizations: [], dates: [], locations: [], monetary_amounts: []}"}' \
--message '{role: user, content: "<document text>"}' \
--temperature 0
const entities = await client.llm.v1.chat.createCompletion({
model: 'openai/gpt-4o',
messages: [
{
role: 'system',
content: 'Extract named entities as JSON: {people: [], organizations: [], dates: [], locations: [], monetary_amounts: []}'
},
{ role: 'user', content: chunks }
],
temperature: 0
});
entities = client.llm.v1.chat.create_completion(
model='openai/gpt-4o',
messages=[
{
'role': 'system',
'content': 'Extract named entities as JSON: {people: [], organizations: [], dates: [], locations: [], monetary_amounts: []}'
},
{'role': 'user', 'content': chunks}
],
temperature=0
)
entities, _ := client.Llm.V1.Chat.NewCompletion(ctx, casedev.LlmV1ChatNewCompletionParams{
Model: casedev.F("openai/gpt-4o"),
Messages: casedev.F([]casedev.LlmV1ChatNewCompletionParamsMessage{
{
Role: casedev.F(casedev.LlmV1ChatNewCompletionParamsMessagesRoleSystem),
Content: casedev.F("Extract named entities as JSON: {people: [], organizations: [], dates: [], locations: [], monetary_amounts: []}"),
},
{
Role: casedev.F(casedev.LlmV1ChatNewCompletionParamsMessagesRoleUser),
Content: casedev.F(chunks),
},
}),
Temperature: casedev.F(float64(0)),
})
fmt.Println(entities.Choices[0].Message.Content)
Generate a PDF Report
Convert the analysis into a formatted document:casedev format:v1 document \
--content "# Report" \
--input-format md --output-format pdf
const report = await client.format.v1.document({
content: `# Legal Analysis Report\n\n**Query:** ${query}\n\n${analysis.choices[0].message.content}`,
input_format: 'md',
output_format: 'pdf'
});
report = client.format.v1.create_document(
content=f'# Legal Analysis Report\n\n**Query:** {query}\n\n{analysis.choices[0].message.content}',
input_format='md',
output_format='pdf'
)
result, _ := client.Format.V1.NewDocument(ctx, casedev.FormatV1NewDocumentParams{
Content: casedev.F("# Report\n\nContent here..."),
InputFormat: casedev.F("md"),
OutputFormat: casedev.F("pdf"),
})
// result is *http.Response with PDF body
Production Tips
Error Handling
# CLI displays errors to stderr with status codes
casedev vault search --id $VAULT_ID --query "analysis query"
# Error: 404 Not Found — check your vault ID
# Error: 429 Too Many Requests — retry after a delay
try {
const result = await analyzeDocuments(vaultId, query);
console.log(result.answer);
} catch (error) {
if (error.status === 404) {
console.error('Vault not found — check your vault ID');
} else if (error.status === 429) {
console.error('Rate limited — retry after a delay');
} else {
console.error('Analysis failed:', error.message);
}
}
try:
result = analyze_documents(vault_id, query)
print(result['answer'])
except casedev.NotFoundError:
print('Vault not found — check your vault ID')
except casedev.RateLimitError:
print('Rate limited — retry after a delay')
except casedev.APIError as e:
print(f'Analysis failed: {e.message}')
result, err := analyzeDocuments(ctx, vaultID, query)
if err != nil {
var apiErr *casedev.Error
if errors.As(err, &apiErr) {
switch apiErr.StatusCode {
case 404:
fmt.Println("Vault not found — check your vault ID")
case 429:
fmt.Println("Rate limited — retry after a delay")
default:
fmt.Printf("Analysis failed: %s\n", apiErr.Message)
}
}
}
Use
temperature: 0 for factual extraction tasks. Try cheaper models like deepseek/deepseek-chat for simpler analysis.Next Steps
- Vault Search Reference — Advanced search methods and filtering
- Chat Completions — Streaming, system prompts, and more
- Format Service — PDF and DOCX generation

