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.0a2.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.0a2-py3-none-any.whl (113.6 kB view details)

Uploaded Python 3

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

Hashes for collective_aisettings-1.0.0a2.tar.gz
Algorithm Hash digest
SHA256 3744e2d69d4ea350f240d22b2e9e6a9ecff3239d4ba4dc1340552d5551b65e01
MD5 fbdfabd4f0b1d0a2680242d60f6b258a
BLAKE2b-256 76d1631293edf60f5054425ebc6bd238c7bc286b37a636877814941ed6d46ee6

See more details on using hashes here.

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

Hashes for collective_aisettings-1.0.0a2-py3-none-any.whl
Algorithm Hash digest
SHA256 8dd630eb5b1e109efbfb8a5585fde815fe08114fb92b1585e67cb7ecf1997efb
MD5 2b26dac4b0bb47634644a6bc7f3f9c48
BLAKE2b-256 82ac4bd87c0eaa6bea4799ada7e83ed1f6cfc4e0fb58298dd0bf4fb29e542df9

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