# ocyan.plugin.ai_assistant

Reusable AI assistant plugin for Ocyan and Wagtail-based projects.

It provides:
- a customer-facing chat interface for website or webshop support questions
- an optional floating website chat widget for reusable frontend integration
- a Wagtail admin assistant for staff guidance on content and operational workflows
- a conservative AI Assistant Platform foundation built around deterministic skills
- persisted chat sessions/messages for support context
- OpenAI-backed responses with safe fallback behavior

## AI Assistant Platform foundation

The plugin now includes a small internal skill system. The current goal is not to
turn the assistant into an autonomous agent, but to let it gather deterministic
site-aware context safely before sending a request to the provider.

Current built-in skill:
- `site_info`
- `page_search`
- `product_search`
- `product_detail`
- `contact`
- `quote`
- `booking`
- `admin_content`
- `seo_analysis`

Planned future skills:
- `translation`
- `content_generation`
- `publishing_workflow_assistance`

The platform foundation preserves the current customer/admin UI and API responses.
Phase A only adds safe prompt orchestration and site-level context injection.

## Endpoints

Customer-facing:
- page: `/ai-assistant/`
- chat API: `/ai-assistant/api/chat/`
- reset API: `/ai-assistant/api/reset/`
- optional widget include: `{% ai_assistant_widget %}`

Wagtail admin:
- page: `/admin/ai-assistant/`
- chat API: `/admin/ai-assistant/api/chat/`
- reset API: `/admin/ai-assistant/api/reset/`

## Installation

1. Add the plugin to `ocyan.json`:

```json
{
  "ocyan_plugins": [
    "ocyan.plugin.ai_assistant"
  ]
}
```

2. Install the package in your project environment.

3. Run migrations:

```bash
bin/manage.py migrate ai_assistant
```

4. Configure Wagtail site settings:

`Settings -> AI Assistant Settings`

## Wagtail site settings

The plugin exposes per-site Wagtail settings for:
- enable/disable customer assistant
- customer display mode: `page`, `widget`, `both`, `disabled`
- enable/disable admin assistant
- OpenAI API key
- OpenAI model
- customer system prompt
- admin system prompt
- max context messages
- escalation email
- widget launcher label
- widget title
- widget welcome text
- widget position
- hide widget on basket/checkout/payment pages
- business description
- assistant tone
- public assistant instructions
- admin assistant instructions
- enabled public skills
- enabled admin skills
- max skill context items
- contact page URL
- quote page URL
- booking page URL
- public support email
- contact instructions

These remain useful for site-level prompt and runtime tuning.

## Skills and site context

The built-in skill registry currently ships with:

- `site_info`: safe site/business context for both public and admin chat
- `page_search`: deterministic lookup across public/live Wagtail pages for the current site
- `product_search`: deterministic lookup across the public Oscar catalogue when the webshop stack is installed
- `product_detail`: deterministic public product detail and specification context for catalogue questions
- `contact`: deterministic contact and human-support routing using configured public-safe site settings
- `quote`: deterministic quote and project-intake routing using configured public-safe site settings
- `booking`: deterministic booking and consultation routing using configured public-safe site settings
- `admin_content`: staff-only, read-only Wagtail page metadata lookup for navigation and structure questions
- `seo_analysis`: staff-only, read-only Wagtail SEO metadata hygiene analysis for the current site

Public/admin skill keys are configured per site using:
- `enabled_public_skills`
- `enabled_admin_skills`

They are stored as comma-separated skill keys. Phase A keeps this simple and
conservative so it remains easy to reason about and migrate.

`site_info` may use:
- Wagtail site name / hostname
- business description
- assistant tone
- public/admin assistant instructions

It does **not** expose:
- API keys
- escalation email
- private settings
- internal-only operational secrets

`page_search` is:
- DB-first and deterministic
- limited to public/live Wagtail pages
- filtered to the current Wagtail site
- locale-aware when a request language is available
- bounded by `max_skill_context_items`

`page_search` may use:
- page title
- SEO title
- search description
- relative page URL
- safe text extracted from common public content fields such as `body`, `intro`, `description`, or `summary`

`page_search` does **not**:
- use Elasticsearch
- use vector search / RAG
- inspect draft content
- expose private or restricted pages
- search staff-only admin content

