Skip to main content

What You’ll Build

An intelligent agent that can:
  • Store documents in an encrypted vault with automatic OCR and embedding generation
  • Answer questions by retrieving relevant document chunks and synthesizing responses
  • Add knowledge dynamically as users provide new information
  • Cite sources with page numbers and document references

Why RAG?

Large Language Models are powerful, but they can only reason on their training data. RAG solves this by:
  1. Embedding your documents into a searchable vector space
  2. Retrieving relevant chunks when a user asks a question
  3. Augmenting the LLM’s context with those chunks
  4. Generating an accurate, grounded response
With Case.dev, you don’t need to manage embeddings, vector databases, or chunking strategies — Vaults handle all of this automatically.

Architecture

Prerequisites

  • Case.dev API key (get one here)
  • Node.js 18+ or Python 3.9+
  • Vercel AI SDK (optional, for streaming UI)

Project Setup

Step 1: Install dependencies

# No installation needed — just set your API key
export CASEDEV_API_KEY="sk_case_YOUR_API_KEY"
brew tap CaseMark/casedev && brew install casedev
npm install casedev ai zod
pip install casedev
go get github.com/CaseMark/casedev-go

Step 2: Set up environment variables

Environment
CASEDEV_API_KEY=sk_case_your_api_key

Step 3: Create a vault for your knowledge base

curl -X POST https://api.case.dev/vault \
  -H "Authorization: Bearer $CASEDEV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Knowledge Base",
    "description": "Document intelligence agent knowledge store"
  }'
casedev vault create \
  --name "Knowledge Base" \
  --description "Document intelligence agent knowledge store"
import Casedev from 'casedev';

const client = new Casedev({ apiKey: process.env.CASEDEV_API_KEY });

// Create a vault to store your knowledge base
const vault = await client.vault.create({
  name: 'Knowledge Base',
  description: 'Document intelligence agent knowledge store'
});

console.log(`Vault created: ${vault.id}`);
// Save this ID - you'll need it for queries
import os
import casedev

client = casedev.Casedev(api_key=os.environ['CASEDEV_API_KEY'])

# Create a vault to store your knowledge base
vault = client.vault.create(
    name='Knowledge Base',
    description='Document intelligence agent knowledge store'
)

print(f'Vault created: {vault.id}')
# Save this ID - you'll need it for queries
vault, _ := client.Vault.New(ctx, casedev.VaultNewParams{
	Name:        casedev.F("Knowledge Base"),
	Description: casedev.F("Document intelligence agent knowledge store"),
})
fmt.Printf("Vault created: %s\n", vault.ID)

Core Functions

1. Add Documents to Knowledge Base

When a user uploads a document or provides information, store it in the vault:
casedev vault upload \
  --id $VAULT_ID \
  --filename "document.pdf" \
  --content-type "application/pdf"
import Casedev from 'casedev';

const client = new Casedev({ apiKey: process.env.CASEDEV_API_KEY });
const VAULT_ID = process.env.VAULT_ID;

/**
 * Add a document or text to the knowledge base
 */
async function addToKnowledgeBase(content: string, metadata?: Record<string, string>) {
  // For text content, create a text file
  const blob = new Blob([content], { type: 'text/plain' });
  const filename = `knowledge-${Date.now()}.txt`;
  
  // Get upload URL
  const upload = await client.vault.upload(VAULT_ID, {
    filename,
    contentType: 'text/plain',
    metadata: {
      source: 'user-input',
      timestamp: new Date().toISOString(),
      ...metadata
    }
  });
  
  // Upload content
  await fetch(upload.uploadUrl, {
    method: 'PUT',
    headers: { 'Content-Type': 'text/plain' },
    body: content
  });
  
  // Trigger ingestion (generates embeddings automatically)
  await client.vault.ingest(VAULT_ID, upload.objectId);
  
  return { objectId: upload.objectId, filename };
}

/**
 * Add a file (PDF, Word, image) to the knowledge base
 */
async function addFileToKnowledgeBase(
  file: Buffer, 
  filename: string, 
  contentType: string
) {
  const upload = await client.vault.upload(VAULT_ID, {
    filename,
    contentType,
    metadata: {
      source: 'file-upload',
      timestamp: new Date().toISOString()
    }
  });
  
  await fetch(upload.uploadUrl, {
    method: 'PUT',
    headers: { 'Content-Type': contentType },
    body: file
  });
  
  // Ingestion handles OCR (if needed) and embedding generation
  const job = await client.vault.ingest(VAULT_ID, upload.objectId);
  
  return { objectId: upload.objectId, jobId: job.id };
}
import os
import requests
from datetime import datetime
import casedev

client = casedev.Casedev(api_key=os.environ['CASEDEV_API_KEY'])
VAULT_ID = os.environ['VAULT_ID']

def add_to_knowledge_base(content: str, metadata: dict = None):
    """Add text content to the knowledge base"""
    filename = f'knowledge-{int(datetime.now().timestamp())}.txt'
    
    # Get upload URL
    upload = client.vault.upload(VAULT_ID,
        filename=filename,
        content_type='text/plain',
        metadata={
            'source': 'user-input',
            'timestamp': datetime.now().isoformat(),
            **(metadata or {})
        }
    )
    
    # Upload content
    requests.put(upload.upload_url, 
        data=content.encode('utf-8'),
        headers={'Content-Type': 'text/plain'}
    )
    
    # Trigger ingestion (generates embeddings automatically)
    client.vault.ingest(upload.object_id, id=VAULT_ID)
    
    return {'object_id': upload.object_id, 'filename': filename}

def add_file_to_knowledge_base(file_path: str, content_type: str):
    """Add a file (PDF, Word, image) to the knowledge base"""
    filename = os.path.basename(file_path)
    
    upload = client.vault.upload(VAULT_ID,
        filename=filename,
        content_type=content_type,
        metadata={
            'source': 'file-upload',
            'timestamp': datetime.now().isoformat()
        }
    )
    
    with open(file_path, 'rb') as f:
        requests.put(upload.upload_url, data=f,
            headers={'Content-Type': content_type})
    
    # Ingestion handles OCR (if needed) and embedding generation
    job = client.vault.ingest(upload.object_id, id=VAULT_ID)
    
    return {'object_id': upload.object_id, 'job_id': job.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)

2. Retrieve Relevant Information

Search the knowledge base for content relevant to a user’s question:
casedev vault search \
  --id $VAULT_ID \
  --query "search query"
/**
 * Find relevant content from the knowledge base
 */
async function findRelevantContent(query: string, limit: number = 5) {
  const results = await client.vault.search(VAULT_ID, {
    query,
    method: 'hybrid',  // Combines semantic + keyword search
    limit
  });
  
  // Format results for LLM context
  return results.chunks.map(chunk => ({
    content: chunk.text,
    source: chunk.filename,
    page: chunk.page,
    score: chunk.hybridScore
  }));
}
def find_relevant_content(query: str, limit: int = 5):
    """Find relevant content from the knowledge base"""
    results = client.vault.search(VAULT_ID,
        query=query,
        method='hybrid',  # Combines semantic + keyword search
        top_k=limit
    )
    
    # Format results for LLM context
    return [{
        'content': chunk.text,
        'source': chunk.filename,
        'page': chunk.page,
        'score': chunk.hybrid_score
    } for chunk in results.chunks]
results, _ := client.Vault.Search(ctx, vaultID, casedev.VaultSearchParams{
	Query: casedev.F("search query"),
	Method: casedev.F(casedev.VaultSearchParamsMethodHybrid),
})
for _, chunk := range results.Chunks {
	fmt.Println(chunk.Text)
}

3. Generate Responses with Context

Use the LLM Gateway to generate responses grounded in your documents:
# Search knowledge base
casedev vault search --id $VAULT_ID \
  --query "What are the key terms?" --method hybrid --limit 10

