# AGENTS.md — django-anyllm

## Project Overview

`django-anyllm` is a **reusable Django package** (not a standalone project) that exposes Django models as LLM-callable tools via [`any-llm-sdk`](https://github.com/any-llm/any-llm-sdk). It lives in `src/django_anyllm/` and is consumed by other Django projects.

Key components:

- **Tools** (`tools/`) — introspection and CRUD tools that `any-llm-sdk` converts to OpenAI tool format. Entry point is `get_tools(registry)`.
- **Agent** (`agent.py`) — `ModelAgent` / `PersistentModelAgent` that handle prompts, streaming, reasoning models, and the tool-call loop.
- **Models** (`models.py`) — abstract `Chat`, `Message`, `MessageFile` bases for persisting conversations. `MessageFile` converts images to `image_url` parts and non-images to `file` content parts for `any-llm-sdk`.
- **Views/URLs** (`views.py`, `urls.py`) — HTMX-based chat UI with SSE streaming. The message form supports multiple file uploads; all attachments are forwarded to the LLM. All views extend `LoginRequiredMixin` (see Auth below).
- **Registry** (`registry.py`) — permission-aware model registry; only explicitly registered models are LLM-accessible.
- **Settings** (`settings.py`) — `DJANGO_ANYLLM` config dict with `get_setting(key)` and `get_extra_tools()` helpers.
- **Prompts** (`prompts.py`) — `build_system_prompt(registry, extra_instructions)` generates the system prompt from the registry, including registered models and their permissions. Uses `SYSTEM_PROMPT_EXTRA` from settings.
- **Serializers** (`serializers.py`) — `serialize_instance(instance, depth)` converts model instances to JSON-safe dicts. `deserialize_data(model, data)` cleans and validates incoming data before create/update. Use these instead of writing custom serialization.
- **Stream types** (`stream_types.py`) — `StreamEvent` dataclass and `StreamEventType` enum yielded by streaming agent methods.
- **Forms** (`forms.py`) — `MessageForm` for the chat UI message input with file upload support.
- **Admin** (`admin.py`) — `ChatAdmin`, `MessageAdmin`, `MessageFileAdmin` registered with the Django admin site.
- **Template tags** (`templatetags/`) — Django template tags used by the chat UI templates.
- **Templates** (`templates/django_anyllm/`) — HTMX-based chat UI templates stored in the package. Host projects can override individual templates by placing files at the same paths in their own `templates/django_anyllm/` directory.

## Build & Environment

- Package manager: `uv`
- Python: >= 3.11
- Install dev dependencies: `uv sync --extra dev`
- Run tests: `uv run pytest`
- Lint / format: `uv run ruff check .` / `uv run ruff format .`

## Testing

- Test configuration lives in `pyproject.toml` under `[tool.pytest.ini_options]`.
- `DJANGO_SETTINGS_MODULE = "tests.settings"`
- Test files follow standard `test_*.py` naming and live in `tests/`.
- When writing tests, import from `tests.models` for test-only Django models (e.g. `Author`, `Book`).
- Coverage is not currently enforced; prefer focused, fast unit tests.

## Code Style

- Follow PEP 8.
- Use type hints where they clarify the public API.
- Keep imports sorted (ruff handles this).
- Docstrings for public functions/classes; not required for private helpers.

## Settings Reference

All settings live under `DJANGO_ANYLLM` in the host project's `settings.py` and are accessed via `get_setting(key)`:

| Key | Default | Description |
|-----|---------|-------------|
| `MAX_QUERY_LIMIT` | `100` | Hard cap on `query_instances` result count |
| `DEFAULT_MODEL` | `"gpt-4"` | Default LLM model for new `Chat` instances |
| `DEFAULT_PROVIDER` | `"openai"` | Default LLM provider for new `Chat` instances |
| `SHOW_INTERNAL` | `False` | Show reasoning blocks and tool calls in the chat UI |
| `SYSTEM_PROMPT_EXTRA` | `""` | Extra instructions appended to the auto-generated system prompt |
| `EXTRA_TOOLS` | `[]` | Dotted import paths to additional LLM-callable callables (e.g. `"myapp.tools.my_tool"`) |
| `LOGIN_REQUIRED` | `False` | Redirect unauthenticated users to `LOGIN_URL` on all chat views |

## Authentication

All views inherit `LoginRequiredMixin` (defined in `views.py`). When `LOGIN_REQUIRED` is `True`, unauthenticated requests are redirected to `settings.LOGIN_URL`. **Any new view added to this package must also inherit `LoginRequiredMixin`.**

## Important Rules

### NEVER modify `pyproject.toml` of the host project

> **CRITICAL:** If you discover that a dependency is missing or needs updating, **do NOT edit `pyproject.toml`**, `uv.lock`, or any packaging metadata of the host project. Simply leave it as-is and inform the user if necessary.
>
> This package is installed into downstream projects; altering its dependency declarations can have unintended side effects on consumers. Only the maintainer should change packaging metadata.

### Other Constraints

- Do not add standalone Django projects, `manage.py`, or deployment configs (this is a library).
- Keep the public API in `src/django_anyllm/__init__.py` minimal and stable.
- Migrations must be reversible and idempotent.
- When adding new views, always apply `LoginRequiredMixin` as the first base class.
- Use `serialize_instance` / `deserialize_data` from `serializers.py` rather than writing custom serialization in tools or views.
- Use `build_system_prompt` from `prompts.py` rather than constructing system prompts inline.

## Common Tasks

| Task | Command |
|------|---------|
| Run all tests | `uv run pytest` |
| Run a specific test file | `uv run pytest tests/test_agent.py` |
| Run with verbose output | `uv run pytest -vvs` |
| Lint | `uv run ruff check .` |
| Format | `uv run ruff format .` |
| Sync lockfile | `uv sync` |
