Metadata-Version: 2.4
Name: privacyforms.ai
Version: 0.1.8
Summary: PrivacyForms AI - LLM integration module
Author-email: Andreas Jung <info@zopyx.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/zopyx/privacyforms.ai
Project-URL: Repository, https://github.com/zopyx/privacyforms.ai
Project-URL: Issues, https://github.com/zopyx/privacyforms.ai/issues
Keywords: llm,cli,openai,anthropic,ollama,privacy,moonshot
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: click>=8.0
Requires-Dist: llm>=0.28
Requires-Dist: llm-anthropic>=0.24
Requires-Dist: llm-moonshot>=0.3.1
Requires-Dist: llm-ollama>=0.15.1
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Requires-Dist: pytest-random-order>=1.1; extra == "dev"
Requires-Dist: pytest-xdist>=3.0; extra == "dev"
Requires-Dist: ruff>=0.9.0; extra == "dev"
Requires-Dist: ty>=0.0.20; extra == "dev"
Requires-Dist: build>=1.0; extra == "dev"
Requires-Dist: wheel>=0.45; extra == "dev"
Requires-Dist: tox>=4.0; extra == "dev"
Requires-Dist: tox-uv>=1.0; extra == "dev"
Dynamic: license-file

# PrivacyForms AI

[![Tests](https://github.com/zopyx/privacyforms.ai/actions/workflows/ci.yml/badge.svg?job=test)](https://github.com/zopyx/privacyforms.ai/actions/workflows/ci.yml)
[![Lint](https://github.com/zopyx/privacyforms.ai/actions/workflows/ci.yml/badge.svg?job=lint)](https://github.com/zopyx/privacyforms.ai/actions/workflows/ci.yml)
[![Build](https://github.com/zopyx/privacyforms.ai/actions/workflows/ci.yml/badge.svg?job=build)](https://github.com/zopyx/privacyforms.ai/actions/workflows/ci.yml)

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 (`-v` for metadata, `-vv` for full prompt payloads) to inspect prompt payloads

## Installation

### Using uv (recommended)

```bash
# 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

```bash
pip install privacyforms-ai
```

## Configuration

Set your API keys as environment variables:

```bash
# 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

```bash
# 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

```bash
privacyforms-ai models

# JSON output
privacyforms-ai models --json-output
```

### Send a Single Prompt

```bash
# 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:

```bash
# 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:

```python
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()`:

```python
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:

```python
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

```bash
# 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

```bash
# Run all tests
make test

# With coverage
make test-cov

# Verbose output
uv run pytest -v
```

### Code Quality

```bash
# 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

```bash
# Build release artifacts into dist/
make dist
```

### Upload Package

```bash
# 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

```bash
# 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](LICENSE) file for details.

## Contributing

Contributions are welcome! Please:
1. Fork the repository
2. Create a feature branch
3. Make your changes with tests
4. Ensure all checks pass (`make check`)
5. Submit a pull request

## Acknowledgments

- Built on top of [Simon Willison's llm](https://github.com/simonw/llm) library
- Uses [Astral's uv](https://github.com/astral-sh/uv) for fast Python package management