`product_search` is:
- DB-first and deterministic
- optional and safe to disable per site
- limited to public/browsable Oscar catalogue products
- filtered through the active site category boundary when `ocyan.plugin.oscar_multi_site` is available
- bounded by `max_skill_context_items`

`product_search` may use:
- product title
- slug
- short/long description
- product URL
- primary category name

`product_search` does **not**:
- require Elasticsearch
- use vector search / RAG
- expose private or non-browsable products
- expose draft or admin-only product data
- mutate basket, order, payment, checkout, or customer state
- invent prices, availability, delivery times, or discounts

`product_detail` is:
- DB-first and deterministic
- optional and safe to disable per site
- limited to public/browsable Oscar catalogue products
- filtered through the active site category boundary when `ocyan.plugin.oscar_multi_site` is available
- intended for product-detail, comparison, specification, and category questions

`product_detail` may use:
- product title
- slug
- public URL
- short/long description
- primary category
- safe attribute/specification values when available
- simple parent/child product relationship hints when available

`product_detail` does **not**:
- mutate basket, order, payment, checkout, or customer state
- expose private or non-browsable products
- expose admin-only catalogue data
- invent prices, stock guarantees, delivery times, or discounts
- require Elasticsearch or vector search

`contact` is:
- deterministic and configuration-driven
- intended for public contact/help/human-support routing
- safe for both public and admin chat when enabled
- bounded to explicit site settings rather than hidden project internals

`contact` may use:
- contact page URL
- booking page URL
- public support email
- public-safe contact instructions

`contact` does **not**:
- send emails
- create tickets, CRM records, or callbacks
- submit forms automatically
- expose `escalation_email` in public mode

In admin mode, the skill may include the internal `escalation_email` to help staff route conversations safely.

`quote` is:
- deterministic and configuration-driven
- intended for public quote, estimate, proposal, and project-intake routing
- safe for both public and admin chat when enabled
- bounded to explicit site settings rather than hidden project internals

`quote` may use:
- quote request page URL
- contact page URL
- public-safe project intake instructions
- safe intake guidance such as project type, business context, timeline, existing site URL, desired features, and preferred contact route

`quote` does **not**:
- create or send quotes, proposals, or project requests
- promise prices, timelines, or delivery commitments
- submit forms
- send emails
- create CRM or ticket records
- expose `escalation_email` in public mode

The quote route is guidance only. Users must complete any project request manually on the configured quote page or through the contact route.

`booking` is:
- deterministic and configuration-driven
- intended for public appointment, consultation, demo, call, or reservation routing
- safe for both public and admin chat when enabled
- bounded to explicit site settings rather than hidden project internals

`booking` may use:
- booking page URL
- contact page URL
- public-safe booking/contact instructions

`booking` does **not**:
- create bookings or reservations
- write to a calendar
- send emails
- submit forms
- mutate checkout, payment, or customer data
- expose `escalation_email` in public mode

The booking route is guidance only. Users must complete any booking manually on the configured page or through the contact route.

`admin_content` is:
- staff-only
- read-only
- site-aware and locale-aware when request context is available
- metadata-focused rather than content-extraction focused
- supports deterministic page target resolution for prompts such as `this page`, `homepage`, `contact page`, and specific page titles when enough context is available

`admin_content` may use:
- page title
- page type
- live/draft status
- URL/path
- slug/path hierarchy
- last updated timestamp
- SEO description presence
- SEO title presence
- homepage/child relationships

`admin_content` does **not**:
- edit, publish, unpublish, move, or delete pages
- expose secrets or private configuration
- extract large private body content
- run in public/customer mode

`seo_analysis` is:
- staff-only
- read-only
- site-aware and locale-aware when request context is available
- metadata-focused rather than content-extraction focused
- able to switch into suggestion guidance for staff requests without changing data
- able to focus on a single resolved target page when staff ask about `this page`, homepage, contact page, services page, or a specific page title

`seo_analysis` may use:
- page title
- page type
- live/draft status
- URL/path
- slug
- SEO title presence
- search description presence
- last updated timestamp

`seo_analysis` does **not**:
- edit, publish, unpublish, move, or delete pages
- expose secrets or private configuration
- inspect private body or StreamField content
- run in public/customer mode
- generate final SEO metadata automatically

Admin page target resolution is:
- staff/admin focused
- deterministic and site-scoped
- locale-aware when request context is available
- limited to safe page identifiers such as title, slug/path, homepage, and simple named page references
- safe to no-op when no confident single page match is found

`seo_analysis` suggestion mode:
- is staff-only
- is still read-only
- provides draft SEO title and meta description guidance only
- labels output as suggestions for manual review
- does not save or publish anything
- requires editors to apply changes manually in Wagtail

## Business Assistant Mode

Business Assistant Mode extends the assistant platform with a third capability layer for internal business workflows.

Important defaults:
- disabled by default
- staff-only by default
- never exposed in public/customer chat
- no real business skills are auto-enabled by installation

Business Assistant Mode must still satisfy all gates before a business skill can run:
- `business_assistant_enabled = True`
- the admin/staff assistant explicitly requests business scope
- the user is allowed by the current staff/business gate
- the skill key is present in `enabled_business_skills`

This mode is intended for future internal capabilities such as:
- form intelligence
- CRM summaries
- booking operations
- business dashboards
- internal reporting

Current platform behavior remains conservative:
- no automatic destructive actions
- no hidden writes
- no public exposure of internal business context
- no mutation workflows in this phase

## External business skill registration

Other `ocyan.plugin.*` packages can register business skills explicitly through the AI assistant registry:

```python
from ocyan.plugin.ai_assistant.skills.registry import register_skill
```

Registration only makes the skill discoverable by the AI assistant platform. It does **not**:
- auto-enable the skill
- bypass Business Assistant Mode
- expose the skill to public/customer chat
- grant permission to use the skill

### Recommended `AppConfig.ready()` pattern

Use a safe explicit registration call from the integrating plugin's `AppConfig.ready()` method.

Example for a future `ocyan.plugin.forms` integration:

```python
from django.apps import AppConfig


class FormsPluginConfig(AppConfig):
    name = "ocyan.plugin.forms"

    def ready(self):
        from ocyan.plugin.ai_assistant.skills.registry import register_skill
        from .ai_skills import FormIntelligenceSkill

        register_skill(FormIntelligenceSkill())
```

Example for a future `ocyan.plugin.crm` integration:

```python
from django.apps import AppConfig


class CrmPluginConfig(AppConfig):
    name = "ocyan.plugin.crm"

    def ready(self):
        from ocyan.plugin.ai_assistant.skills.registry import register_skill
        from .ai_skills import CrmLeadLookupSkill

        register_skill(CrmLeadLookupSkill())
```

### Example read-only business skill

The example below is illustrative only. It is not enabled by default and is not included as a real workflow plugin.

```python
from ocyan.plugin.ai_assistant.constants import (
    CAPABILITY_LEVEL_READ_ONLY,
    MODE_ADMIN,
    SKILL_SCOPE_BUSINESS,
)
from ocyan.plugin.ai_assistant.skills.base import (
    AiAssistantSkill,
    SkillContextResult,
    SkillMatch,
)


class ExampleBusinessSummarySkill(AiAssistantSkill):
    key = "example_business_summary"
    label = "Example business summary"
    allowed_modes = (MODE_ADMIN,)
    public_safe = False
    skill_scope = SKILL_SCOPE_BUSINESS
    capability_level = CAPABILITY_LEVEL_READ_ONLY

    def is_enabled(self, site, settings_obj, mode):
        return self.key in settings_obj.enabled_business_skill_keys()

    def match(self, request_text, request, site, mode, settings_obj):
        if "summary" in (request_text or "").lower():
            return SkillMatch(score=50, reason="example_summary_intent")
        return SkillMatch(score=0, reason="no_match")

    def build_context(self, request_text, request, site, mode, settings_obj):
        return SkillContextResult.from_items(
            ["Example business summary context"],
            instruction=(
                "This context is internal and read-only. "
                "Do not claim to edit, submit, or execute anything."
            ),
            metadata={"skill": self.key},
        )
```

### Activation requirements

Even after registration, the skill remains inactive until all of the following are true:
- Business Assistant Mode is enabled for the site
- the admin assistant explicitly requests business scope
- the current user satisfies staff/business access requirements
- `enabled_business_skills` contains the skill key

That means a plugin can safely register a business skill without changing default assistant behavior.

## API key resolution

API key lookup is env-first:

1. `AI_ASSISTANT_OPENAI_API_KEY`
2. `OPENAI_API_KEY`
3. Wagtail site setting `openai_api_key`

This keeps deployment-time configuration possible without storing the primary key in Wagtail.

## Optional Django settings

### Timeout

```python
AI_ASSISTANT_OPENAI_TIMEOUT = 20
```

Controls the OpenAI client timeout in seconds.

### Throttling

Public/customer chat:

```python
AI_ASSISTANT_PUBLIC_THROTTLE_LIMIT = 10
AI_ASSISTANT_PUBLIC_THROTTLE_WINDOW_SECONDS = 60
```

Admin/staff chat:

```python
AI_ASSISTANT_ADMIN_THROTTLE_LIMIT = 30
AI_ASSISTANT_ADMIN_THROTTLE_WINDOW_SECONDS = 60
```

Public chat is throttled more aggressively than staff chat.

## Behavior

### Customer chat

- browser-session conversation flow
- ownership tied to the visitor/session cookie
- reset endpoint closes the current owned session
- clean validation errors for empty or oversized messages

### Customer display modes

- `page`: only `/ai-assistant/` is available
- `widget`: only the floating widget is available
- `both`: both the page and the widget are available
- `disabled`: no public customer assistant is exposed

### Admin chat

- staff-only access
- session ownership bound to the authenticated staff user
- reset endpoint closes only the requesting staff user’s own session

### Provider behavior

- explicit OpenAI timeout
- sanitized provider failure responses
- structured logging without raw secret leakage
- safe fallback copy when the provider is unavailable

### Safety guardrails

The plugin adds a system safety layer instructing the assistant to:
- never ask for passwords, API keys, tokens, private keys, or other secrets
- never claim actions were performed when they were not
- recommend contacting staff when uncertain

The skill system follows the same boundary:
- public mode may only use public-safe skills
- contact, quote, and booking routing are guidance only and do not perform actions

## Service and category routing

`contact`, `quote`, and `booking` can use deterministic service/category routing to point users at a more specific public page when one is available on the current site.

Common detected categories include:
- website
- webshop / ecommerce
- support
- maintenance
- SEO
- AI automation
- custom work
- multilingual website
- booking / appointment / consultation / demo

When a relevant live public page is found, the assistant may include:
- detected service category
- matched public page title
- matched public URL/path
- matched keywords

If no specific public page is found, the assistant falls back to the configured `contact_page_url`, `quote_page_url`, or `booking_page_url`.

This routing layer is:
- public-page-only
- site-scoped
- deterministic
- read-only

It does **not**:
- expose draft or private pages
- read admin-only content
- perform writes or external actions

## Contact routing

The `contact` skill is for support and human-routing requests such as:
- "how can I contact you?"
- "can someone call me?"
- "where is your contact page?"

The skill guides users to configured public routes only. It may surface:
- `contact_page_url`
- `booking_page_url`
- `public_support_email`
- `contact_instructions`
- a service-specific public page when category routing finds one

It intentionally does **not**:
- send mail
- create tickets
- submit project requests
- book appointments
- expose internal-only support addresses in public mode

If you want the assistant to show an email address to customers, use `public_support_email`.
Keep `escalation_email` for internal/staff routing only.

## Quote and project intake routing

The `quote` skill is for quote and project-intake requests such as:
- "I want a quote"
- "Can I get a price estimate?"
- "I need a proposal for a webshop"
- "How do I start a website project?"

The skill guides users to configured public routes only. It may surface:
- `quote_page_url`
- `contact_page_url`
- `contact_instructions`
- a service-specific public page when category routing finds one

It can also suggest safe, non-sensitive intake details to prepare, such as:
- project type
- business name or industry
- preferred deadline
- existing website URL
- desired features
- preferred contact route
- budget range only if the user volunteers it

It intentionally does **not**:
- create quotes or proposals
- promise prices or timelines
- submit forms
- send emails
- create CRM or ticket records

Any future proposal generation or intake automation should be a separate, explicit, permissioned workflow.

## Booking routing

