Thanks to visit codestin.com
Credit goes to github.com

Skip to content

collective/collective-ai

Repository files navigation

Collective AI 🚀

Built with Cookieplone Black code style CI

Connect your AI models to Plone.

collective.ai 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 [email protected]:collective/collective-ai.git
    cd collective-ai
  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.

  • Docker images for Backend and Frontend 🖼️
  • A stack with a Traefik router and a PostgreSQL database 🗃️
  • Accessible at http://collective-ai.localhost 🌐

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"],
        "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": []
  }
]

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.

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 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.

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.ai.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.ai. 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. 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

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors