Skip to content

demo: AI Assistants - #26076

Closed
thykel wants to merge 86 commits into
devfrom
ai_assistant_demo
Closed

thykel wants to merge 86 commits into
devfrom
ai_assistant_demo

Conversation

@thykel

@thykel thykel commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

demo-only offshoot of #25976

thykel and others added 30 commits October 12, 2026 22:23
The Automations update service drops actions that fail validation and
applies the rest. Callers that need all-or-nothing pass strict: true.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One private conversation per user and work package. Answers can propose
changes, which the user applies like the fixes of a finding. The prompt
is an instance setting, not code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s to AI::Assistance

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Seed QA assistance with Check health, Chat and Generate test cases, with prompts
- Share the LLM call, the event log, the user locale, polling and the feature guard
- Move lost references, comment-only and the proposal state into AI::Proposal
- One list of changeable attributes (ChangeOptions::ATTRIBUTE_KEYS)
- Run Comment after Notify, so Notify cannot replace a proposed comment
- Neutral apply errors for chats, and a translated review state in Details
- Remove unused code, columns (uuid, error_message) and the failed run status
- Use Primer utilities instead of inline styles where they exist
- Share the admin form views, validate the skill type in the model
- Update the concept doc and outdated specs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nsistent

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The run service spec created a skill with an unknown key, which the skill_key
validation now refuses. The run spec called terminal?, which the cleanup removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Users with the global permission manage_own_ai_assistants create their own
assistants and skills at My account > AI assistants. A personal assistant shows
only in its owner's menu, after the global ones, and uses the default model of
the LLM connection. It is deleted with its owner.

The admin list shows every assistant with its owner and filters by owner. The
admin and the My account controllers share their actions and views.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the associations on User that Danger asks for, as for AI text transform
runs, and cover the deletion in the Principals::DeleteJob integration spec.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rules report pull request findings in that area, but the review schema
did not offer it to the model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Render the menu items with the button, and remove the items endpoint.
- Remove AI::Assistance::Locale and ModelChoice: User.execute_as sets the
  locale, and the model choice is AI::Assistant#effective_model_id.
- Rules return attribute hashes instead of a Finding value object.
- Comments use journals.internal_visible, as the jobs run as the user.
- Share the skill lookup and the dialog responses between the controllers,
  and show apply errors of findings as a flash, as in chats.
- One pages helper for the admin and My account controllers.
- Remove finished_at on runs, unused proposal helpers and skill type hooks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The owner of an assistant is now a principal: none for a global assistant, a
user for a personal one, or a group, whose direct members see it in their
menu. An admin chooses the group when creating the assistant, and the list
filters by Global, Groups and Personal. Owned assistants are deleted with
their principal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Comms assistance" belongs to the Marketing & Communications department and
has two presets, Draft an announcement and Check the wording, and a chat.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PM assistant is global, COMMS assistant belongs to the Marketing &
Communications department, and Tom's assistant is a personal assistant of the
admin user. The development seeder replaces the group-only one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
thykel and others added 24 commits October 15, 2026 13:25
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the prompts

The full system prompt preview on the skill page now changes while the
admin types the skill prompt (ai-assistance--prompt-preview), as it
already follows "Use OpenProject MCP tools".

"System prompt" named only one part of the system prompt. The skill field
is now "Skill prompt", and the assistant's "Instructions" field is
"Assistant prompt". The column names stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The button is now an icon in the app header, next to the other header
buttons, instead of a labelled button in the work package toolbars. It
follows the work package in the URL: a split view (also on boards and
calendars) or the full view, and it hides on other pages.

It uses the extension points of a module, so it can move into a plugin
as it is: a view hook (view_layouts_base_top_menu), a dynamic Stimulus
controller, and Angular services through the plugin context for the
refresh after Apply. The Angular toolbar slot is removed, and the
work package toolbars are back to the dev version.