# Answer with LLM
casedev llm:v1:chat create-completion \
  --model anthropic/claude-sonnet-4.5 \
  --message '{role: system, content: "Answer using only the provided context. Cite sources."}' \
  --message '{role: user, content: "Context: <search results>\n\nQuestion: What are the key terms?"}' \
  --temperature 0.3 --max-tokens 1000
/**
 * Answer a question using the knowledge base
 */
async function answerQuestion(question: string) {
  // 1. Retrieve relevant context
  const relevantContent = await findRelevantContent(question);
  
  if (relevantContent.length === 0) {
    return {
      answer: "I don't have any information about that in my knowledge base.",
      sources: []
    };
  }
  
  // 2. Format context for the LLM
  const context = relevantContent
    .map((c, i) => `[${i + 1}] ${c.content} (Source: ${c.source}, Page ${c.page})`)
    .join('\n\n');
  
  // 3. Generate response with LLM
  const response = await client.llm.v1.chat.createCompletion({
    model: 'anthropic/claude-sonnet-4.5',
    messages: [
      {
        role: 'system',
        content: `You are a helpful assistant that answers questions based on the provided context.
        
Rules:
- Only use information from the provided context
- Cite sources using [1], [2], etc.
- If the context doesn't contain relevant information, say so
- Be concise and accurate`
      },
      {
        role: 'user',
        content: `Context:\n${context}\n\nQuestion: ${question}`
      }
    ],
    temperature: 0.3,
    max_tokens: 1000
  });
  
  return {
    answer: response.choices[0].message.content,
    sources: relevantContent.map(c => ({
      filename: c.source,
      page: c.page,
      excerpt: c.content.substring(0, 200) + '...'
    }))
  };
}
def answer_question(question: str):
    """Answer a question using the knowledge base"""
    # 1. Retrieve relevant context
    relevant_content = find_relevant_content(question)
    
    if not relevant_content:
        return {
            'answer': "I don't have any information about that in my knowledge base.",
            'sources': []
        }
    
    # 2. Format context for the LLM
    context = '\n\n'.join([
        f"[{i+1}] {c['content']} (Source: {c['source']}, Page {c['page']})"
        for i, c in enumerate(relevant_content)
    ])
    
    # 3. Generate response with LLM
    response = client.llm.v1.chat.create_completion(
        model='anthropic/claude-sonnet-4.5',
        messages=[
            {
                'role': 'system',
                'content': '''You are a helpful assistant that answers questions based on the provided context.
        
Rules:
- Only use information from the provided context
- Cite sources using [1], [2], etc.
- If the context doesn't contain relevant information, say so
- Be concise and accurate'''
            },
            {
                'role': 'user',
                'content': f'Context:\n{context}\n\nQuestion: {question}'
            }
        ],
        temperature=0.3,
        max_tokens=1000
    )
    
    return {
        'answer': response.choices[0].message.content,
        'sources': [{
            'filename': c['source'],
            'page': c['page'],
            'excerpt': c['content'][:200] + '...'
        } for c in relevant_content]
    }
// 1. Retrieve relevant context from vault
results, _ := client.Vault.Search(ctx, vaultID, casedev.VaultSearchParams{
	Query:  casedev.F(question),
	Method: casedev.F(casedev.VaultSearchParamsMethodHybrid),
	TopK:   casedev.F(int64(10)),
})

// Format context
var context string
for i, c := range results.Chunks {
	context += fmt.Sprintf("[%d] %s (Source: %s)\n\n", i+1, c.Text, c.Filename)
}

// 2. Generate answer with LLM
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 helpful assistant. Answer using only the provided context. Cite sources using [1], [2], etc."),
		},
		{
			Role:    casedev.F(casedev.LlmV1ChatNewCompletionParamsMessagesRoleUser),
			Content: casedev.F("Context:\n" + context + "\n\nQuestion: " + question),
		},
	}),
	Temperature: casedev.F(float64(0.3)),
	MaxTokens:   casedev.F(int64(1000)),
})
fmt.Println(resp.Choices[0].Message.Content)

Building the Agent with Tools

For a more sophisticated agent that can decide when to search vs. add knowledge, use tool calling:
# Tool-calling agents require programmatic control flow.
# Use the Go or TypeScript SDK for agent loops.
# For simple questions, pipe vault search into LLM:
casedev vault search --id $VAULT_ID --query "What deadlines are coming up?"

casedev llm:v1:chat create-completion \
  --model anthropic/claude-sonnet-4.5 \
  --message '{role: system, content: "You are a document intelligence agent."}' \
  --message '{role: user, content: "Based on these documents: <search results>\n\nWhat deadlines are coming up?"}'
import { z } from 'zod';

// Define tools for the agent
const tools = {
  searchKnowledgeBase: {
    description: 'Search the knowledge base for information relevant to a question',
    parameters: z.object({
      query: z.string().describe('The search query')
    }),
    execute: async ({ query }) => {
      const results = await findRelevantContent(query);
      return results.length > 0 
        ? results 
        : 'No relevant information found in knowledge base.';
    }
  },
  
  addToKnowledgeBase: {
    description: 'Add new information to the knowledge base. Use this when the user provides facts or documents.',
    parameters: z.object({
      content: z.string().describe('The content to add'),
      topic: z.string().optional().describe('Topic or category')
    }),
    execute: async ({ content, topic }) => {
      const result = await addToKnowledgeBase(content, { topic });
      return `Added to knowledge base: ${result.filename}`;
    }
  }
};

/**
 * Run the agent with tool support
 */
async function runAgent(userMessage: string, conversationHistory: any[] = []) {
  const messages = [
    {
      role: 'system',
      content: `You are a helpful document intelligence assistant.

Your capabilities:
1. Search your knowledge base to answer questions
2. Add new information when users provide it

Always search the knowledge base before answering factual questions.
If you don't find relevant information, say so honestly.
When adding information, confirm what was added.`
    },
    ...conversationHistory,
    { role: 'user', content: userMessage }
  ];
  
  // First call - may request tool use
  let response = await client.llm.v1.chat.createCompletion({
    model: 'anthropic/claude-sonnet-4.5',
    messages,
    tools: ,
      }
    tool_choice: 'auto'
  });
  
  // Handle tool calls
  while (response.choices[0].message.tool_calls) {
    const toolCalls = response.choices[0].message.tool_calls;
    
    // Execute each tool
    const toolResults = await Promise.all(
      toolCalls.map(async (call) => {
        const tool = tools[call.function.name];
        const args = JSON.parse(call.function.arguments);
        const result = await tool.execute(args);
        return {
          role: 'tool',
          tool_call_id: call.id,
          content: JSON.stringify(result)
        };
      })
    );
    
    // Continue conversation with tool results
    messages.push(response.choices[0].message);
    messages.push(...toolResults);
    
    response = await client.llm.v1.chat.createCompletion({
      model: 'anthropic/claude-sonnet-4.5',
      messages,
      tools: ,
        }
      }))
    });
  }
  
  return response.choices[0].message.content;
}
import json

# Define tools for the agent
tools = [
    {
        'type': 'function',
        'function': {
            'name': 'search_knowledge_base',
            'description': 'Search the knowledge base for information relevant to a question',
            'parameters': {
                'type': 'object',
                'properties': {
                    'query': {
                        'type': 'string',
                        'description': 'The search query'
                    }
                },
                'required': ['query']
            }
        }
    },
    {
        'type': 'function',
        'function': {
            'name': 'add_to_knowledge_base',
            'description': 'Add new information to the knowledge base',
            'parameters': {
                'type': 'object',
                'properties': {
                    'content': {
                        'type': 'string',
                        'description': 'The content to add'
                    },
                    'topic': {
                        'type': 'string',
                        'description': 'Topic or category'
                    }
                },
                'required': ['content']
            }
        }
    }
]

def execute_tool(name: str, args: dict):
    """Execute a tool by name"""
    if name == 'search_knowledge_base':
        results = find_relevant_content(args['query'])
        return results if results else 'No relevant information found.'
    elif name == 'add_to_knowledge_base':
        result = add_to_knowledge_base(args['content'], {'topic': args.get('topic', '')})
        return f"Added to knowledge base: {result['filename']}"

def run_agent(user_message: str, conversation_history: list = None):
    """Run the agent with tool support"""
    messages = [
        {
            'role': 'system',
            'content': '''You are a helpful document intelligence assistant.

Your capabilities:
1. Search your knowledge base to answer questions
2. Add new information when users provide it

Always search the knowledge base before answering factual questions.
If you don't find relevant information, say so honestly.'''
        },
        *(conversation_history or []),
        {'role': 'user', 'content': user_message}
    ]
    
    # First call - may request tool use
    response = client.llm.v1.chat.create_completion(
        model='anthropic/claude-sonnet-4.5',
        messages=messages
    )
    
    # Handle tool calls
    while response.choices[0].message.tool_calls:
        tool_calls = response.choices[0].message.tool_calls
        
        # Add assistant message with tool calls
        messages.append(response.choices[0].message)
        
        # Execute each tool and add results
        for call in tool_calls:
            args = json.loads(call.function.arguments)
            result = execute_tool(call.function.name, args)
            messages.append({
                'role': 'tool',
                'tool_call_id': call.id,
                'content': json.dumps(result)
            })
        
        # Continue conversation
        response = client.llm.v1.chat.create_completion(
            model='anthropic/claude-sonnet-4.5',
            messages=messages
        )
    
    return response.choices[0].message.content
// Tool-calling agent loop in Go
// 1. Initial request with tools
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 document intelligence agent with access to a knowledge base."),
		},
		{
			Role:    casedev.F(casedev.LlmV1ChatNewCompletionParamsMessagesRoleUser),
			Content: casedev.F(question),
		},
	}),
	// Tools defined as JSON schema (searchKnowledgeBase, addToKnowledgeBase)
})

// 2. If tool calls returned, execute them and continue
// See TypeScript example for full tool-calling loop pattern
fmt.Println(resp.Choices[0].Message.Content)

Integration with Vercel AI SDK

For Next.js applications, integrate with the Vercel AI SDK for streaming responses:
Typescript
// app/api/chat/route.ts
import { streamText, tool } from 'ai';
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
import { z } from 'zod';
import Casedev from 'casedev';

const client = new Casedev({ apiKey: process.env.CASEDEV_API_KEY });
const VAULT_ID = process.env.VAULT_ID;

// Set up Case.dev as an OpenAI-compatible provider for Vercel AI SDK
const casedev = createOpenAICompatible({
  name: 'casedev',
  baseURL: 'https://api.case.dev/llm/v1',
  headers: { Authorization: `Bearer ${process.env.CASEDEV_API_KEY}` },
});

export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = streamText({
    model: casedev('anthropic/claude-sonnet-4.5'),
    system: `You are a document intelligence assistant. 
Check your knowledge base before answering questions.
Only respond using information from tool calls.
If no relevant information is found, say "I don't have information about that."`,
    messages,
    maxSteps: 5,
    tools: {
      searchDocuments: tool({
        description: 'Search the document knowledge base',
        parameters: z.object({
          query: z.string().describe('The search query')
        }),
        execute: async ({ query }) => {
          const results = await client.vault.search(VAULT_ID, {
            query,
            method: 'hybrid',
            limit: 5
          });
          return results.chunks.map(c => ({
            text: c.text,
            source: c.filename,
            page: c.page
          }));
        }
      }),
      
      addDocument: tool({
        description: 'Add information to the knowledge base',
        parameters: z.object({
          content: z.string().describe('Content to add')
        }),
        execute: async ({ content }) => {
          const upload = await client.vault.upload(VAULT_ID, {
            filename: `note-${Date.now()}.txt`,
            contentType: 'text/plain'
          });
          await fetch(upload.uploadUrl, {
            method: 'PUT',
            body: content
          });
          await client.vault.ingest(VAULT_ID, upload.objectId);
          return 'Added to knowledge base successfully';
        }
      })
    }
  });

  return result.toDataStreamResponse();
}

Example Usage

Typescript
// Add some knowledge
await addToKnowledgeBase(
  'The Smith v. Jones case was filed on March 15, 2024. The plaintiff alleges negligence in the maintenance of the property.',
  { topic: 'case-facts' }
);

await addToKnowledgeBase(
  'Deposition of John Smith on April 2, 2024: Witness stated he observed water damage on the ceiling two weeks before the incident.',
  { topic: 'depositions' }
);

// Ask questions
const result = await answerQuestion('When was the Smith v. Jones case filed?');
console.log(result.answer);
// "The Smith v. Jones case was filed on March 15, 2024 [1]."

const result2 = await answerQuestion('What did John Smith observe?');
console.log(result2.answer);
// "John Smith observed water damage on the ceiling two weeks before the incident [1]."

// Using the agent
const response = await runAgent('My favorite pizza topping is pepperoni. Remember that.');
console.log(response);
// "I've added that to my knowledge base. Your favorite pizza topping is pepperoni."

const response2 = await runAgent('What is my favorite pizza topping?');
console.log(response2);
// "According to my knowledge base, your favorite pizza topping is pepperoni."

Best Practices

Chunking is automatic. Case.dev Vaults automatically chunk documents into semantic segments optimized for retrieval. You don’t need to implement chunking yourself.
Combine semantic and keyword search for best results:
casedev vault search --id $VAULT_ID \
  --query "liability insurance coverage limits" \
  --method hybrid --limit 10
const results = await client.vault.search(VAULT_ID, {
  query: 'liability insurance coverage limits',
  method: 'hybrid',  // Best of both worlds
  limit: 10
});
results = client.vault.search(VAULT_ID,
    query='liability insurance coverage limits',
    method='hybrid',  # Best of both worlds
    top_k=10
)
results, _ := client.Vault.Search(ctx, vaultID, casedev.VaultSearchParams{
    Query:  casedev.F("liability insurance coverage limits"),
    Method: casedev.F(casedev.VaultSearchParamsMethodHybrid),
    TopK:   casedev.F(int64(10)),
})

2. Set appropriate temperature

Use low temperature for factual retrieval:
curl -X POST https://api.case.dev/llm/v1/chat/completions \
  -H "Authorization: Bearer $CASEDEV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
casedev llm:v1:chat create-completion \
  --model anthropic/claude-sonnet-4.5 \
  --message '{role: user, content: "..."}' \
  --temperature 0.2
const response = await client.llm.v1.chat.createCompletion({
  model: 'anthropic/claude-sonnet-4.5',
  messages: [...],
  temperature: 0.2  // More deterministic for factual tasks
});
response = client.llm.v1.chat.create_completion(
    model='anthropic/claude-sonnet-4.5',
    messages=[...],
    temperature=0.2  # More deterministic for factual tasks
)
resp, _ := client.Llm.V1.Chat.NewCompletion(ctx, casedev.LlmV1ChatNewCompletionParams{
    Model:       casedev.F("anthropic/claude-sonnet-4.5"),
    Messages:    casedev.F(messages),
    Temperature: casedev.F(float64(0.2)),
})

3. Structure your prompts

Be explicit about using only provided context:
Typescript
const systemPrompt = `You are a legal research assistant.

Rules:
- ONLY use information from the provided context
- If information is not in the context, say "I don't have that information"
- Always cite sources using [1], [2], etc.
- Never make up or infer facts not explicitly stated`;

4. Handle no results gracefully

Typescript
const results = await findRelevantContent(query);

if (results.length === 0 || results[0].score < 0.5) {
  return "I couldn't find relevant information in the knowledge base.";
}

Next Steps