In part 1 I laid out the architecture — one coordinator, two specialists. This part builds the data layer underneath it. The agents are only as good as what they can read and write, and a support system needs two very different kinds of storage.
Two kinds of data, two kinds of store
Bookings, customers, transactions — that's structured, relational data. You want exact answers, foreign keys, and transactions you can trust. SQLite in development, PostgreSQL in production.
Policies, FAQs, help articles — that's unstructured text, and customers never ask for it using the words it's written in. That needs semantic search, which is what a vector database is for. I cover the retrieval side properly in part 3; here I'm just standing both stores up.
┌─────────────────┐ ┌─────────────────┐
│ Relational DB │ │ Vector Database │
│ (SQLite/PG) │ │ (Embeddings) │
├─────────────────┤ ├─────────────────┤
│ • Customers │ │ • Policy Docs │
│ • Bookings │ │ • FAQ Content │
│ • Transactions │ │ • Knowledge Base│
│ • Train Data │ │ • Help Articles │
└─────────────────┘ └─────────────────┘
Each store does the thing it's good at: the relational database for precise operations, the vector database for "find me the policy that answers this." The system reaches for whichever the question needs.
The relational schema
Four tables. Nothing fancy — the point is that they're clean and the agents can reason about them.
1. Customers Table
CREATE TABLE customers (
customer_id TEXT PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
address TEXT,
phone TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Design rationale: Simple customer profiles with essential contact information. The address field will be used for location intelligence in later tutorials.
2. Bookings Table
CREATE TABLE bookings (
booking_id TEXT PRIMARY KEY,
customer_id TEXT,
departure_station TEXT,
arrival_station TEXT,
departure_datetime TIMESTAMP,
ticket_type TEXT,
price DECIMAL(10,2),
status TEXT,
FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
);
Key features:
- Human-readable booking IDs (e.g., "UKC001")
- Flexible status field for booking lifecycle management
- Precise pricing with decimal support
3. Transactions Table
CREATE TABLE transactions (
transaction_id TEXT PRIMARY KEY,
booking_id TEXT,
amount DECIMAL(10,2),
transaction_type TEXT,
payment_method TEXT,
status TEXT,
processed_at TIMESTAMP,
FOREIGN KEY (booking_id) REFERENCES bookings(booking_id)
);
Supports: Payment processing, refund tracking, financial reporting
4. Fares Table
CREATE TABLE fares (
route_id INTEGER PRIMARY KEY,
departure_station TEXT,
arrival_station TEXT,
distance_miles INTEGER,
standard_price DECIMAL(10,2),
flexible_price DECIMAL(10,2)
);
Enables: Dynamic pricing, route planning, availability checking
Database Initialization
Setting it up is two commands — one to create the schema, one to fill it with sample data:
# Navigate to the project directory
cd production-ai-customer-support
# Create database schema
python -c "from utils.create_schema import create_database_schema; create_database_schema()"
# Populate with realistic sample data
python -c "from utils.populate_data import populate_data; populate_data()"
This creates a SQLite database with complete sample data including 15 realistic customers and 25+ varied booking scenarios.
Vector Database for RAG Implementation
The Challenge of Knowledge Retrieval
Traditional keyword search fails when customers ask questions like:
- "Can I get my money back if I cancel?" (instead of "refund policy")
- "What happens if my train is delayed?" (instead of "compensation terms")
Solution: Semantic search using vector embeddings that understand meaning, not just keywords.
Vector Database Architecture
Our RAG system follows this pipeline:
Policy Documents → Text Chunking → Embeddings → Vector Storage → Semantic Search
Implementation Details
1. Document Processing
We start with company policy documents stored in database/UKConnect_policy.txt:
# Document contains:
- Refund policies and procedures
- Terms & conditions
- Fare rules and regulations
- Compensation policies
- Special circumstances handling
2. Intelligent Chunking Strategy
# From utils/chunking_data.py
chunk_config = {
"chunk_size": 500-1000, # characters per chunk
"overlap": 100, # character overlap between chunks
"preserve_boundaries": True # Don't break sentences/paragraphs
}
Why chunking matters:
- Manageable size: Fits LLM context windows efficiently
- Overlap: Ensures context continuity across chunk boundaries
- Boundary preservation: Maintains semantic coherence
3. Embedding Generation
# From database/vector_db.py
import google.generativeai as genai
def generate_embeddings(text_chunks):
embeddings = []
for chunk in text_chunks:
result = genai.embed_content(
model="models/embedding-001",
content=chunk
)
embeddings.append(result['embedding'])
return embeddings
4. Vector Storage
For development, we use pickle storage. Production systems would use dedicated vector databases like Pinecone, Weaviate, or ChromaDB.
# Initialize vector database
python -c "from database.vector_db import setup_vector_database; setup_vector_database()"
Configuration Setup
Environment Variables
Create a .env file with your configuration:
# API Configuration
GOOGLE_API_KEY=your_google_api_key_here
# Database Configuration
DATABASE_URL=sqlite:///database/ukconnect_rail.db
# Vector Database
VECTOR_DB_PATH=database/vector_db.pkl
Project Structure
production-ai-customer-support/
├── database/
│ ├── ukconnect_rail.db # SQLite operational database
│ ├── vector_db.pkl # Vector embeddings storage
│ ├── UKConnect_policy.txt # Source policy documents
│ ├── ukconnect_qa_pairs.json # Pre-processed Q&A pairs
│ └── ukconnect_rag_chunks.json # Processed text chunks
├── utils/
│ ├── create_schema.py # Database schema creation
│ ├── populate_data.py # Sample data generation
│ └── chunking_data.py # Document processing
Sample Data Population
Realistic Customer Profiles
Our sample data includes diverse, realistic customer profiles:
# Example customer data
{
"customer_id": "CUS001",
"name": "James Thompson",
"email": "james.thompson@email.co.uk",
"address": "45 Baker Street, London, W1U 7EW",
"phone": "+44 20 7946 0958"
}
15 customers covering:
- Different geographic locations (London, Manchester, Birmingham, Glasgow)
- Various demographics and communication preferences
- Mixed booking histories and support interactions
Complex Booking Scenarios
# Example booking data
{
"booking_id": "UKC001",
"customer_id": "CUS001",
"departure_station": "London Euston",
"arrival_station": "Manchester Piccadilly",
"departure_datetime": "2024-08-15 09:30:00",
"ticket_type": "Standard",
"price": 85.50,
"status": "Active"
}
25+ bookings including:
- Various routes and pricing tiers
- Different booking statuses (Active, Cancelled, Modified)
- Edge cases for testing (same-day changes, refunds, etc.)
RAG System Deep Dive
The Retrieval Process
When a customer asks a question, our RAG system follows these steps:
- Query Analysis: Extract semantic meaning from the customer's question
- Vector Search: Find the most relevant policy documents using cosine similarity
- Context Preparation: Combine retrieved information with query context
- Response Generation: LLM creates a response using the retrieved knowledge
Example RAG Flow
# Customer query: "Can I cancel my ticket and get a refund?"
# 1. Generate query embedding
query_embedding = embed_content("Can I cancel my ticket and get a refund?")
# 2. Search vector database
relevant_chunks = vector_db.search(query_embedding, top_k=3)
# Returns: [
# "UKConnect Refund Policy: Tickets can be cancelled up to...",
# "Cancellation fees apply as follows: Standard tickets...",
# "Refund processing typically takes 3-5 business days..."
# ]
# 3. Generate contextual response
response = llm.generate(
query="Can I cancel my ticket and get a refund?",
context=relevant_chunks,
customer_data=customer_profile
)
Performance Optimization Strategies
1. Chunk Overlap Management
overlap_config = {
"size": 100, # characters
"purpose": "context_continuity"
}
Ensures important information isn't lost at chunk boundaries.
2. Relevance Filtering
similarity_threshold = 0.7 # Only return highly relevant results
filtered_results = [r for r in results if r.similarity > similarity_threshold]
3. Caching Strategy
# Cache frequent queries for faster response times
cache_config = {
"ttl": 3600, # 1 hour cache lifetime
"max_size": 1000, # Maximum cached queries
}
4. Batch Processing
# Process multiple documents efficiently
batch_size = 32
embeddings = process_embeddings_batch(text_chunks, batch_size)
System Testing and Validation
Database Health Verification
# Test database connectivity and data integrity
python -c "
from database.database import DatabaseManager
db = DatabaseManager()
print('Customers loaded:', len(db.get_all_customers()))
print('Bookings loaded:', len(db.get_all_bookings()))
print('Database health: OK' if db.health_check() else 'ERROR')
"
Vector Database Testing
# Verify vector search functionality
python -c "
from database.vector_db import VectorDatabase
vdb = VectorDatabase()
results = vdb.search('refund policy', top_k=3)
print('Vector search working:', len(results) > 0)
print('Sample result:', results[0][:100] + '...' if results else 'No results')
"
End-to-End Integration Test
# Run a complete system test with sample customer interaction
python run_test_scenarios.py --session 1
This executes a full customer support scenario, testing both database operations and RAG retrieval.
Monitoring and Maintenance
Database Health Monitoring
def check_database_health():
"""Monitor database performance and integrity"""
checks = {
"table_integrity": verify_table_schemas(),
"data_consistency": check_foreign_key_constraints(),
"query_performance": measure_average_query_time(),
"storage_usage": get_database_size_metrics()
}
return all(checks.values())
Vector Database Maintenance
def update_vector_database():
"""Maintain vector database accuracy and performance"""
tasks = [
reprocess_modified_documents(),
regenerate_outdated_embeddings(),
update_search_indices(),
cleanup_unused_vectors()
]
return execute_maintenance_tasks(tasks)
Production Deployment Considerations
Database Scaling
For production environments, consider:
- PostgreSQL: Replace SQLite for concurrent access and advanced features
- Connection pooling: Manage database connections efficiently
- Read replicas: Distribute read queries for better performance
- Backup strategy: Automated backups and point-in-time recovery
Vector Database Scaling
Production vector storage options:
- Pinecone: Managed vector database with excellent performance
- Weaviate: Open-source with GraphQL interface
- ChromaDB: Simple, powerful embeddings database
- Qdrant: High-performance vector search engine
Common Troubleshooting Issues
Issue: Vector Search Returns Poor Results
Solutions:
- Verify embedding model consistency
- Check chunk size and overlap settings
- Adjust similarity thresholds
- Review document preprocessing quality
Issue: Database Connection Timeouts
Solutions:
- Implement connection pooling
- Optimize query performance with indexes
- Consider read/write splitting
- Monitor concurrent connection limits
Issue: RAG Responses Lack Context
Solutions:
- Increase chunk overlap
- Include more context in retrieval
- Improve document chunking boundaries
- Enhance query preprocessing
Next in the series
The data layer is in place — precise operational data, and a knowledge base the system can search by meaning. Next I build the first agent that uses it: the Policy Agent, which retrieves from this vector store and answers from what it actually finds, not what it half-remembers.
Designing data layers that AI can reason over — not just store — is what I do at twentytwotensors. Get in touch.