Skip to content

About

Connect your Plone instance to AI models

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Collective AI πŸš€

Built with Cookieplone Black code style CI

Connect your AI models to Plone.

collective.aisettings lets a Plone site talk to one or more OpenAI-compatible Large Language Model services (Ollama, Lemonade, OpenAI, vLLM, etc.) from both backend Python code and the Volto frontend, with per-model capability declaration and permission gating.

What you get

  • A registry-backed control panel to declare AI connections and the models hosted on each β€” usable from both the classic Plone UI and Volto.
  • A reusable IAIService utility so any Plone addon, view, subscriber, or block can perform chat, think, analyze_image, embed, or tool_call operations against the configured models.
  • An asynchronous @ai REST endpoint that enqueues calls on a worker thread so long-running model invocations don't hit proxy/LB request timeouts, with a polling endpoint @ai-task/<id> for the result.
  • Capability-based resolution (completion, embedding, vision, tools, thinking) so callers ask for what they need rather than for a specific model name. Callers can still pin a specific model name when they need to.
  • Per-model permission gating with OR semantics over any number of Plone permissions, so e.g. expensive vision models can be restricted to Editors while text completion is available to everyone.
  • Generic passthrough connections β€” declare an endpoint with no pinned models and let callers ask for any model name the upstream service hosts.

Quick start 🏁

Prerequisites βœ…

Installation πŸ”§

  1. Clone this repository, then change your working directory.

    git clone git@github.com:collective/collective-ai-settings.git
    cd collective-ai-settings
  2. Install this code base.

    make install

Fire up the servers πŸ”₯

  1. Create a new Plone site on your first run.

    make backend-create-site
  2. Start the backend at http://localhost:8080/.

    make backend-start
  3. In a new shell session, start the frontend at http://localhost:3000/.

    make frontend-start

Voila! Your Plone site should be live and kicking πŸŽ‰

Local stack deployment πŸ“¦

Deploy a local Docker Compose environment that includes the following.

Run the following commands in a shell session.

make stack-create-site
make stack-start

Configuring AI connections

After installing the addon on a Plone site, the AI Settings control panel is registered under Site Setup β†’ General. It supports both UIs:

  • Volto: http://localhost:3000/controlpanel/ai-settings
  • Classic Plone: http://localhost:8080/Plone/@@ai-settings

Both UIs render the same data structure (a single JSONField in the registry) with a feature-equivalent rich editor.

Data model

The control panel stores a list of connections. Each connection is one remote AI service endpoint, and can optionally pin one or more models that live behind that endpoint:

[
  {
    "url": "http://localhost:11434",
    "api_key": "",
    "models": [
      {
        "model": "llama3.2",
        "capabilities": ["completion", "tools"],
        "only_for_authenticated": false,
        "protect_with_permission": false,
        "permissions": []
      },
      {
        "model": "llava",
        "capabilities": ["vision"],
        "only_for_authenticated": true,
        "protect_with_permission": true,
        "permissions": ["Modify portal content"]
      }
    ]
  },
  {
    "url": "https://api.openai.com",
    "api_key": "sk-…",
    "models": []
  }
]

A connection with an empty models list is a generic passthrough: it won't be picked by capability-based resolution, but it can serve any model name that an @ai caller asks for explicitly.

Environment-based configuration (connections file)

For deployments you often want to inject the endpoint, API key and models at deploy time rather than clicking them into the ZODB β€” to keep secrets out of the database and make the configuration reproducible. Set the environment variable COLLECTIVE_AISETTINGS_CONNECTIONS to the absolute path of a JSON file holding the same array of connections the control panel edits:

[
  {
    "url": "https://api.openai.com",
    "api_key_env": "OPENAI_API_KEY",   // read from the environment at load time
    "models": [
      { "model": "gpt-4o", "capabilities": ["completion", "vision", "tools"] },
      { "model": "text-embedding-3-small", "capabilities": ["embedding"] }
    ]
  },
  {
    "url": "http://localhost:11434",     // no key needed
    "models": []                          // generic passthrough
  }
]

Notes:

  • Same schema as the control panel, plus one extra optional field per connection: api_key_env names an environment variable to read the key from, so the secret need not live in the file. When set and the variable is present it wins over any inline api_key; if the variable is unset it falls back to the inline api_key (and logs a warning).
  • File-first precedence. File connections are merged before the registry ones, so on any overlap (same capability, or an explicitly named model) the file wins; registry connections still extend the set for capabilities/model names the file does not cover.
  • Live reload. The file is re-read when its modification time changes β€” no restart needed β€” and otherwise cached.
  • Fail-safe. A missing, unreadable, malformed or schema-invalid file is logged as an error and ignored; the site keeps running on the registry-configured connections.
  • Control-panel confirmation. Whenever the environment variable is set and the file loads and validates without errors, both the classic and Volto control panels show the file-provided connections read-only and greyed out at the top (with a "Managed by environment configuration" notice), as confirmation that the file was found and parsed correctly. The panel below stays fully editable, so you can still add your own connections through the web UI β€” they are stored in the registry and merged after the file ones at runtime. API keys are never exposed to the browser: a connection using api_key_env shows only the variable name (the value is never read), and an inline api_key is shown as a masked "set" indicator. If the variable is unset β€” or the file fails to load β€” no read-only section is shown and the panel behaves normally.

Resolution rules

When a caller asks for an AI operation, the addon walks the connections in declared order and picks the first match:

  • Caller passes model=X (an explicit model name):
    1. The first pinned model anywhere with model == X β†’ that connection + that model definition.
    2. Otherwise the first generic-passthrough connection (empty models) β†’ uses its URL/api key with X as the model name to send.
    3. Otherwise fail.
  • Caller passes no model, just a capability:
    1. The first pinned model whose capabilities list contains the requested capability β†’ use it.
    2. Otherwise fail. Generic-passthrough connections are skipped here because they declare no capability metadata.