The `booking` skill is for appointment, consultation, demo, or reservation routing requests such as:
- "I want to book an appointment"
- "Can I schedule a consultation?"
- "Book a demo"

The skill guides users to configured public routes only. It may surface:
- `booking_page_url`
- `contact_page_url`
- `contact_instructions`
- a service-specific public page when category routing finds one

It intentionally does **not**:
- create bookings or reservations
- write to a calendar
- submit forms
- send emails

Any future booking automation should be a separate, explicit, permissioned workflow.

## Admin content navigation

Phase F adds `admin_content`, a read-only staff skill for questions such as:
- what pages exist?
- where is the contact page?
- find pages about services
- show draft or live pages
- what pages are under the homepage?
- find pages missing SEO description
- explain site structure or navigation hierarchy

The skill is available only in admin mode and only when enabled through `enabled_admin_skills`.

It helps staff navigate Wagtail by surfacing concise metadata and structure hints. It can suggest where to look in Wagtail, but it does not perform any content changes.

Future editor-focused skills are expected to stay separate:
- translation assistance
- content generation
- publishing workflow assistance

Across the platform:
- admin mode may use admin-enabled skills
- no autonomous actions are executed
- no private/admin-only context is exposed in public chat
- public page search only uses live, public, site-scoped page data

## Cleanup command

Old sessions can be reviewed or cleaned up with:

```bash
bin/manage.py cleanup_ai_assistant --days 30 --mode customer
```

Dry-run is the default.

Useful options:

```bash
bin/manage.py cleanup_ai_assistant --days 30 --mode customer
bin/manage.py cleanup_ai_assistant --days 30 --mode admin --apply
bin/manage.py cleanup_ai_assistant --days 90 --mode all --apply
bin/manage.py cleanup_ai_assistant --days 90 --mode all --delete --apply
```

Options:
- `--days`
- `--mode customer|admin|all`
- `--apply`
- `--delete`

Behavior:
- without `--apply`: dry-run only
- with `--apply`: closes matching old sessions
- with `--delete --apply`: deletes matching sessions and related messages

## Security notes

- Do not paste secrets into the chat UI.
- Public and admin endpoints are rate-limited.
- Sessions are ownership-bound:
  - customer chats to browser/session identity
  - admin chats to the authenticated staff user
- Provider errors are sanitized in responses and persisted metadata.
- Raw exception text is not returned to the user.

## Development and test

Checks used for this plugin:

```bash
ruff check ocyan/plugin/ai_assistant
black --check --exclude 'migrations/*' ocyan
DJANGO_SETTINGS_MODULE=ocyan.plugin.ai_assistant.test_settings python -m django test --testrunner=django.test.runner.DiscoverRunner ocyan.plugin.ai_assistant.tests
```

## Known limitations

- Persisted history is not rendered back into the chat UI on page reload yet.
- There is no provider-side moderation layer yet.
- There is no browser screenshot verification included in this package yet.
- Skills remain deterministic and bounded; there is no autonomous tool calling.
- `page_search` is still intentionally simple:
  - no Elasticsearch
  - no semantic/vector ranking
  - relevance is based on deterministic keyword matching and safe field extraction
- `seo_analysis` is metadata-only today:
  - no automatic SEO rewriting or saving
  - no body-content analysis
  - no publish/edit workflow actions
  - any future “apply suggestion” workflow should be a separate, explicit, permissioned feature
- `booking` is routing-only today:
  - no booking creation
  - no calendar writes
  - no email or form submission
  - any future booking automation should be a separate, explicit, permissioned workflow
- `quote` is routing-only today:
  - no quote or proposal creation
  - no pricing or timeline commitments
  - no email, form, or CRM submission
  - any future quote automation should be a separate, explicit, permissioned workflow

## Notes for integrators

- Namespace remains `ocyan.plugin.*`
- Frontend is Bootstrap/Wagtail admin compatible with minimal vanilla JavaScript
- No React dependency is required
- Recommended frontend integration is in the shared site base template:

```django
{% load ai_assistant_tags %}
...
{% ai_assistant_widget %}
```

- The widget renders nothing when:
  - public chat is disabled
  - display mode is `page`
  - the current request is in Wagtail/Django admin
  - the current request is a basket/checkout/payment flow and the site setting says to hide it
