Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .sdd/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,15 @@
"tier": 1,
"subWidgets": ["CallControl", "CallControlCAD", "IncomingTask", "OutdialCall", "TaskList"]
},
{
"id": "ai-assistant",
"package": "@webex/cc-ai-assistant",
"path": "packages/contact-center/ai-assistant",
"spec": "packages/contact-center/ai-assistant/ai-docs/ai-assistant-spec.md",
"responsibility": "AI Assistant widget: panel chrome, Real-time Assist requests, transcript, feedback actions.",
"coverageState": "DRAFT",
"tier": 1
},
{
"id": "ui-logging",
"package": "@webex/cc-ui-logging",
Expand Down
1 change: 1 addition & 0 deletions ai-docs/SPEC_INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
| `station-login/` | Agent login: team + device selection | DRAFT | `packages/contact-center/station-login/ai-docs/station-login-spec.md` |
| `user-state/` | Agent state: state, idle codes, timer | DRAFT | `packages/contact-center/user-state/ai-docs/user-state-spec.md` |
| `task/` | Task widget bundle: CallControl, IncomingTask, OutdialCall, TaskList, CallControlCAD | DRAFT | `packages/contact-center/task/ai-docs/task-spec.md` |
| `ai-assistant/` | AI Assistant widget: chrome + Real-time Assist requests, transcript, feedback | DRAFT | `packages/contact-center/ai-assistant/ai-docs/ai-assistant-spec.md` |
| `ui-logging/` | Metrics/telemetry: `withMetrics`, `metricsLogger` | DRAFT | `packages/contact-center/ui-logging/ai-docs/ui-logging-spec.md` |
| `test-fixtures/` | Shared test mocks/helpers | DRAFT | `packages/contact-center/test-fixtures/ai-docs/test-fixtures-spec.md` |
| `@webex/widgets/` | Legacy meetings widgets (separate family) | DRAFT | `packages/@webex/widgets/ai-docs/widgets-spec.md` |
Expand Down
2 changes: 2 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ module.exports = {
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
moduleNameMapper: {
'^.+\\.(css|less|scss)$': 'babel-jest',
'^.+\\.svg$': '<rootDir>/jest.file-mock.js',
'^adaptivecards$': '<rootDir>/node_modules/adaptivecards/dist/adaptivecards.js',
},
testEnvironment: 'jsdom',
testMatch: ['**/tooling/tests/**/*.js'],
Expand Down
1 change: 1 addition & 0 deletions jest.file-mock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = 'test-file-stub';
206 changes: 206 additions & 0 deletions packages/contact-center/ai-assistant/ai-docs/ai-assistant-spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
# ai-assistant — SPEC

> Start here → root [`AGENTS.md`](../../../../AGENTS.md) (agent entry) · router [`SPEC_INDEX.md`](../../../../ai-docs/SPEC_INDEX.md) · system [`ARCHITECTURE.md`](../../../../ai-docs/ARCHITECTURE.md). This is the module's canonical spec: orientation, requirements, design, flows, and tests.
> Context-efficiency: link to canonical docs — don't duplicate them. Load specs on demand per `SPEC_INDEX.md`.

## Metadata
| Field | Value |
|---|---|
| Module id | `ai-assistant` |
| Source path(s) | `packages/contact-center/ai-assistant/src/` |
| Doc kind | Module spec |
| Coverage score | Pending coverage assessment |
| Generated from | `module-spec` @ SDLC template library `0.1.0-draft` |
| generated_by / approved_by / updated_at | generated_by `ai-assistant feature work` / approved_by `pending` / updated_at `2026-07-29` |
| Validation status | not-run |

## Evidence Rules
Every requirement below cites concrete source evidence using `file path`. Source evidence, test evidence,
and gaps are kept separate so validators and future agents can distinguish truth from context.

## Overview
`ai-assistant` is the AI Assistant widget for Webex Contact Center, published as `@webex/cc-ai-assistant`.
It owns the assistant's chrome (launcher → open → minimized → fullscreen) and the **Real-time Assist**
feature: on request, the SDK returns suggestion Adaptive Cards for the active interaction, which the agent
can refine with extra context and act on with like / dislike / copy.

The package is the container layer only. All UI lives in `@webex/cc-components`
(`AIAssistantComponent`, `RealTimeAssist`, `AdaptiveCardRenderer`), all SDK access goes through
`@webex/cc-store`, and the Web Component wrapper lives in `@webex/cc-widgets`. A maintainer should start
at `src/ai-assistant/index.tsx` (observer container), then `src/helper.ts` (the two hooks that hold all
behavior), then `src/ai-assistant.types.ts` (public props).

## Purpose / Responsibility
Owns: assistant chrome state, the real-time assist request lifecycle, chat transcript assembly from
store payloads, and forwarding agent feedback actions to the SDK. Does NOT own: card rendering, panel
markup, SDK transport, or the store's `realTimeAssist` event plumbing.

## Stack
TypeScript 5.6.3, React (peer `>=18.3.1`) function components + hooks, MobX via `mobx-react-lite`
`observer`, `react-error-boundary`. Tests: Jest 29.7.0 + React Testing Library 16.0.1, jsdom. Build:
Webpack 5 to `dist/`. Source of truth: `package.json`.

## Folder / Package Structure
```
packages/contact-center/ai-assistant/
├── src/
│ ├── index.ts # Barrel: AIAssistant + public types
│ ├── ai-assistant/index.tsx # observer container + ErrorBoundary
│ ├── helper.ts # useAIAssistantChrome, useRealTimeAssist, useAiAssistant
│ └── ai-assistant.types.ts # IAIAssistantProps + hook input types
├── tests/
│ ├── ai-assistant/index.tsx # container render/integration tests
│ ├── ai-assistant/feedback.tsx# like/dislike/copy → SDK tests
│ └── helper.ts # hook unit tests
└── package.json / tsconfig* / webpack.config.js / jest.config.js
```

## Key Files (source of truth)
| File | Holds |
|---|---|
| `packages/contact-center/ai-assistant/src/index.ts` | Public export barrel (`AIAssistant` + types). |
| `packages/contact-center/ai-assistant/src/ai-assistant/index.tsx` | Store wiring: feature flag, active interaction, logger, deprecated-callback bridge, ErrorBoundary. |
| `packages/contact-center/ai-assistant/src/helper.ts` | All behavior: chrome transitions, request lifecycle, chat assembly, feedback dispatch, `REAL_TIME_ASSIST_FLAG`. |
| `packages/contact-center/ai-assistant/src/ai-assistant.types.ts` | `IAIAssistantProps` — the host-facing contract. |
| `packages/contact-center/cc-widgets/src/wc.ts` | `widget-cc-ai-assistant` custom element and its prop map. |

## Public Surface
| Contract ID | Type | Surface | Purpose | Compatibility | Detail |
|---|---|---|---|---|---|
| `ai-assistant.AIAssistant` | SDK | `<AIAssistant {...IAIAssistantProps} />` | React entry point for the widget. | stable | `src/ai-assistant/index.tsx` |
| `ai-assistant.IAIAssistantProps` | SDK | Host callbacks + `className` | Opt-in host notifications. | additive props = minor | `src/ai-assistant.types.ts` |
| `widget-cc-ai-assistant` | Custom element | r2wc wrapper | Web Component host integration. | prop map must mirror `IAIAssistantProps` | `packages/contact-center/cc-widgets/src/wc.ts` |

`onSuggestionReceived` is deprecated in favour of `onRealTimeAssistReceived`; the container accepts either
(`src/ai-assistant/index.tsx`) and both are registered on the custom element.

## Requires (dependencies)
- `@webex/cc-store` — `store.cc.apiAIAssistant` (`getRealTimeAssistance`, `sendRealTimeAssistanceUserAction`), `store.realTimeAssist`, `store.currentTask`, `store.agentId`, `store.agentProfile`, `store.featureFlags`, `store.logger`.
- `@webex/cc-components` — `AIAssistantComponent` and its types.
- `@webex/cc-ui-logging` — `withMetrics` (applied inside `cc-components`).
- `mobx-react-lite`, `react-error-boundary`.

## Requirements
| ID | WHAT | WHY | Source Evidence | Test Evidence | Confidence |
|---|---|---|---|---|---|
| `ai-assistant-R-001` | Chrome moves closed → open → minimized → open and fires the matching host callback on each transition; closing preserves chat state. | The agent must be able to park the panel mid-interaction without losing context. | `src/helper.ts` (`useAIAssistantChrome`) | `tests/helper.ts`, `tests/ai-assistant/index.tsx` | PRESENT |
| `ai-assistant-R-002` | The landing view is shown when the feature flag is off or no interaction is active, and `requestStatus` stays `idle` in that case (never `error`). | Absence of a call is not a failure; showing an error would be misleading. | `packages/contact-center/cc-components/src/components/AIAssistant/ai-assistant.tsx`, `src/helper.ts` (early return in `requestRealTimeAssist`) | `packages/contact-center/cc-components/tests/components/AIAssistant/ai-assistant.tsx` | PRESENT |
| `ai-assistant-R-003` | The opening prompt stays until the first `getRealTimeAssistance` call resolves: in-flight shows a spinner, failure keeps the prompt plus an error message, success switches to listening mode permanently. | A failed first request must be retryable; later failures must not unwind an established session. | `src/helper.ts` (`hasInitialRequestSucceeded` set after `await`), `packages/contact-center/cc-components/src/components/AIAssistant/RealTimeAssist/real-time-assist.tsx` | `packages/contact-center/cc-components/tests/components/AIAssistant/ai-assistant.tsx`, `tests/helper.ts` | PRESENT |
| `ai-assistant-R-004` | Concurrent requests are suppressed: while a `getRealTimeAssistance` call is in flight, further requests are ignored until it settles. | Double-clicking a request control must not fan out duplicate SDK calls. | `src/helper.ts` (`inFlightRef`) | `tests/helper.ts` | PRESENT |
| `ai-assistant-R-005` | Every unseen `realTimeAssist` payload invokes `onRealTimeAssistReceived` once, even when several land before React commits. | Hosts mirror the transcript; a dropped payload is unrecoverable. | `src/helper.ts` (response effect loops from the previous cursor) | `tests/helper.ts` | PRESENT |
| `ai-assistant-R-006` | Changing `interactionId` resets request status, error, context draft, pending flag, first-success flag, user messages, and the payload cursor. | Session state from one customer must never surface on the next. | `src/helper.ts` (reset effect keyed on `interactionId`) | `tests/helper.ts` | PRESENT |
| `ai-assistant-R-007` | `chatEntries` is a chronological transcript — greeting (only after first success), then user and assistant entries ordered by timestamp, user first on ties. | The agent reads context and suggestion in the order they happened. | `src/helper.ts` (`chatEntries` memo) | `tests/helper.ts` | PRESENT |
| `ai-assistant-R-008` | Like / dislike / copy call `sendRealTimeAssistanceUserAction` and return its promise; a rejection is logged and reverts the card's optimistic state. | The UI must not claim feedback was recorded when it was not. | `src/helper.ts` (`handleRealTimeAssistAction`), `packages/contact-center/cc-components/src/components/AIAssistant/AdaptiveCardRenderer/adaptive-card-renderer.tsx` (`captureControlState`) | `tests/ai-assistant/feedback.tsx` | PRESENT |
| `ai-assistant-R-009` | When the action cannot be sent (missing `adaptiveCardId`, ids, or SDK method), the reason is logged via `store.logger.warn` and the returned promise rejects. | A silent no-op leaves both agent and support blind. | `src/helper.ts` | `tests/ai-assistant/feedback.tsx` | PRESENT |
| `ai-assistant-R-010` | A render failure is contained by the widget's `ErrorBoundary` and reported through `store.onErrorCallback('AIAssistant', error)`. | A crashing assistant must not take the agent desktop down. | `src/ai-assistant/index.tsx` | `tests/ai-assistant/index.tsx` | PRESENT |

## Design Overview
`src/ai-assistant/index.tsx` is a thin `observer` container: it reads the store, derives
`isFeatureEnabled` from `featureFlags[REAL_TIME_ASSIST_FLAG]` and the active interaction from
`currentTask.data.interactionId`, resolves the deprecated callback alias, and hands everything to
`useAiAssistant`, whose result is spread straight onto `AIAssistantComponent`.

`src/helper.ts` holds two independent hooks composed by `useAiAssistant`:

- `useAIAssistantChrome` — chrome state machine plus host callbacks. Pure UI state, no SDK.
- `useRealTimeAssist` — the request lifecycle. `requestRealTimeAssist(context?)` is the single entry
point for both the initial request and extra-context refinements; the presence of `context`
distinguishes them. It early-returns to `idle` when the feature is off or there is no interaction,
guards against overlapping calls with `inFlightRef`, and only flips `hasInitialRequestSucceeded`
after the SDK call resolves. Store payloads arrive asynchronously through `store.realTimeAssist`, so
a separate effect watches that array, advances a cursor, and replays every unseen entry to the host.

Refs (`pendingRequestRef`, `requestStatusRef`, `onRealTimeAssistReceivedRef`) keep the response effect
dependent on `realTimeAssist` alone, so unrelated state changes don't replay payloads.

## Data Flow
```mermaid
graph LR
Agent[Agent] -->|Get Assistance / Send context| RTA[RealTimeAssist UI]
RTA -->|requestRealTimeAssist| Hook[useRealTimeAssist]
Hook -->|getRealTimeAssistance| API["store.cc.apiAIAssistant"]
API --> SDK[(Contact Center SDK)]
SDK -->|SUGGESTED_RESPONSE event| SW["storeEventsWrapper"]
SW -->|"store.realTimeAssist[interactionId]"| Container[AIAssistant container]
Container --> Hook
Hook -->|chatEntries| RTA
RTA -->|like / dislike / copy| Hook
Hook -->|sendRealTimeAssistanceUserAction| API
```

## Sequence Diagram(s)
| Operation group | Diagram | Failure coverage |
|---|---|---|
| First request → suggestion → feedback | "Real-time assist round trip" | request rejection keeps the opening prompt; feedback rejection reverts the card |

```mermaid
sequenceDiagram
participant A as Agent
participant UI as RealTimeAssist
participant H as useRealTimeAssist
participant S as store / SDK

A->>UI: Get Assistance
UI->>H: requestRealTimeAssist()
H->>H: inFlight guard, status = listening
H->>S: getRealTimeAssistance({agentId, interactionId, actionTimeStamp})
alt resolves
S-->>H: ok
H->>H: hasInitialRequestSucceeded = true
S-->>H: SUGGESTED_RESPONSE payload(s)
H->>UI: chatEntries + status ready
else rejects
S-->>H: error
H->>UI: stay on prompt, show error, keep button
end

A->>UI: Like
UI->>H: onRealTimeAssistAction
H->>S: sendRealTimeAssistanceUserAction
alt rejects
S-->>H: error
H->>UI: log + reject → card reverts icon
end
```

## Use Cases
- **UC-1 Ask for a suggestion:** agent opens the panel during a call and clicks "Get Assistance" → spinner → listening mode with the assistant greeting; cards stream in as the conversation progresses.
- **UC-2 Refine with context:** agent types context and sends → an `ADD_SUGGESTIONS_EXTRA_CONTEXT` request goes out, the message appears in the transcript, and the panel remains in listening mode whether or not it succeeds.
- **UC-3 Give feedback:** agent likes/dislikes/copies a card → the SDK records the action; a failure reverts the icon.
- **UC-4 No active call:** agent opens the panel with no interaction or the feature disabled → landing view listing AI features, no error state.

## Error Handling & Failure Modes
| Condition | Signal | Recovery |
|---|---|---|
| Feature disabled / no interaction | `requestStatus` stays `idle`; landing view | None needed — informational state |
| First `getRealTimeAssistance` rejects | `requestStatus = 'error'`, message under the prompt | Agent clicks the button again |
| Later request rejects | Panel stays in listening mode | Next successful push resumes the transcript |
| Overlapping request | Ignored while one is in flight | Wait for the current call to settle |
| Feedback action fails | `store.logger.error`, promise rejects, card reverts | Agent can click again |
| Render crash | `ErrorBoundary` renders nothing, `store.onErrorCallback('AIAssistant', error)` | Host decides |

## Pitfalls
- `hasInitialRequestSucceeded` means *the first request resolved*, not *a request was fired*. Setting it before the `await` would strand the agent in listening mode after a failure.
- `requestStatus === 'listening'` is the steady post-success state, not an "in flight" flag. Use `inFlightRef` / `pendingRequest` for concurrency questions.
- The response effect must depend on `realTimeAssist` only; adding state to its dependency array replays payloads to the host.
- The widget never imports the SDK directly — everything goes through `store.cc`.
- `cc-components` cannot import the store, so failures there (clipboard, card render) surface through the `logger` prop this package passes down.

## Module Do's / Don'ts
- DO put behavior in `helper.ts` and keep `ai-assistant/index.tsx` a thin store-to-props adapter.
- DO pass `store.logger` down so `cc-components` can report failures.
- DON'T import from `@webex/cc-widgets` (circular) or from `@webex/contact-center` directly.
- DON'T add UI markup to this package; it belongs in `cc-components`.
- DON'T remove `onSuggestionReceived` without a major bump; bridge it instead.

## Test-Case Strategy (module)
`tests/helper.ts` drives the hooks with `renderHook`: chrome transitions, request success/failure, the
in-flight guard, interaction reset, batched payload replay, and transcript ordering.
`tests/ai-assistant/index.tsx` renders the container against a mocked store for the landing/listening
views and the ErrorBoundary path. `tests/ai-assistant/feedback.tsx` covers like/dislike/copy dispatch,
the missing-`adaptiveCardId` warning, and revert-on-rejection. UI-level rendering (spinner, error text,
snapshots) is covered in `cc-components/tests/components/AIAssistant/`.

## Traceability
- Repo architecture: `../../../../ai-docs/ARCHITECTURE.md` · Registry: `../../../../ai-docs/SPEC_INDEX.md`
- Coverage state & contracts baseline: `.sdd/manifest.json`
3 changes: 3 additions & 0 deletions packages/contact-center/ai-assistant/bable.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const baseConfig = require('../../../babel.config.js');

module.exports = baseConfig;
20 changes: 20 additions & 0 deletions packages/contact-center/ai-assistant/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import globals from 'globals';
import pluginJs from '@eslint/js';
import tseslint from 'typescript-eslint';
import pluginReact from 'eslint-plugin-react';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import eslintConfigPrettier from 'eslint-config-prettier';

export default [
{files: ['**/src/**/*.{js,mjs,cjs,ts,jsx,tsx}']},
{ignores: ['.babelrc.js', '*config.{js,ts}', 'dist', 'node_modules', 'coverage']},
{languageOptions: {globals: globals.browser}},
pluginJs.configs.recommended,
...tseslint.configs.recommended,
{
...pluginReact.configs.flat.recommended,
settings: {react: {version: 'detect'}},
},
eslintPluginPrettierRecommended,
eslintConfigPrettier,
];
6 changes: 6 additions & 0 deletions packages/contact-center/ai-assistant/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const jestConfig = require('../../../jest.config.js');

jestConfig.rootDir = '../../../';
jestConfig.testMatch = ['**/ai-assistant/tests/**/*.ts', '**/ai-assistant/tests/**/*.tsx'];

module.exports = jestConfig;
Loading
Loading