Skip to main content

What You’ll Build

A script that:
  1. Searches your indexed documents in a vault
  2. Analyzes the results with an LLM
  3. Returns structured insights with source citations
Time to complete: 15 minutes

Architecture

┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│   Your App   │ ──▶ │ Vault Search │ ──▶ │   LLM        │
│              │     │   (Hybrid)   │     │   Analysis   │
│  vaultId     │     │  10 chunks   │     │  Structured  │
│  query       │     │  + sources   │     │  response    │
└──────────────┘     └──────────────┘     └──────────────┘

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

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}`);
Save the returned id — you’ll need it.

Upload a document

// 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}`);

Ingest (index) the document

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}`);
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:
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`);
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:
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);
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:
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);

Extending the Analyzer

Add Entity Extraction

Run a second LLM pass to extract structured entities:
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
});

Generate a PDF Report

Convert the analysis into a formatted document:
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'
});

Production Tips

Error Handling

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);
  }
}
Use temperature: 0 for factual extraction tasks. Try cheaper models like deepseek/deepseek-chat for simpler analysis.

Next Steps