Content
View differences
Updated by Jonas Jabari 4 days ago
## <img class="op-uc-image op-uc-image_inline" src="/api/v3/attachments/923080/content">
##
## Summary
Persistence Introduce the persistence layer for the description assistant: a model for admin-managed
text-transform
actions actions, plus seeded defaults. Defaults are plain seeds — created once on fresh
installations, never touched afterwards (same semantics as statuses/types). seeding of the default action set with upgrade-safe semantics.
No UI, no API.
Implemented API in PR: https://github.com/opf/openproject/pull/24847 this work package.
## Acceptance criteria
Model `AI::TextTransformAction` (table `ai_text_transform_actions`) * A model (working name: `Ai::TextTransformAction`, naming open) exists with:
-
* `builtin_key` (string, nullable, unique) — set for seeded actions, `NULL` for custom ones
* `label` (string, required)
-
* `prompt` (text, required)
- `usage_scope` (string enum, (text) — **nullable**: for builtin actions, `NULL` means "use the shipped default `everywhere`): `everywhere` | `all_work_package_types` | `specific_work_package_types` — single-select; future scopes (comments, wikis, long text custom fields) are new enum values, no migration needed
- text"; a resolver method returns the effective prompt (stored text, or the shipped default looked up by `builtin_key`)
* `active` (boolean, default true)
- `true`)
* `position` (integer, acts_as_list)
- `injects_type_template` (integer) — list order in the dropdown/admin list
* `model` (string, nullable) — optional LLM model override; `NULL` = default from LLM settings
* type restriction: `applies_to_all_types` (boolean, default false) — type description template is injected at execution time
Type restriction via `true`) plus a join model `AI::TextTransformActionType` table to `types` for restricted actions (pattern: `project_types`),
targeting types — variants custom fields ↔ types)
* Validations: label presence; prompt presence for custom actions (builtin actions may be `NULL`); temperature/model values are included automatically at runtime. Types required iff
`specific_work_package_types`.
not validated against the LLM connection (loose coupling).
* An `available_for_type?(type)` query method encapsulates: `active` AND type restriction AND, for the `sort_into_template` builtin, "type has a non-blank default description".
* Seeder `BasicData::AiTextTransformActionSeeder` (`ModelSeeder`) creates `fix_grammar`,
the builtin actions (`fix_grammar`, `make_concise`, `translate`, `sort_into_template` (only `sort_into_template`) with `prompt: NULL`, translatable labels and stable positions.
* Upgrade semantics verified by specs:
* Re-running the latter seeder on an existing installation creates missing builtins and never
touches existing rows (user edits, ordering, active flags survive).
* A builtin with template injection).
Labels translatable (`t_` mechanism), prompts untranslated. Seeding `prompt: NULL` resolves to the _current_ shipped default after an upgrade.
* Deleting a builtin row is a no-op once any
record exists (verified by specs); seeded not possible via model API (builtins are deactivated instead); custom actions are ordinary records — editable and
deletable. Prompt can be destroyed.
* Shipped default prompt texts live in `app/seeders/common.yml` (single a dedicated, reviewable location (e.g. YAML under the seeder or a `Prompts` module) — single source of truth for
AI-97); texts are working drafts, finalization is AI-97.
also used by the evals alignment task.
## Technical notes
- `builtin_key` / NULL-prompt * Follow `BasicData::ModelSeeder` conventions where possible, but note its `applicable?` skips seeding entirely once records exist — this seeder must instead upsert missing builtins (idempotent per `builtin_key`).
* Translatable labels via the `t_`\-prefix mechanism (`Source::Translate`, crowdin pipeline).
* The `NULL`\-prompt resolution / upgrade upserts deliberately dropped: existing installations don't receive new or changed defaults on upgrade (accepted).
- replaces any digest/"customized\_at" bookkeeping — keep it simple.
* Type restriction join table only consulted when `applies_to_all_types` is `false`.
* No per-action model override `enabled` global toggle here — LLM model is configured elsewhere (AI-3).
- Availability logic is not part of the model: template existence depends on the project's active type variant, so resolution feature toggle belongs to AI-100.
- Zeitwerk inflection `"ai" => "AI"` added (matches the existing AS acronym).
admin UI/settings work package.
## Out of scope
* Admin UI (AI-98) · transform (separate work package)
* Prompt assembly/resolution against LLM requests (transform API (AI-100) · per-project work package)
* Per-project scoping · upgrade-refresh of shipped defaults
actions
##
##
Persistence
actions
installations, never touched afterwards (same semantics as statuses/types).
Implemented
## Acceptance criteria
Model `AI::TextTransformAction` (table `ai_text_transform_actions`)
-
* `builtin_key` (string, nullable, unique) — set for seeded actions, `NULL` for custom ones
*
-
*
- `usage_scope` (string enum,
-
*
-
*
- `injects_type_template`
* `model` (string, nullable) — optional LLM model override; `NULL` = default from LLM settings
* type restriction: `applies_to_all_types`
Type restriction via
targeting types — variants
* Validations: label presence; prompt presence for custom actions (builtin actions may be `NULL`); temperature/model values
`specific_work_package_types`.
* An `available_for_type?(type)` query method encapsulates: `active` AND type restriction AND, for the `sort_into_template` builtin, "type has a non-blank default description".
*
* Upgrade semantics verified by specs:
* Re-running
touches existing rows (user edits, ordering, active flags survive).
* A builtin
Labels translatable (`t_` mechanism), prompts untranslated. Seeding
* Deleting a builtin row
record exists (verified by specs); seeded
deletable. Prompt
* Shipped default prompt
AI-97); texts are working drafts, finalization is AI-97.
- `builtin_key` / NULL-prompt
* Translatable labels via the `t_`\-prefix mechanism (`Source::Translate`, crowdin pipeline).
* The `NULL`\-prompt
-
* Type restriction join table only consulted when `applies_to_all_types` is `false`.
*
- Availability logic is not part of the model: template existence depends on the project's active type variant, so resolution
- Zeitwerk inflection `"ai" => "AI"` added (matches the existing AS acronym).
* Prompt assembly/resolution against LLM requests (transform
* Per-project