Task Breakdown — RFC Phase 2: AI-Assisted Refinement (refine proxy + Refine rail)
Source RFC: phase-2-ai-assisted-refinement.md
Slicing: Horizontal (Phase 1 UI mocked → Phase 2 API integration) · Blocked tasks: included inline · Repos: chatbot (BE, Rails/Grape) · chatbot-fe (FE, Nuxt 3) · qontak-designer (design prototype, read-only)
File paths + line numbers below are grounded against the real working trees (
../chatbot,../chatbot-fe,../qontak-designer) as of 2026-07-08. Two RFC corrections are already applied here: BE shared-scope paths (REV-2 —helpers//middlewares/live atapp/api/frontend_service/, not under theai_agent/subtree) and re-grounded line numbers (REV-1 —AiAgentEditor.vueis 5,529 lines, prototype[id].vueis 9,475 lines).
Effort Summary
| Phase / Area | FE days | BE days | QA days | Total |
|---|---|---|---|---|
| Phase 1 — UI (mocked) | 10 | — | 2.5 | 12.5 |
| Phase 2 — API integration | 2 | 8 | 2 | 12 |
| Grand total | 12 | 8 | 4.5 | 24.5 |
Confidence: medium. Key assumptions / unknowns that could move this: (1) REV-6 field-map — the RFC never tabulates the
updated_capability_pack(profile/capabilities/routing) → flatAgentDetailConfig(name/goalsText/toneOfVoice/guidances/guardrails/capabilities) mapping; recon confirms the form model is flatter than the pack, so theuseRefineAgentdiffer +applyPendingDataport (Tasks 1.1/1.3) carry the biggest risk. (2) OQ-1a upstream deployment/exposure is unverified — blocks only the real-upstream staging pass (Task 2.7), not chunks 1–8. (3) OQ-11/OQ-12 multi-turn context shape + no OpenAPI spec — BE built against companion-doc prose.
Phase 1 — UI (APIs mocked)
Components touched (each appears in exactly one task): useRefineAgent.ts (1.1) · RefinePanel.vue + RefineOptionCard.vue (1.2) · AiAgentEditor.vue (1.3) · analytics/i18n wiring across the above (1.4).
Task 1.1: [FE] useRefineAgent composable — thread state, history cap, pack differ (REFINE-S01, S03)
A user can carry a multi-turn refine conversation whose state (turns, options, streaming) is owned client-side, with proposed changes rendered as readable per-field diffs.
Status: ✅ Actionable — built against a mocked refine() service; real endpoint wired in Task 2.6.
Design reference: n/a — Figma pending (OQ-5); canonical design SoT: qontak-designer prototype app/pages/bot-automation/ai-agents/[id].vue — interfaces RefineMessage:4073 / RefineOption:4063 / ProposedChange:4039 / PendingData:4048; streamRefineText:4643; buildRefineOptions:4865 · DS: Mekari Pixel · Design QA: Wulan Febyazzahra.
What to build
A composable (mirroring useGenerateAgent.ts) owning refineMessages[], refineIsGenerating single-flight, a 10-turn chat_history cap before send, and a pure differ that turns the current form model + an option's mapped updated_capability_pack into ProposedChange[]. Includes the capability_pack → AgentDetailConfig field/label map (REV-6).
Implementation Plan
| Action | File | What changes |
|---|---|---|
| create | modules/bot-automation/composables/useRefineAgent.ts | thread refs, single-flight, history cap (10), error extraction, differ + field-map |
| create | tests/unit/modules/bot-automation/composables/useRefineAgent.spec.ts | cap slices to last 10; differ add/update/remove rows for tone scalar, capability-by-id, routing-by-id; abort on dispose |
Implementation steps
- Explore — Open
modules/bot-automation/composables/useGenerateAgent.ts(isGenerating:61, service call:74, error extraction?.response?._data?.error?.messages:79–81) andmodules/bot-automation/composables/useAgentStore.ts(AgentDetailConfig:79–86;GuidanceData:25,CapabilityData:56). Read the prototypebuildRefineOptions:4865 /applyPendingData:4705 to see the intendedProposedChange/PendingDatashapes. - Write failing tests (red) — Create the spec; cover history-cap (12 turns → last 10 sent), differ correctness per pack section, and abort-on-dispose.
pnpm test→ confirm red. - Scaffold — Refs (
refineMessages,refineIsGenerating), arefine(agentId, userMessage)that appends the user turn, truncates history, and calls a mockedrefine()returning a canned{reply, options[]}. - Wire the differ + field-map — Implement a deterministic diff over the three pack sections (profile scalars, capabilities array by
id, routing array byid) →ProposedChange[]; author thecapability_pack→AgentDetailConfigfield/label map (REV-6 — the single biggest unknown; mapprofile.tone→toneOfVoice,profile.goals→goalsText, capabilities/guardrails arrays by id). DerivependingDatain the flat form-model shapeapplyPendingDatawill consume. - Implement behavior — Single-flight guard, error extraction,
AbortControllerwiring (aborted by the host on unmount — Task 1.3). - Go green —
pnpm testuntil green. - Quality gate —
pnpm lint.
Acceptance criteria
- A 12-turn thread sends exactly the last 10 turns (REFINE-S03/AC-2).
- Differ emits correct add/update/remove
ProposedChangerows for a profile scalar, a capability-by-id, and a routing-by-id change (ADR-7). - The field-map produces
pendingDatainAgentDetailConfigshape (REV-6). - Reload → fresh thread (no persistence) is structurally guaranteed (in-memory refs only) (REFINE-S03/AC-3).
- A failed turn is retryable with prior turns intact (REFINE-S03/ERR-1).
Test strategy
Vitest over the composable in isolation. Key mock: a fake refine() returning fixtures. Key assertions: exact chat_history slice length/content, and ProposedChange[]/pendingData output for hand-built pack fixtures.
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | 3 |
| Backend | — |
| QA | 0.5 |
| Total | 3.5 |
Assumptions: reuses the
useGenerateAgentpattern; +1 day carried for the REV-6 field-map since the RFC tabulates no explicit map and the form model is flatter than the pack. If the map turns out 1:1 simpler, drops ~1 day.
Run to verify
pnpm test -- useRefineAgent && pnpm lint
Depends on
- None (mocked service). Feeds Task 1.2, 1.3; real service arrives in Task 2.6.
Task 1.2: [FE] RefinePanel + RefineOptionCard components (REFINE-S01, S04)
A user sees a chat panel with an empty-state + suggestion chips, sends a message (or an audit prompt), watches a streamed reply, and reviews 1–3 option cards with per-field diffs and a Recommended flag.
Status: ✅ Actionable — renders useRefineAgent state; no direct API dependency.
Design reference: n/a — Figma pending (OQ-5); canonical: prototype [id].vue — empty state + chips:1806–1841, message thread/streaming:~1850–1943, option card:~1882–1933, .recommended-border-anim:9422 · DS: Mekari Pixel (MpText/MpButton/MpBadge) · Design QA: Wulan Febyazzahra.
What to build
Two new components under modules/bot-automation/components/refine/: RefinePanel.vue (empty state, 4 verbatim chips, input with Enter-to-send, streamed thread, warnings list, state matrix) and RefineOptionCard.vue (Recommended banner, label/description, diff rows, Accept button / status badge).
Implementation Plan
| Action | File | What changes |
|---|---|---|
| create | modules/bot-automation/components/refine/RefinePanel.vue | empty/loading/error/no-change/success states; chips; role="log" aria-live thread; emits accept-option |
| create | modules/bot-automation/components/refine/RefineOptionCard.vue | option card (diff rows + Accept), emits accept(optionId) |
| create | tests/unit/modules/bot-automation/components/refine/RefinePanel.spec.ts | states + chip strings + Enter submit + no v-html |
| create | tests/unit/modules/bot-automation/components/refine/RefineOptionCard.spec.ts | Recommended banner, diff rows, Accept emit, status badge |
Implementation steps
- Explore — Read the prototype panel/card markup (
[id].vue:1806–1943) and an existing Pixel-based component undermodules/bot-automation/components/forMp*usage + module style conventions. - Write failing tests (red) — Panel: empty state renders the 4 verbatim chips ("The refund answer is not correct, fix it" · "Add order tracking capability" · "Make it faster to escalate to a human agent" · "Make the tone more formal"); loading/error/no-change/success states; Enter submits. Card: Recommended banner when
isRecommended, diff rows fromchanges,acceptemit.pnpm test→ red. - Scaffold — Props per §2.A (
RefinePanelProps: agentId/engineVersion/currentPack;RefineOptionCardProps: option/index/disabled). Template shells. - Wire state — Bind to
useRefineAgentrefs; conditional rendering (loadingOptionsskeletons, warnings under reply); disable input whilerefineIsGenerating. - Implement behavior — Chip click → submit; streamed word opacity animation (client-side, respect
prefers-reduced-motion);.recommended-border-animCSS ported from prototype:9422; all LLM text via{{ }}interpolation — nov-html(security rule). - Go green —
pnpm testuntil green. - Quality gate —
pnpm lint.
Acceptance criteria
- Empty state shows heading + 4 verbatim suggestion chips (REFINE-S01, S04 chip is OQ-10b design fast-follow — not built here).
- Success renders reply + 1–3 option cards, exactly one Recommended flagged.
-
options: []→ "no actionable change" turn, no cards (REFINE-S01/AC-3, S04/AC-2). - Error turn renders fallback + Retry, prior turns intact (REFINE-S01/ERR-1..2).
- No
v-htmlin either component (XSS mitigation, §3 Security). -
role="log"+aria-live="polite"; chips/Accept are<button>(WCAG AA, §3.E).
Test strategy
Vitest + Vue Test Utils with a stubbed useRefineAgent. Key assertions: chip strings verbatim, per-state rendering, accept-option/accept emissions, absence of v-html.
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | 3 |
| Backend | — |
| QA | 0.5 |
| Total | 3.5 |
Assumptions: design is fully specced in the prototype (ported, not designed); streaming is client-side animation over a non-streamed response (no SSE this phase).
Run to verify
pnpm test -- refine/RefinePanel refine/RefineOptionCard && pnpm lint
Depends on
- Task 1.1 (
useRefineAgenttypes/state).
Task 1.3: [FE] AiAgentEditor.vue — two-tab rail, Refine gate, Accept-stages-into-form (REFINE-S01, S02, S01-NEG)
A user opens the Refine tab in the editor's right rail (only on autonomous agents with the flag ON), accepts an option, and sees the change staged into the form with the field highlighted and the correct tab focused — persisted only by the existing Save.
Status: ✅ Actionable — staging is pure client-side state; Save reuses the existing PATCH path (no BE change).
Design reference: n/a — Figma pending (OQ-5); canonical: prototype [id].vue — rightRailTab:4084, rail tabs:1762–1792, acceptRefineOption:4732, applyPendingData:4705, markChanged:4687, aiHighlightClass:9178 · DS: Mekari Pixel · Design QA: Wulan Febyazzahra.
What to build
Modify the existing 5,529-line editor: generalise showPreview into a rightRailTab: "preview" | "refine" two-tab rail (Preview markup preserved as the preview pane); add aiChangedFields reactive map + an apply handler that ports applyPendingData into the real flat AgentDetailConfig form model, sets highlight flags, and switches activeTab; gate the Refine tab on engineVersion === 2 && rolloutPrefEnabled('rollout','ai_agent_refine'). Abort the in-flight refine request on unmount/rail-close.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| extend | modules/bot-automation/components/AiAgentEditor.vue | showPreview(:3081) → rightRailTab; mount RefinePanel in the rail (Preview aside :1668); aiChangedFields + apply handler writing pendingData into form model; activeTab(:3076) switch (mutation pattern :3582/:3611/:3738); flag+engine gate |
| extend | tests/unit/modules/bot-automation/components/AiAgentEditor.spec.ts | rail tab toggle; gate (absent when flag OFF / legacy agent); Accept mutates form + highlights + switches tab, no HTTP |
Implementation steps
- Explore — Read the Preview rail (
<aside v-if="showPreview">:1668),showPreview:3081,tabs:3031,activeTab:3076 + mutation sites (:3582/:3611/:3738/:3781/:3788),handleSave:3778. Read the flag pattern inmodules/bot-automation/composables/useKnowledgeSourceTypeAvailability.ts(rolloutPrefEnabled:75, key${groupCode}_${code}:76). Read prototypeacceptRefineOption:4732 +applyPendingData:4705. - Write failing tests (red) — Extend the existing editor spec: tab absent when flag OFF or
engineVersion !== 2(REFINE-S01-NEG/NEG-1); rail toggles Preview↔Refine; onaccept-option, form model mutates,aiChangedFieldsset,activeTabswitches to the owning tab, and zero HTTP calls fire (REFINE-S02/AC-1, NEG-2).pnpm test→ red. - Scaffold — Replace
showPreviewboolean withrightRailTab; wrap the existing Preview markup as thepreviewpane; add rail tab buttons (role="tab",aria-selected). - Wire state — Mount
<RefinePanel>(props: agentId, engineVersion, currentPack fromgetConfig); addaiChangedFields = reactive({}). - Implement behavior — Apply handler: write
pendingDataintoAgentDetailConfig(name/goalsText/toneOfVoice/guidances/guardrails/capabilities), set highlight flags,activeTab.value = n(0 Profile / 1 Capabilities / 2 Routing), dismiss sibling options; move focus to first highlighted field via the existingnextTick+focus pattern (§3.E). Abort controller on unmount. - Go green —
pnpm testuntil green. - Quality gate —
pnpm lint && pnpm build(build matters — large shared file).
Acceptance criteria
- Refine tab renders only when
engineVersion === 2and flag ON; hidden otherwise (REFINE-S01-NEG/NEG-1). - Preview rail behavior unchanged as the default
previewpane. - Accept stages
pendingDatainto the form, sets highlights, switches to the owning tab, dismisses siblings, and issues no HTTP (REFINE-S02/AC-1, S01-NEG/NEG-2). - Save is the existing
PATCHpath — unchanged (REFINE-S02/AC-2);pnpm buildpasses.
Test strategy
Extend the existing AiAgentEditor.spec.ts. Key mock: stubbed preferencesStore().lists + a RefineOption fixture. Key assertion: after accept-option, form state + aiChangedFields + activeTab change with a spy proving no service call.
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | 3 |
| Backend | — |
| QA | 1 |
| Total | 4 |
Assumptions: additive, flag-gated changes to a large shared file; QA raised to 1 day because this file is shared with all agent editing (regression surface). Depends on the REV-6 field-map from Task 1.1 for
applyPendingData.
Run to verify
pnpm test -- AiAgentEditor && pnpm lint && pnpm build
Depends on
- Task 1.1 (field-map /
pendingDatashape), Task 1.2 (RefinePanelmount).
Task 1.4: [FE] Refine analytics events + i18n keys (REFINE-S01, S02)
Product can see the refine funnel (requested → succeeded → accepted → applied → discarded) in Mixpanel, and users see localized, content-free error/empty strings.
Status: ✅ Actionable — events fire on the mocked flow; content-scrub rule applies regardless of real vs mock.
Design reference: n/a — copy strings only (Detail 3.C i18n keys); DS: Mekari Pixel.
What to build
Wire trackEvent calls for refine_requested | succeeded | failed | accepted | applied | discarded with count/id-only properties (never message text), and add the FE locale keys from Detail 3.C.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| extend | modules/bot-automation/composables/useRefineAgent.ts | fire refine_requested/succeeded/failed (counts only) |
| extend | modules/bot-automation/components/AiAgentEditor.vue | fire refine_accepted/applied/discarded |
| extend | <module locale files> | keys bot_automation.refine.error_generic/error_invalid/error_forbidden |
| create | tests/unit/modules/bot-automation/composables/useRefineAgent.analytics.spec.ts | events fire with no message-content properties |
Implementation steps
- Explore — Read
common/utils/tracking.tstrackEvent(name, properties?, jimoTrack?):51 and an existingtrackEventcaller in the module for the property convention. - Write failing tests (red) — Assert each event fires at its trigger and that no property carries
user_message/content(§3 PII rule).pnpm test→ red. - Implement — Add the six
trackEventcalls at their trigger points; add locale keys. - Go green + gate —
pnpm test && pnpm lint.
Acceptance criteria
- Six events fire at the correct triggers with count/id-only properties (no message text).
- Locale keys resolve for the three refine error surfaces (Detail 3.C).
Test strategy
Spy on trackEvent; assert names + that serialized properties contain no pasted content.
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | 1 |
| Backend | — |
| QA | 0.5 |
| Total | 1.5 |
Assumptions:
trackEventcontext auto-adds user/company ids; i18n namespace already exists.
Run to verify
pnpm test -- useRefineAgent.analytics && pnpm lint
Depends on
- Tasks 1.1, 1.3 (event trigger sites exist).
Phase 2 — API Integration
Task 2.1: [BE] Extract SkillPackBuilder from SyncToAiService (behavior-preserving) — RFC Chunk 1
Enables refine to serialise an agent's
capability_pack → skill_packwithout side effects, while Phase-1 save behavior stays byte-identical.
Status: ✅ Actionable — pure refactor of existing code; the critical-path unlock for all refine BE work.
What to build
Extract the pure shaping logic (build_skill_pack, build_skill, build_skill_actions, build_completion, build_routing_rules) into Mappers::SkillPackBuilder, parameterised by an injected vector-store resolver. SyncToAiService passes a resolver wrapping its existing stateful resolve_capability_vector_store; refine will pass a read-only ->(capability) { capability['vector_store'] }. Lock Phase-1 behavior with a byte-identical request-body regression spec.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| create | app/api/frontend_service/v2/ai_agent/use_cases/mappers/skill_pack_builder.rb | SkillPackBuilder.new(ai_agent:, vector_store_resolver:).call — pure shaping |
| create | spec/api/frontend_service/v2/ai_agent/use_cases/mappers/skill_pack_builder_spec.rb | builder emits correct pack; read-only resolver returns persisted vector_store with no vector-DB creation (mock-asserted) |
| extend | app/api/frontend_service/v2/ai_agent/repositories/sync_to_ai_service.rb | delegate build_skill_pack(:93)/build_skill(:121)/build_skill_actions(:417)/build_completion(:502)/build_routing_rules(:516) to the builder with a stateful resolver; keep resolve_capability_vector_store(:147) as the resolver body |
| extend | spec/api/frontend_service/v2/ai_agent/repositories/sync_to_ai_service_spec.rb | full-fixture request-body snapshot proving byte-identical body pre/post extraction; vector create/reuse/purge flows unchanged |
Implementation steps
- Explore — Read
repositories/sync_to_ai_service.rb: the@mode == :update ? …(:39) dispatch,build_skill_pack(:93),build_skill(:121) (note siblingbuild_skills:117), and the statefulresolve_capability_vector_store(:147). Read the existingspec/.../repositories/sync_to_ai_service_spec.rbfor fixture conventions. - Write regression test first (red→green pin) — Add a full-fixture snapshot of the current
PUT /ai-agentrequest body to the existing sync spec; runbundle exec rspec spec/api/frontend_service/v2/ai_agent/repositories/sync_to_ai_service_spec.rbbefore refactoring to capture the golden body. - Scaffold builder — Create
SkillPackBuilderwithinitialize(ai_agent:, vector_store_resolver:); move the shaping method bodies in unchanged, replacing inlineresolve_capability_vector_storecalls withvector_store_resolver.call(capability). - Rewire sync — In
sync_to_ai_service.rb, delegate shaping toSkillPackBuilder.new(ai_agent:, vector_store_resolver: ->(cap){ resolve_capability_vector_store(cap) }).call; keep the stateful resolver +persist_vector_storesin the service. - Builder spec — Read-only resolver returns the persisted
vector_store(or nil) and asserts (via mock) nocreate_vector_dbcall. - Go green —
bundle exec rspec spec/api/frontend_service/v2/ai_agent/— all existing sync specs green + snapshot byte-identical. - Quality gate —
bundle exec rubocop.
Acceptance criteria
-
SyncToAiServiceemits a byte-identical request body for a fixture agent before/after extraction (Success Criterion 3). - Vector-store create/reuse/purge flows on the save path are unchanged.
- Builder with a read-only resolver returns the persisted
vector_storeand creates no vector DB (mock-asserted).
Test strategy
RSpec. Golden-snapshot the sync request body; assert builder purity via a mocked resolver that fails the test if any vector-creation method is invoked.
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | — |
| Backend | 3 |
| QA | 0 |
| Total | 3 |
Assumptions: internal refactor, no user-facing behavior (QA 0); risk sits in the shared Phase-1 file — the snapshot spec is the guard. This is the true critical path: start immediately, land alone.
Run to verify
bundle exec rspec spec/api/frontend_service/v2/ai_agent/ && bundle exec rubocop
Depends on
- None. Unblocks Tasks 2.4, 2.5.
Task 2.2: [BE] Rollout flag row + predicate ai_agent_refine — RFC Chunk 2
Ops can turn Refine on/off per workspace with no deploy; the endpoint and FE tab both gate on this one flag.
Status: ✅ Actionable.
What to build
Provision the system_preferences row {group_code: 'rollout', code: 'ai_agent_refine', enabled: false} (console/seed per existing practice — OQ-8) and expose a flag predicate the controller can call, following the system_preference.rb pattern.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| extend | app/models/system_preference.rb | ai_agent_refine rollout predicate (pattern at :42/:48) |
| create | seed/ops note | document row provisioning per environment (OQ-8) |
| create | spec/models/system_preference_spec.rb (flag case) | predicate true only when row enabled; absent → false |
Implementation steps
- Explore — Read
app/models/system_preference.rbrolloutfind_by(group_code: 'rollout', enabled: true):42/:48. - Write failing test (red) — predicate returns true only for an enabled row; default/absent → false.
- Implement — Add the predicate; write the seeding note (confirm console-vs-seed convention with BE — OQ-8).
- Go green + gate —
bundle exec rspec+rubocop.
Acceptance criteria
- Predicate true only when the row exists and
enabled: true; absent → false. - Seeding convention documented per environment (OQ-8).
Test strategy
RSpec model spec toggling the row.
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | — |
| Backend | 0.5 |
| QA | 0 |
| Total | 0.5 |
Assumptions: no migration (data row only); mirrors existing rollout flags.
Run to verify
bundle exec rspec spec/models/system_preference_spec.rb && bundle exec rubocop
Depends on
- None. Consumed by Task 2.5.
Task 2.3: [BE] Upstream client method refine_skill_pack — RFC Chunk 3
The BE can call the Data/ML
refine-skill-packendpoint with drafter-parity timeouts.
Status: ⚠️ Partially blocked — code is actionable against the documented contract now; the live endpoint deployment/exposure is unverified (OQ-1a) — real calls succeed only after Data/ML confirms staging/prod exposure (Task 2.7).
What to build
Add refine_skill_pack(body:) to the AI-service client, POSTing to /qontak-ai-noncore-mrag/api/ai-agent/refine-skill-pack with open_timeout: 60, read_timeout: 60, no retry — mirroring draft_skill_pack.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| extend | lib/ai_service/ai_agent.rb | refine_skill_pack(body:) mirroring draft_skill_pack(:49–52); new path constant |
| create | spec/lib/ai_service/ai_agent_spec.rb (refine case) | POST to exact path with body + 60s timeouts (mock Http) |
Implementation steps
- Explore — Read
lib/ai_service/ai_agent.rb:draft_skill_pack:49 (path const :50,@http.call(... open_timeout: 60, read_timeout: 60):52) andupdate_ai_agentPUT:43. - Write failing test (red) — Stub
@http; assert POST to the exact path with body + timeouts. - Implement — Add the method + path constant.
- Go green + gate —
bundle exec rspec+rubocop.
Acceptance criteria
-
refine_skill_packissues a POST to/qontak-ai-noncore-mrag/api/ai-agent/refine-skill-packwith the body and 60s open/read timeouts (mock-asserted); no automatic retry (ADR-5).
Test strategy
Client spec mocking the Http client; assert method/URL/body/timeouts.
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | — |
| Backend | 0.5 |
| QA | 0 |
| Total | 0.5 |
Assumptions: drafter parity; no new auth/secret (reuses existing
Httpclient credentials).
Run to verify
bundle exec rspec spec/lib/ai_service/ai_agent_spec.rb && bundle exec rubocop
Depends on
- None (unit-mockable). Live use gated by OQ-1a → Task 2.7.
Task 2.4: [BE] Repositories::Refine — assemble upstream request + call client — RFC Chunk 4
Given a serialised pack and tools, the BE builds the exact as-built upstream request and proxies the call.
Status: ✅ Actionable against the documented §2.4 contract (trace omitted v1 per OQ-2 resolved).
What to build
Create Repositories::Refine assembling {company_id, current_skill_pack, user_message, chat_history, available_tools} — current_skill_pack from the Chunk-1 SkillPackBuilder (read-only resolver), available_tools reusing the generate.rb query formatted with type: 'qontak_function_call', then calling refine_skill_pack.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| create | app/api/frontend_service/v2/ai_agent/repositories/refine.rb | request-body assembly + client call (mirrors repositories/generate.rb) |
| create | spec/api/frontend_service/v2/ai_agent/repositories/refine_spec.rb | body matches §2.4 upstream schema verbatim for a fixture agent |
Implementation steps
- Explore — Read
repositories/generate.rb:request_body:26,available_tools:in body:29,available_toolsqueryAiAgentTool … .where.not(tool_id: nil):52–53. Confirm theSkillPackBuilderAPI from Task 2.1. - Write failing test (red) — Assert the assembled body equals the §2.4 upstream schema for a fixture (pack from the read-only builder;
traceabsent). - Scaffold + implement — Build the body; gather tools; call
AiService::AiAgent#refine_skill_pack; pass the result through. - Go green + gate —
bundle exec rspec+rubocop.
Acceptance criteria
- Request body matches the §2.4 upstream schema verbatim (
company_id,current_skill_pack,user_message,chat_history,available_toolswithtype: 'qontak_function_call');traceomitted (OQ-2). - Uses
SkillPackBuilderwith a read-only resolver (no vector-store side effects).
Test strategy
RSpec with a mocked client; assert the exact outbound body for a fixture agent.
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | — |
| Backend | 1 |
| QA | 0 |
| Total | 1 |
Assumptions: reuses
generate.rbtools query and the Chunk-1 builder.
Run to verify
bundle exec rspec spec/api/frontend_service/v2/ai_agent/repositories/refine_spec.rb && bundle exec rubocop
Depends on
- Task 2.1 (
SkillPackBuilder), Task 2.3 (client method).
Task 2.5: [BE] UseCases::RefineAiAgent + route + response model — RFC Chunk 5
POST /v2/ai_agents/:id/refinereturnsreply+options[]for an autonomous, flagged agent — writing nothing — and rejects legacy agents (422) and unauthorized callers (403).
Status: ✅ Actionable against the documented contract (stub/fixture upstream). Real-upstream pass is Task 2.7 (OQ-1a).
What to build
Create the use case (dry-schema contract: id, org/company ids, user_message 1..4000, chat_history ≤10, optional trace; guards: flag → 403, engine_version != 2 → 422 not_autonomous_agent), map each upstream option via SkillPackMapper, wrap the single upstream proposal into options: [one] (empty patches → options: []), build models/refine_response.rb, and add the Grape route with set_role(%w[owner supervisor admin]).
Implementation Plan
| Action | File | What changes |
|---|---|---|
| create | app/api/frontend_service/v2/ai_agent/use_cases/refine_ai_agent.rb | contract + guards + Repositories::Refine + SkillPackMapper per option + wrap single→options[] |
| create | app/api/frontend_service/v2/ai_agent/models/refine_response.rb | §2.4 response shape |
| extend | app/api/frontend_service/v2/ai_agent/ai_agents_controller.rb | post '/:id/refine' with set_role(%w[owner supervisor admin]) + flag gate (mirror post '/generate':361/:362; patch '/:id':292) |
| create | spec/api/frontend_service/v2/ai_agent/refine_ai_agent_spec.rb | 200/400/403/404/422 matrix; DB row unchanged; warnings/options passthrough; upstream 5xx → 422 + error log |
Implementation steps
- Explore — Read
use_cases/generate.rb(class:7,contract do:8,SkillPackMapper.call:46) as the skeleton;ai_agents_controller.rbroute+set_roleshape (:361/:362, :292/:293);skill_pack_mapper.rbself.call(skill_pack, organization_id: nil, agent_name: nil):32 (defaulted kwargs). Note the corrected shared-scope paths:helpers/authorization_helpers.rbset_role:6 andmiddlewares/ownership.rb403:7 live atapp/api/frontend_service/{helpers,middlewares}/(RFC REV-2 fix), not under theai_agent/subtree. - Write failing tests (red) — Full status matrix: 200 + options for autonomous+flag-ON; 403 flag OFF / bad role; 404 wrong org; 422 legacy (
not_autonomous_agent); 400 bad params (user_messagelength, >10 history). Assertparameters+updated_atunchanged after the call (Success Criterion 1). Stub upstream 5xx → 422 +V2 RefineAiAgent failedlog.bundle exec rspec spec/api/frontend_service/v2/ai_agent/refine_ai_agent_spec.rb→ red. - Scaffold — Use case contract + guards; response model.
- Wire — Call
Repositories::Refine;SkillPackMapperper option; wrap single upstream proposal intooptions: [one](emptypatches→[]); build the §2.4 response. - Route — Add
post '/:id/refine'withset_role+ the Chunk-2 flag gate; render viaDry::Matcher::ResultMatcher. - Go green — until the full matrix passes.
- Quality gate —
bundle exec rubocop && bundle exec brakeman.
Acceptance criteria
- Status matrix green: 200/400/403/404/422 per §2.4 / §3.B.
-
parameters+updated_atunchanged after any refine call (Success Criterion 1; REFINE-S01-NEG/NEG-2). - Legacy agent (
engine_version != 2) → 422not_autonomous_agent(REFINE-S01-NEG/NEG-1). -
warnings[]/options[]passthrough; emptypatches→options: [](REFINE-S01/AC-3). - Upstream 5xx/timeout → 422 +
V2 RefineAiAgent failedlog (REFINE-S01/ERR-1);user_message/chat_historynever logged (§3 PII).
Test strategy
RSpec request/use-case specs with a stubbed Repositories::Refine/client. Key assertion: side-effect-free (DB unchanged) + full status matrix + log-scrub.
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | — |
| Backend | 3 |
| QA | 0.5 |
| Total | 3.5 |
Assumptions: reuses
generateskeleton +SkillPackMapperverbatim; the single→options[] wrap and no-write assertions are the net-new logic.
Run to verify
bundle exec rspec spec/api/frontend_service/v2/ai_agent/refine_ai_agent_spec.rb && bundle exec rubocop && bundle exec brakeman
Depends on
- Task 2.1 (builder), 2.2 (flag), 2.3 (client), 2.4 (
Repositories::Refine).
Task 2.6: [FE] Service + endpoint wiring — swap mock for real refine() — RFC Chunk 6 (service half)
The Refine panel calls the real BE endpoint instead of the Phase-1 mock.
Status: ✅ Actionable once Task 2.5's route is live in a dev/stub environment.
What to build
Add v2.ai_agents.refine to endpoint.ts, add refine() to bot-automation-agents.ts returning { fetch, controller }, and swap the mocked call inside useRefineAgent.ts for the real service.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| extend | common/services/main/endpoint.ts | refine: "/v2/ai_agents/:id:/refine" under v2.ai_agents (block :269) |
| extend | common/services/main/v2/bot-automation-agents.ts | refine() (mirror generate():250 / update():232; { fetch, controller } + AbortController) |
| extend | modules/bot-automation/composables/useRefineAgent.ts | replace mock with botAutomationAgentsService.refine(...) |
| extend | tests/unit/modules/bot-automation/composables/useRefineAgent.spec.ts | assert real service called with :id-substituted URL + payload |
Implementation steps
- Explore — Read
bot-automation-agents.tsupdate():232 (endpoint.v2.ai_agents.update.replace(":id:", id):242, PATCH:243, return{ fetch, controller }) andgenerate():250;endpoint.tsv2 block:269–275. - Write failing test (red) — Assert
refine()POSTs to the:id-substituted/v2/ai_agents/:id/refinewith the payload, returns{ fetch, controller }. - Implement — Add endpoint entry + service method; swap the mock in
useRefineAgent. - Go green + gate —
pnpm test && pnpm lint.
Acceptance criteria
-
refine()POSTs to/v2/ai_agents/:id/refinewith{user_message, chat_history}, returns{ fetch, controller }. -
useRefineAgentuses the real service; abort wired to the returned controller.
Test strategy
Vitest asserting URL substitution + payload + abort wiring.
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | 1 |
| Backend | — |
| QA | 0.5 |
| Total | 1.5 |
Assumptions: pure wiring; the composable/differ already exist from Task 1.1.
Run to verify
pnpm test -- useRefineAgent && pnpm lint
Depends on
- Task 1.1 (composable), Task 2.5 (endpoint contract).
Task 2.7: [FE+BE] E2E + staging verification (incl. config-audit scenario) — RFC Chunk 9
The full flow works end-to-end (thread → options → accept → staged form → save) against a mocked BE, and — once upstream is live — against the real refiner including a complaint-free config-audit prompt.
Status: ⚠️ Partially blocked — the Playwright E2E against a mocked BE is actionable now; the real-upstream staging pass (incl. the REFINE-S04 config-audit scenario) is 🚫 blocked on OQ-1a (upstream deployment/exposure) and depends on OQ-1g (audit-prompt tuning) + OQ-11 (multi-turn context shape).
What to build
A Playwright spec driving the refine happy path against a mocked BE, plus a staging checklist: refine turn ≤ 10s p95, Accept→Save applies the staged pack, a PaperTrail version is written; and the seeded two-flaw config-audit scenario (Detail 4.C).
Implementation Plan
| Action | File | What changes |
|---|---|---|
| create | tests/e2e/.../refine.spec.ts (Playwright) | thread → options → accept → form staged → save payload contains staged pack (mocked BE) |
| create | staging checklist doc | latency, save-applies-pack, PaperTrail version; [blocked] real-upstream + audit scenario (OQ-1a/1g/11) |
Implementation steps
- Explore — Read an existing Playwright spec under
tests/e2e/for the mocked-BE harness pattern. - Write E2E (mocked) — Drive open Refine tab → send → render options → Accept → assert form staged + tab switched → Save → assert
PATCHpayload carries the staged pack.pnpm test:e2e. - Staging (mocked/stub upstream) — Run the checklist against the ML stub; capture latency.
- [Blocked] Real-upstream pass — Once OQ-1a confirms exposure: seed a two-flaw agent (routing rule → missing capability id; action gated on an unreachable milestone), send "Review my configuration and find potential issues"; assert
replynames ≥1 flaw, ≥1 option'spatchesfixes it, applied+saved passesCapabilityRefPresence(Success Criterion 6, REFINE-S04).
Acceptance criteria
- E2E green against mocked BE: thread → options → accept → staged → save payload contains staged pack.
- Staging (stub): refine turn ≤ 10s p95; Save applies staged pack; PaperTrail version +1.
- (pending OQ-1a) Real-upstream contract holds on the wire.
- (pending OQ-1a/1g) Config-audit scenario: reply names ≥1 seeded flaw + ≥1 fixing option; applied fix passes ref validation (REFINE-S04).
Test strategy
Playwright against a mocked BE for the deterministic path; manual staging checklist for latency + real-upstream + audit (the last three gated on Data/ML).
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | 1 |
| Backend | — |
| QA | 1 |
| Total | 2 |
Assumptions: E2E/staging harness exists; the real-upstream + audit portions are excluded from this estimate's actionable scope (blocked — see below). QA carries the staging checklist.
Run to verify
pnpm test:e2e -- refine
Depends on
- All Phase-1 tasks + Tasks 2.5, 2.6. Real-upstream portion blocked on OQ-1a (Data/ML).
Ordering rationale
- Task 2.1 (
SkillPackBuilderextraction) is the true critical path — start it first, in parallel with Phase 1. It touches the one shared Phase-1 file, is regression-locked, and unblocks the entire refine BE chain (2.4, 2.5). Despite being "Phase 2" in the horizontal layout, it has no dependency on any UI work. - Phase 1 (FE) and Phase 2 BE (2.1–2.5) can run fully in parallel across two people — Phase 1 is built against a mocked service, so no FE task waits on the BE. They converge only at Task 2.6 (swap mock for real) and Task 2.7 (E2E).
- Within FE: 1.1 (composable/differ) → 1.2 (components) → 1.3 (editor integration) → 1.4 (analytics/i18n). The REV-6 field-map in 1.1 is the highest-risk item — resolve it early; it feeds 1.3's
applyPendingData. - Within BE: 2.1 → {2.2, 2.3 in parallel} → 2.4 → 2.5. 2.2/2.3 are tiny and independent.
- Nothing external blocks build. The entire feature (chunks 1–8 / Tasks 1.1–2.6) is executable today against the documented §2.4 contract. Only Task 2.7's real-upstream + config-audit pass waits on Data/ML (OQ-1a) — push on that verification in parallel so it's ready when the code lands (Data/ML needed-by: 2026-07-15).
Skipped / blocked items (full-picture mode)
| Item | Status | Unblocking condition |
|---|---|---|
| Task 2.7 — real-upstream staging pass | 🚫 Blocked | OQ-1a: Data/ML confirm refine-skill-pack is deployed + exposed via the noncore-mrag gateway path in staging/prod |
| Task 2.7 — REFINE-S04 config-audit scenario (real upstream) | 🚫 Blocked | OQ-1g (complaint-free audit prompts tuned/tested) + OQ-1a; also OQ-11 (multi-turn context shape) |
| REFINE-S04 5th "Check my configuration" suggestion chip | 🚫 Blocked (design) | OQ-10b: Wulan adds the chip to the prototype (change-request, not built by us) — engineering cost zero, same flow |
| Multi-option per turn (design shows 1–3; as-built returns 1) | Deferred | OQ-1e: upstream fast-follow — FE options[] already absorbs 1..N with zero contract change (ADR-6) |
Option label/description from upstream (v1 uses a fixed label) | Deferred | OQ-1b: Data/ML add the fields — cosmetic, not a gate |
trace request field | Out of scope (v1) | OQ-2 resolved — omitted entirely; workflow_state lives in the mekari-agent DB, not suppliable by chatbot BE |
Rate limiting on /refine | Pre-GA action | OQ-7: add per-org throttling before GA (parity with generate today) |
Stale-preview / concurrent-edit lock (lock_version) | Out of scope | OQ-6: FE updated_at freshness warning is the phase mitigation; real fix deferred |
| Server-side revert-from-UI (Draft versioning) | Deferred / at risk | OQ-13: old-engine, unshipped; needs Data/ML alignment for the new engine — PaperTrail per-agent revert is the phase-2 mechanism |
Note on REFINE-S02 (Accept & Save): intentionally has no standalone task. Client-side staging is covered by Task 1.3; persistence reuses the existing, unchanged
PATCH /v2/ai_agents/:idsave path (update_ai_agent.rb+ PaperTrail
SyncToAiServicerollback) — zero net-new build.