4 Case Study: Building SimpleBot - A Python Development Workflow Example
This case study demonstrates how to apply the Python development pipeline practices to a real project. We’ll walk through the development of SimpleBot, a lightweight wrapper for Large Language Models (LLMs) designed for educational settings.
4.1 Project Overview
SimpleBot is an educational tool that makes it easy for students to interact with Large Language Models through simple Python functions. Key features include:
- Simple API for sending prompts to LLMs
- Pre-defined personality bots (pirate, Shakespeare, emoji, etc.)
- Error handling and user-friendly messages
- Support for local LLM servers like Ollama
This project is ideal for our case study because: - It solves a real problem (making LLMs accessible in educational settings) - It’s small enough to understand quickly but complex enough to demonstrate real workflow practices - It includes both pure Python and compiled Cython components
Let’s see how we can develop this project using our Python development pipeline.
4.2 1. Setting the Foundation
4.2.1 Project Structure
We’ll set up the project using the recommended src
layout:
simplebot/
├── src/
│ └── simplebot/
│ ├── __init__.py
│ ├── core.py
│ └── personalities.py
├── tests/
│ ├── __init__.py
│ ├── test_core.py
│ └── test_personalities.py
├── docs/
│ ├── index.md
│ └── examples.md
├── .gitignore
├── README.md
├── requirements.in
├── pyproject.toml
└── LICENSE
4.2.2 Setting Up Version Control
First, we initialize a Git repository and create a .gitignore
file:
# Initialize Git repository
git init
# Create a file named README.md with the following contents:d .gitignore with the following contents:
# Virtual environments
.venv/
venv/
env/
# Python cache files
__pycache__/
*.py[cod]
*$py.class
.pytest_cache/
# Distribution / packaging
dist/
build/
*.egg-info/
# Cython generated files
*.c
*.so
# Local development settings
.env
.vscode/
# Coverage reports
htmlcov/
.coverage
EOF
# Initial commit
git add .gitignore
git commit -m "Initial commit: Add .gitignore"
4.2.3 Creating Essential Files
Let’s create the basic files:
# Create the project structure
mkdir -p src/simplebot tests docs
# Create a file name
# SimpleBot
> LLMs made simple for students and educators
SimpleBot is a lightweight Python wrapper that simplifies interactions with Large Language Models (LLMs) for educational settings.
## Installation
\`\`\`bash
pip install simplebot
\`\`\`
## Quick Start
\`\`\`python
from simplebot import get_response, pirate_bot
# Basic usage
response = get_response("Tell me about planets")
print(response)
# Use a personality bot
pirate_response = pirate_bot("Tell me about sailing ships")
print(pirate_response)
\`\`\`
## License
This project is licensed under the MIT License - see the LICENSE file for details.
EOF
# Create a file named LICENSE with the following contents:
MIT License
Copyright (c) 2025 SimpleBot Authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
EOF
git add README.md LICENSE
git commit -m "Add README and LICENSE"
4.2.4 Virtual Environment Setup
We’ll create a virtual environment and install basic development packages:
# Create virtual environment
python -m venv .venv
# Activate the environment (Linux/macOS)
source .venv/bin/activate
# On Windows: .venv\Scripts\activate
# Initial package installation for development
pip install pytest ruff mypy build
4.3 2. Building the Core Functionality
Let’s start with the core module implementation:
# Create the package structure
mkdir -p src/simplebot
# Create the package __init__.py
# Create a file named src/simplebot/__init__.py with the following contents:
"""SimpleBot - LLMs made simple for students and educators."""
from .core import get_response
from .personalities import (
pirate_bot,
shakespeare_bot,
emoji_bot,
teacher_bot,
coder_bot,
)
__version__ = "0.1.0"
__all__ = [
"get_response",
"pirate_bot",
"shakespeare_bot",
"teacher_bot",
"emoji_bot",
"coder_bot",
]
# Create the core module
# Create a file named src/simplebot/core.py with the following contents:
"""Core functionality for SimpleBot."""
import requests
import random
import time
from typing import Optional, Dict, Any
# Cache for the last used model to avoid redundant loading messages
_last_model: Optional[str] = None
def get_response(
prompt: str,
model: str = "llama3",
system: str = "You are a helpful assistant.",
stream: bool = False,
api_url: Optional[str] = None,
) -> str:
"""
Send a prompt to the LLM API and retrieve the model's response.
Args:
prompt: The text prompt to send to the language model
model: The name of the model to use
system: System instructions that control the model's behavior
stream: Whether to stream the response
api_url: Custom API URL (defaults to local Ollama server)
Returns:
The model's response text, or an error message if the request fails
"""
global _last_model
# Default to local Ollama if no API URL is provided
if api_url is None:
api_url = "http://localhost:11434/api/generate"
# Handle model switching with friendly messages
if model != _last_model:
warmup_messages = [
f"🧠 Loading model '{model}' into RAM... give me a sec...",
f"💾 Spinning up the AI core for '{model}'...",
f"⏳ Summoning the knowledge spirits... '{model}' booting...",
f"🤖 Thinking really hard with '{model}'...",
f"⚙️ Switching to model: {model} ... (may take a few seconds)",
]
print(random.choice(warmup_messages))
# Short pause to simulate/allow for model loading
time.sleep(1.5)
_last_model = model
# Validate input
if not prompt.strip():
return "⚠️ Empty prompt."
# Prepare the request payload
payload: Dict[str, Any] = {
"model": model,
"prompt": prompt,
"system": system,
"stream": stream
}
try:
# Send request to the LLM API
response = requests.post(
api_url,
json=payload,
timeout=10
)
response.raise_for_status()
data = response.json()
return data.get("response", "⚠️ No response from model.")
except requests.RequestException as e:
return f"❌ Connection Error: {str(e)}"
except Exception as e:
return f"❌ Error: {str(e)}"
EOF
# Create the personalities module
# Create a file named src/simplebot/personalities.py with the following contents:
"""Pre-defined personality bots for SimpleBot."""
from .core import get_response
from typing import Optional
def pirate_bot(prompt: str, model: Optional[str] = None) -> str:
"""
Generate a response in the style of a 1700s pirate with nautical slang.
Args:
prompt: The user's input text/question
model: Optional model override
Returns:
A response written in pirate vernacular
"""
return get_response(
prompt,
system="You are a witty pirate from the 1700s. "
"Use nautical slang, say 'arr' occasionally, "
"and reference sailing, treasure, and the sea.",
model=model or "llama3"
)
def shakespeare_bot(prompt: str, model: Optional[str] = None) -> str:
"""
Generate a response in the style of William Shakespeare.
Args:
prompt: The user's input text/question
model: Optional model override
Returns:
A response written in Shakespearean style
"""
return get_response(
prompt,
system="You respond in the style of William Shakespeare, "
"using Early Modern English vocabulary and phrasing.",
model=model or "llama3"
)
def emoji_bot(prompt: str, model: Optional[str] = None) -> str:
"""
Generate a response primarily using emojis with minimal text.
Args:
prompt: The user's input text/question
model: Optional model override
Returns:
A response composed primarily of emojis
"""
return get_response(
prompt,
system="You respond using mostly emojis, mixing minimal words "
"and symbols to convey meaning. You love using expressive "
"emoji strings.",
model=model or "llama3"
)
def teacher_bot(prompt: str, model: Optional[str] = None) -> str:
"""
Generate a response in the style of a patient, helpful educator.
Args:
prompt: The user's input text/question
model: Optional model override
Returns:
A response with an educational approach
"""
return get_response(
prompt,
system="You are a patient, encouraging teacher who explains "
"concepts clearly at an appropriate level. Break down "
"complex ideas into simpler components and use analogies "
"when helpful.",
model=model or "llama3"
)
def coder_bot(prompt: str, model: Optional[str] = None) -> str:
"""
Generate a response from a coding assistant optimized for programming help.
Args:
prompt: The user's input programming question or request
model: Optional model override (defaults to a coding-specific model)
Returns:
A technical response focused on code-related assistance
"""
return get_response(
prompt,
system="You are a skilled coding assistant who explains and writes "
"code clearly and concisely. Prioritize best practices, "
"readability, and proper error handling.",
model=model or "codellama"
)
EOF
git add src/
git commit -m "Add core SimpleBot functionality"
4.4 3. Package Configuration
Let’s set up the package configuration in pyproject.toml
:
# Create pyproject.toml directory
Note on Modern Packaging: This case study uses the newer
pyproject.toml
-only approach for simplicity and to follow current best practices. Many existing Python projects still usesetup.py
, either alongsidepyproject.toml
or as their primary configuration. Thesetup.py
approach remains valuable for packages with complex build requirements, custom build steps, or when supporting older tools and Python versions. For SimpleBot, our straightforward package requirements allow us to use the cleaner, declarativepyproject.toml
approach.
4.5 Create a file named pyproject.toml with the following contents:
Let’s set up the package configuration in pyproject.toml
:
# Create a file named pyproject.toml with the following contents:
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "simplebot"
version = "0.1.0"
description = "LLMs made simple for students and educators"
readme = "README.md"
requires-python = ">=3.7"
license = {text = "MIT"}
authors = [
{name = "SimpleBot Team", email = "example@example.com"}
]
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Intended Audience :: Education",
"Topic :: Education :: Computer Aided Instruction (CAI)",
]
dependencies = [
"requests>=2.25.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"pytest-cov",
"ruff",
"mypy",
]
[project.urls]
"Homepage" = "https://github.com/simplebot-team/simplebot"
"Bug Tracker" = "https://github.com/simplebot-team/simplebot/issues"
# Tool configurations
[tool.ruff]
select = ["E", "F", "I"]
line-length = 88
[tool.ruff.per-file-ignores]
"__init__.py" = ["F401"]
[tool.mypy]
python_version = "3.7"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false
[tool.pytest.ini_options]
testpaths = ["tests"]
EOF
# Create requirements.in file
# Create a file named requirements.in with the following contents:
# Direct dependencies
requests>=2.25.0
EOF
# Create requirements-dev.in
# Create a file named requirements-dev.in with the following contents:
# Development dependencies
pytest>=7.0.0
pytest-cov
ruff
mypy
build
twine
EOF
git add pyproject.toml requirements*.in
git commit -m "Add package configuration and dependency files"
4.6 4. Writing Tests
Let’s create some tests for our SimpleBot functionality:
# Create test files
# Create a file named tests/__init__.py with the following contents:
"""SimpleBot test package."""
EOF
# Create a file named tests/test_core.py with the following contents:
"""Tests for the SimpleBot core module."""
import pytest
from unittest.mock import patch, MagicMock
from simplebot.core import get_response
@patch("simplebot.core.requests.post")
def test_successful_response(mock_post):
"""Test that a successful API response is handled correctly."""
# Setup mock
mock_response = MagicMock()
mock_response.json.return_value = {"response": "Test response"}
mock_post.return_value = mock_response
# Call function
result = get_response("Test prompt")
# Assertions
assert result == "Test response"
mock_post.assert_called_once()
@patch("simplebot.core.requests.post")
def test_empty_prompt(mock_post):
"""Test that empty prompts are handled correctly."""
result = get_response("")
assert "Empty prompt" in result
mock_post.assert_not_called()
@patch("simplebot.core.requests.post")
def test_api_error(mock_post):
"""Test that API errors are handled gracefully."""
# Setup mock to raise an exception
mock_post.side_effect = Exception("Test error")
# Call function
result = get_response("Test prompt")
# Assertions
assert "Error" in result
assert "Test error" in result
EOF
# Create a file named tests/test_personalities.py with the following contents:
"""Tests for the SimpleBot personalities module."""
import pytest
from unittest.mock import patch
from simplebot import (
pirate_bot,
shakespeare_bot,
emoji_bot,
teacher_bot,
coder_bot,
)
@patch("simplebot.personalities.get_response")
def test_pirate_bot(mock_get_response):
"""Test that pirate_bot calls get_response with correct parameters."""
# Setup
mock_get_response.return_value = "Arr, test response!"
# Call function
result = pirate_bot("Test prompt")
# Assertions
assert result == "Arr, test response!"
mock_get_response.assert_called_once()
# Check that system prompt contains pirate references
system_arg = mock_get_response.call_args[1]["system"]
assert "pirate" in system_arg.lower()
@patch("simplebot.personalities.get_response")
def test_custom_model(mock_get_response):
"""Test that personality bots accept custom model parameter."""
# Setup
mock_get_response.return_value = "Custom model response"
# Call functions with custom model
shakespeare_bot("Test", model="custom-model")
# Assertions
assert mock_get_response.call_args[1]["model"] == "custom-model"
EOF
git add tests/
git commit -m "Add unit tests for SimpleBot"
4.7 5. Applying Code Quality Tools
Let’s run our code quality tools and fix any issues:
# Install development dependencies
pip install -r requirements-dev.in
# Run Ruff for formatting and linting
ruff format .
ruff check .
# Run mypy for type checking
mypy src/
# Fix any issues identified by the tools
git add .
git commit -m "Apply code formatting and fix linting issues"
4.8 6. Documentation
Let’s create basic documentation:
# Create docs directory
mkdir -p docs
# Create main documentation file
# Create a file named docs/index.md with the following contents:
# SimpleBot Documentation
> LLMs made simple for students and educators
SimpleBot is a lightweight Python wrapper that simplifies interactions with Large Language Models (LLMs) for educational settings. It abstracts away the complexity of API calls, model management, and error handling, allowing students to focus on learning programming concepts through engaging AI interactions.
## Installation
\`\`\`bash
pip install simplebot
\`\`\`
## Basic Usage
\`\`\`python
from simplebot import get_response
# Basic usage with default model
response = get_response("Tell me about planets")
print(response)
\`\`\`
## Personality Bots
SimpleBot comes with several pre-defined personality bots:
\`\`\`python
from simplebot import pirate_bot, shakespeare_bot, emoji_bot, teacher_bot, coder_bot
# Get a response in pirate speak
pirate_response = pirate_bot("Tell me about sailing ships")
print(pirate_response)
# Get a response in Shakespearean style
shakespeare_response = shakespeare_bot("What is love?")
print(shakespeare_response)
# Get a response with emojis
emoji_response = emoji_bot("Explain happiness")
print(emoji_response)
# Get an educational response
teacher_response = teacher_bot("How do photosynthesis work?")
print(teacher_response)
# Get coding help
code_response = coder_bot("Write a Python function to check if a string is a palindrome")
print(code_response)
\`\`\`
## API Reference
### get_response()
\`\`\`python
def get_response(
prompt: str,
model: str = "llama3",
system: str = "You are a helpful assistant.",
stream: bool = False,
api_url: Optional[str] = None,
) -> str:
\`\`\`
The core function for sending prompts to an LLM and getting responses.
#### Parameters:
- `prompt`: The text prompt to send to the language model
- `model`: The name of the model to use (default: "llama3")
- `system`: System instructions that control the model's behavior
- `stream`: Whether to stream the response (default: False)
- `api_url`: Custom API URL (defaults to local Ollama server)
#### Returns:
- A string containing the model's response or an error message
EOF
# Create examples file
# Create a file named docs/examples.md with the following contents:
# SimpleBot Examples
Here are some examples of using SimpleBot in educational settings.
## Creating Custom Bot Personalities
You can create custom bot personalities:
\`\`\`python
from simplebot import get_response
def scientist_bot(prompt):
"""A bot that responds like a scientific researcher."""
return get_response(
prompt,
system="You are a scientific researcher. Provide evidence-based "
"responses with references to studies when possible. "
"Be precise and methodical in your explanations."
)
result = scientist_bot("What happens during photosynthesis?")
print(result)
\`\`\`
## Building a Simple Quiz System
\`\`\`python
from simplebot import teacher_bot
quiz_questions = [
"What is the capital of France?",
"Who wrote Romeo and Juliet?",
"What is the chemical symbol for water?"
]
def generate_quiz():
print("=== Quiz Time! ===")
for i, question in enumerate(quiz_questions, 1):
print(f"Question {i}: {question}")
user_answer = input("Your answer: ")
# Generate feedback on the answer
feedback = teacher_bot(
f"Question: {question}\nStudent answer: {user_answer}\n"
"Provide brief, encouraging feedback on whether this answer is "
"correct. If incorrect, provide the correct answer."
)
print(f"Feedback: {feedback}\n")
# Run the quiz
generate_quiz()
\`\`\`
## Simulating a Conversation Between Bots
\`\`\`python
from simplebot import pirate_bot, shakespeare_bot
def bot_conversation(topic, turns=3):
"""Simulate a conversation between two bots on a given topic."""
print(f"=== A conversation about {topic} ===")
# Start with the pirate
current_message = f"Tell me about {topic}"
current_bot = "pirate"
for i in range(turns):
if current_bot == "pirate":
response = pirate_bot(current_message)
print(f"🏴☠️ Pirate: {response}")
current_message = f"Respond to this: {response}"
current_bot = "shakespeare"
else:
response = shakespeare_bot(current_message)
print(f"🎭 Shakespeare: {response}")
current_message = f"Respond to this: {response}"
current_bot = "pirate"
print()
# Run a conversation about the ocean
bot_conversation("the ocean", turns=4)
\`\`\`
EOF
git add docs/
git commit -m "Add documentation"
4.9 7. Setup CI/CD with GitHub Actions
Now let’s set up continuous integration:
# Create GitHub Actions workflow directory
mkdir -p .github/workflows
# Create CI workflow file
# Create a file named .github/workflows/ci.yml with the following contents:
name: Python CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10"]
steps:
- uses: actions/checkout@v3
- name: Set up Python \${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: \${{ matrix.python-version }}
cache: pip
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
- name: Check formatting with Ruff
run: ruff format --check .
- name: Lint with Ruff
run: ruff check .
- name: Type check with mypy
run: mypy src/
- name: Test with pytest
run: pytest --cov=src/ tests/
- name: Build package
run: python -m build
EOF
# Create release workflow
# Create a file named .github/workflows/release.yml with the following contents:
name: Publish to PyPI
on:
release:
types: [created]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build twine
- name: Build and publish
env:
TWINE_USERNAME: \${{ secrets.PYPI_USERNAME }}
TWINE_PASSWORD: \${{ secrets.PYPI_PASSWORD }}
run: |
python -m build
twine check dist/*
twine upload dist/*
EOF
git add .github/
git commit -m "Add CI/CD workflows"
4.10 8. Finalizing for Distribution
Let’s prepare for distribution:
# Install the package in development mode
pip install -e .
# Run the tests
pytest
# Build the package
python -m build
# Verify the package
twine check dist/*
4.11 9. Project Summary
By following the Python Development Workflow, we’ve transformed the SimpleBot concept into a well-structured, tested, and documented Python package that’s ready for distribution. Let’s review what we’ve accomplished:
- Project Foundation:
- Created a clear, organized directory structure
- Set up version control with Git
- Added essential files (README, LICENSE)
- Development Environment:
- Created a virtual environment
- Managed dependencies cleanly
- Code Quality:
- Applied type hints throughout the codebase
- Used Ruff for formatting and linting
- Used mypy for static type checking
- Testing:
- Created comprehensive unit tests with pytest
- Used mocking to test external API interactions
- Documentation:
- Added clear docstrings
- Created usage documentation with examples
- Packaging & Distribution:
- Configured the package with pyproject.toml
- Set up CI/CD with GitHub Actions
4.12 10. Next Steps
If we were to continue developing SimpleBot, potential next steps might include:
- Enhanced Features:
- Add more personality bots
- Support for conversation memory/context
- Configuration file support
- Advanced Documentation:
- Set up MkDocs for a full documentation site
- Add tutorials for classroom usage
- Performance Improvements:
- Add caching for responses
- Implement Cython optimization for performance-critical sections
- Security Enhancements:
- Add API key management
- Implement content filtering for educational settings
This case study demonstrates how following a structured Python development workflow leads to a high-quality, maintainable, and distributable package—even for relatively small projects.