The menu answers an id that the user cannot see with an empty frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The journal entry of an applied proposal now names the user who
applied it instead of "the user". For the owner of the chat, it also
links to the chat, which opens in the chat dialog. Chats stay private:
other users see the text without the link.

A chat whose skill is no longer offered (inactive, or its conditions
no longer match) now opens read-only, from the link or from History.
It has no question field and no Apply or Dismiss buttons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each assistant gets a plain heading in the menu, instead of a label
with its name on every skill. Personal assistants come first, then
group assistants, then global ones.

The menu has a fixed size (480 px wide, up to 600 px high), so its
position no longer changes with the length of the skill texts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A skill has an icon, chosen from a list of Octicons. Without a choice,
it uses the icon of its type, as before. The menu and the history show
it.

The skill form shows the icon as a button joined to the Label field.
The button opens a menu with each icon and its name, and shows the
chosen icon (the ai-assistance--icon-picker Stimulus controller).

The assistant icon was only shown in the assistant list. It is
removed, with a migration, and the list shows the owner instead: a
person, a group or everyone. The seeded skills get icons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A unique index allows one pending answer per chat, also for two
  requests at the same moment. The service answers the index error
  with the usual "wait for the answer" message.
- Starting a skill while the user's last start of it on the work
  package is still answering opens that chat. An advisory lock per
  user, work package and skill covers two clicks at the same moment.
- The reply job has a GoodJob concurrency key per answer. A run after
  a crashed worker still runs.
- An answer stores its result only while it is still pending. A
  result after the time limit is dropped and logged as late.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The header button now shows on every page for users who have "Use AI
Assistance" in a project. Its menu follows the place in the URL: the
open work package, else the project of the page, else no place.

- A skill is offered on "Work packages" (as before) or "Everywhere".
  On a work package, every matching skill is offered. On other pages,
  only Everywhere skills, without proposals. Their context is the user
  and, on a project page, the project. An Everywhere skill cannot be
  limited to types or statuses, and a project or role condition hides
  it off matching projects.
- The permission is "Use AI Assistance" in the project of the place,
  or in any project on pages without one.
- A chat belongs to a work package, a project, or neither
  (AI::Assistance::Place). History lists all the user's chats with
  their place, and each opens with its own context and permission.
- The routes move from /work_packages/:id/ai_assistance to
  /ai_assistance, with the place as a query parameter.
- The seeded Chat skill is offered everywhere.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The assistant list shows one section per assistant, laid out like the
project attributes: global assistants first, then group and personal
ones. The header names the owner with an icon (globe, group, user) and
links to the group or user. Each skill is a row with its icon, a link,
where it is offered, and a menu to edit or delete it.

The owner filter and the owner column are gone. Skills saved before
"Offered on" existed read it as "Work packages".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An assistant is copied with all its skills, for the same owner. A
skill is copied into its own assistant, after its last skill, with
"(copy)" in its label. A copy is inactive, so users see no duplicate
in the menu, and it opens on its edit page. A second copy of an
assistant gets a numbered name, because names are unique per owner.

The copy is in the menus of the assistant list, and on the assistant
page, next to Delete and for each skill. It works in Administration
and in My account.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The OP assistant (basic data) offers only Chat, everywhere.
- The admin's personal assistant is "My assistant", with Refresh work
  package.
- The COMMS assistant offers "Assign to least busy dev": it proposes
  the team developer with the fewest open work packages as the
  assignee.
- The development data adds Kabiru, Bruno, Judith and Ihor Developer
  to Web & IT and to the COMMS team, names the admin "Tom Admin" (also
  a team member), and adds COMMS to the demo project, so that its
  members can be assignees.
- The MCP tool search_users takes a group, by id or name, and finds
  the members of a group that the user can see. The skill uses it.
- Skills can use the "people" icon.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- count_work_packages and search_work_packages take a list of
  assignee ids. Grouped by assignee, the count lists every requested
  person, also with no work packages, and gives each group its id. A
  model no longer calls again to find people who are missing because
  they have nothing.
