@servicetitan/titan-chatbot-api 8.0.0 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/api-client/__mocks__/chatbot-api-client.mock.d.ts +1 -0
- package/dist/api-client/__mocks__/chatbot-api-client.mock.d.ts.map +1 -1
- package/dist/api-client/__mocks__/chatbot-api-client.mock.js +1 -0
- package/dist/api-client/__mocks__/chatbot-api-client.mock.js.map +1 -1
- package/dist/api-client/base/chatbot-api-client.d.ts +7 -0
- package/dist/api-client/base/chatbot-api-client.d.ts.map +1 -1
- package/dist/api-client/base/chatbot-api-client.js.map +1 -1
- package/dist/api-client/index.d.ts +0 -1
- package/dist/api-client/index.d.ts.map +1 -1
- package/dist/api-client/index.js +0 -2
- package/dist/api-client/index.js.map +1 -1
- package/dist/api-client/titan-chat/__tests__/chatbot-api-client-stream.test.d.ts +2 -0
- package/dist/api-client/titan-chat/__tests__/chatbot-api-client-stream.test.d.ts.map +1 -0
- package/dist/api-client/titan-chat/__tests__/chatbot-api-client-stream.test.js +240 -0
- package/dist/api-client/titan-chat/__tests__/chatbot-api-client-stream.test.js.map +1 -0
- package/dist/api-client/titan-chat/chatbot-api-client.d.ts +11 -0
- package/dist/api-client/titan-chat/chatbot-api-client.d.ts.map +1 -1
- package/dist/api-client/titan-chat/chatbot-api-client.js +29 -0
- package/dist/api-client/titan-chat/chatbot-api-client.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/models/__tests__/chatbot-customizations.test.d.ts +2 -0
- package/dist/models/__tests__/chatbot-customizations.test.d.ts.map +1 -0
- package/dist/models/__tests__/chatbot-customizations.test.js +36 -0
- package/dist/models/__tests__/chatbot-customizations.test.js.map +1 -0
- package/dist/models/chatbot-customizations.d.ts +17 -0
- package/dist/models/chatbot-customizations.d.ts.map +1 -1
- package/dist/models/chatbot-customizations.js +6 -1
- package/dist/models/chatbot-customizations.js.map +1 -1
- package/dist/models/index.d.ts +1 -1
- package/dist/models/index.d.ts.map +1 -1
- package/dist/models/index.js +1 -1
- package/dist/models/index.js.map +1 -1
- package/dist/stores/__tests__/chatbot-ui-backend.store.observability.test.d.ts +2 -0
- package/dist/stores/__tests__/chatbot-ui-backend.store.observability.test.d.ts.map +1 -0
- package/dist/stores/__tests__/chatbot-ui-backend.store.observability.test.js +107 -0
- package/dist/stores/__tests__/chatbot-ui-backend.store.observability.test.js.map +1 -0
- package/dist/stores/__tests__/chatbot-ui-backend.store.streaming.test.d.ts +2 -0
- package/dist/stores/__tests__/chatbot-ui-backend.store.streaming.test.d.ts.map +1 -0
- package/dist/stores/__tests__/chatbot-ui-backend.store.streaming.test.js +312 -0
- package/dist/stores/__tests__/chatbot-ui-backend.store.streaming.test.js.map +1 -0
- package/dist/stores/chatbot-ui-backend.store.d.ts +26 -2
- package/dist/stores/chatbot-ui-backend.store.d.ts.map +1 -1
- package/dist/stores/chatbot-ui-backend.store.js +129 -4
- package/dist/stores/chatbot-ui-backend.store.js.map +1 -1
- package/dist/streaming/__tests__/agent-stream.test.d.ts +2 -0
- package/dist/streaming/__tests__/agent-stream.test.d.ts.map +1 -0
- package/dist/streaming/__tests__/agent-stream.test.js +92 -0
- package/dist/streaming/__tests__/agent-stream.test.js.map +1 -0
- package/dist/streaming/agent-stream.d.ts +83 -0
- package/dist/streaming/agent-stream.d.ts.map +1 -0
- package/dist/streaming/agent-stream.js +28 -0
- package/dist/streaming/agent-stream.js.map +1 -0
- package/dist/streaming/index.d.ts +3 -0
- package/dist/streaming/index.d.ts.map +1 -0
- package/dist/streaming/index.js +4 -0
- package/dist/streaming/index.js.map +1 -0
- package/dist/streaming/run-agent-stream.d.ts +23 -0
- package/dist/streaming/run-agent-stream.d.ts.map +1 -0
- package/dist/streaming/run-agent-stream.js +83 -0
- package/dist/streaming/run-agent-stream.js.map +1 -0
- package/package.json +6 -3
- package/src/api-client/__mocks__/chatbot-api-client.mock.ts +1 -0
- package/src/api-client/base/chatbot-api-client.ts +11 -0
- package/src/api-client/index.ts +0 -1
- package/src/api-client/titan-chat/__tests__/chatbot-api-client-stream.test.ts +208 -0
- package/src/api-client/titan-chat/chatbot-api-client.ts +46 -0
- package/src/index.ts +6 -1
- package/src/models/__tests__/chatbot-customizations.test.ts +26 -0
- package/src/models/chatbot-customizations.ts +20 -0
- package/src/models/index.ts +1 -1
- package/src/stores/__tests__/chatbot-ui-backend.store.observability.test.ts +105 -0
- package/src/stores/__tests__/chatbot-ui-backend.store.streaming.test.ts +261 -0
- package/src/stores/chatbot-ui-backend.store.ts +179 -4
- package/src/streaming/__tests__/agent-stream.test.ts +80 -0
- package/src/streaming/agent-stream.ts +103 -0
- package/src/streaming/index.ts +2 -0
- package/src/streaming/run-agent-stream.ts +109 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/api-client/help-center/__tests__/converter-from-models.test.d.ts +0 -2
- package/dist/api-client/help-center/__tests__/converter-from-models.test.d.ts.map +0 -1
- package/dist/api-client/help-center/__tests__/converter-from-models.test.js +0 -67
- package/dist/api-client/help-center/__tests__/converter-from-models.test.js.map +0 -1
- package/dist/api-client/help-center/__tests__/converter-to-models.test.d.ts +0 -2
- package/dist/api-client/help-center/__tests__/converter-to-models.test.d.ts.map +0 -1
- package/dist/api-client/help-center/__tests__/converter-to-models.test.js +0 -83
- package/dist/api-client/help-center/__tests__/converter-to-models.test.js.map +0 -1
- package/dist/api-client/help-center/chatbot-api-client.d.ts +0 -32
- package/dist/api-client/help-center/chatbot-api-client.d.ts.map +0 -1
- package/dist/api-client/help-center/chatbot-api-client.js +0 -101
- package/dist/api-client/help-center/chatbot-api-client.js.map +0 -1
- package/dist/api-client/help-center/converter-from-models.d.ts +0 -13
- package/dist/api-client/help-center/converter-from-models.d.ts.map +0 -1
- package/dist/api-client/help-center/converter-from-models.js +0 -117
- package/dist/api-client/help-center/converter-from-models.js.map +0 -1
- package/dist/api-client/help-center/converter-to-models.d.ts +0 -13
- package/dist/api-client/help-center/converter-to-models.d.ts.map +0 -1
- package/dist/api-client/help-center/converter-to-models.js +0 -101
- package/dist/api-client/help-center/converter-to-models.js.map +0 -1
- package/dist/api-client/help-center/index.d.ts +0 -3
- package/dist/api-client/help-center/index.d.ts.map +0 -1
- package/dist/api-client/help-center/index.js +0 -3
- package/dist/api-client/help-center/index.js.map +0 -1
- package/dist/api-client/help-center/native-client.d.ts +0 -1268
- package/dist/api-client/help-center/native-client.d.ts.map +0 -1
- package/dist/api-client/help-center/native-client.js +0 -4550
- package/dist/api-client/help-center/native-client.js.map +0 -1
- package/src/api-client/help-center/__tests__/converter-from-models.test.ts +0 -41
- package/src/api-client/help-center/__tests__/converter-to-models.test.ts +0 -89
- package/src/api-client/help-center/chatbot-api-client.ts +0 -122
- package/src/api-client/help-center/converter-from-models.ts +0 -133
- package/src/api-client/help-center/converter-to-models.ts +0 -127
- package/src/api-client/help-center/index.ts +0 -2
- package/src/api-client/help-center/native-client.ts +0 -5727
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-stream.d.ts","sourceRoot":"","sources":["../../src/streaming/agent-stream.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEvC,kEAAkE;AAClE,MAAM,MAAM,cAAc,GACpB,SAAS,GACT,aAAa,GACb,iBAAiB,GACjB,aAAa,GACb,OAAO,GACP,SAAS,CAAC;AAEhB,MAAM,WAAW,aAAa;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAC;CAChC;AAED;;;;;GAKG;AACH,MAAM,WAAW,oBAAqB,SAAQ,MAAM,CAAC,WAAW;IAC5D,oFAAoF;IACpF,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,0DAA0D;IAC1D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wDAAwD;IACxD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,GAAG,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAChC,yFAAyF;IACzF,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,MAAM,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,CAAC,CAAC,KAAK,EAAE,aAAa,EAAE,GAAG,IAAI,CAAC;IACtC;;;;;OAKG;IACH,YAAY,CAAC,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,gBAAgB,CAAC,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACpD,YAAY,CAAC,IAAI,IAAI,CAAC;IACtB,WAAW,CAAC,IAAI,IAAI,CAAC;IACrB,cAAc,CAAC,IAAI,IAAI,CAAC;IACxB,SAAS,CAAC,IAAI,IAAI,CAAC;IACnB,WAAW,CAAC,IAAI,IAAI,CAAC;CACxB;AAED,yDAAyD;AACzD,qBAAa,gBAAiB,SAAQ,KAAK;gBAC3B,OAAO,CAAC,EAAE,MAAM;CAI/B;AAED,8EAA8E;AAC9E,eAAO,MAAM,iBAAiB,2BAA2B,CAAC;AAE1D,6DAA6D;AAC7D,eAAO,MAAM,WAAW;;;;;;;;CAQd,CAAC;AAEX;;;;;;GAMG;AACH,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,oBAAoB,GAAG,MAAM,CAAC,UAAU,CAE5F"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Models } from '../api-client';
|
|
2
|
+
/** Raised when the backend emits a `run.error` event. */ export class AgentStreamError extends Error {
|
|
3
|
+
constructor(message){
|
|
4
|
+
super(message !== null && message !== void 0 ? message : 'Something went wrong during this step. Please try again.');
|
|
5
|
+
this.name = 'AgentStreamError';
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
/** Path of the v2 streaming endpoint, appended to the configured base URL. */ export const AGENT_STREAM_PATH = '/api/v2/message/stream';
|
|
9
|
+
/** SSE event names emitted by the backend agent pipeline. */ export const AGENT_EVENT = {
|
|
10
|
+
RunStarted: 'run.started',
|
|
11
|
+
RunFinished: 'run.finished',
|
|
12
|
+
RunError: 'run.error',
|
|
13
|
+
StatusChanged: 'status.changed',
|
|
14
|
+
TextAppended: 'text.appended',
|
|
15
|
+
PlanProposed: 'plan.proposed',
|
|
16
|
+
InputRequested: 'input.requested'
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Map a terminal `run.finished` payload to the shared `BotMessage` model. The payload is already
|
|
20
|
+
* BotMessage-shaped, so `fromJS` does everything: it reads the backend's `guardFlag`/`isGuardrailed`
|
|
21
|
+
* verbatim, wraps nested data into real instances (`scoredUrls`, `agentOptions`, `workflowPlan` and
|
|
22
|
+
* its `steps`/`inputs`) so `toJSON()` can serialize it for session storage, and ignores the
|
|
23
|
+
* streaming-only metadata (`status`, `durationMs`, `runId`, `seq`).
|
|
24
|
+
*/ export function convertAgentFinishToBotMessage(data) {
|
|
25
|
+
return Models.BotMessage.fromJS(data);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
//# sourceMappingURL=agent-stream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/streaming/agent-stream.ts"],"sourcesContent":["import { Models } from '../api-client';\n\n/** Final run status reported by the backend on `run.finished`. */\nexport type AgentRunStatus =\n | 'Success'\n | 'Guardrailed'\n | 'PendingApproval'\n | 'Bifurcation'\n | 'Error'\n | 'Timeout';\n\nexport interface AgentPlanStep {\n id: string;\n title: string;\n description?: string;\n}\n\nexport interface AgentInputOption {\n label: string;\n value: string;\n style?: string;\n}\n\nexport interface AgentInputRequest {\n kind: string;\n prompt?: string;\n options?: AgentInputOption[];\n}\n\n/**\n * Decoded payload of the terminal `run.finished` event. The backend sends a full `BotMessage`\n * (same field names: `answer`, `guardFlag`, `isGuardrailed`, `scoredUrls`, `agentOptions`,\n * `workflowPlan`, `sessionId`, …) plus a little streaming-only metadata — so the mapping to the\n * shared model is trivial (see {@link convertAgentFinishToBotMessage}).\n */\nexport interface AgentRunFinishedData extends Models.IBotMessage {\n /** Final run status (streaming-only metadata, not part of the BotMessage model). */\n status?: AgentRunStatus;\n /** Total run duration in ms (streaming-only metadata). */\n durationMs?: number;\n /** Backend run identifier (streaming-only metadata). */\n runId?: string;\n /** Monotonic event sequence number (streaming-only metadata). */\n seq?: number;\n}\n\n/**\n * Callbacks + config the consumer supplies to a streamed message. Maps the chatbot's agent events\n * onto progress updates and exposes the generic connection lifecycle for logging / keepalive / fallback.\n */\nexport interface AgentStreamHandlers {\n /** Inactivity threshold before `onInactivity` fires (defaults applied by the caller). */\n inactivityTimeoutMs?: number;\n onStatus?(text: string): void;\n onText?(text: string): void;\n onPlan?(steps: AgentPlanStep[]): void;\n /**\n * Marks the plan step with the given id as the active one (earlier steps become done, later\n * ones stay pending). Driven by an explicit `activeStepId` on `plan.proposed` or a `stepId` on\n * `status.changed` when the backend supplies it; absent that, the first step is activated on\n * `plan.proposed`. Mid-plan advancement therefore requires backend step correlation.\n */\n onStepActive?(id: string): void;\n onInputRequested?(request: AgentInputRequest): void;\n onInactivity?(): void;\n onConnected?(): void;\n onDisconnected?(): void;\n onTimeout?(): void;\n onCompleted?(): void;\n}\n\n/** Raised when the backend emits a `run.error` event. */\nexport class AgentStreamError extends Error {\n constructor(message?: string) {\n super(message ?? 'Something went wrong during this step. Please try again.');\n this.name = 'AgentStreamError';\n }\n}\n\n/** Path of the v2 streaming endpoint, appended to the configured base URL. */\nexport const AGENT_STREAM_PATH = '/api/v2/message/stream';\n\n/** SSE event names emitted by the backend agent pipeline. */\nexport const AGENT_EVENT = {\n RunStarted: 'run.started',\n RunFinished: 'run.finished',\n RunError: 'run.error',\n StatusChanged: 'status.changed',\n TextAppended: 'text.appended',\n PlanProposed: 'plan.proposed',\n InputRequested: 'input.requested',\n} as const;\n\n/**\n * Map a terminal `run.finished` payload to the shared `BotMessage` model. The payload is already\n * BotMessage-shaped, so `fromJS` does everything: it reads the backend's `guardFlag`/`isGuardrailed`\n * verbatim, wraps nested data into real instances (`scoredUrls`, `agentOptions`, `workflowPlan` and\n * its `steps`/`inputs`) so `toJSON()` can serialize it for session storage, and ignores the\n * streaming-only metadata (`status`, `durationMs`, `runId`, `seq`).\n */\nexport function convertAgentFinishToBotMessage(data: AgentRunFinishedData): Models.BotMessage {\n return Models.BotMessage.fromJS(data);\n}\n"],"names":["Models","AgentStreamError","Error","message","name","AGENT_STREAM_PATH","AGENT_EVENT","RunStarted","RunFinished","RunError","StatusChanged","TextAppended","PlanProposed","InputRequested","convertAgentFinishToBotMessage","data","BotMessage","fromJS"],"mappings":"AAAA,SAASA,MAAM,QAAQ,gBAAgB;AAuEvC,uDAAuD,GACvD,OAAO,MAAMC,yBAAyBC;IAClC,YAAYC,OAAgB,CAAE;QAC1B,KAAK,CAACA,oBAAAA,qBAAAA,UAAW;QACjB,IAAI,CAACC,IAAI,GAAG;IAChB;AACJ;AAEA,4EAA4E,GAC5E,OAAO,MAAMC,oBAAoB,yBAAyB;AAE1D,2DAA2D,GAC3D,OAAO,MAAMC,cAAc;IACvBC,YAAY;IACZC,aAAa;IACbC,UAAU;IACVC,eAAe;IACfC,cAAc;IACdC,cAAc;IACdC,gBAAgB;AACpB,EAAW;AAEX;;;;;;CAMC,GACD,OAAO,SAASC,+BAA+BC,IAA0B;IACrE,OAAOf,OAAOgB,UAAU,CAACC,MAAM,CAACF;AACpC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/streaming/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/streaming/index.ts"],"sourcesContent":["export * from './agent-stream';\nexport * from './run-agent-stream';\n"],"names":[],"mappings":"AAAA,cAAc,iBAAiB;AAC/B,cAAc,qBAAqB"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Models } from '../api-client';
|
|
2
|
+
import { AgentStreamHandlers } from './agent-stream';
|
|
3
|
+
export interface RunAgentStreamOptions {
|
|
4
|
+
/** Fully-constructed streaming endpoint URL. */
|
|
5
|
+
url: string;
|
|
6
|
+
/** Request headers (auth, X-Client-ID, …). */
|
|
7
|
+
headers?: Record<string, string>;
|
|
8
|
+
/** Request body, serialized as JSON by the SSE client. */
|
|
9
|
+
body: unknown;
|
|
10
|
+
/** Custom `fetch` (same as the regular API client) so auth/headers/credentials apply identically. */
|
|
11
|
+
fetch?: typeof fetch;
|
|
12
|
+
/** Progress + lifecycle callbacks (and the inactivity timeout). */
|
|
13
|
+
handlers: AgentStreamHandlers;
|
|
14
|
+
abortSignal?: AbortSignal;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Shared SSE consumption used by every {@link IChatbotApiClient} adapter: opens a {@link ChatSseClient},
|
|
18
|
+
* maps the agent events onto the supplied handlers, and resolves the final {@link Models.BotMessage}
|
|
19
|
+
* from `run.finished` (or rejects on `run.error` / a fatal connection error). The only per-adapter
|
|
20
|
+
* differences — the URL, headers and body — are passed in by the caller.
|
|
21
|
+
*/
|
|
22
|
+
export declare function runAgentStream({ abortSignal, body, fetch, handlers, headers, url, }: RunAgentStreamOptions): Promise<Models.BotMessage>;
|
|
23
|
+
//# sourceMappingURL=run-agent-stream.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-agent-stream.d.ts","sourceRoot":"","sources":["../../src/streaming/run-agent-stream.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACvC,OAAO,EAKH,mBAAmB,EAEtB,MAAM,gBAAgB,CAAC;AAExB,MAAM,WAAW,qBAAqB;IAClC,gDAAgD;IAChD,GAAG,EAAE,MAAM,CAAC;IACZ,8CAA8C;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,0DAA0D;IAC1D,IAAI,EAAE,OAAO,CAAC;IACd,qGAAqG;IACrG,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,mEAAmE;IACnE,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,WAAW,CAAC,EAAE,WAAW,CAAC;CAC7B;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAC3B,WAAW,EACX,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,OAAO,EACP,GAAG,GACN,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAsEpD"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { ChatSseClient } from '@servicetitan/titan-chat-ui-common';
|
|
2
|
+
import { AGENT_EVENT, AgentStreamError, convertAgentFinishToBotMessage } from './agent-stream';
|
|
3
|
+
/**
|
|
4
|
+
* Shared SSE consumption used by every {@link IChatbotApiClient} adapter: opens a {@link ChatSseClient},
|
|
5
|
+
* maps the agent events onto the supplied handlers, and resolves the final {@link Models.BotMessage}
|
|
6
|
+
* from `run.finished` (or rejects on `run.error` / a fatal connection error). The only per-adapter
|
|
7
|
+
* differences — the URL, headers and body — are passed in by the caller.
|
|
8
|
+
*/ export function runAgentStream({ abortSignal, body, fetch, handlers, headers, url }) {
|
|
9
|
+
return new Promise((resolve, reject)=>{
|
|
10
|
+
let finished;
|
|
11
|
+
const client = new ChatSseClient({
|
|
12
|
+
url,
|
|
13
|
+
headers,
|
|
14
|
+
fetch,
|
|
15
|
+
signal: abortSignal,
|
|
16
|
+
body,
|
|
17
|
+
inactivityTimeoutMs: handlers.inactivityTimeoutMs,
|
|
18
|
+
isTerminalEvent: (e)=>e.event === AGENT_EVENT.RunFinished || e.event === AGENT_EVENT.RunError,
|
|
19
|
+
handlers: {
|
|
20
|
+
[AGENT_EVENT.StatusChanged]: (d)=>{
|
|
21
|
+
var _handlers_onStatus;
|
|
22
|
+
(_handlers_onStatus = handlers.onStatus) === null || _handlers_onStatus === void 0 ? void 0 : _handlers_onStatus.call(handlers, d.text);
|
|
23
|
+
if (d.stepId !== undefined) {
|
|
24
|
+
var _handlers_onStepActive;
|
|
25
|
+
(_handlers_onStepActive = handlers.onStepActive) === null || _handlers_onStepActive === void 0 ? void 0 : _handlers_onStepActive.call(handlers, d.stepId);
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
[AGENT_EVENT.TextAppended]: (d)=>{
|
|
29
|
+
var _handlers_onText;
|
|
30
|
+
return (_handlers_onText = handlers.onText) === null || _handlers_onText === void 0 ? void 0 : _handlers_onText.call(handlers, d.text);
|
|
31
|
+
},
|
|
32
|
+
[AGENT_EVENT.PlanProposed]: (d)=>{
|
|
33
|
+
var _d_activeStepId;
|
|
34
|
+
var _handlers_onPlan, _d_steps_, _d_steps;
|
|
35
|
+
(_handlers_onPlan = handlers.onPlan) === null || _handlers_onPlan === void 0 ? void 0 : _handlers_onPlan.call(handlers, d.steps);
|
|
36
|
+
// Activate the explicitly-signalled step, else default to the first.
|
|
37
|
+
const activeId = (_d_activeStepId = d.activeStepId) !== null && _d_activeStepId !== void 0 ? _d_activeStepId : (_d_steps = d.steps) === null || _d_steps === void 0 ? void 0 : (_d_steps_ = _d_steps[0]) === null || _d_steps_ === void 0 ? void 0 : _d_steps_.id;
|
|
38
|
+
if (activeId !== undefined) {
|
|
39
|
+
var _handlers_onStepActive;
|
|
40
|
+
(_handlers_onStepActive = handlers.onStepActive) === null || _handlers_onStepActive === void 0 ? void 0 : _handlers_onStepActive.call(handlers, activeId);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
[AGENT_EVENT.InputRequested]: (d)=>{
|
|
44
|
+
var _handlers_onInputRequested;
|
|
45
|
+
return (_handlers_onInputRequested = handlers.onInputRequested) === null || _handlers_onInputRequested === void 0 ? void 0 : _handlers_onInputRequested.call(handlers, {
|
|
46
|
+
kind: d.kind,
|
|
47
|
+
prompt: d.prompt,
|
|
48
|
+
options: d.options
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
[AGENT_EVENT.RunFinished]: (d)=>{
|
|
52
|
+
finished = d;
|
|
53
|
+
},
|
|
54
|
+
[AGENT_EVENT.RunError]: (d)=>{
|
|
55
|
+
reject(new AgentStreamError(d.message));
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
onConnected: handlers.onConnected,
|
|
59
|
+
onDisconnected: handlers.onDisconnected,
|
|
60
|
+
onTimeout: handlers.onTimeout,
|
|
61
|
+
onInactivity: handlers.onInactivity,
|
|
62
|
+
onCompleted: handlers.onCompleted,
|
|
63
|
+
onError: reject
|
|
64
|
+
});
|
|
65
|
+
client.start().then(()=>{
|
|
66
|
+
if (finished) {
|
|
67
|
+
try {
|
|
68
|
+
resolve(convertAgentFinishToBotMessage(finished));
|
|
69
|
+
} catch (err) {
|
|
70
|
+
reject(err);
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
/*
|
|
75
|
+
* Stream completed without `run.finished`. A run.error / fatal onError may have
|
|
76
|
+
* already rejected (subsequent reject calls are no-ops); otherwise the server closed
|
|
77
|
+
* the connection unexpectedly — reject so the returned Promise never hangs.
|
|
78
|
+
*/ reject(new AgentStreamError('Stream ended without a run.finished event'));
|
|
79
|
+
}, reject);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
//# sourceMappingURL=run-agent-stream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/streaming/run-agent-stream.ts"],"sourcesContent":["import { ChatSseClient } from '@servicetitan/titan-chat-ui-common';\nimport { Models } from '../api-client';\nimport {\n AGENT_EVENT,\n AgentPlanStep,\n AgentRunFinishedData,\n AgentStreamError,\n AgentStreamHandlers,\n convertAgentFinishToBotMessage,\n} from './agent-stream';\n\nexport interface RunAgentStreamOptions {\n /** Fully-constructed streaming endpoint URL. */\n url: string;\n /** Request headers (auth, X-Client-ID, …). */\n headers?: Record<string, string>;\n /** Request body, serialized as JSON by the SSE client. */\n body: unknown;\n /** Custom `fetch` (same as the regular API client) so auth/headers/credentials apply identically. */\n fetch?: typeof fetch;\n /** Progress + lifecycle callbacks (and the inactivity timeout). */\n handlers: AgentStreamHandlers;\n abortSignal?: AbortSignal;\n}\n\n/**\n * Shared SSE consumption used by every {@link IChatbotApiClient} adapter: opens a {@link ChatSseClient},\n * maps the agent events onto the supplied handlers, and resolves the final {@link Models.BotMessage}\n * from `run.finished` (or rejects on `run.error` / a fatal connection error). The only per-adapter\n * differences — the URL, headers and body — are passed in by the caller.\n */\nexport function runAgentStream({\n abortSignal,\n body,\n fetch,\n handlers,\n headers,\n url,\n}: RunAgentStreamOptions): Promise<Models.BotMessage> {\n return new Promise<Models.BotMessage>((resolve, reject) => {\n let finished: AgentRunFinishedData | undefined;\n\n const client = new ChatSseClient({\n url,\n headers,\n fetch,\n signal: abortSignal,\n body,\n inactivityTimeoutMs: handlers.inactivityTimeoutMs,\n isTerminalEvent: e =>\n e.event === AGENT_EVENT.RunFinished || e.event === AGENT_EVENT.RunError,\n handlers: {\n [AGENT_EVENT.StatusChanged]: (d: { text: string; stepId?: string }) => {\n handlers.onStatus?.(d.text);\n if (d.stepId !== undefined) {\n handlers.onStepActive?.(d.stepId);\n }\n },\n [AGENT_EVENT.TextAppended]: (d: { text: string }) => handlers.onText?.(d.text),\n [AGENT_EVENT.PlanProposed]: (d: {\n steps: AgentPlanStep[];\n activeStepId?: string;\n }) => {\n handlers.onPlan?.(d.steps);\n // Activate the explicitly-signalled step, else default to the first.\n const activeId = d.activeStepId ?? d.steps?.[0]?.id;\n if (activeId !== undefined) {\n handlers.onStepActive?.(activeId);\n }\n },\n [AGENT_EVENT.InputRequested]: (d: any) =>\n handlers.onInputRequested?.({\n kind: d.kind,\n prompt: d.prompt,\n options: d.options,\n }),\n [AGENT_EVENT.RunFinished]: (d: AgentRunFinishedData) => {\n finished = d;\n },\n [AGENT_EVENT.RunError]: (d: { message?: string }) => {\n reject(new AgentStreamError(d.message));\n },\n },\n onConnected: handlers.onConnected,\n onDisconnected: handlers.onDisconnected,\n onTimeout: handlers.onTimeout,\n onInactivity: handlers.onInactivity,\n onCompleted: handlers.onCompleted,\n onError: reject,\n });\n\n client.start().then(() => {\n if (finished) {\n try {\n resolve(convertAgentFinishToBotMessage(finished));\n } catch (err) {\n reject(err);\n }\n return;\n }\n /*\n * Stream completed without `run.finished`. A run.error / fatal onError may have\n * already rejected (subsequent reject calls are no-ops); otherwise the server closed\n * the connection unexpectedly — reject so the returned Promise never hangs.\n */\n reject(new AgentStreamError('Stream ended without a run.finished event'));\n }, reject);\n });\n}\n"],"names":["ChatSseClient","AGENT_EVENT","AgentStreamError","convertAgentFinishToBotMessage","runAgentStream","abortSignal","body","fetch","handlers","headers","url","Promise","resolve","reject","finished","client","signal","inactivityTimeoutMs","isTerminalEvent","e","event","RunFinished","RunError","StatusChanged","d","onStatus","text","stepId","undefined","onStepActive","TextAppended","onText","PlanProposed","onPlan","steps","activeId","activeStepId","id","InputRequested","onInputRequested","kind","prompt","options","message","onConnected","onDisconnected","onTimeout","onInactivity","onCompleted","onError","start","then","err"],"mappings":"AAAA,SAASA,aAAa,QAAQ,qCAAqC;AAEnE,SACIC,WAAW,EAGXC,gBAAgB,EAEhBC,8BAA8B,QAC3B,iBAAiB;AAgBxB;;;;;CAKC,GACD,OAAO,SAASC,eAAe,EAC3BC,WAAW,EACXC,IAAI,EACJC,KAAK,EACLC,QAAQ,EACRC,OAAO,EACPC,GAAG,EACiB;IACpB,OAAO,IAAIC,QAA2B,CAACC,SAASC;QAC5C,IAAIC;QAEJ,MAAMC,SAAS,IAAIf,cAAc;YAC7BU;YACAD;YACAF;YACAS,QAAQX;YACRC;YACAW,qBAAqBT,SAASS,mBAAmB;YACjDC,iBAAiBC,CAAAA,IACbA,EAAEC,KAAK,KAAKnB,YAAYoB,WAAW,IAAIF,EAAEC,KAAK,KAAKnB,YAAYqB,QAAQ;YAC3Ed,UAAU;gBACN,CAACP,YAAYsB,aAAa,CAAC,EAAE,CAACC;wBAC1BhB;qBAAAA,qBAAAA,SAASiB,QAAQ,cAAjBjB,yCAAAA,wBAAAA,UAAoBgB,EAAEE,IAAI;oBAC1B,IAAIF,EAAEG,MAAM,KAAKC,WAAW;4BACxBpB;yBAAAA,yBAAAA,SAASqB,YAAY,cAArBrB,6CAAAA,4BAAAA,UAAwBgB,EAAEG,MAAM;oBACpC;gBACJ;gBACA,CAAC1B,YAAY6B,YAAY,CAAC,EAAE,CAACN;wBAAwBhB;4BAAAA,mBAAAA,SAASuB,MAAM,cAAfvB,uCAAAA,sBAAAA,UAAkBgB,EAAEE,IAAI;;gBAC7E,CAACzB,YAAY+B,YAAY,CAAC,EAAE,CAACR;wBAMRA;wBAFjBhB,kBAEmCgB,WAAAA;qBAFnChB,mBAAAA,SAASyB,MAAM,cAAfzB,uCAAAA,sBAAAA,UAAkBgB,EAAEU,KAAK;oBACzB,qEAAqE;oBACrE,MAAMC,YAAWX,kBAAAA,EAAEY,YAAY,cAAdZ,6BAAAA,mBAAkBA,WAAAA,EAAEU,KAAK,cAAPV,gCAAAA,YAAAA,QAAS,CAAC,EAAE,cAAZA,gCAAAA,UAAca,EAAE;oBACnD,IAAIF,aAAaP,WAAW;4BACxBpB;yBAAAA,yBAAAA,SAASqB,YAAY,cAArBrB,6CAAAA,4BAAAA,UAAwB2B;oBAC5B;gBACJ;gBACA,CAAClC,YAAYqC,cAAc,CAAC,EAAE,CAACd;wBAC3BhB;4BAAAA,6BAAAA,SAAS+B,gBAAgB,cAAzB/B,iDAAAA,gCAAAA,UAA4B;wBACxBgC,MAAMhB,EAAEgB,IAAI;wBACZC,QAAQjB,EAAEiB,MAAM;wBAChBC,SAASlB,EAAEkB,OAAO;oBACtB;;gBACJ,CAACzC,YAAYoB,WAAW,CAAC,EAAE,CAACG;oBACxBV,WAAWU;gBACf;gBACA,CAACvB,YAAYqB,QAAQ,CAAC,EAAE,CAACE;oBACrBX,OAAO,IAAIX,iBAAiBsB,EAAEmB,OAAO;gBACzC;YACJ;YACAC,aAAapC,SAASoC,WAAW;YACjCC,gBAAgBrC,SAASqC,cAAc;YACvCC,WAAWtC,SAASsC,SAAS;YAC7BC,cAAcvC,SAASuC,YAAY;YACnCC,aAAaxC,SAASwC,WAAW;YACjCC,SAASpC;QACb;QAEAE,OAAOmC,KAAK,GAAGC,IAAI,CAAC;YAChB,IAAIrC,UAAU;gBACV,IAAI;oBACAF,QAAQT,+BAA+BW;gBAC3C,EAAE,OAAOsC,KAAK;oBACVvC,OAAOuC;gBACX;gBACA;YACJ;YACA;;;;aAIC,GACDvC,OAAO,IAAIX,iBAAiB;QAChC,GAAGW;IACP;AACJ"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@servicetitan/titan-chatbot-api",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.0.0",
|
|
4
4
|
"description": "Chatbot client API package",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -17,10 +17,13 @@
|
|
|
17
17
|
"push:local": "yalc push"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@servicetitan/titan-chat-ui-common": "^
|
|
20
|
+
"@servicetitan/titan-chat-ui-common": "^9.0.0",
|
|
21
21
|
"lodash": "^4.18.1",
|
|
22
22
|
"nanoid": "^5.1.5"
|
|
23
23
|
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@microsoft/fetch-event-source": "^2.0.1"
|
|
26
|
+
},
|
|
24
27
|
"peerDependencies": {
|
|
25
28
|
"@servicetitan/log-service": ">=27",
|
|
26
29
|
"@servicetitan/react-ioc": ">=24",
|
|
@@ -42,5 +45,5 @@
|
|
|
42
45
|
"cli": {
|
|
43
46
|
"webpack": false
|
|
44
47
|
},
|
|
45
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "028073fe571014741da70bcff173d617e2738125"
|
|
46
49
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { symbolToken } from '@servicetitan/react-ioc';
|
|
2
2
|
import { Models } from '..';
|
|
3
|
+
import { AgentStreamHandlers } from '../../streaming';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Token for injecting the chatbot client settings for proper initialization of the client.
|
|
@@ -25,6 +26,16 @@ export interface IChatbotClientSettings {}
|
|
|
25
26
|
export interface IChatbotApiClient {
|
|
26
27
|
postFeedback(body: Models.IFeedback, abortSignal?: AbortSignal): Promise<Models.Feedback>;
|
|
27
28
|
postMessage(body: Models.IUserMessage, abortSignal?: AbortSignal): Promise<Models.BotMessage>;
|
|
29
|
+
/**
|
|
30
|
+
* Streaming variant of {@link postMessage}: consumes the SSE agent-progress stream, forwarding
|
|
31
|
+
* progress events to `handlers`, and resolves with the final {@link Models.BotMessage}.
|
|
32
|
+
* Optional — clients that do not support streaming omit it, and consumers fall back to {@link postMessage}.
|
|
33
|
+
*/
|
|
34
|
+
streamMessage?(
|
|
35
|
+
body: Models.IUserMessage,
|
|
36
|
+
handlers: AgentStreamHandlers,
|
|
37
|
+
abortSignal?: AbortSignal
|
|
38
|
+
): Promise<Models.BotMessage>;
|
|
28
39
|
postFollowUpEmail(
|
|
29
40
|
body: Models.IUserMessage,
|
|
30
41
|
abortSignal?: AbortSignal
|
package/src/api-client/index.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export * as ApiClientTitanChat from './titan-chat';
|
|
2
|
-
export * as ApiClientHelpCenter from './help-center';
|
|
3
2
|
export * as Models from './models';
|
|
4
3
|
export * as ModelsMocks from './models/__mocks__/models.mock';
|
|
5
4
|
export * as ClientMocks from './__mocks__/chatbot-api-client.mock';
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, jest, test } from '@jest/globals';
|
|
2
|
+
import { type EventSourceMessage, fetchEventSource } from '@microsoft/fetch-event-source';
|
|
3
|
+
import { Models } from '../..';
|
|
4
|
+
import type { AgentStreamHandlers } from '../../../streaming';
|
|
5
|
+
import { ChatbotApiClient, IChatbotClientSettingsTitanChat } from '../chatbot-api-client';
|
|
6
|
+
|
|
7
|
+
jest.mock('@microsoft/fetch-event-source');
|
|
8
|
+
|
|
9
|
+
const fetchEventSourceMock = fetchEventSource as jest.MockedFunction<typeof fetchEventSource>;
|
|
10
|
+
|
|
11
|
+
const okResponse = () =>
|
|
12
|
+
({ ok: true, status: 200, headers: { get: () => 'text/event-stream' } }) as unknown as Response;
|
|
13
|
+
const evt = (event: string, data: unknown): EventSourceMessage => ({
|
|
14
|
+
id: '',
|
|
15
|
+
event,
|
|
16
|
+
data: JSON.stringify(data),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/** Mock that opens, exposes init, and stays pending until the client aborts. */
|
|
20
|
+
function captureOpen() {
|
|
21
|
+
const capture: { init?: any; opened: Promise<void> } = { opened: undefined as any };
|
|
22
|
+
capture.opened = new Promise<void>(markOpened => {
|
|
23
|
+
fetchEventSourceMock.mockImplementation((_url, init) => {
|
|
24
|
+
capture.init = init;
|
|
25
|
+
markOpened();
|
|
26
|
+
return Promise.resolve(init.onopen?.(okResponse())).then(
|
|
27
|
+
() =>
|
|
28
|
+
new Promise<void>(resolve => {
|
|
29
|
+
const s: AbortSignal | null | undefined = init.signal;
|
|
30
|
+
if (s?.aborted) {
|
|
31
|
+
resolve();
|
|
32
|
+
} else {
|
|
33
|
+
s?.addEventListener('abort', () => resolve(), { once: true });
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
return capture;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The same custom fetch the regular API client would use (injects auth, etc.). */
|
|
43
|
+
const customFetch = ((_url: any, _init?: any) => Promise.resolve(new Response())) as typeof fetch;
|
|
44
|
+
|
|
45
|
+
const settings: IChatbotClientSettingsTitanChat = {
|
|
46
|
+
version: '2',
|
|
47
|
+
clientId: 'help-center',
|
|
48
|
+
constructorParametersFactory: () => ({
|
|
49
|
+
baseUrl: 'https://api.test/base',
|
|
50
|
+
httpClient: { fetch: customFetch },
|
|
51
|
+
}),
|
|
52
|
+
streamHeadersFactory: () => ({ Authorization: 'Bearer t' }),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
describe('ChatbotApiClient (titan-chat).streamMessage', () => {
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
fetchEventSourceMock.mockReset();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('opens the versioned stream endpoint with X-Client-ID + extra headers and the message body', async () => {
|
|
61
|
+
const capture = captureOpen();
|
|
62
|
+
const client = new ChatbotApiClient(settings);
|
|
63
|
+
|
|
64
|
+
const promise = client.streamMessage(
|
|
65
|
+
new Models.UserMessage({
|
|
66
|
+
sessionId: 9,
|
|
67
|
+
question: 'hi',
|
|
68
|
+
experience: Models.Experience.MultiTurn,
|
|
69
|
+
}),
|
|
70
|
+
{}
|
|
71
|
+
);
|
|
72
|
+
await capture.opened;
|
|
73
|
+
|
|
74
|
+
expect(String((fetchEventSourceMock.mock.calls[0] as any[])[0])).toBe(
|
|
75
|
+
'https://api.test/base/api/v2/message/stream'
|
|
76
|
+
);
|
|
77
|
+
expect(capture.init.method).toBe('POST');
|
|
78
|
+
expect(capture.init.headers).toMatchObject({
|
|
79
|
+
'X-Client-ID': 'help-center',
|
|
80
|
+
'Authorization': 'Bearer t',
|
|
81
|
+
'Accept': 'text/event-stream',
|
|
82
|
+
});
|
|
83
|
+
const sent = JSON.parse(capture.init.body);
|
|
84
|
+
expect(sent.question).toBe('hi');
|
|
85
|
+
expect(sent.sessionId).toBe(9);
|
|
86
|
+
|
|
87
|
+
capture.init.onmessage(evt('run.finished', { seq: 1, status: 'Success', answer: 'done' }));
|
|
88
|
+
await promise;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('forwards the configured httpClient.fetch so SSE uses the same request config as the API', async () => {
|
|
92
|
+
const capture = captureOpen();
|
|
93
|
+
const client = new ChatbotApiClient(settings);
|
|
94
|
+
|
|
95
|
+
const promise = client.streamMessage(
|
|
96
|
+
new Models.UserMessage({ question: 'q', experience: Models.Experience.MultiTurn }),
|
|
97
|
+
{}
|
|
98
|
+
);
|
|
99
|
+
await capture.opened;
|
|
100
|
+
|
|
101
|
+
// fetchEventSource receives the same fetch the regular client uses → auth/headers apply identically.
|
|
102
|
+
expect(capture.init.fetch).toBe(customFetch);
|
|
103
|
+
|
|
104
|
+
capture.init.onmessage(evt('run.finished', { seq: 1, status: 'Success', answer: 'done' }));
|
|
105
|
+
await promise;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('maps agent events and resolves a BotMessage from run.finished', async () => {
|
|
109
|
+
const capture = captureOpen();
|
|
110
|
+
const handlers: AgentStreamHandlers = { onStatus: jest.fn(), onText: jest.fn() };
|
|
111
|
+
const client = new ChatbotApiClient(settings);
|
|
112
|
+
|
|
113
|
+
const promise = client.streamMessage(
|
|
114
|
+
new Models.UserMessage({ question: 'q', experience: Models.Experience.MultiTurn }),
|
|
115
|
+
handlers
|
|
116
|
+
);
|
|
117
|
+
await capture.opened;
|
|
118
|
+
|
|
119
|
+
capture.init.onmessage(evt('status.changed', { seq: 1, text: 'Thinking…' }));
|
|
120
|
+
capture.init.onmessage(evt('text.appended', { seq: 2, text: '✓ done step' }));
|
|
121
|
+
capture.init.onmessage(
|
|
122
|
+
evt('run.finished', { seq: 3, status: 'Success', answer: 'final', sessionId: 5 })
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const bot = await promise;
|
|
126
|
+
expect(handlers.onStatus).toHaveBeenCalledWith('Thinking…');
|
|
127
|
+
expect(handlers.onText).toHaveBeenCalledWith('✓ done step');
|
|
128
|
+
expect(bot).toBeInstanceOf(Models.BotMessage);
|
|
129
|
+
expect(bot.answer).toBe('final');
|
|
130
|
+
expect(bot.sessionId).toBe(5);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('plan.proposed forwards steps and activates the first step by default', async () => {
|
|
134
|
+
const capture = captureOpen();
|
|
135
|
+
const handlers: AgentStreamHandlers = { onPlan: jest.fn(), onStepActive: jest.fn() };
|
|
136
|
+
const client = new ChatbotApiClient(settings);
|
|
137
|
+
|
|
138
|
+
const promise = client.streamMessage(
|
|
139
|
+
new Models.UserMessage({ question: 'q', experience: Models.Experience.MultiTurn }),
|
|
140
|
+
handlers
|
|
141
|
+
);
|
|
142
|
+
await capture.opened;
|
|
143
|
+
|
|
144
|
+
capture.init.onmessage(
|
|
145
|
+
evt('plan.proposed', {
|
|
146
|
+
seq: 1,
|
|
147
|
+
steps: [
|
|
148
|
+
{ id: 'a', title: 'Look up' },
|
|
149
|
+
{ id: 'b', title: 'Answer' },
|
|
150
|
+
],
|
|
151
|
+
})
|
|
152
|
+
);
|
|
153
|
+
capture.init.onmessage(evt('run.finished', { seq: 2, status: 'Success', answer: 'x' }));
|
|
154
|
+
|
|
155
|
+
await promise;
|
|
156
|
+
expect(handlers.onPlan).toHaveBeenCalledWith([
|
|
157
|
+
{ id: 'a', title: 'Look up' },
|
|
158
|
+
{ id: 'b', title: 'Answer' },
|
|
159
|
+
]);
|
|
160
|
+
expect(handlers.onStepActive).toHaveBeenCalledWith('a');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('plan.proposed honors an explicit activeStepId', async () => {
|
|
164
|
+
const capture = captureOpen();
|
|
165
|
+
const handlers: AgentStreamHandlers = { onStepActive: jest.fn() };
|
|
166
|
+
const client = new ChatbotApiClient(settings);
|
|
167
|
+
|
|
168
|
+
const promise = client.streamMessage(
|
|
169
|
+
new Models.UserMessage({ question: 'q', experience: Models.Experience.MultiTurn }),
|
|
170
|
+
handlers
|
|
171
|
+
);
|
|
172
|
+
await capture.opened;
|
|
173
|
+
|
|
174
|
+
capture.init.onmessage(
|
|
175
|
+
evt('plan.proposed', {
|
|
176
|
+
seq: 1,
|
|
177
|
+
activeStepId: 'b',
|
|
178
|
+
steps: [
|
|
179
|
+
{ id: 'a', title: 'Look up' },
|
|
180
|
+
{ id: 'b', title: 'Answer' },
|
|
181
|
+
],
|
|
182
|
+
})
|
|
183
|
+
);
|
|
184
|
+
capture.init.onmessage(evt('run.finished', { seq: 2, status: 'Success', answer: 'x' }));
|
|
185
|
+
|
|
186
|
+
await promise;
|
|
187
|
+
expect(handlers.onStepActive).toHaveBeenCalledWith('b');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('status.changed advances the active step when it carries a stepId', async () => {
|
|
191
|
+
const capture = captureOpen();
|
|
192
|
+
const handlers: AgentStreamHandlers = { onStatus: jest.fn(), onStepActive: jest.fn() };
|
|
193
|
+
const client = new ChatbotApiClient(settings);
|
|
194
|
+
|
|
195
|
+
const promise = client.streamMessage(
|
|
196
|
+
new Models.UserMessage({ question: 'q', experience: Models.Experience.MultiTurn }),
|
|
197
|
+
handlers
|
|
198
|
+
);
|
|
199
|
+
await capture.opened;
|
|
200
|
+
|
|
201
|
+
capture.init.onmessage(evt('status.changed', { seq: 1, text: 'Searching…', stepId: 'b' }));
|
|
202
|
+
capture.init.onmessage(evt('run.finished', { seq: 2, status: 'Success', answer: 'x' }));
|
|
203
|
+
|
|
204
|
+
await promise;
|
|
205
|
+
expect(handlers.onStatus).toHaveBeenCalledWith('Searching…');
|
|
206
|
+
expect(handlers.onStepActive).toHaveBeenCalledWith('b');
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { inject, injectable } from '@servicetitan/react-ioc';
|
|
2
2
|
import { Models } from '..';
|
|
3
|
+
import { AgentStreamHandlers, runAgentStream } from '../../streaming';
|
|
3
4
|
import {
|
|
4
5
|
CHATBOT_CLIENT_SETTINGS,
|
|
5
6
|
FirstParameterType,
|
|
@@ -10,6 +11,9 @@ import {
|
|
|
10
11
|
// eslint-disable-next-line no-restricted-imports
|
|
11
12
|
import { Client } from './native-client';
|
|
12
13
|
|
|
14
|
+
/** Streaming endpoint template, mirroring the generated client's `/api/v{version}/message`. */
|
|
15
|
+
const AGENT_STREAM_PATH_TEMPLATE = '/api/v{version}/message/stream';
|
|
16
|
+
|
|
13
17
|
/**
|
|
14
18
|
* Settings for the chatbot client, which includes a factory for constructor parameters, and additional settings
|
|
15
19
|
* like version and clientId which are used in the API calls.
|
|
@@ -21,6 +25,11 @@ export interface IChatbotClientSettingsTitanChat extends IChatbotClientSettings
|
|
|
21
25
|
};
|
|
22
26
|
version: string;
|
|
23
27
|
clientId: string;
|
|
28
|
+
/**
|
|
29
|
+
* Extra headers for the SSE streaming request. `X-Client-ID` is always sent (from `clientId`);
|
|
30
|
+
* use this for anything the fetch http client would otherwise add (e.g. Authorization). Optional.
|
|
31
|
+
*/
|
|
32
|
+
streamHeadersFactory?: () => Record<string, string>;
|
|
24
33
|
}
|
|
25
34
|
|
|
26
35
|
/**
|
|
@@ -30,6 +39,10 @@ export interface IChatbotClientSettingsTitanChat extends IChatbotClientSettings
|
|
|
30
39
|
@injectable()
|
|
31
40
|
export class ChatbotApiClient implements IChatbotApiClient {
|
|
32
41
|
private readonly client: Client;
|
|
42
|
+
/** Same base URL the generated `Client` uses, so streaming URLs are built identically. */
|
|
43
|
+
private readonly baseUrl: string;
|
|
44
|
+
/** Same `fetch` the generated `Client` uses, so the SSE request gets identical auth/headers. */
|
|
45
|
+
private readonly httpFetch?: typeof fetch;
|
|
33
46
|
|
|
34
47
|
get version(): string {
|
|
35
48
|
return this.chatbotApiClientSettings.version;
|
|
@@ -46,6 +59,13 @@ export class ChatbotApiClient implements IChatbotApiClient {
|
|
|
46
59
|
const { baseUrl, httpClient } =
|
|
47
60
|
this.chatbotApiClientSettings.constructorParametersFactory();
|
|
48
61
|
this.client = new Client(baseUrl, httpClient);
|
|
62
|
+
// Mirror the generated client's `this.baseUrl = baseUrl ?? ""`.
|
|
63
|
+
this.baseUrl = baseUrl ?? '';
|
|
64
|
+
/*
|
|
65
|
+
* Reuse the same fetch the generated client uses, so the SSE request goes through the
|
|
66
|
+
* consumer's request configuration (auth token, X-HC-Instance, credentials, …).
|
|
67
|
+
*/
|
|
68
|
+
this.httpFetch = httpClient?.fetch as typeof fetch | undefined;
|
|
49
69
|
}
|
|
50
70
|
|
|
51
71
|
postFeedback(body: Models.Feedback, abortSignal?: AbortSignal) {
|
|
@@ -56,6 +76,32 @@ export class ChatbotApiClient implements IChatbotApiClient {
|
|
|
56
76
|
return this.client.message(this.version, this.clientId, body, abortSignal);
|
|
57
77
|
}
|
|
58
78
|
|
|
79
|
+
streamMessage(
|
|
80
|
+
body: Models.IUserMessage,
|
|
81
|
+
handlers: AgentStreamHandlers,
|
|
82
|
+
abortSignal?: AbortSignal
|
|
83
|
+
): Promise<Models.BotMessage> {
|
|
84
|
+
/*
|
|
85
|
+
* Build the URL exactly as the generated Client does for `/api/v{version}/message`:
|
|
86
|
+
* substitute {version}, then strip a dangling query separator.
|
|
87
|
+
*/
|
|
88
|
+
const url = (this.baseUrl + AGENT_STREAM_PATH_TEMPLATE)
|
|
89
|
+
.replace('{version}', encodeURIComponent('' + this.version))
|
|
90
|
+
.replace(/[?&]$/, '');
|
|
91
|
+
|
|
92
|
+
return runAgentStream({
|
|
93
|
+
url,
|
|
94
|
+
fetch: this.httpFetch,
|
|
95
|
+
headers: {
|
|
96
|
+
'X-Client-ID': this.clientId,
|
|
97
|
+
...(this.chatbotApiClientSettings.streamHeadersFactory?.() ?? {}),
|
|
98
|
+
},
|
|
99
|
+
body,
|
|
100
|
+
handlers,
|
|
101
|
+
abortSignal,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
59
105
|
postFollowUpEmail(body: Models.UserMessage, abortSignal?: AbortSignal) {
|
|
60
106
|
return this.client.followUpEmail(this.version, this.clientId, body, abortSignal);
|
|
61
107
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
/* eslint-disable no-restricted-imports */
|
|
2
2
|
export * from './api-client';
|
|
3
3
|
export * from './stores';
|
|
4
|
+
export * from './streaming';
|
|
4
5
|
export type * from './models';
|
|
6
|
+
// Runtime helpers from models (the `export type *` above is type-only).
|
|
7
|
+
export {
|
|
8
|
+
isChatbotStreamingEnabled,
|
|
9
|
+
DEFAULT_STREAMING_INACTIVITY_TIMEOUT_MS,
|
|
10
|
+
} from './models/chatbot-customizations';
|
|
5
11
|
export * from './hooks/use-customization-chatbot';
|
|
6
|
-
export * as NativeClientHC from './api-client/help-center/native-client';
|
|
7
12
|
export * as NativeClientTC from './api-client/titan-chat/native-client';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, test } from '@jest/globals';
|
|
2
|
+
import { ChatbotCustomizations, isChatbotStreamingEnabled } from '../chatbot-customizations';
|
|
3
|
+
|
|
4
|
+
describe('ChatbotCustomizations streaming setting', () => {
|
|
5
|
+
test('streaming is disabled when customizations are undefined', () => {
|
|
6
|
+
expect(isChatbotStreamingEnabled(undefined)).toBe(false);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test('streaming is disabled when the streaming setting is absent', () => {
|
|
10
|
+
const c: ChatbotCustomizations = { feedback: { title: 'x' } };
|
|
11
|
+
expect(isChatbotStreamingEnabled(c)).toBe(false);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('streaming is disabled when explicitly false', () => {
|
|
15
|
+
const c: ChatbotCustomizations = { streaming: { enabled: false } };
|
|
16
|
+
expect(isChatbotStreamingEnabled(c)).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('streaming is enabled only when explicitly true', () => {
|
|
20
|
+
const c: ChatbotCustomizations = {
|
|
21
|
+
streaming: { enabled: true, inactivityTimeoutMs: 16_000 },
|
|
22
|
+
};
|
|
23
|
+
expect(isChatbotStreamingEnabled(c)).toBe(true);
|
|
24
|
+
expect(c.streaming?.inactivityTimeoutMs).toBe(16_000);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -10,6 +10,19 @@ export type FilterDefaultSelectionFn = (filterKey: string, optionKey: string) =>
|
|
|
10
10
|
|
|
11
11
|
export type ChatbotCustomizations = ChatCustomizations &
|
|
12
12
|
Partial<{
|
|
13
|
+
/**
|
|
14
|
+
* Agent-progress SSE streaming. Disabled by default; when disabled (or when the streaming
|
|
15
|
+
* endpoint is unreachable at connect time) the widget falls back to the non-streaming path.
|
|
16
|
+
*/
|
|
17
|
+
streaming?: {
|
|
18
|
+
/** Opt in to the SSE streaming send-path. Defaults to false. */
|
|
19
|
+
enabled?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Inactivity threshold (ms) before the "Still working on it…" keepalive is shown.
|
|
22
|
+
* Defaults to 16000 (matching the legacy hard timeout).
|
|
23
|
+
*/
|
|
24
|
+
inactivityTimeoutMs?: number;
|
|
25
|
+
};
|
|
13
26
|
timeouts?: {
|
|
14
27
|
chatbotRequestTimeoutMs?: number;
|
|
15
28
|
};
|
|
@@ -31,3 +44,10 @@ export type ChatbotCustomizations = ChatCustomizations &
|
|
|
31
44
|
isDefaultSelected?: FilterDefaultSelectionFn;
|
|
32
45
|
};
|
|
33
46
|
}>;
|
|
47
|
+
|
|
48
|
+
/** Default inactivity threshold before the keepalive message is shown (ms). */
|
|
49
|
+
export const DEFAULT_STREAMING_INACTIVITY_TIMEOUT_MS = 16_000;
|
|
50
|
+
|
|
51
|
+
/** Whether the SSE streaming send-path is enabled. Defaults to off. */
|
|
52
|
+
export const isChatbotStreamingEnabled = (customizations?: ChatbotCustomizations): boolean =>
|
|
53
|
+
customizations?.streaming?.enabled ?? false;
|
package/src/models/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export * from './chatbot-customizations';
|