Chapter 8: Implementation Guide
Setting Up Your Development Environment
Required Tools
# Node.js and npm
node --version # v20+
npm --version # v10+
# Python
python --version # v3.9+
pip --version
# Git
git --version
# Docker (optional)
docker --version
API Keys Setup
Create a .env file:
# OpenAI
OPENAI_API_KEY=sk-...
# Qdrant Cloud
QDRANT_URL=https://xxxxx.qdrant.io
QDRANT_API_KEY=...
QDRANT_COLLECTION=book_knowledge
# Application
PORT=8000
ENVIRONMENT=development
Building the Complete System
Project Structure
ai-driven-book/
├── frontend/ # Docusaurus site
│ ├── docs/
│ ├── src/
│ ├── static/
│ └── docusaurus.config.ts
├── backend/ # FastAPI server
│ ├── app/
│ ├── requirements.txt
│ └── Dockerfile
├── scripts/ # Utility scripts
│ └── ingest_docs.py
└── docker-compose.yml
Backend Implementation
Dependencies
# requirements.txt
fastapi==0.104.1
uvicorn[standard]==0.24.0
openai==1.3.0
qdrant-client==1.7.0
pydantic==2.5.0
pydantic-settings==2.1.0
python-multipart==0.0.6
python-dotenv==1.0.0
Configuration
# app/config.py
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
# API Keys
OPENAI_API_KEY: str
QDRANT_URL: str
QDRANT_API_KEY: str
QDRANT_COLLECTION: str = "book_knowledge"
# Application
PORT: int = 8000
ENVIRONMENT: str = "development"
CORS_ORIGINS: list[str] = ["http://localhost:3000"]
class Config:
env_file = ".env"
settings = Settings()
Document Ingestion Script
# scripts/ingest_docs.py
import os
import asyncio
from pathlib import Path
from qdrant_client import AsyncQdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
from openai import AsyncOpenAI
import uuid
async def ingest_docusaurus_docs():
"""Ingest all Docusaurus markdown files"""
# Initialize clients
openai = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY"))
qdrant = AsyncQdrantClient(
url=os.getenv("QDRANT_URL"),
api_key=os.getenv("QDRANT_API_KEY")
)
collection_name = "book_knowledge"
# Create collection
try:
await qdrant.create_collection(
collection_name=collection_name,
vectors_config=VectorParams(
size=1536,
distance=Distance.COSINE
)
)
print(f"Created collection: {collection_name}")
except Exception as e:
print(f"Collection exists or error: {e}")
# Find all markdown files
docs_path = Path("../frontend/docs")
md_files = list(docs_path.rglob("*.md")) + list(docs_path.rglob("*.mdx"))
print(f"Found {len(md_files)} markdown files")
points = []
for md_file in md_files:
print(f"Processing {md_file}...")
# Read content
content = md_file.read_text(encoding="utf-8")
# Remove frontmatter
if content.startswith("---"):
parts = content.split("---", 2)
if len(parts) >= 3:
content = parts[2].strip()
# Chunk document
chunks = chunk_document(content)
# Create embeddings
for i, chunk in enumerate(chunks):
if len(chunk.strip()) < 50: # Skip very small chunks
continue
# Get embedding
response = await openai.embeddings.create(
model="text-embedding-3-small",
input=chunk
)
embedding = response.data[0].embedding
# Create point
point = PointStruct(
id=str(uuid.uuid4()),
vector=embedding,
payload={
"text": chunk,
"source": str(md_file.relative_to(docs_path)),
"chunk_index": i,
"file_path": str(md_file)
}
)
points.append(point)
print(f" Created {len(chunks)} chunks")
# Upload to Qdrant
print(f"\nUploading {len(points)} points to Qdrant...")
await qdrant.upsert(
collection_name=collection_name,
points=points
)
print("Ingestion complete!")
def chunk_document(text: str, chunk_size: int = 1000, overlap: int = 200) -> list[str]:
"""Split document into overlapping chunks"""
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunk = text[start:end]
# Try to break at paragraph or sentence
if end < len(text):
# Look for paragraph break
last_para = chunk.rfind('\n\n')
if last_para > chunk_size * 0.5:
end = start + last_para
else:
# Look for sentence break
last_period = chunk.rfind('.')
if last_period > chunk_size * 0.7:
end = start + last_period + 1
chunk = text[start:end]
if chunk.strip():
chunks.append(chunk.strip())
start = end - overlap
return chunks
if __name__ == "__main__":
asyncio.run(ingest_docusaurus_docs())
Frontend Implementation
Install ChatKit
cd frontend
npm install @openai/chatkit
Create Chatbot Component
// src/components/Chatbot/index.tsx
import React, { useState } from 'react';
import styles from './styles.module.css';
interface Message {
role: 'user' | 'assistant';
content: string;
sources?: Array<{ text: string; score: number }>;
}
export default function Chatbot() {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [selectedText, setSelectedText] = useState('');
// Listen for text selection
React.useEffect(() => {
const handleSelection = () => {
const selection = window.getSelection();
const text = selection?.toString().trim();
if (text && text.length > 10) {
setSelectedText(text);
}
};
document.addEventListener('mouseup', handleSelection);
return () => document.removeEventListener('mouseup', handleSelection);
}, []);
const sendMessage = async () => {
if (!input.trim()) return;
const userMessage: Message = { role: 'user', content: input };
setMessages(prev => [...prev, userMessage]);
setInput('');
setIsLoading(true);
try {
const response = await fetch('http://localhost:8000/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: input,
context: selectedText || null
})
});
const data = await response.json();
const assistantMessage: Message = {
role: 'assistant',
content: data.response,
sources: data.sources
};
setMessages(prev => [...prev, assistantMessage]);
setSelectedText(''); // Clear selection after use
} catch (error) {
console.error('Error:', error);
setMessages(prev => [...prev, {
role: 'assistant',
content: 'Sorry, I encountered an error. Please try again.'
}]);
} finally {
setIsLoading(false);
}
};
return (
<div className={styles.chatbot}>
{selectedText && (
<div className={styles.contextBanner}>
Context selected: "{selectedText.substring(0, 50)}..."
<button onClick={() => setSelectedText('')}>Clear</button>
</div>
)}
<div className={styles.messages}>
{messages.map((msg, idx) => (
<div key={idx} className={styles[msg.role]}>
<div className={styles.content}>{msg.content}</div>
{msg.sources && msg.sources.length > 0 && (
<div className={styles.sources}>
<strong>Sources:</strong>
{msg.sources.map((source, i) => (
<div key={i} className={styles.source}>
{source.text} (relevance: {(source.score * 100).toFixed(1)}%)
</div>
))}
</div>
)}
</div>
))}
{isLoading && <div className={styles.loading}>Thinking...</div>}
</div>
<div className={styles.input}>
<input
type="text"
value={input}
onChange={e => setInput(e.target.value)}
onKeyPress={e => e.key === 'Enter' && sendMessage()}
placeholder={
selectedText
? "Ask about the selected text..."
: "Ask a question about the book..."
}
/>
<button onClick={sendMessage} disabled={!input.trim() || isLoading}>
Send
</button>
</div>
</div>
);
}
Styles
/* src/components/Chatbot/styles.module.css */
.chatbot {
position: fixed;
bottom: 20px;
right: 20px;
width: 400px;
height: 600px;
background: white;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
z-index: 1000;
}
.contextBanner {
background: #e3f2fd;
padding: 10px;
font-size: 12px;
border-bottom: 1px solid #90caf9;
display: flex;
justify-content: space-between;
align-items: center;
}
.contextBanner button {
background: #1976d2;
color: white;
border: none;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
}
.messages {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.user, .assistant {
margin-bottom: 16px;
padding: 12px;
border-radius: 8px;
}
.user {
background: #e3f2fd;
margin-left: 40px;
}
.assistant {
background: #f5f5f5;
margin-right: 40px;
}
.content {
margin-bottom: 8px;
}
.sources {
font-size: 12px;
color: #666;
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid #ddd;
}
.source {
margin-top: 4px;
padding: 4px;
background: white;
border-radius: 4px;
}
.loading {
text-align: center;
color: #666;
font-style: italic;
}
.input {
display: flex;
padding: 16px;
border-top: 1px solid #eee;
}
.input input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
margin-right: 8px;
}
.input button {
padding: 10px 20px;
background: #1976d2;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.input button:disabled {
background: #ccc;
cursor: not-allowed;
}
Integrate into Docusaurus
// src/theme/Root.tsx
import React from 'react';
import Chatbot from '../components/Chatbot';
export default function Root({ children }) {
return (
<>
{children}
<Chatbot />
</>
);
}
Docker Deployment
Backend Dockerfile
# backend/Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app/ ./app/
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Docker Compose
# docker-compose.yml
version: '3.8'
services:
backend:
build: ./backend
ports:
- "8000:8000"
env_file:
- ./backend/.env
environment:
- PORT=8000
restart: unless-stopped
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
ports:
- "3000:3000"
depends_on:
- backend
restart: unless-stopped
Testing the System
Test Backend
cd backend
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
uvicorn app.main:app --reload
Visit http://localhost:8000/docs for API documentation.
Test Frontend
cd frontend
npm install
npm start
Visit http://localhost:3000
Test Chatbot
- Select some text on the page
- Ask a question in the chatbot
- Verify it uses the selected context
- Try asking without selection
- Check source citations
Optimization Tips
Performance
- Cache embeddings - Don't regenerate for same text
- Batch processing - Process multiple documents at once
- Async operations - Use async/await throughout
- Connection pooling - Reuse database connections
Accuracy
- Chunk size tuning - Experiment with 500-2000 characters
- Overlap adjustment - 10-20% overlap prevents context loss
- Retrieval tuning - Adjust top_k based on query complexity
- Prompt engineering - Refine system prompts for better responses
Cost
- Use smaller models - text-embedding-3-small is cheaper
- Cache responses - Store common queries
- Optimize chunk count - Fewer chunks = lower storage costs
- Rate limiting - Prevent abuse
Summary
In this chapter, you implemented:
- Complete FastAPI backend with RAG
- Document ingestion pipeline
- Docusaurus chatbot integration
- Text selection context feature
- Docker deployment configuration
Next Chapter: Advanced topics including multi-agent systems and custom Claude Code skills.
🛠️ Implementation Workflow
📊 AI-Assisted Implementation Process
Rendering diagram...
Step-by-step implementation process with AI assistance.
\n## 🎴 Test Your Knowledge
🎴 Chapter Flashcards
Card 1 of 8✅ Mastered: 0/8
Question
What are vector embeddings?
Click to flip →
Answer
Numerical representations of text that capture semantic meaning, enabling similarity search and semantic retrieval.
← Click to flip back
📝 Chapter Quiz
📝 Chapter 8 Quiz
Test your understanding with these multiple choice questions
Question 1
What is the first phase of implementing an AI-driven project?
Question 2
What should you do FIRST when debugging AI-generated code?
Question 3
Which testing strategy is most important for AI-generated code?
Question 4
How should AI-generated codebases be maintained over time?
Question 5
What is the role of human oversight in AI-driven development?