- "Assign to the least busy developer" (renamed from "Assign to least
  busy dev") counts the team's open work in one call.
- The development data gives the COMMS developers open work packages
  in the demo project (Kabiru 3, Ihor 2, Bruno 1, Judith 0), so the
  skill has one answer.
- The Refresh work package description no longer names pull requests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The section heading of each assistant shows its owner kind, as in the
assistant list: a globe, a group or a person.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each tool call event now keeps the full result, and Details shows it
next to the arguments. The model's text on a turn that calls tools is
logged as a "thought" event, so a loop can be explained. After one
identical call, the next one gets "answer now" instead of three more
rounds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The model's reasoning on a tool-calling turn comes in a separate
field, not in its text, so the "thought" event now logs it too.

In about half the runs, the model repeats its last tool call before it
answers, with and without the answer schema. Only a result that says
"answer now" ends that, so the first repeat now says it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In OpenProject's theme, color-fg-subtle is as dark as the heading
text, so the icon did not look dimmed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A pronoun guessed from a name can be wrong, so the output rules of
every skill ask the model to refer to people by name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…heck

dev adds a migration with the version 20261012120000, which the AI
assistants migration had. The AI one is now 20261012120001.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Temporary, for previews, and not meant for dev:

- The development people (departments, users, Tom Admin, the COMMS
  team and the AI assistants) are seeded also outside development.
  They need no factory_bot, unlike the rest of the development data.
- After the demo data, the COMMS developers' workloads and a Refresh
  work package demo are seeded: the conference work package with the
  comments and meeting outcomes that its subject, description and
  fields do not show yet.
- Pull previews switch on the ai_assistance feature flag. The LLM
  connection is still configured by hand.
- The root seeder spec is skipped, because its counts no longer match.

A fresh seed without development data was checked: all assistants,
the team, the workloads and the Refresh case are there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Deploying openproject with ⚡ PullPreview

Field Value
Latest commit 8f294ad
Job deploy
Status 🗑️ Preview destroyed
Preview URL Destroyed

View logs

@github-actions

Copy link
Copy Markdown
2 Warnings
⚠️ Attention developer & reviewer: Files with potential user references found:

  • db/migrate/20261012120001_create_ai_assistants.rb (migration with user reference)

Please make sure:

  1. You've added proper relationships (has_many, belongs_to, …) and their inverse in your models
  2. You deal with model destruction dependencies: dependent: :destroy or dependent: :delete_all
  3. You add behavior and tests for the Principal::DeleteJob (app/workers/principals/delete_job.rb)
  4. You replace references to users with deleted user in Principal::ReplaceReferencesService (app/services/principals/replace_references_service.rb) by adding to the replacements initailizer (config/initializers/replace_references_service.rb) for the core, or using the replace_principal_references helper in modules/plugins.
  5. You test the above behaviors with an integration test (e.g., like this one to confirm deletion of users is possible.

This helps prevent dangling database objects when users are deleted and resulting bugs.

⚠️ Attention developer & reviewer: migration looks up an index by hardcoded name:

  • db/migrate/20261015130000_add_one_pending_answer_per_ai_chat.rb

remove_index ..., name: and rename_index MAY fail when the live name differs from
the canonical one (for example, old databases pgloader prefixes MySQL indexes with idx_<oid>_; pre-squash
databases may still use the original migration name).

Please use Migration::Utils instead:

require Rails.root.join("db/migrate/migration_utils/utils")

class SomeMigration < ActiveRecord::Migration[8.0]
  include Migration::Utils

  def up
    remove_index_on :workflows, "wkfs_role_type_old_status", %w[role_id type_id old_status_id]
    rename_index_on :workflows, "wkfs_role_type_old_status", "wkfs_role_type_variant_old_status",
                    %w[role_id type_id old_status_id]
  end
end

Those helpers match the given name, or the name with pgloader prefix, then fall back to columns.

Generated by 🚫 Danger

@thykel thykel closed this Oct 16, 2026
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 16, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant