Skip to main content

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.
  • IAIService global utility with methods for chat, reasoning, vision, embeddings, and tool/function calls.
  • Async @ai REST 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/models on 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/models metadata. 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

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

collective_aisettings-1.0.0.tar.gz (99.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

collective_aisettings-1.0.0-py3-none-any.whl (113.6 kB view details)

Uploaded Python 3

File details

Details for the file collective_aisettings-1.0.0.tar.gz.

File metadata

  • Download URL: collective_aisettings-1.0.0.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

Hashes for collective_aisettings-1.0.0.tar.gz
Algorithm Hash digest
SHA256 ae9e4c4bf79d201acf520e3ebb38558cf371caa3a6597058c32722818fc41ac6
MD5 1e357106d22e2c59e98c6fe221e8a154
BLAKE2b-256 6e97e99e0f9491b1b1ea4f6f95f264cd202f659a83ebcc862e8e1c07441a9aaf

See more details on using hashes here.

File details

Details for the file collective_aisettings-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: collective_aisettings-1.0.0-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

Hashes for collective_aisettings-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 76d33344f87a6a6117da4703db318fcdafe65df0dd806e0c225e9bf304dfe742
MD5 45027daf7c4b95d57fca9ef28864c7cd
BLAKE2b-256 48b31588c694893fd6133d051fb499bd392704c10c819f63eda852ab1ce5d4a3

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page