Connect your AI models to Plone
Project description
collective.aisettings
Connect your AI models to Plone — the backend (Python) half.
This Plone addon registers a control panel, a global utility, and a small family of REST API endpoints that let a Plone site talk to one or more OpenAI-compatible LLM services.
The addon works on classic Plone as well as Volto. For the Volto-side
UI integration see ../frontend/README.md.
Features
- AI Settings control panel under Site Setup → General, exposed
on both classic Plone (
@@ai-settings) and Volto (@controlpanels/ai-settings). Both UIs edit the same registry-backed JSON list. IAIServiceglobal utility with methods for chat, reasoning, vision, embeddings, and tool/function calls.- Async
@aiREST endpoint with a polling counterpart@ai-task/<id>so calls that take minutes don't hit proxy / load-balancer timeouts. - Capability-based model resolution (
completion,embedding,vision,tools,thinking) — callers ask for what they need; the addon picks the right configured model. - Per-model permission gating so individual models can be restricted to particular roles via Plone permissions.
- Generic passthrough connections — declare an endpoint without pinning any model and let callers ask for any model name the upstream service hosts.
Installation
Add the package to your Plone installation. For development:
uv add collective.aisettings
Then install the addon profile via Site Setup → Add-ons (or by
adding collective.aisettings:default to the site creation profile list).
Configuration
Open Site Setup → AI Settings. The registry stores a list of connections, each holding one URL + optional API key and zero or more pinned models.
JSON shape
The control panel renders this shape (mirrored on Volto via the
custom widget; in classic Plone via the z3c.form AIModelsWidget):
[
{
"url": "http://localhost:11434", // required
"api_key": "", // optional
"models": [
{
"model": "llama3.2", // required when present
"capabilities": ["completion", "tools"],
"protect_with_permission": false,
"permissions": []
},
{
"model": "llava",
"capabilities": ["vision"],
"protect_with_permission": true,
"permissions": ["Modify portal content"]
}
]
},
{
"url": "https://api.openai.com",
"api_key": "sk-…",
"models": [] // ← empty list = generic passthrough
}
]
Connections and models are both ordered; the first match wins during resolution. The classic widget supports drag-and-drop reordering at both scopes.
Capabilities vocabulary
The token strings in capabilities are tracked by the
collective.aisettings.Capabilities named vocabulary (defined in
vocabularies/capabilities.py).
They match the strings Ollama returns from /api/show, so the
widget can auto-detect a model's capabilities when it's selected.
| Token | Use |
|---|---|
completion |
Chat / text completion |
embedding |
Text embeddings (/v1/embeddings) |
vision |
Image understanding |
tools |
Function calling / tool use |
thinking |
Reasoning / chain-of-thought models |
Permission gate
A model with protect_with_permission=true is only usable if the
current user holds at least one of the Plone permission titles
listed in permissions (e.g. "View", "Modify portal content",
"Manage portal") on the call's context. The check is
permissions.entry_permits which
in turn calls AccessControl.getSecurityManager().checkPermission.
Generic-passthrough connections cannot be gated per-model (they have no per-model entries).
Using the addon
From Python — the IAIService utility
from collective.aisettings.interfaces import IAIService
from zope.component import queryUtility
service = queryUtility(IAIService)
# Chat completion — uses the first model whose capabilities include "completion"
text = service.chat("Summarise this article: …")
# System prompt
text = service.chat("Summarise this article: …", system="You are a helpful editor.")
# Pin an explicit model — must be configured somewhere
text = service.chat("…", model="llama3.1:70b")
# Vision
caption = service.analyze_image(
"Describe this picture",
"https://…/photo.jpg", # URL the AI service can fetch, or a data: URI
)
# Embeddings — single string in, single vector out
vec = service.embed("Hello world")
# Reasoning model
answer = service.think("Walk me through this proof: …")
# Tool / function calling — returns the full assistant message dict
reply = service.tool_call(
messages=[{"role": "user", "content": "…"}],
tools=[{"type": "function", "function": {…}}],
)
# Permission-gated call: pass context= so the gate evaluates against
# the right object. Defaults to the portal root if omitted.
text = service.chat("…", context=self.context)
All methods return None when no matching model is configured or
when the permission gate denies the call (the denial is logged at
INFO level). Network and parsing failures are logged at WARNING
level and also return None.
From Python — lower-level helpers
If you want to drive HTTP yourself but still let the addon pick the right connection:
from collective.aisettings.utils import resolve_model
entry = resolve_model("completion", override=None)
# entry is a flat dict: {url, api_key, model, capabilities,
# protect_with_permission, permissions}
if entry is None:
...
For the actual HTTP calls, client.py
exposes chat_completion, chat_completion_message (full assistant
message), and embeddings. The utility uses these internally.
From HTTP / Volto — the @ai endpoint
By default the endpoint is synchronous: it runs the AI call in the
request thread and returns the result in the response body. For
long-running calls (vision, long-context generations) that risk
exceeding proxy timeouts, pass "async": true to defer the call onto
a worker thread; the endpoint then returns a task id immediately and
the client polls.
POST /Plone/<path>/++api++/@ai
Accept: application/json
Content-Type: application/json
{
"capability": "chat",
"prompt": "Summarise …",
"system": "You are a helpful editor.",
"model": "llama3.1:70b"
}
Synchronous response (HTTP 200):
{
"status": "done", // or "error"
"result": { "response": "…" }
}
On a synchronous failure the endpoint returns HTTP 502 with
{"status": "error", "error": "..."}.
Async mode
Add "async": true to the body. Response (HTTP 202):
{ "task_id": "1244133e-7506-…", "status": "running" }
Then poll:
GET /Plone/<path>/++api++/@ai-task/1244133e-7506-…
Accept: application/json
{
"task_id": "1244133e-7506-…",
"status": "done", // or "running" | "error"
"started_at": 1779291357.7,
"finished_at": 1779291488.8,
"result": { "response": "…" }
}
The endpoint is registered for IDexterityContent (so the URL can
be rooted at any content item including the site root) with the
zope2.View permission. The matched model's permission gate is
evaluated against self.context; on denial the endpoint returns
HTTP 403.
Body shapes per capability
capability |
required body | optional | result key |
|---|---|---|---|
chat |
prompt |
system |
response |
think |
prompt |
system |
response |
vision |
prompt, image (URL or data: URI) |
— | response |
embed |
input (string or list of strings) |
— | embedding |
tools |
messages (array), tools (array) |
— | response |
All variants accept an optional top-level model to override
capability-based selection.
Helper endpoints (for control-panel widgets)
These endpoints power the model-list and capability-detection behavior in the AI Settings widget; they're useful from any client that needs to introspect the upstream service:
POST /++api++/@ai-list-models— body{url, api_key?}→ returns{models: [...]}. Calls/v1/modelson the supplied URL. Permission:cmf.ManagePortal.POST /++api++/@ai-model-capabilities— body{url, api_key?, model}→ returns{capabilities: ["completion", …]}. Calls/api/show(Ollama) for the model; falls back to/v1/modelsmetadata. Permission:cmf.ManagePortal.
From a Plone event subscriber
Subscribers that want to invoke the AI on content events should pass
the content object as context so the permission gate evaluates
correctly:
from collective.aisettings.interfaces import IAIService
from zope.component import queryUtility
def my_subscriber(obj, event):
service = queryUtility(IAIService)
if service is None:
return
result = service.chat("…", context=obj)
Module reference
Where to look when you need to find / change something:
| Concern | File |
|---|---|
Registry schema (JSON shape) + IAIService interface |
interfaces.py |
| Capability resolution (overrides, passthrough, etc.) | utils.py |
| Permission gate | permissions.py |
IAIService implementation |
service.py |
| Low-level HTTP (chat/embed/etc.) | client.py |
Async REST endpoint @ai |
services/ai.py |
Task polling endpoint @ai-task |
services/task_status.py |
| Task registry (in-memory) | services/tasks.py |
| Helper REST: list models | services/list_models.py |
| Helper REST: model capabilities | services/model_capabilities.py |
| Capabilities vocabulary | vocabularies/capabilities.py |
Helper: query /v1/models |
vocabularies/models.py |
| Classic Plone form + Volto REST adapter | controlpanels/ai.py |
| Classic z3c.form widget for the JSONField | controlpanels/widgets.py |
| Classic widget template | controlpanels/templates/ai_models_widget.pt |
| Classic widget JS | static/ai-models-widget.js |
| Classic widget CSS | static/ai-models-widget.css |
For internals and editing rules, see AGENTS.md.
Contribute
Prerequisites
- An operating system that runs all the requirements mentioned.
- uv
- Make
- Git
- Docker (optional)
Development setup
git clone git@github.com:collective/collective-ai.git
cd collective-ai/backend
make install
make create-site
make start
Add features using plonecli or bobtemplates.plone
This package provides markers as strings (<!-- extra stuff goes here -->) that are compatible with plonecli and bobtemplates.plone.
These markers act as hooks to add all kinds of features through subtemplates, including behaviors, control panels, upgrade steps, or other subtemplates from bobtemplates.plone.
To add a feature as a subtemplate to your package, use the following command pattern.
make add <template_name>
For example:
make add content_type
make add behavior
You can check the list of available subtemplates in the [`bobtemplates.plone` `README.md` file](https://github.com/plone/bobtemplates.plone/?tab=readme-ov-file#provided-subtemplates).
See also the documentation of [Mockup and Patternslib](https://6.docs.plone.org/classic-ui/mockup.html) for how to build the UI toolkit for Classic UI.
License
The project is licensed under GPLv2.
Credits and acknowledgements 🙏
Generated using Cookieplone (2.0.0a2) and cookieplone-templates (b0189a8) on 2026-05-20 10:53:22.434266. A special thanks to all contributors and supporters!
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file collective_aisettings-1.0.0a2.tar.gz.
File metadata
- Download URL: collective_aisettings-1.0.0a2.tar.gz
- Upload date:
- Size: 99.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3744e2d69d4ea350f240d22b2e9e6a9ecff3239d4ba4dc1340552d5551b65e01
|
|
| MD5 |
fbdfabd4f0b1d0a2680242d60f6b258a
|
|
| BLAKE2b-256 |
76d1631293edf60f5054425ebc6bd238c7bc286b37a636877814941ed6d46ee6
|
File details
Details for the file collective_aisettings-1.0.0a2-py3-none-any.whl.
File metadata
- Download URL: collective_aisettings-1.0.0a2-py3-none-any.whl
- Upload date:
- Size: 113.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8dd630eb5b1e109efbfb8a5585fde815fe08114fb92b1585e67cb7ecf1997efb
|
|
| MD5 |
2b26dac4b0bb47634644a6bc7f3f9c48
|
|
| BLAKE2b-256 |
82ac4bd87c0eaa6bea4799ada7e83ed1f6cfc4e0fb58298dd0bf4fb29e542df9
|