The ordering of connections, and the ordering of models within a connection, both matter. The UI supports drag-and-drop reordering at both scopes.

Capabilities

Each pinned model can advertise zero or more capabilities. The vocabulary mirrors the strings returned by Ollama's /api/show endpoint, so the widget can auto-detect capabilities by querying the service:

Token Description
completion Chat / text completion
embedding Text embeddings (/v1/embeddings)
vision Image understanding
tools Function calling / tool use
thinking Reasoning / chain-of-thought models

Permission gating

Each pinned model can opt in to two independent gates:

  • Only for authenticated β€” when enabled, the call is denied for anonymous callers; any logged-in user is allowed. No specific permission is required.
  • Protect with permission β€” when enabled, the call is only allowed if the current user holds at least one of the listed Plone permission titles (e.g. View, Modify portal content, Manage portal) on the call's context. The widget surfaces checkboxes for the three common permissions and a free-text + add-button for custom ones, with selected entries displayed as removable chips.

When both are enabled they combine with AND semantics: the caller must be authenticated and hold one of the listed permissions.

Generic passthrough connections cannot be gated per-model (they have no per-model definitions).

Using the AI from Python

Any Plone addon, browser view, event subscriber, or block can use the registered global utility:

from collective.aisettings.interfaces import IAIService
from zope.component import queryUtility

service = queryUtility(IAIService)

# Capability-based selection (uses the first configured model that
# advertises `completion`)
text = service.chat("Summarise this article: …")

# Pin a specific model by name
text = service.chat("Summarise …", model="llama3.1:70b")

# Vision model
description = service.analyze_image(
    "Describe the image", "https://…/photo.jpg",
)

# Embeddings (single string in, single vector out)
vector = 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, tools)

# Permission-gated call: pass `context=` to scope the check
text = service.chat("…", context=self.context)

When the resolved model has protect_with_permission on, the utility runs the gate against context (defaulting to the portal root) and returns None if denied, logging the denial.

See backend/README.md for the full Python API reference.

Using the AI from the Volto frontend (or any HTTP client)

The asynchronous REST endpoint accepts the same operations:

POST /Plone/<path>/++api++/@ai
Content-Type: application/json
Accept: application/json

{
  "capability": "chat",         // chat | think | vision | embed | tools
  "prompt": "Summarise …",
  "model": "llama3.1:70b",       // optional; resolution falls back to capability
  "system": "You are a helpful editor."   // optional system instruction
}

The endpoint replies immediately with HTTP 202 and a task id:

{ "task_id": "1244133e-…", "status": "running" }

The client then polls until the task is done:

GET /Plone/<path>/++api++/@ai-task/<task_id>
{
  "task_id": "1244133e-…",
  "status": "done",
  "started_at": 1779291357.7,
  "finished_at": 1779291488.8,
  "result": { "response": "…" }
}

The endpoint is registered for any IDexterityContent (so the URL can be rooted at the site or at any content item), with zope2.View as the required permission. The permission gate on the matched model is checked against the called context and returns HTTP 403 on denial.

Body shapes per capability:

capability required body fields optional result key
chat prompt system response
think prompt system response
vision prompt, image β€” response
embed input (string or list) β€” embedding
tools messages (array), tools (array) β€” response

image may be either a URL the AI service can fetch or a data: URI.

See frontend/README.md for the Volto-specific integration.

Project structure πŸ—οΈ

This monorepo consists of the following distinct sections:

  • backend/ β€” Plone addon collective.aisettings. Houses the registry schema, control-panel form, classic z3c.form widget, IAIService utility, async REST endpoint, capabilities vocabulary, and permission helpers. See backend/README.md and backend/AGENTS.md.
  • frontend/ β€” Volto addon volto-collective-ai-settings. Houses the custom control-panel widget (ModelsWidget) that renders the connection / model UI in Volto. See frontend/README.md and frontend/AGENTS.md.
  • devops/ β€” Docker stack, Ansible playbooks, cache settings.
  • docs/ β€” Scaffold for end-user documentation.

For agents working on this codebase, start at AGENTS.md.

Code quality assurance 🧐

To check your code against quality standards, run the following shell command.

make check

Format the codebase

To format and rewrite the code base, ensuring it adheres to quality standards, run the following shell command.

make format
Section Tool Description Configuration
backend Ruff Python code formatting, imports sorting backend/pyproject.toml
backend zpretty XML and ZCML formatting --
frontend ESLint Fixes most common frontend issues frontend/.eslintrc.js
frontend prettier Format JS and TypeScript code frontend/.prettierrc
frontend Stylelint Format styles (css, less, sass) frontend/.stylelintrc

Formatters can also be run within the backend or frontend folders.

Lint the codebase

make lint
Section Tool Description Configuration
backend Ruff Checks code formatting, imports sorting backend/pyproject.toml
backend Pyroma Checks Python package metadata --
backend check-python-versions Checks Python version information --
backend zpretty Checks XML and ZCML formatting --
frontend ESLint Checks JS / TypeScript lint frontend/.eslintrc.js
frontend prettier Check JS / TypeScript formatting frontend/.prettierrc
frontend Stylelint Check styles (css, less, sass) formatting frontend/.stylelintrc

Linters can be run individually within the backend or frontend folders.

Internationalization 🌐

Generate translation files for Plone and Volto with ease:

make i18n

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!

About

Connect your Plone instance to AI models

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages