Skip to main content

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 at app/api/frontend_service/, not under the ai_agent/ subtree) and re-grounded line numbers (REV-1 — AiAgentEditor.vue is 5,529 lines, prototype [id].vue is 9,475 lines).

Effort Summary

Phase / AreaFE daysBE daysQA daysTotal
Phase 1 — UI (mocked)102.512.5
Phase 2 — API integration28212
Grand total1284.524.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) → flat AgentDetailConfig (name/goalsText/toneOfVoice/guidances/guardrails/capabilities) mapping; recon confirms the form model is flatter than the pack, so the useRefineAgent differ + applyPendingData port (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

ActionFileWhat changes
createmodules/bot-automation/composables/useRefineAgent.tsthread refs, single-flight, history cap (10), error extraction, differ + field-map
createtests/unit/modules/bot-automation/composables/useRefineAgent.spec.tscap slices to last 10; differ add/update/remove rows for tone scalar, capability-by-id, routing-by-id; abort on dispose

Implementation steps

  1. Explore — Open modules/bot-automation/composables/useGenerateAgent.ts (isGenerating:61, service call:74, error extraction ?.response?._data?.error?.messages:79–81) and modules/bot-automation/composables/useAgentStore.ts (AgentDetailConfig:79–86; GuidanceData:25, CapabilityData:56). Read the prototype buildRefineOptions:4865 / applyPendingData:4705 to see the intended ProposedChange/PendingData shapes.
  2. 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.
  3. Scaffold — Refs (refineMessages, refineIsGenerating), a refine(agentId, userMessage) that appends the user turn, truncates history, and calls a mocked refine() returning a canned {reply, options[]}.
  4. Wire the differ + field-map — Implement a deterministic diff over the three pack sections (profile scalars, capabilities array by id, routing array by id) → ProposedChange[]; author the capability_packAgentDetailConfig field/label map (REV-6 — the single biggest unknown; map profile.tonetoneOfVoice, profile.goalsgoalsText, capabilities/guardrails arrays by id). Derive pendingData in the flat form-model shape applyPendingData will consume.
  5. Implement behavior — Single-flight guard, error extraction, AbortController wiring (aborted by the host on unmount — Task 1.3).
  6. Go greenpnpm test until green.
  7. Quality gatepnpm lint.

Acceptance criteria

  • A 12-turn thread sends exactly the last 10 turns (REFINE-S03/AC-2).
  • Differ emits correct add/update/remove ProposedChange rows for a profile scalar, a capability-by-id, and a routing-by-id change (ADR-7).
  • The field-map produces pendingData in AgentDetailConfig shape (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

DisciplineDays
Frontend3
Backend
QA0.5
Total3.5

Assumptions: reuses the useGenerateAgent pattern; +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

ActionFileWhat changes
createmodules/bot-automation/components/refine/RefinePanel.vueempty/loading/error/no-change/success states; chips; role="log" aria-live thread; emits accept-option
createmodules/bot-automation/components/refine/RefineOptionCard.vueoption card (diff rows + Accept), emits accept(optionId)
createtests/unit/modules/bot-automation/components/refine/RefinePanel.spec.tsstates + chip strings + Enter submit + no v-html
createtests/unit/modules/bot-automation/components/refine/RefineOptionCard.spec.tsRecommended banner, diff rows, Accept emit, status badge

Implementation steps

  1. Explore — Read the prototype panel/card markup ([id].vue:1806–1943) and an existing Pixel-based component under modules/bot-automation/components/ for Mp* usage + module style conventions.
  2. 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 from changes, accept emit. pnpm test → red.
  3. Scaffold — Props per §2.A (RefinePanelProps: agentId/engineVersion/currentPack; RefineOptionCardProps: option/index/disabled). Template shells.
  4. Wire state — Bind to useRefineAgent refs; conditional rendering (loadingOptions skeletons, warnings under reply); disable input while refineIsGenerating.
  5. Implement behavior — Chip click → submit; streamed word opacity animation (client-side, respect prefers-reduced-motion); .recommended-border-anim CSS ported from prototype:9422; all LLM text via {{ }} interpolation — no v-html (security rule).
  6. Go greenpnpm test until green.
  7. Quality gatepnpm 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-html in 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

DisciplineDays
Frontend3
Backend
QA0.5
Total3.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 (useRefineAgent types/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].vuerightRailTab: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

ActionFileWhat changes
extendmodules/bot-automation/components/AiAgentEditor.vueshowPreview(: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
extendtests/unit/modules/bot-automation/components/AiAgentEditor.spec.tsrail tab toggle; gate (absent when flag OFF / legacy agent); Accept mutates form + highlights + switches tab, no HTTP

Implementation steps

  1. 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 in modules/bot-automation/composables/useKnowledgeSourceTypeAvailability.ts (rolloutPrefEnabled:75, key ${groupCode}_${code}:76). Read prototype acceptRefineOption:4732 + applyPendingData:4705.
  2. 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; on accept-option, form model mutates, aiChangedFields set, activeTab switches to the owning tab, and zero HTTP calls fire (REFINE-S02/AC-1, NEG-2). pnpm test → red.
  3. Scaffold — Replace showPreview boolean with rightRailTab; wrap the existing Preview markup as the preview pane; add rail tab buttons (role="tab", aria-selected).
  4. Wire state — Mount <RefinePanel> (props: agentId, engineVersion, currentPack from getConfig); add aiChangedFields = reactive({}).
  5. Implement behavior — Apply handler: write pendingData into AgentDetailConfig (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 existing nextTick+focus pattern (§3.E). Abort controller on unmount.
  6. Go greenpnpm test until green.
  7. Quality gatepnpm lint && pnpm build (build matters — large shared file).

Acceptance criteria

  • Refine tab renders only when engineVersion === 2 and flag ON; hidden otherwise (REFINE-S01-NEG/NEG-1).
  • Preview rail behavior unchanged as the default preview pane.
  • Accept stages pendingData into 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 PATCH path — unchanged (REFINE-S02/AC-2); pnpm build passes.

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

DisciplineDays
Frontend3
Backend
QA1
Total4

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 / pendingData shape), Task 1.2 (RefinePanel mount).

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

ActionFileWhat changes
extendmodules/bot-automation/composables/useRefineAgent.tsfire refine_requested/succeeded/failed (counts only)
extendmodules/bot-automation/components/AiAgentEditor.vuefire refine_accepted/applied/discarded
extend<module locale files>keys bot_automation.refine.error_generic/error_invalid/error_forbidden
createtests/unit/modules/bot-automation/composables/useRefineAgent.analytics.spec.tsevents fire with no message-content properties

Implementation steps

  1. Explore — Read common/utils/tracking.ts trackEvent(name, properties?, jimoTrack?):51 and an existing trackEvent caller in the module for the property convention.
  2. 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.
  3. Implement — Add the six trackEvent calls at their trigger points; add locale keys.
  4. Go green + gatepnpm 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

DisciplineDays
Frontend1
Backend
QA0.5
Total1.5

Assumptions: trackEvent context 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_pack without 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

ActionFileWhat changes
createapp/api/frontend_service/v2/ai_agent/use_cases/mappers/skill_pack_builder.rbSkillPackBuilder.new(ai_agent:, vector_store_resolver:).call — pure shaping
createspec/api/frontend_service/v2/ai_agent/use_cases/mappers/skill_pack_builder_spec.rbbuilder emits correct pack; read-only resolver returns persisted vector_store with no vector-DB creation (mock-asserted)
extendapp/api/frontend_service/v2/ai_agent/repositories/sync_to_ai_service.rbdelegate 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
extendspec/api/frontend_service/v2/ai_agent/repositories/sync_to_ai_service_spec.rbfull-fixture request-body snapshot proving byte-identical body pre/post extraction; vector create/reuse/purge flows unchanged

Implementation steps

  1. Explore — Read repositories/sync_to_ai_service.rb: the @mode == :update ? …(:39) dispatch, build_skill_pack(:93), build_skill(:121) (note sibling build_skills:117), and the stateful resolve_capability_vector_store(:147). Read the existing spec/.../repositories/sync_to_ai_service_spec.rb for fixture conventions.
  2. Write regression test first (red→green pin) — Add a full-fixture snapshot of the current PUT /ai-agent request body to the existing sync spec; run bundle exec rspec spec/api/frontend_service/v2/ai_agent/repositories/sync_to_ai_service_spec.rb before refactoring to capture the golden body.
  3. Scaffold builder — Create SkillPackBuilder with initialize(ai_agent:, vector_store_resolver:); move the shaping method bodies in unchanged, replacing inline resolve_capability_vector_store calls with vector_store_resolver.call(capability).
  4. Rewire sync — In sync_to_ai_service.rb, delegate shaping to SkillPackBuilder.new(ai_agent:, vector_store_resolver: ->(cap){ resolve_capability_vector_store(cap) }).call; keep the stateful resolver + persist_vector_stores in the service.
  5. Builder spec — Read-only resolver returns the persisted vector_store (or nil) and asserts (via mock) no create_vector_db call.
  6. Go greenbundle exec rspec spec/api/frontend_service/v2/ai_agent/ — all existing sync specs green + snapshot byte-identical.
  7. Quality gatebundle exec rubocop.

Acceptance criteria

  • SyncToAiService emits 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_store and 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

DisciplineDays
Frontend
Backend3
QA0
Total3

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

ActionFileWhat changes
extendapp/models/system_preference.rbai_agent_refine rollout predicate (pattern at :42/:48)
createseed/ops notedocument row provisioning per environment (OQ-8)
createspec/models/system_preference_spec.rb (flag case)predicate true only when row enabled; absent → false

Implementation steps

  1. Explore — Read app/models/system_preference.rb rollout find_by(group_code: 'rollout', enabled: true):42/:48.
  2. Write failing test (red) — predicate returns true only for an enabled row; default/absent → false.
  3. Implement — Add the predicate; write the seeding note (confirm console-vs-seed convention with BE — OQ-8).
  4. Go green + gatebundle 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

DisciplineDays
Frontend
Backend0.5
QA0
Total0.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-pack endpoint 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

ActionFileWhat changes
extendlib/ai_service/ai_agent.rbrefine_skill_pack(body:) mirroring draft_skill_pack(:49–52); new path constant
createspec/lib/ai_service/ai_agent_spec.rb (refine case)POST to exact path with body + 60s timeouts (mock Http)

Implementation steps

  1. Explore — Read lib/ai_service/ai_agent.rb: draft_skill_pack:49 (path const :50, @http.call(... open_timeout: 60, read_timeout: 60):52) and update_ai_agent PUT:43.
  2. Write failing test (red) — Stub @http; assert POST to the exact path with body + timeouts.
  3. Implement — Add the method + path constant.
  4. Go green + gatebundle exec rspec + rubocop.

Acceptance criteria

  • refine_skill_pack issues a POST to /qontak-ai-noncore-mrag/api/ai-agent/refine-skill-pack with 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

DisciplineDays
Frontend
Backend0.5
QA0
Total0.5

Assumptions: drafter parity; no new auth/secret (reuses existing Http client 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

ActionFileWhat changes
createapp/api/frontend_service/v2/ai_agent/repositories/refine.rbrequest-body assembly + client call (mirrors repositories/generate.rb)
createspec/api/frontend_service/v2/ai_agent/repositories/refine_spec.rbbody matches §2.4 upstream schema verbatim for a fixture agent

Implementation steps

  1. Explore — Read repositories/generate.rb: request_body:26, available_tools: in body:29, available_tools query AiAgentTool … .where.not(tool_id: nil):52–53. Confirm the SkillPackBuilder API from Task 2.1.
  2. Write failing test (red) — Assert the assembled body equals the §2.4 upstream schema for a fixture (pack from the read-only builder; trace absent).
  3. Scaffold + implement — Build the body; gather tools; call AiService::AiAgent#refine_skill_pack; pass the result through.
  4. Go green + gatebundle exec rspec + rubocop.

Acceptance criteria

  • Request body matches the §2.4 upstream schema verbatim (company_id, current_skill_pack, user_message, chat_history, available_tools with type: 'qontak_function_call'); trace omitted (OQ-2).
  • Uses SkillPackBuilder with 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

DisciplineDays
Frontend
Backend1
QA0
Total1

Assumptions: reuses generate.rb tools 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/refine returns reply + 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 patchesoptions: []), build models/refine_response.rb, and add the Grape route with set_role(%w[owner supervisor admin]).

Implementation Plan

ActionFileWhat changes
createapp/api/frontend_service/v2/ai_agent/use_cases/refine_ai_agent.rbcontract + guards + Repositories::Refine + SkillPackMapper per option + wrap single→options[]
createapp/api/frontend_service/v2/ai_agent/models/refine_response.rb§2.4 response shape
extendapp/api/frontend_service/v2/ai_agent/ai_agents_controller.rbpost '/:id/refine' with set_role(%w[owner supervisor admin]) + flag gate (mirror post '/generate':361/:362; patch '/:id':292)
createspec/api/frontend_service/v2/ai_agent/refine_ai_agent_spec.rb200/400/403/404/422 matrix; DB row unchanged; warnings/options passthrough; upstream 5xx → 422 + error log

Implementation steps

  1. Explore — Read use_cases/generate.rb (class:7, contract do:8, SkillPackMapper.call:46) as the skeleton; ai_agents_controller.rb route+set_role shape (:361/:362, :292/:293); skill_pack_mapper.rb self.call(skill_pack, organization_id: nil, agent_name: nil):32 (defaulted kwargs). Note the corrected shared-scope paths: helpers/authorization_helpers.rb set_role:6 and middlewares/ownership.rb 403:7 live at app/api/frontend_service/{helpers,middlewares}/ (RFC REV-2 fix), not under the ai_agent/ subtree.
  2. 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_message length, >10 history). Assert parameters + updated_at unchanged after the call (Success Criterion 1). Stub upstream 5xx → 422 + V2 RefineAiAgent failed log. bundle exec rspec spec/api/frontend_service/v2/ai_agent/refine_ai_agent_spec.rb → red.
  3. Scaffold — Use case contract + guards; response model.
  4. Wire — Call Repositories::Refine; SkillPackMapper per option; wrap single upstream proposal into options: [one] (empty patches[]); build the §2.4 response.
  5. Route — Add post '/:id/refine' with set_role + the Chunk-2 flag gate; render via Dry::Matcher::ResultMatcher.
  6. Go green — until the full matrix passes.
  7. Quality gatebundle exec rubocop && bundle exec brakeman.

Acceptance criteria

  • Status matrix green: 200/400/403/404/422 per §2.4 / §3.B.
  • parameters + updated_at unchanged after any refine call (Success Criterion 1; REFINE-S01-NEG/NEG-2).
  • Legacy agent (engine_version != 2) → 422 not_autonomous_agent (REFINE-S01-NEG/NEG-1).
  • warnings[]/options[] passthrough; empty patchesoptions: [] (REFINE-S01/AC-3).
  • Upstream 5xx/timeout → 422 + V2 RefineAiAgent failed log (REFINE-S01/ERR-1); user_message/chat_history never 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

DisciplineDays
Frontend
Backend3
QA0.5
Total3.5

Assumptions: reuses generate skeleton + SkillPackMapper verbatim; 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

ActionFileWhat changes
extendcommon/services/main/endpoint.tsrefine: "/v2/ai_agents/:id:/refine" under v2.ai_agents (block :269)
extendcommon/services/main/v2/bot-automation-agents.tsrefine() (mirror generate():250 / update():232; { fetch, controller } + AbortController)
extendmodules/bot-automation/composables/useRefineAgent.tsreplace mock with botAutomationAgentsService.refine(...)
extendtests/unit/modules/bot-automation/composables/useRefineAgent.spec.tsassert real service called with :id-substituted URL + payload

Implementation steps

  1. Explore — Read bot-automation-agents.ts update():232 (endpoint.v2.ai_agents.update.replace(":id:", id):242, PATCH:243, return { fetch, controller }) and generate():250; endpoint.ts v2 block:269–275.
  2. Write failing test (red) — Assert refine() POSTs to the :id-substituted /v2/ai_agents/:id/refine with the payload, returns { fetch, controller }.
  3. Implement — Add endpoint entry + service method; swap the mock in useRefineAgent.
  4. Go green + gatepnpm test && pnpm lint.

Acceptance criteria

  • refine() POSTs to /v2/ai_agents/:id/refine with {user_message, chat_history}, returns { fetch, controller }.
  • useRefineAgent uses the real service; abort wired to the returned controller.

Test strategy

Vitest asserting URL substitution + payload + abort wiring.

Effort estimate

DisciplineDays
Frontend1
Backend
QA0.5
Total1.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

ActionFileWhat changes
createtests/e2e/.../refine.spec.ts (Playwright)thread → options → accept → form staged → save payload contains staged pack (mocked BE)
createstaging checklist doclatency, save-applies-pack, PaperTrail version; [blocked] real-upstream + audit scenario (OQ-1a/1g/11)

Implementation steps

  1. Explore — Read an existing Playwright spec under tests/e2e/ for the mocked-BE harness pattern.
  2. Write E2E (mocked) — Drive open Refine tab → send → render options → Accept → assert form staged + tab switched → Save → assert PATCH payload carries the staged pack. pnpm test:e2e.
  3. Staging (mocked/stub upstream) — Run the checklist against the ML stub; capture latency.
  4. [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 reply names ≥1 flaw, ≥1 option's patches fixes it, applied+saved passes CapabilityRefPresence (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

DisciplineDays
Frontend1
Backend
QA1
Total2

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 (SkillPackBuilder extraction) 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)

ItemStatusUnblocking condition
Task 2.7 — real-upstream staging pass🚫 BlockedOQ-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)🚫 BlockedOQ-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)DeferredOQ-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)DeferredOQ-1b: Data/ML add the fields — cosmetic, not a gate
trace request fieldOut of scope (v1)OQ-2 resolved — omitted entirely; workflow_state lives in the mekari-agent DB, not suppliable by chatbot BE
Rate limiting on /refinePre-GA actionOQ-7: add per-org throttling before GA (parity with generate today)
Stale-preview / concurrent-edit lock (lock_version)Out of scopeOQ-6: FE updated_at freshness warning is the phase mitigation; real fix deferred
Server-side revert-from-UI (Draft versioning)Deferred / at riskOQ-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/:id save path (update_ai_agent.rb + PaperTrail

  • SyncToAiService rollback) — zero net-new build.