PrivacyForms AI
A Python CLI tool for interacting with Large Language Models (LLMs) via Simon Willison's llm library. Supports multiple providers including OpenAI, Anthropic, Moonshot, and Ollama.
Features
- 🔧 Simple CLI - Easy-to-use command-line interface with colored output
- 💬 Interactive Chat - Multi-turn conversations with context/memory
- 🚀 Multiple Providers - Works with OpenAI, Anthropic, Moonshot, Ollama, and more
- 🧪 Well Tested - 100 % test coverage enforced via
make test-cov - ⚡ Fast - Built with modern Python tooling
- 🔍 Observable - Optional verbose logging (
-vfor metadata,-vvfor full prompt payloads) to inspect prompt payloads
Installation
Using uv (recommended)
# Clone the repository
git clone https://github.com/zopyx/privacyforms.ai.git
cd privacyforms.ai
# Install with uv
uv sync
# Or install in development mode
uv sync --all-extras --dev
Using pip
pip install privacyforms-ai
Configuration
Set your API keys as environment variables:
# OpenAI
export OPENAI_API_KEY="your-key"
# Anthropic
export ANTHROPIC_API_KEY="your-key"
# Moonshot
export MOONSHOT_API_KEY="your-key"
For Ollama, make sure the Ollama server is running locally.
Usage
Global Options
# Show version
privacyforms-ai --version
# Show help
privacyforms-ai --help
# Enable verbose output (shows prompt logs on stderr)
privacyforms-ai -v models
# Enable debug output
privacyforms-ai -vv prompt gpt-4o-mini "Hello!"
List Available Models
privacyforms-ai models
# JSON output
privacyforms-ai models --json-output
Send a Single Prompt
# Basic prompt
privacyforms-ai prompt gpt-4o-mini "What is the capital of France?"
# With system prompt
privacyforms-ai prompt gpt-4o-mini "Explain recursion" --system "You are a computer science tutor"
# With file attachment
privacyforms-ai prompt gpt-4o-mini "Summarize this" -a document.pdf
# Start interactive chat with file attachment
privacyforms-ai chat gpt-4o-mini -a document.pdf
# Use python -m
python -m privacyforms_ai --help
Interactive Chat
Start an interactive chat session with conversation history:
# Basic chat
privacyforms-ai chat moonshot/kimi-k2.5
# With system prompt
privacyforms-ai chat gpt-4o-mini -s "You are a helpful coding assistant"
Chat Commands:
/quit,/exit,/q- End the chat session/clear- Clear conversation history/model- Show current model
Example session:
Starting chat with model: moonshot/kimi-k2.5
Type /quit, /exit, or /q to end the session. Type /clear to reset history.
--------------------------------------------------
You: Hello!
AI: Hello! How can I help you today?
You: What can you do?
AI: I can help with a variety of tasks including...
You: /quit
Goodbye!
Custom OpenAI-compatible Endpoints
Besides the providers registered through llm, the Python API can talk to any
OpenAI-compatible endpoint by passing an (api_url, api_key, model_name) triple —
for example DeepSeek, Groq, Together, or a local vLLM/LiteLLM proxy:
from pathlib import Path
from privacyforms_ai import AI
model = AI.get_custom_model(
model_name="deepseek-v4-pro",
api_url="https://api.deepseek.com",
api_key=Path("deepseekv4.token").read_text().strip(),
)
response = AI.send_prompt(model, "Hello!")
print(AI.extract_response_text(response))
For multi-turn conversations use AI.get_custom_conversation():
conversation = AI.get_custom_conversation(
model_name="deepseek-v4-pro",
api_url="https://api.deepseek.com",
api_key=Path("deepseekv4.token").read_text().strip(),
system="You are a helpful assistant.",
)
response = AI.send_conversation_prompt(conversation, "Hello!")
print(AI.extract_response_text(response))
Pass vision=True to get_custom_model() for endpoints whose models accept image
attachments. Keep token files like deepseekv4.token out of version control — the
repo's .gitignore already covers this one.
Authentication note: the api_key passed to get_custom_model() is sent to
the endpoint as-is. Internally the model keeps llm's needs_key flag truthy so
that the explicitly passed key is used. Do not set model.needs_key = None
on the returned model: llm then treats the model as key-less and substitutes
the literal placeholder DUMMY_KEY as the Bearer token, which the endpoint
rejects with HTTP 401 (DeepSeek reports this as
Your api key: ****_KEY is invalid — the _KEY suffix is the placeholder,
not your key).
A ready-made smoke test for the DeepSeek endpoint lives at scripts/deepseek_smoke.py
(run with uv run python scripts/deepseek_smoke.py; requires a valid key in
deepseekv4.token and network access).
Python API
All functionality is exposed through the AI class:
| Method | Description |
|---|---|
AI.get_models() |
List all registered models as {key, name, provider} dicts |
AI.get_model(key) |
Fetch a registered llm model by its key |
AI.get_conversation(model_key, system=None) |
Start a multi-turn conversation with a registered model |
AI.get_custom_model(model_name, api_url, api_key, *, vision=False, can_stream=True) |
Create a model for an arbitrary OpenAI-compatible endpoint |
AI.get_custom_conversation(model_name, api_url, api_key, system=None, *, vision=False) |
Start a conversation with a custom endpoint |
AI.send_prompt(model, prompt, system=None, attachments=None) |
Send a single prompt; returns the llm response object |
AI.send_conversation_prompt(conversation, prompt, attachments=None) |
Continue a conversation |
AI.extract_response_text(response) |
Extract plain text from an llm response |
AI.create_attachment(file_path, mime_type=None) |
Build an attachment from a local file (MIME type auto-detected) |
AI.prompt_with_attachment(model, prompt, file_path, mime_type=None) |
Send a prompt with a file attachment, returns the response text |
Example covering the full lifecycle:
from privacyforms_ai import AI
# 1. List registered models
for m in AI.get_models():
print(m["key"], "-", m["name"], f"({m['provider']})")
# 2. Registered model, single prompt
model = AI.get_model("gpt-4o-mini")
response = AI.send_prompt(model, "Hello!", system="Be concise.")
print(AI.extract_response_text(response))
# 3. Registered model, multi-turn conversation
conversation = AI.get_conversation("gpt-4o-mini", system="You are a helpful assistant.")
print(AI.extract_response_text(AI.send_conversation_prompt(conversation, "What is 2+2?")))
# 4. File attachments
print(AI.prompt_with_attachment(model, "Summarize this", "document.pdf"))
llm response objects are lazy: the network call only happens when the response
is consumed (e.g. via AI.extract_response_text(response)).
Development
Setup
# Clone and setup
git clone https://github.com/zopyx/privacyforms.ai.git
cd privacyforms.ai
uv sync --all-extras --dev
source .venv/bin/activate
Running Tests
# Run all tests
make test
# With coverage
make test-cov
# Verbose output
uv run pytest -v
Code Quality
# Format code
make format
# Check formatting
make format-check
# Lint
make lint
# Auto-fix linting issues
make fix
# Type check
make type-check
# Run the full local gate
make check
Build Package
# Build release artifacts into dist/
make dist
Upload Package
# Upload to PyPI using twine and your ~/.pypirc or TWINE_* credentials
make upload
# Upload to another configured repository, e.g. TestPyPI
make upload TWINE_REPOSITORY=testpypi
Create a Release
# 1. Update the version in pyproject.toml, src/privacyforms_ai/_version.py, README, and tests
# 2. Refresh the lockfile if needed
uv sync --all-extras --dev
# 3. Verify and build
make check
make dist
# 4. Upload
make upload
# 5. Commit and tag
git add pyproject.toml src/privacyforms_ai/_version.py src/privacyforms_ai/__init__.py tests/ uv.lock CHANGELOG.md .gitattributes LICENSE
git commit -m "Release X.Y.Z"
git tag vX.Y.Z
git push origin HEAD
git push origin vX.Y.Z
Project Structure
privacyforms.ai/
├── src/privacyforms_ai/
│ ├── __init__.py
│ ├── _version.py # Package version
│ ├── ai.py # AI class for LLM interactions
│ └── cli.py # Click CLI commands
├── tests/
│ ├── conftest.py # Pytest fixtures
│ ├── test_ai.py # AI class tests
│ └── test_cli.py # CLI tests
├── pyproject.toml # Project configuration
├── uv.lock # Locked dependencies
├── CHANGELOG.md # Release notes
├── LICENSE # MIT license
├── .gitattributes # Line-ending configuration
└── README.md
CI/CD
GitHub Actions workflow runs on:
- Python 3.12, 3.13, 3.14, 3.14t (free-threaded)
- Ubuntu Linux
Jobs:
- test - Run pytest with coverage
- lint - ruff (formatting, linting) and ty (type checking)
- build - Build package artifacts and validate with twine
License
MIT License - see LICENSE file for details.
Contributing
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Make your changes with tests
- Ensure all checks pass (
make check) - Submit a pull request
Acknowledgments
- Built on top of Simon Willison's llm library
- Uses Astral's uv for fast Python package management
Release files for privacyforms.ai 0.1.8
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| privacyforms_ai-0.1.8.tar.gz | 20.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| privacyforms_ai-0.1.8-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 33.4 kB
Release files / privacyforms_ai-0.1.8.tar.gz
| Download URL | privacyforms_ai-0.1.8.tar.gz |
|---|---|
| Size | 20.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
fdaf7948af053ac1ec5905d2291e7efc1a9b8101a9b8ae2297dcda56e0baeb52
|
|
BLAKE2b-256 checksum How to use checksums |
b12a020c4f7ba1128b6fb1e9908578d7e9d0ba11cffbf872c4c02b1dfd2180ae
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 23, 2026.
Transparency logRelease files / privacyforms_ai-0.1.8-py3-none-any.whl
| Download URL | privacyforms_ai-0.1.8-py3-none-any.whl |
|---|---|
| Size | 13.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
46f0681b177de7fc870a178ec07c93baa3cb5e181cb685bafccbafd6e885f148
|
|
BLAKE2b-256 checksum How to use checksums |
1ba4d404c2f1f8389364a4c6f03f5c3fbe350b74d0c1b09d02617849c3360bcb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 23, 2026.
Transparency log