Project: Build a Local AI Assistant
Project Overview
Build a fully-featured AI assistant that runs entirely on your local machine. This project combines everything you've learned into a practical, deployable application.
What You'll Build:
- Multi-functional AI assistant with specialized modes
- Voice-to-text and text-to-voice capabilities (optional)
- Document analysis and Q&A
- Code generation and review
- Task scheduling and reminders
- Web interface with real-time chat
Technology Stack:
- Backend: Python + FastAPI
- AI Engine: Ollama (multiple models)
- Frontend: HTML + JavaScript (or React)
- Optional: Speech recognition, document processing
Architecture
Part 1: Project Setup
Directory Structure
local-ai-assistant/
├── app/
│ ├── __init__.py
│ ├── main.py # FastAPI application
│ ├── router.py # Model routing logic
│ ├── handlers/
│ │ ├── __init__.py
│ │ ├── chat.py # Chat functionality
│ │ ├── code.py # Code assistance
│ │ ├── documents.py # Document Q&A
│ │ └── tasks.py # Task management
│ ├── models/
│ │ ├── __init__.py
│ │ └── schemas.py # Pydantic models
│ └── utils/
│ ├── __init__.py
│ ├── db.py # Database utilities
│ └── session.py # Session management
├── static/
│ ├── css/
│ │ └── style.css
│ ├── js/
│ │ └── app.js
│ └── index.html
├── data/
│ └── sessions.db # SQLite database
├── requirements.txt
├── config.py
└── README.md
Initialize Project
# Create directory structure
mkdir -p local-ai-assistant/{app/{handlers,models,utils},static/{css,js},data}
cd local-ai-assistant
# Create __init__.py files
touch app/__init__.py app/handlers/__init__.py app/models/__init__.py app/utils/__init__.py
Requirements File
Create requirements.txt:
fastapi==0.104.1
uvicorn[standard]==0.24.0
python-multipart==0.0.6
pydantic==2.5.0
pydantic-settings==2.1.0
ollama==0.1.6
sqlalchemy==2.0.23
aiosqlite==0.19.0
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
python-dateutil==2.8.2
Install dependencies:
pip install -r requirements.txt
Part 2: Core Implementation
Configuration (config.py)
"""Configuration settings for Local AI Assistant."""
from pydantic_settings import BaseSettings
from typing import Dict
class Settings(BaseSettings):
"""Application settings."""
# Application
app_name: str = "Local AI Assistant"
app_version: str = "1.0.0"
debug: bool = True
# Server
host: str = "0.0.0.0"
port: int = 8000
# Ollama
ollama_host: str = "http://localhost:11434"
# Models for different tasks
models: Dict[str, str] = {
"chat": "llama3.3:8b",
"code": "qwen2.5-coder:7b",
"fast": "phi3:3.8b",
"reasoning": "qwen2.5:7b"
}
# Database
database_url: str = "sqlite+aiosqlite:///./data/sessions.db"
# Security
secret_key: str = "your-secret-key-change-in-production"
algorithm: str = "HS256"
access_token_expire_minutes: int = 30
class Config:
env_file = ".env"
settings = Settings()
Data Models (app/models/schemas.py)
"""Pydantic models for request/response schemas."""
from pydantic import BaseModel, Field
from typing import List, Optional, Dict, Any
from datetime import datetime
from enum import Enum
class MessageRole(str, Enum):
"""Message roles."""
SYSTEM = "system"
USER = "user"
ASSISTANT = "assistant"
class Message(BaseModel):
"""Chat message."""
role: MessageRole
content: str
timestamp: Optional[datetime] = None
class ChatRequest(BaseModel):
"""Request for chat completion."""
message: str
session_id: Optional[str] = None
model: Optional[str] = None
stream: bool = False
options: Dict[str, Any] = Field(default_factory=dict)
class ChatResponse(BaseModel):
"""Response from chat completion."""
message: str
session_id: str
model: str
tokens: Optional[int] = None
duration: Optional[float] = None
class CodeRequest(BaseModel):
"""Request for code assistance."""
task: str # 'generate', 'explain', 'review', 'debug'
language: Optional[str] = None
code: Optional[str] = None
description: Optional[str] = None
error: Optional[str] = None
class CodeResponse(BaseModel):
"""Response from code assistance."""
result: str
language: Optional[str] = None
model: str
class DocumentRequest(BaseModel):
"""Request for document Q&A."""
document: str
question: str
session_id: Optional[str] = None
class DocumentResponse(BaseModel):
"""Response from document Q&A."""
answer: str
session_id: str
model: str
class TaskRequest(BaseModel):
"""Request for task management."""
task: str
action: str # 'create', 'list', 'complete', 'delete'
class TaskResponse(BaseModel):
"""Response from task management."""
result: str
tasks: Optional[List[Dict[str, Any]]] = None
class SessionInfo(BaseModel):
"""Session information."""
session_id: str
created_at: datetime
last_activity: datetime
message_count: int
model: str
Database Utilities (app/utils/db.py)
"""Database utilities for session management."""
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.orm import declarative_base
from sqlalchemy import Column, String, DateTime, Integer, JSON
from datetime import datetime
from config import settings
import uuid
Base = declarative_base()
class Session(Base):
"""Session model."""
__tablename__ = "sessions"
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
created_at = Column(DateTime, default=datetime.utcnow)
last_activity = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
model = Column(String)
messages = Column(JSON, default=list)
metadata = Column(JSON, default=dict)
# Create async engine
engine = create_async_engine(
settings.database_url,
echo=settings.debug
)
# Create session maker
AsyncSessionLocal = async_sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False
)
async def init_db():
"""Initialize database."""
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
async def get_db():
"""Get database session."""
async with AsyncSessionLocal() as session:
try:
yield session
finally:
await session.close()
Model Router (app/router.py)
"""Smart router to select best model for each query."""
import re
from typing import Literal
from config import settings
class ModelRouter:
"""Route queries to appropriate models."""
def __init__(self):
self.models = settings.models
def classify_query(self, query: str) -> str:
"""Classify query to determine best model."""
query_lower = query.lower()
# Code-related keywords
code_keywords = [
'code', 'function', 'python', 'javascript', 'programming',
'debug', 'algorithm', 'class', 'method', 'import', 'api',
'write a program', 'script', 'implementation'
]
# Fast query indicators (simple questions)
fast_indicators = [
'what is', 'who is', 'when', 'where', 'define',
'meaning of', 'translate'
]
# Reasoning indicators
reasoning_keywords = [
'why', 'explain', 'analyze', 'compare', 'evaluate',
'how does', 'reason', 'solve', 'proof'
]
# Check for code
if any(keyword in query_lower for keyword in code_keywords):
return 'code'
# Check for simple questions
if len(query.split()) < 15 and any(ind in query_lower for ind in fast_indicators):
return 'fast'
# Check for reasoning
if any(keyword in query_lower for keyword in reasoning_keywords):
return 'reasoning'
# Default to general chat
return 'chat'
def get_model(self, category: str = None, query: str = None) -> str:
"""Get model name for category or query."""
if category:
return self.models.get(category, self.models['chat'])
if query:
category = self.classify_query(query)
return self.models[category]
return self.models['chat']
router = ModelRouter()
Chat Handler (app/handlers/chat.py)
"""Chat functionality handler."""
import ollama
from typing import List, Dict, AsyncGenerator
from app.models.schemas import Message, ChatRequest, ChatResponse
from app.router import router
from datetime import datetime
import time
async def chat_completion(request: ChatRequest, history: List[Dict] = None) -> ChatResponse:
"""Generate chat completion."""
# Select model
model = request.model or router.get_model(query=request.message)
# Prepare messages
messages = history or []
messages.append({
'role': 'user',
'content': request.message
})
# Generate response
start_time = time.time()
response = ollama.chat(
model=model,
messages=messages,
options=request.options
)
duration = time.time() - start_time
assistant_message = response['message']['content']
return ChatResponse(
message=assistant_message,
session_id=request.session_id or "default",
model=model,
duration=round(duration, 2)
)
async def chat_stream(request: ChatRequest, history: List[Dict] = None) -> AsyncGenerator[str, None]:
"""Stream chat completion."""
# Select model
model = request.model or router.get_model(query=request.message)
# Prepare messages
messages = history or []
messages.append({
'role': 'user',
'content': request.message
})
# Stream response
stream = ollama.chat(
model=model,
messages=messages,
stream=True,
options=request.options
)
for chunk in stream:
if 'message' in chunk:
yield chunk['message']['content']
Code Handler (app/handlers/code.py)
"""Code assistance handler."""
import ollama
from app.models.schemas import CodeRequest, CodeResponse
from config import settings
SYSTEM_PROMPTS = {
'generate': """You are an expert programmer. Generate clean, efficient,
well-commented code based on the user's requirements. Include example usage.""",
'explain': """You are a code educator. Explain code clearly and thoroughly,
including purpose, step-by-step breakdown, and key concepts.""",
'review': """You are a senior code reviewer. Provide constructive feedback
on code quality, bugs, performance, and security. Suggest specific improvements.""",
'debug': """You are a debugging expert. Identify the issue, explain why it happens,
provide fixed code, and suggest preventive measures."""
}
async def handle_code_request(request: CodeRequest) -> CodeResponse:
"""Handle code assistance request."""
model = settings.models['code']
# Build prompt based on task
if request.task == 'generate':
prompt = f"""Write {request.language or 'Python'} code for:
{request.description}
Provide clean code with comments and example usage."""
elif request.task == 'explain':
prompt = f"""Explain this code:
```{request.language or ''}
{request.code}
Include:
-
Overall purpose
-
Step-by-step breakdown
-
Key concepts
-
Potential improvements"""
elif request.task == 'review': prompt = f"""Review this code:
{request.code}
Provide:
-
Potential bugs
-
Code quality assessment
-
Performance considerations
-
Security concerns
-
Specific improvements"""
elif request.task == 'debug': prompt = f"""Debug this code:
Code:
{request.code}
Error:
{request.error}
Provide:
-
Issue identification
-
Explanation
-
Fixed code
-
Prevention tips"""
else: raise ValueError(f"Unknown task: {request.task}")
Generate response
response = ollama.generate( model=model, prompt=prompt, system=SYSTEM_PROMPTS.get(request.task, "") )
return CodeResponse( result=response['response'], language=request.language, model=model )
### Main Application (`app/main.py`)
```python
"""Main FastAPI application."""
from fastapi import FastAPI, HTTPException, Depends
from fastapi.staticfiles import StaticFiles
from fastapi.responses import StreamingResponse, FileResponse
from sqlalchemy.ext.asyncio import AsyncSession
from contextlib import asynccontextmanager
import ollama
from config import settings
from app.models.schemas import (
ChatRequest, ChatResponse,
CodeRequest, CodeResponse,
DocumentRequest, DocumentResponse,
TaskRequest, TaskResponse,
SessionInfo
)
from app.handlers.chat import chat_completion, chat_stream
from app.handlers.code import handle_code_request
from app.utils.db import init_db, get_db, Session
from sqlalchemy import select
from datetime import datetime
import uuid
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Lifespan context manager."""
# Startup
await init_db()
print(f"🚀 {settings.app_name} v{settings.app_version} started")
print(f"📍 Server running on http://{settings.host}:{settings.port}")
print(f"🤖 Using Ollama at {settings.ollama_host}")
print(f"📚 Models: {settings.models}")
# Check Ollama connection
try:
ollama.list()
print("✅ Ollama connection successful")
except Exception as e:
print(f"⚠️ Ollama connection failed: {e}")
yield
# Shutdown
print("👋 Shutting down...")
app = FastAPI(
title=settings.app_name,
version=settings.app_version,
lifespan=lifespan
)
# Mount static files
app.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/")
async def root():
"""Serve main page."""
return FileResponse("static/index.html")
@app.get("/health")
async def health_check():
"""Health check endpoint."""
try:
ollama.list()
ollama_status = "healthy"
except:
ollama_status = "unhealthy"
return {
"status": "healthy",
"ollama": ollama_status,
"version": settings.app_version
}
@app.post("/api/chat", response_model=ChatResponse)
async def chat(
request: ChatRequest,
db: AsyncSession = Depends(get_db)
):
"""Chat endpoint."""
# Get or create session
session_id = request.session_id or str(uuid.uuid4())
result = await db.execute(
select(Session).where(Session.id == session_id)
)
session = result.scalar_one_or_none()
if not session:
session = Session(
id=session_id,
model=request.model or "auto",
messages=[]
)
db.add(session)
# Get history
history = session.messages or []
# Generate response
if request.stream:
async def stream_generator():
async for chunk in chat_stream(request, history):
yield f"data: {chunk}\n\n"
return StreamingResponse(
stream_generator(),
media_type="text/event-stream"
)
else:
response = await chat_completion(request, history)
# Update session
history.append({'role': 'user', 'content': request.message})
history.append({'role': 'assistant', 'content': response.message})
session.messages = history
session.last_activity = datetime.utcnow()
await db.commit()
response.session_id = session_id
return response
@app.post("/api/code", response_model=CodeResponse)
async def code_assist(request: CodeRequest):
"""Code assistance endpoint."""
return await handle_code_request(request)
@app.get("/api/models")
async def list_models():
"""List available models."""
try:
models = ollama.list()
return {
"available": [m['name'] for m in models['models']],
"configured": settings.models
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/sessions/{session_id}", response_model=SessionInfo)
async def get_session(
session_id: str,
db: AsyncSession = Depends(get_db)
):
"""Get session information."""
result = await db.execute(
select(Session).where(Session.id == session_id)
)
session = result.scalar_one_or_none()
if not session:
raise HTTPException(status_code=404, detail="Session not found")
return SessionInfo(
session_id=session.id,
created_at=session.created_at,
last_activity=session.last_activity,
message_count=len(session.messages or []),
model=session.model
)
@app.delete("/api/sessions/{session_id}")
async def delete_session(
session_id: str,
db: AsyncSession = Depends(get_db)
):
"""Delete session."""
result = await db.execute(
select(Session).where(Session.id == session_id)
)
session = result.scalar_one_or_none()
if session:
await db.delete(session)
await db.commit()
return {"message": "Session deleted"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"app.main:app",
host=settings.host,
port=settings.port,
reload=settings.debug
)
Part 3: Frontend
HTML (static/index.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Local AI Assistant</title>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<div class="container">
<header>
<h1>🤖 Local AI Assistant</h1>
<div class="status">
<span id="status-indicator" class="status-dot"></span>
<span id="status-text">Connecting...</span>
</div>
</header>
<div class="tabs">
<button class="tab-button active" data-tab="chat">Chat</button>
<button class="tab-button" data-tab="code">Code</button>
<button class="tab-button" data-tab="settings">Settings</button>
</div>
<!-- Chat Tab -->
<div id="chat-tab" class="tab-content active">
<div class="chat-container">
<div id="chat-messages" class="messages"></div>
<div class="input-container">
<textarea id="chat-input" placeholder="Type your message..." rows="3"></textarea>
<button id="send-button" class="primary-button">Send</button>
</div>
</div>
</div>
<!-- Code Tab -->
<div id="code-tab" class="tab-content">
<div class="code-container">
<select id="code-task">
<option value="generate">Generate Code</option>
<option value="explain">Explain Code</option>
<option value="review">Review Code</option>
<option value="debug">Debug Code</option>
</select>
<input id="language" type="text" placeholder="Language (e.g., Python)" />
<textarea id="code-input" placeholder="Paste code or describe what you need..." rows="10"></textarea>
<textarea id="error-input" placeholder="Error message (for debugging)" rows="3" style="display: none;"></textarea>
<button id="code-submit" class="primary-button">Submit</button>
<div id="code-output" class="output"></div>
</div>
</div>
<!-- Settings Tab -->
<div id="settings-tab" class="tab-content">
<div class="settings-container">
<h3>Model Settings</h3>
<label>Chat Model:</label>
<select id="chat-model">
<option value="auto">Auto-select</option>
</select>
<label>Temperature (0-2):</label>
<input id="temperature" type="range" min="0" max="2" step="0.1" value="0.7" />
<span id="temperature-value">0.7</span>
<label>Top P (0-1):</label>
<input id="top-p" type="range" min="0" max="1" step="0.05" value="0.9" />
<span id="top-p-value">0.9</span>
<button id="clear-session" class="danger-button">Clear Session</button>
</div>
</div>
</div>
<script src="/static/js/app.js"></script>
</body>
</html>
CSS (static/css/style.css)
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
overflow: hidden;
}
header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
display: flex;
justify-content: space-between;
align-items: center;
}
header h1 {
font-size: 28px;
font-weight: 700;
}
.status {
display: flex;
align-items: center;
gap: 8px;
}
.status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: #ffd700;
animation: pulse 2s infinite;
}
.status-dot.connected {
background: #00ff00;
}
.status-dot.error {
background: #ff0000;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.tabs {
display: flex;
background: #f5f5f5;
border-bottom: 1px solid #ddd;
}
.tab-button {
flex: 1;
padding: 15px 30px;
border: none;
background: transparent;
cursor: pointer;
font-size: 16px;
font-weight: 500;
transition: all 0.3s;
}
.tab-button:hover {
background: #e0e0e0;
}
.tab-button.active {
background: white;
border-bottom: 3px solid #667eea;
}
.tab-content {
display: none;
padding: 30px;
}
.tab-content.active {
display: block;
}
.chat-container {
display: flex;
flex-direction: column;
height: 600px;
}
.messages {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
}
.message {
max-width: 70%;
padding: 12px 16px;
border-radius: 12px;
animation: slideIn 0.3s;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message.user {
background: #667eea;
color: white;
align-self: flex-end;
border-bottom-right-radius: 4px;
}
.message.assistant {
background: #f0f0f0;
color: #333;
align-self: flex-start;
border-bottom-left-radius: 4px;
}
.message.system {
background: #fffacd;
color: #666;
align-self: center;
font-size: 14px;
font-style: italic;
max-width: 90%;
}
.input-container {
display: flex;
gap: 10px;
padding: 20px;
border-top: 1px solid #ddd;
}
#chat-input {
flex: 1;
padding: 12px;
border: 2px solid #ddd;
border-radius: 8px;
resize: none;
font-family: inherit;
font-size: 15px;
}
#chat-input:focus {
outline: none;
border-color: #667eea;
}
.primary-button {
padding: 12px 32px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.primary-button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.primary-button:active {
transform: translateY(0);
}
.primary-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.code-container, .settings-container {
display: flex;
flex-direction: column;
gap: 15px;
}
.code-container select,
.code-container input,
.code-container textarea {
padding: 12px;
border: 2px solid #ddd;
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 14px;
}
.code-container select:focus,
.code-container input:focus,
.code-container textarea:focus {
outline: none;
border-color: #667eea;
}
.output {
background: #f5f5f5;
padding: 20px;
border-radius: 8px;
min-height: 200px;
white-space: pre-wrap;
font-family: 'Courier New', monospace;
font-size: 14px;
}
.settings-container label {
font-weight: 600;
margin-top: 10px;
}
.settings-container input[type="range"] {
width: 100%;
}
.danger-button {
padding: 12px 32px;
background: #dc3545;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
margin-top: 20px;
}
.danger-button:hover {
background: #c82333;
}
JavaScript (static/js/app.js)
class LocalAIAssistant {
constructor() {
this.sessionId = null;
this.currentModel = 'auto';
this.temperature = 0.7;
this.topP = 0.9;
this.init();
}
async init() {
this.setupEventListeners();
await this.checkHealth();
await this.loadModels();
}
setupEventListeners() {
// Tab switching
document.querySelectorAll('.tab-button').forEach(button => {
button.addEventListener('click', (e) => this.switchTab(e.target.dataset.tab));
});
// Chat
document.getElementById('send-button').addEventListener('click', () => this.sendMessage());
document.getElementById('chat-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.sendMessage();
}
});
// Code
document.getElementById('code-task').addEventListener('change', (e) => {
const errorInput = document.getElementById('error-input');
errorInput.style.display = e.target.value === 'debug' ? 'block' : 'none';
});
document.getElementById('code-submit').addEventListener('click', () => this.submitCode());
// Settings
document.getElementById('temperature').addEventListener('input', (e) => {
this.temperature = parseFloat(e.target.value);
document.getElementById('temperature-value').textContent = this.temperature;
});
document.getElementById('top-p').addEventListener('input', (e) => {
this.topP = parseFloat(e.target.value);
document.getElementById('top-p-value').textContent = this.topP;
});
document.getElementById('clear-session').addEventListener('click', () => this.clearSession());
document.getElementById('chat-model').addEventListener('change', (e) => {
this.currentModel = e.target.value;
});
}
switchTab(tabName) {
document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
event.target.classList.add('active');
document.getElementById(`${tabName}-tab`).classList.add('active');
}
async checkHealth() {
try {
const response = await fetch('/health');
const data = await response.json();
const indicator = document.getElementById('status-indicator');
const text = document.getElementById('status-text');
if (data.status === 'healthy' && data.ollama === 'healthy') {
indicator.classList.add('connected');
text.textContent = 'Connected';
} else {
indicator.classList.add('error');
text.textContent = 'Ollama unavailable';
}
} catch (error) {
const indicator = document.getElementById('status-indicator');
const text = document.getElementById('status-text');
indicator.classList.add('error');
text.textContent = 'Connection error';
}
}
async loadModels() {
try {
const response = await fetch('/api/models');
const data = await response.json();
const select = document.getElementById('chat-model');
data.available.forEach(model => {
const option = document.createElement('option');
option.value = model;
option.textContent = model;
select.appendChild(option);
});
} catch (error) {
console.error('Failed to load models:', error);
}
}
addMessage(role, content) {
const messagesDiv = document.getElementById('chat-messages');
const messageDiv = document.createElement('div');
messageDiv.className = `message ${role}`;
messageDiv.textContent = content;
messagesDiv.appendChild(messageDiv);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
async sendMessage() {
const input = document.getElementById('chat-input');
const message = input.value.trim();
if (!message) return;
// Add user message
this.addMessage('user', message);
input.value = '';
// Disable input
const sendButton = document.getElementById('send-button');
sendButton.disabled = true;
sendButton.textContent = 'Thinking...';
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
message: message,
session_id: this.sessionId,
model: this.currentModel === 'auto' ? null : this.currentModel,
options: {
temperature: this.temperature,
top_p: this.topP
}
})
});
const data = await response.json();
// Store session ID
if (data.session_id) {
this.sessionId = data.session_id;
}
// Add assistant message
this.addMessage('assistant', data.message);
// Show model used
if (data.model) {
this.addMessage('system', `Model: ${data.model} (${data.duration}s)`);
}
} catch (error) {
this.addMessage('system', `Error: ${error.message}`);
} finally {
sendButton.disabled = false;
sendButton.textContent = 'Send';
}
}
async submitCode() {
const task = document.getElementById('code-task').value;
const language = document.getElementById('language').value;
const code = document.getElementById('code-input').value;
const error = document.getElementById('error-input').value;
const output = document.getElementById('code-output');
output.textContent = 'Processing...';
try {
const response = await fetch('/api/code', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
task: task,
language: language || null,
code: code || null,
description: task === 'generate' ? code : null,
error: error || null
})
});
const data = await response.json();
output.textContent = data.result;
} catch (error) {
output.textContent = `Error: ${error.message}`;
}
}
async clearSession() {
if (!this.sessionId) {
alert('No active session');
return;
}
if (!confirm('Clear current session?')) return;
try {
await fetch(`/api/sessions/${this.sessionId}`, {
method: 'DELETE'
});
this.sessionId = null;
document.getElementById('chat-messages').innerHTML = '';
this.addMessage('system', 'Session cleared');
} catch (error) {
alert(`Error: ${error.message}`);
}
}
}
// Initialize app
const app = new LocalAIAssistant();
Part 4: Running the Project
Start the Application
# Navigate to project directory
cd local-ai-assistant
# Ensure Ollama is running
ollama serve # If not already running
# Pull required models
ollama pull llama3.3:8b
ollama pull qwen2.5-coder:7b
ollama pull phi3:3.8b
# Run the application
python -m app.main
# Or with uvicorn directly
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
Access the Application
Open browser to: http://localhost:8000
Part 5: Testing
Test each feature:
- Chat: Have a conversation
- Code Generation: "Write a Python function to reverse a string"
- Code Explanation: Paste some code
- Code Review: Submit code for review
- Settings: Try different models and parameters
Extensions (Optional)
Add Document Upload
from fastapi import File, UploadFile
import PyPDF2
@app.post("/api/upload")
async def upload_document(file: UploadFile = File(...)):
"""Handle document upload."""
# Read PDF
pdf_reader = PyPDF2.PdfReader(file.file)
text = ""
for page in pdf_reader.pages:
text += page.extract_text()
return {"text": text, "pages": len(pdf_reader.pages)}
Add Voice Input (Web Speech API)
// Add to app.js
startVoiceInput() {
const recognition = new webkitSpeechRecognition();
recognition.onresult = (event) => {
const transcript = event.results[0][0].transcript;
document.getElementById('chat-input').value = transcript;
this.sendMessage();
};
recognition.start();
}
Summary
You've built a complete local AI assistant with:
✅ Multi-model support with smart routing ✅ Chat with conversation history ✅ Code generation, explanation, review, debugging ✅ Web interface with real-time updates ✅ Session management and persistence ✅ Configurable models and parameters
Deployment Options:
- Desktop app (Electron wrapper)
- Docker container for easy distribution
- Systemd service for always-on operation
- Mobile app (React Native + local AI)
Navigation
- Previous: Exercises
- Next: Assessment
- Back to Workshop Overview