Estimated time: 4–6 hours · Portfolio grade
This is the portfolio project for Chapter 1. It demonstrates everything a hiring manager expects an AI engineer to know before their first interview: calling multiple LLM providers, managing conversation state, extracting structured data, handling errors and cost, and shipping a usable CLI tool.
You are building a polished command-line AI assistant that supports both OpenAI and Anthropic, has persistent memory, a document analysis mode, and a cost dashboard. When you finish, this is something you can show in an interview or point to in your portfolio.
.envuv add openai anthropic pydantic python-dotenv pypdf (PyPDF2 is deprecated — use pypdf)A single CLI application (ai.py) with four modes:
uv run ai.py chat --provider openai
uv run ai.py chat --provider anthropic
uv run ai.py chat --provider anthropic --model claude-haiku-4-5-20251001
Persistent conversation with history saved per session name. Dark terminal output — model replies in a different colour from the user prompt.
uv run ai.py analyse report.pdf --provider openai
uv run ai.py analyse contract.txt --provider anthropic --detail detailed
Structured summary extraction using the provider specified. Output includes all DocumentSummary fields plus cost.
uv run ai.py compare "Explain async Python in one paragraph"
Sends the same prompt to both OpenAI and Anthropic simultaneously (using asyncio), prints both responses side-by-side with their costs, and shows which was faster.
uv run ai.py costs
Reads a local log file and prints: total spend today, total spend this week, cost breakdown by provider and mode, most expensive single call.
LLMClient abstraction from Lesson 7 — all provider-specific code is isolated in OpenAIClient and AnthropicClientCostLogger class writes one JSON line per API call to cost_log.jsonl (JSONL: one JSON object per line)sessions/<name>.json, one file per named session--session <name> flag (default: default) — loads and saves to sessions/<name>.json--system <text> flag overrides the default system prompt--stream flag enables streaming outputRateLimitError with exponential backoff.txt and .pdf files--detail brief | standard | detailed--json outputs raw JSONasyncio.gather()cost_log.jsonl.envai-foundations/
├── ai.py ← CLI entry point (argparse subcommands)
├── llm/
│ ├── __init__.py
│ ├── base.py ← LLMClient ABC
│ ├── openai_client.py
│ └── anthropic_client.py
├── tools/
│ ├── __init__.py
│ ├── document.py ← file reading, DocumentSummary model
│ └── costs.py ← CostLogger, dashboard renderer
├── sessions/ ← created at runtime
├── cost_log.jsonl ← created at runtime
├── .env
├── .env.example
├── .gitignore
└── pyproject.toml
llm/base.py, openai_client.py, anthropic_client.py. Test both clients with a simple call before touching the CLI.tools/costs.py — CostLogger.log() writes a line; CostLogger.dashboard() reads and aggregates.tools/document.py — file reader and DocumentSummary model.ai.py subcommand by subcommand: chat first, then analyse, then compare, then costs.asyncio concurrency.asyncio.run() as the entry point and asyncio.gather() to run both provider calls in parallel. Each LLM client will need an async version of complete() — or you can run the synchronous clients in a ThreadPoolExecutor via loop.run_in_executor(). Either approach is acceptable.
uv run ai.py chat --provider openai starts a session that persists across restartsuv run ai.py chat --provider anthropic works with the same interfaceuv run ai.py analyse file.txt prints a structured summaryuv run ai.py analyse file.pdf --json prints valid JSONuv run ai.py compare "..." shows responses from both providersuv run ai.py costs shows a real cost breakdown from logged callsllm/ — ai.py imports only LLMClientcost_log.jsonl and sessions/ are in .gitignoreWhen a hiring manager asks about your portfolio, lead with the architecture: “I built an LLM-provider abstraction layer so the calling code is completely provider-agnostic. Swapping between OpenAI and Anthropic is a single flag.”
Then walk them through the cost dashboard: every call is logged with latency, tokens, and cost, and the dashboard aggregates by provider and mode. This shows you think in terms of production economics, not just demos.
The compare mode is the conversation-starter: “I wanted to A/B test the same prompt across providers, so I built a concurrent comparison mode with asyncio.” This demonstrates async Python, which is a core skill for Chapter 3.
/chat that accepts a message and returns a streamed response — a preview of Chapter 3ai.py describe <image.png> mode that calls GPT-4o or Claude’s vision API with an image fileDAILY_BUDGET_USD=1.00 in .env)Finished this project? Share your GitHub repo link in the discussion below. Browse what others built to get inspiration and give feedback.