My calendar has 12 recurring meetings. My email gets 80+ messages a day. My task list lives in three different apps. A generic Claude conversation can't fix any of that — it doesn't know my context, forgets everything between sessions, and can't reach my tools.
A personal AI assistant built on Claude is different. It knows your preferences, remembers facts you've told it, runs on a schedule, and connects to the tools you actually use. This guide covers building one from scratch: persistent memory with SQLite, a context-aware system prompt, daily briefings, Google Calendar integration, and a CLI interface you can actually run every morning.
What makes this different from a generic AI demo
Most "build an AI assistant" tutorials show you how to wire up an API call. That's not an assistant — it's a wrapper.
A real personal AI assistant has four properties a demo doesn't:
- Persistent memory — it remembers facts across sessions without you repeating yourself
- Personal context — its system prompt is populated with your data, not placeholders
- Scheduled operation — it runs tasks without you initiating them (morning briefings, daily summaries)
- Tool access — it can read your calendar, check your task list, and take actions
The architecture is straightforward:
User input (CLI / Telegram / web)
→ Memory retrieval (SQLite)
→ Claude with personal context injected
→ Response + memory update
→ Optional: tool calls (calendar, email, web)
No vector database required. For a single user, SQLite is fast enough, requires zero infrastructure, and keeps all data local.
Setting up persistent memory with SQLite
Skip the complexity of Pinecone or Chroma for personal use. SQLite runs on any machine, stores everything in one file, and handles the query patterns you need.
import sqlite3, json
from datetime import datetime
def init_db():
conn = sqlite3.connect("assistant.db")
conn.execute("""
CREATE TABLE IF NOT EXISTS memories (
id INTEGER PRIMARY KEY,
category TEXT, -- 'preference', 'fact', 'task', 'conversation'
content TEXT,
created_at TEXT
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS conversations (
id INTEGER PRIMARY KEY,
role TEXT,
content TEXT,
timestamp TEXT
)
""")
conn.commit()
return conn
def save_memory(conn, category: str, content: str):
conn.execute(
"INSERT INTO memories (category, content, created_at) VALUES (?, ?, ?)",
(category, content, datetime.now().isoformat())
)
conn.commit()
def get_memories(conn, category: str = None) -> list[dict]:
query = "SELECT category, content FROM memories"
params = []
if category:
query += " WHERE category = ?"
params = [category]
return [{"category": r[0], "content": r[1]} for r in conn.execute(query, params)]
Four memory categories covers most personal assistant needs:
preference— "Prefers bullet points over prose", "Schedules meetings after 10am"fact— "Works at Acme Corp", "Timezone is Asia/Kolkata", "Has a standup every Tuesday at 9am"task— Open action items that persist across sessionsconversation— Rolling context from recent exchanges (optional, for continuity)
Building the context-aware system prompt
The system prompt is where a personal AI assistant earns its name. Every message Claude receives should include your preferences and relevant facts — pulled live from the database.
def build_system_prompt(conn) -> str:
preferences = get_memories(conn, "preference")
facts = get_memories(conn, "fact")
pref_text = "\n".join(f"- {m['content']}" for m in preferences)
fact_text = "\n".join(f"- {m['content']}" for m in facts)
return f"""You are a personal AI assistant for [Name].
Personal context:
{fact_text}
User preferences:
{pref_text}
Today is {datetime.now().strftime('%A, %B %d, %Y')}.
When you learn something new about the user's preferences or important facts,
signal it with [REMEMBER: category | content] at the end of your response.
"""
The [REMEMBER: ...] tag is the key pattern here. Claude signals when it's learned something worth persisting — you parse those tags and store them automatically. This means memory grows naturally through conversation rather than requiring you to manually update a config file.
Auto-extracting memories from responses
Parse Claude's responses for memory signals before displaying them to the user:
import re
def extract_and_store_memories(conn, response_text: str) -> str:
"""Extract [REMEMBER: category | content] tags, store them, strip from output."""
pattern = r'\[REMEMBER:\s*(\w+)\s*\|\s*(.+?)\]'
matches = re.findall(pattern, response_text)
for category, content in matches:
save_memory(conn, category.strip(), content.strip())
print(f"[Remembered: {category} → {content}]")
# Strip the tags from the displayed response
clean_response = re.sub(pattern, '', response_text).strip()
return clean_response
After a few weeks of daily use, your assistant accumulates a dense picture of your context: your preferred meeting times, your timezone, your current projects, the fact that you hate two-line email replies. Every conversation gets marginally smarter.
Daily briefing via cron
This is the feature that actually changes your morning. Run a briefing at 8am without any interaction from you:
import anthropic
client = anthropic.Anthropic()
def daily_briefing(conn):
tasks = get_memories(conn, "task")
task_list = "\n".join(f"- {t['content']}" for t in tasks)
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=500,
system=build_system_prompt(conn),
messages=[{
"role": "user",
"content": f"Give me a brief good morning summary. My open tasks:\n{task_list}"
}]
)
return response.content[0].text
if __name__ == "__main__":
conn = init_db()
print(daily_briefing(conn))
Schedule it with cron on Linux or launchd on macOS. Add it to your crontab:
0 8 * * 1-5 /usr/bin/python3 /home/you/assistant/briefing.py >> /home/you/assistant/briefing.log 2>&1
Pipe the output to a file, a Telegram message, or your email. I use a Telegram bot — the briefing hits my phone before I open any other app.
Google Calendar integration
Read-only calendar access means Claude can reference your actual schedule in responses and briefings:
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
def get_todays_events(creds_path: str) -> list[dict]:
creds = Credentials.from_authorized_user_file(creds_path)
service = build("calendar", "v3", credentials=creds)
now = datetime.utcnow().isoformat() + "Z"
end = (datetime.utcnow().replace(hour=23, minute=59)).isoformat() + "Z"
events = service.events().list(
calendarId="primary",
timeMin=now,
timeMax=end,
singleEvents=True,
orderBy="startTime"
).execute()
return events.get("items", [])
def format_events_for_prompt(events: list[dict]) -> str:
if not events:
return "No events today."
lines = []
for e in events:
start = e.get("start", {}).get("dateTime", e.get("start", {}).get("date", ""))
lines.append(f"- {e.get('summary', 'Untitled')} at {start}")
return "\n".join(lines)
To get creds_path, you'll go through Google Cloud Console's OAuth flow once. Follow the Google Calendar API Python quickstart — it takes about 15 minutes and produces a token.json file you can reuse indefinitely.
Once connected, inject the calendar context into your briefing:
def daily_briefing_with_calendar(conn, creds_path: str):
tasks = get_memories(conn, "task")
task_list = "\n".join(f"- {t['content']}" for t in tasks)
events = get_todays_events(creds_path)
calendar_text = format_events_for_prompt(events)
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=600,
system=build_system_prompt(conn),
messages=[{
"role": "user",
"content": f"""Good morning briefing.
Today's calendar:
{calendar_text}
Open tasks:
{task_list}
Give me a focused summary: what should I prioritize today?"""
}]
)
return response.content[0].text
The CLI conversation loop
For daily use, a simple CLI loop is enough. You don't need a web interface:
def chat(conn):
conversation_history = []
system = build_system_prompt(conn)
print("Personal assistant ready. Type 'exit' to quit, 'briefing' for daily summary.\n")
while True:
user_input = input("You: ").strip()
if not user_input:
continue
if user_input.lower() == "exit":
break
if user_input.lower() == "briefing":
print(f"\nAssistant: {daily_briefing(conn)}\n")
continue
conversation_history.append({"role": "user", "content": user_input})
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
system=system,
messages=conversation_history
)
raw_response = response.content[0].text
clean_response = extract_and_store_memories(conn, raw_response)
# Rebuild system prompt to include any new memories
system = build_system_prompt(conn)
conversation_history.append({"role": "assistant", "content": clean_response})
print(f"\nAssistant: {clean_response}\n")
if __name__ == "__main__":
conn = init_db()
chat(conn)
The conversation history stays in memory for the session (so Claude remembers context within a conversation) while durable facts get persisted to SQLite (so they survive restarts). Both layers matter.
Keeping it running: launchd on macOS
To run scheduled tasks (briefings, digests, task reminders) without manual intervention, set up a launchd service. Create ~/Library/LaunchAgents/com.assistant.briefing.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.assistant.briefing</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/python3</string>
<string>/Users/you/assistant/briefing.py</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>8</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<key>StandardOutPath</key>
<string>/Users/you/assistant/briefing.log</string>
</dict>
</plist>
Load it with launchctl load ~/Library/LaunchAgents/com.assistant.briefing.plist. On Linux, use a systemd timer or standard cron — the Python code is identical.
On a Raspberry Pi, you get the same assistant running 24/7 for ~$5/year in electricity. If you want it accessible from anywhere (not just your local machine), a Hostinger KVM 1 VPS runs it 24/7 for ~₹300/month — cheap enough that the productivity gain pays for itself in the first hour.
Privacy and cost
All data stays local. The SQLite file is on your machine — no third-party memory store, no cloud sync. The only data leaving your machine is what you send to Claude's API in each message (your memories and conversation). If that's a concern, use Ollama with a local model, though quality drops noticeably for complex reasoning.
Cost at real usage: ~50 messages/day with Claude Sonnet 4.6, each with ~2,000 tokens of context (memories + conversation) = roughly 100,000 input tokens/day. At $3/1M tokens, that's $0.30/day or ~$9/month. Swap to Haiku for routine briefings and reserve Sonnet for complex tasks — you'll get that under $3/month.
For a comparison of what Claude can do natively without any setup, the Claude Projects guide is worth reading. Projects give you persistent context inside Claude.ai but without programmatic access or scheduling.
What to build next
This foundation handles 80% of personal assistant use cases. The remaining 20% worth building:
Email triage: Use Gmail's API to fetch unread messages, summarize threads, and draft replies. The pattern is the same — fetch data, inject into context, get structured output.
Task capture from conversation: When you say "remind me to review the contract tomorrow," parse that as a task with a due date and store it with save_memory(conn, "task", "Review contract — due 2026-05-31").
Weekly review: A Friday afternoon cron job that summarizes the week's conversations, completed tasks, and surfaces patterns in what you've been working on.
If you want mobile access rather than a CLI, the Telegram bot tutorial covers turning this same backend into a bot you can message from your phone. The memory and briefing code is reusable without changes — you're just swapping the input/output layer.
The assistant compounds over time. After 30 days of daily use, the memory database knows enough about your working patterns that the briefings get genuinely useful. After 90 days, you'll forget it wasn't always there.



