@memberjunction/ng-conversations 6.2.0-edge.1 → 6.2.0-edge.2
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/README.md +9 -1
- package/dist/lib/components/conversation/conversation-agent-picker.component.js +1 -1
- package/dist/lib/components/conversation/conversation-agent-picker.component.js.map +1 -1
- package/dist/lib/components/conversation/conversation-chat-area.component.d.ts +102 -2
- package/dist/lib/components/conversation/conversation-chat-area.component.d.ts.map +1 -1
- package/dist/lib/components/conversation/conversation-chat-area.component.js +457 -119
- package/dist/lib/components/conversation/conversation-chat-area.component.js.map +1 -1
- package/dist/lib/components/conversation/pinned-messages-panel.component.d.ts +3 -1
- package/dist/lib/components/conversation/pinned-messages-panel.component.d.ts.map +1 -1
- package/dist/lib/components/conversation/pinned-messages-panel.component.js +22 -8
- package/dist/lib/components/conversation/pinned-messages-panel.component.js.map +1 -1
- package/dist/lib/components/message/actionable-commands.component.d.ts +12 -0
- package/dist/lib/components/message/actionable-commands.component.d.ts.map +1 -1
- package/dist/lib/components/message/actionable-commands.component.js +60 -12
- package/dist/lib/components/message/actionable-commands.component.js.map +1 -1
- package/dist/lib/components/message/message-input.component.d.ts +68 -4
- package/dist/lib/components/message/message-input.component.d.ts.map +1 -1
- package/dist/lib/components/message/message-input.component.js +161 -22
- package/dist/lib/components/message/message-input.component.js.map +1 -1
- package/dist/lib/components/message/message-item.component.d.ts +80 -1
- package/dist/lib/components/message/message-item.component.d.ts.map +1 -1
- package/dist/lib/components/message/message-item.component.js +193 -23
- package/dist/lib/components/message/message-item.component.js.map +1 -1
- package/dist/lib/components/message/message-list.component.d.ts +10 -1
- package/dist/lib/components/message/message-list.component.d.ts.map +1 -1
- package/dist/lib/components/message/message-list.component.js +38 -1
- package/dist/lib/components/message/message-list.component.js.map +1 -1
- package/dist/lib/components/workspace/conversation-workspace.component.d.ts.map +1 -1
- package/dist/lib/components/workspace/conversation-workspace.component.js +4 -1
- package/dist/lib/components/workspace/conversation-workspace.component.js.map +1 -1
- package/dist/lib/models/agent-turn.model.d.ts +12 -2
- package/dist/lib/models/agent-turn.model.d.ts.map +1 -1
- package/dist/lib/models/agent-turn.model.js.map +1 -1
- package/dist/lib/services/conversation-agent.service.d.ts +21 -0
- package/dist/lib/services/conversation-agent.service.d.ts.map +1 -1
- package/dist/lib/services/conversation-agent.service.js +28 -2
- package/dist/lib/services/conversation-agent.service.js.map +1 -1
- package/dist/lib/services/conversation-liveness-dom.service.d.ts +42 -0
- package/dist/lib/services/conversation-liveness-dom.service.d.ts.map +1 -0
- package/dist/lib/services/conversation-liveness-dom.service.js +74 -0
- package/dist/lib/services/conversation-liveness-dom.service.js.map +1 -0
- package/dist/lib/services/ui-command-handler.service.d.ts +40 -3
- package/dist/lib/services/ui-command-handler.service.d.ts.map +1 -1
- package/dist/lib/services/ui-command-handler.service.js +78 -9
- package/dist/lib/services/ui-command-handler.service.js.map +1 -1
- package/dist/lib/utils/agent-turn-routing.d.ts +7 -6
- package/dist/lib/utils/agent-turn-routing.d.ts.map +1 -1
- package/dist/lib/utils/agent-turn-routing.js +4 -16
- package/dist/lib/utils/agent-turn-routing.js.map +1 -1
- package/dist/lib/utils/decision-routing.d.ts +68 -0
- package/dist/lib/utils/decision-routing.d.ts.map +1 -0
- package/dist/lib/utils/decision-routing.js +101 -0
- package/dist/lib/utils/decision-routing.js.map +1 -0
- package/dist/public-api.d.ts +2 -0
- package/dist/public-api.d.ts.map +1 -1
- package/dist/public-api.js +5 -0
- package/dist/public-api.js.map +1 -1
- package/package.json +32 -32
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conversation-liveness-dom.service.js","sourceRoot":"","sources":["../../../src/lib/services/conversation-liveness-dom.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,uCAAuC,CAAC;;AAE7E;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,OAAO,8BAA8B;IACvC;;;;;OAKG;IACK,sBAAsB,GAAwB,IAAI,CAAC;IACnD,kBAAkB,GAAwB,IAAI,CAAC;IAC/C,OAAO,GAAG,KAAK,CAAC;IAExB,sEAAsE;IAC/D,KAAK;QACR,IAAI,IAAI,CAAC,OAAO,IAAI,OAAO,QAAQ,KAAK,WAAW,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;YACnF,OAAO;QACX,CAAC;QAED,IAAI,CAAC,sBAAsB,GAAG,GAAG,EAAE;YAC/B,oFAAoF;YACpF,4DAA4D;YAC5D,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;gBACnB,oBAAoB,CAAC,QAAQ,CAAC,QAAQ,CAAC,gBAAgB,EAAE,CAAC;YAC9D,CAAC;QACL,CAAC,CAAC;QACF,IAAI,CAAC,kBAAkB,GAAG,GAAG,EAAE,CAAC,oBAAoB,CAAC,QAAQ,CAAC,QAAQ,CAAC,mBAAmB,EAAE,CAAC;QAE7F,QAAQ,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,IAAI,CAAC,sBAAsB,CAAC,CAAC;QAC3E,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,IAAI,CAAC,kBAAkB,CAAC,CAAC;QAC3D,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;IACxB,CAAC;IAED,8DAA8D;IACvD,IAAI;QACP,IAAI,IAAI,CAAC,sBAAsB,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,CAAC;YACjE,QAAQ,CAAC,mBAAmB,CAAC,kBAAkB,EAAE,IAAI,CAAC,sBAAsB,CAAC,CAAC;YAC9E,IAAI,CAAC,sBAAsB,GAAG,IAAI,CAAC;QACvC,CAAC;QACD,IAAI,IAAI,CAAC,kBAAkB,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;YAC3D,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,IAAI,CAAC,kBAAkB,CAAC,CAAC;YAC9D,IAAI,CAAC,kBAAkB,GAAG,IAAI,CAAC;QACnC,CAAC;QACD,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC;IACzB,CAAC;IAEM,WAAW;QACd,IAAI,CAAC,IAAI,EAAE,CAAC;IAChB,CAAC;wHA9CQ,8BAA8B;gEAA9B,8BAA8B,WAA9B,8BAA8B,mBADjB,MAAM;;iFACnB,8BAA8B;cAD1C,UAAU;eAAC,EAAE,UAAU,EAAE,MAAM,EAAE","sourcesContent":["import { Injectable, OnDestroy } from '@angular/core';\nimport { ConversationsRuntime } from '@memberjunction/conversations-runtime';\n\n/**\n * Translates browser events into liveness signals for the framework-agnostic\n * `ConversationLiveness` supervisor in `@memberjunction/conversations-runtime` (MJ #4222).\n *\n * WHY THE SPLIT. The supervisor lives at L0 because that package already depends on\n * `@memberjunction/graphql-dataprovider` and has no Angular dependency — and because the\n * obvious alternative is closed to us: Explorer's `ServerConnectivityService` does almost\n * exactly this, but `@memberjunction/ng-conversations` declares `\"mjUILayer\": \"widgets\"`, and\n * `packages/Standards/src/checks/ui-layers.ts` forbids that layer from importing any\n * `ng-explorer*` package at severity `error`. L0 cannot own the DOM half either: the runtime is\n * documented as Node-consumable, so `document` and `window` may not exist there. Hence this thin\n * Angular-side adapter, which is the only piece that touches the DOM.\n *\n * WHAT THE SIGNALS MEAN. Neither event proves anything is reachable — `navigator.onLine` reads\n * true behind a captive portal or a dead VPN, and a tab regaining focus says nothing about the\n * server. They are prompts to re-check, and the supervisor coalesces them with the transport-level\n * signals before anyone acts.\n *\n * Root-provided so one instance serves every chat area; several open conversations must not each\n * raise their own reconciliation for the same lid-open.\n */\n@Injectable({ providedIn: 'root' })\nexport class ConversationLivenessDomService implements OnDestroy {\n /**\n * Stored bound references — `removeEventListener` only detaches a listener when handed the\n * same function object `addEventListener` received. Matches the pattern already used for the\n * resize handlers in `conversation-chat-area.component.ts` and in\n * `packages/Angular/Generic/shared/src/lib/theme.service.ts`.\n */\n private boundVisibilityHandler: (() => void) | null = null;\n private boundOnlineHandler: (() => void) | null = null;\n private started = false;\n\n /** Begin translating DOM events into liveness signals. Idempotent. */\n public Start(): void {\n if (this.started || typeof document === 'undefined' || typeof window === 'undefined') {\n return;\n }\n\n this.boundVisibilityHandler = () => {\n // Only the hidden → visible edge matters. A tab going away needs no reconciliation,\n // and signalling on both edges would double every recovery.\n if (!document.hidden) {\n ConversationsRuntime.Instance.Liveness.NotifyTabVisible();\n }\n };\n this.boundOnlineHandler = () => ConversationsRuntime.Instance.Liveness.NotifyBrowserOnline();\n\n document.addEventListener('visibilitychange', this.boundVisibilityHandler);\n window.addEventListener('online', this.boundOnlineHandler);\n this.started = true;\n }\n\n /** Detach both listeners. Safe to call when never started. */\n public Stop(): void {\n if (this.boundVisibilityHandler && typeof document !== 'undefined') {\n document.removeEventListener('visibilitychange', this.boundVisibilityHandler);\n this.boundVisibilityHandler = null;\n }\n if (this.boundOnlineHandler && typeof window !== 'undefined') {\n window.removeEventListener('online', this.boundOnlineHandler);\n this.boundOnlineHandler = null;\n }\n this.started = false;\n }\n\n public ngOnDestroy(): void {\n this.Stop();\n }\n}\n"]}
|
|
@@ -6,18 +6,32 @@ export interface ActionableCommandRequest {
|
|
|
6
6
|
command: ActionableCommand;
|
|
7
7
|
conversationId?: string | null;
|
|
8
8
|
conversationDetailId?: string | null;
|
|
9
|
+
/**
|
|
10
|
+
* Set only on the compose:email over-length fallback: whether the draft body is now on the
|
|
11
|
+
* user's clipboard. The copy overwrites whatever the user had copied, so the host must say it
|
|
12
|
+
* happened, and must not claim it when this is false (no body, clipboard unavailable over plain
|
|
13
|
+
* HTTP, or denied by permissions policy).
|
|
14
|
+
*/
|
|
15
|
+
DraftCopiedToClipboard?: boolean;
|
|
9
16
|
}
|
|
10
17
|
/**
|
|
11
18
|
* Service for handling UI commands from agents.
|
|
12
19
|
*
|
|
13
|
-
* Generic commands (open:url
|
|
14
|
-
* App-specific commands (open:resource
|
|
20
|
+
* Generic commands (open:url, and compose:email while it fits in a mailto: URL) are handled
|
|
21
|
+
* directly by this service. App-specific commands (open:resource, and the compose:email
|
|
22
|
+
* over-length fallback) are emitted for the host application to handle.
|
|
15
23
|
*/
|
|
16
24
|
export declare class UICommandHandlerService {
|
|
17
25
|
private dataCacheService;
|
|
18
26
|
/**
|
|
19
27
|
* Event emitted when an actionable command requires host-app handling.
|
|
20
|
-
*
|
|
28
|
+
*
|
|
29
|
+
* open:resource is always emitted. compose:email is emitted ONLY as a fallback, when the draft
|
|
30
|
+
* is too long for a mailto: URL and the host must open the draft artifact instead; a draft
|
|
31
|
+
* within the limit is handled here and never reaches the host. On that fallback the host also
|
|
32
|
+
* owns telling the user why their mail client did not open, because only the host knows whether
|
|
33
|
+
* a draft artifact opened (see {@link ActionableCommandRequest.DraftCopiedToClipboard}).
|
|
34
|
+
* open:url is always handled here.
|
|
21
35
|
*/
|
|
22
36
|
ActionableCommandRequested: EventEmitter<ActionableCommandRequest>;
|
|
23
37
|
/** @deprecated Use {@link ActionableCommandRequested}. */
|
|
@@ -46,6 +60,29 @@ export declare class UICommandHandlerService {
|
|
|
46
60
|
* Data URIs are converted to Blob URLs because Chrome blocks window.open with data: URIs.
|
|
47
61
|
*/
|
|
48
62
|
private handleOpenUrl;
|
|
63
|
+
/**
|
|
64
|
+
* Best-effort copy of an over-length draft's body, so the text is not lost even when the host
|
|
65
|
+
* cannot open the draft artifact.
|
|
66
|
+
*
|
|
67
|
+
* Never rejects, so a clipboard that is unavailable (plain HTTP) or denied by permissions policy
|
|
68
|
+
* cannot stop the fallback. Nothing on the click path awaits before writeText is called, so it
|
|
69
|
+
* still runs inside the click's user activation; the fallback then awaits the outcome only so
|
|
70
|
+
* the host's notice can say whether the copy happened.
|
|
71
|
+
*
|
|
72
|
+
* @returns true only when the body is on the clipboard, so the host never claims a copy that
|
|
73
|
+
* did not happen.
|
|
74
|
+
*/
|
|
75
|
+
private copyDraftBody;
|
|
76
|
+
/**
|
|
77
|
+
* Open the user's own mail client with a compose:email draft's fields pre-filled, via a
|
|
78
|
+
* synthesized anchor click.
|
|
79
|
+
*
|
|
80
|
+
* NOTHING IS SENT HERE. The agent drafted; the user sends. This only opens a compose window.
|
|
81
|
+
*
|
|
82
|
+
* Deliberately not window.open: Chrome treats window.open with a non-http scheme as a popup and
|
|
83
|
+
* strands an about:blank tab behind the compose window.
|
|
84
|
+
*/
|
|
85
|
+
private openMailto;
|
|
49
86
|
/**
|
|
50
87
|
* Open a data URI by converting it to a Blob URL.
|
|
51
88
|
* Chrome blocks window.open('data:...') for security, so we create a Blob URL instead.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui-command-handler.service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/ui-command-handler.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,YAAY,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,
|
|
1
|
+
{"version":3,"file":"ui-command-handler.service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/ui-command-handler.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,YAAY,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,EACL,iBAAiB,EACjB,gBAAgB,EAKjB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;;AAExD,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,iBAAiB,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,oBAAoB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED;;;;;;GAMG;AACH,qBAGa,uBAAuB;IAsChC,OAAO,CAAC,gBAAgB;IArC1B;;;;;;;;;OASG;IACI,0BAA0B,yCAAgD;IAEjF,0DAA0D;IAC1D,IAAW,0BAA0B,2CAEpC;IACD,0DAA0D;IAC1D,IAAW,0BAA0B,CAAC,KAAK,wCAAA,EAE1C;IAED;;;OAGG;IACI,yBAAyB,iCAAwC;IAExE,yDAAyD;IACzD,IAAW,yBAAyB,mCAEnC;IACD,yDAAyD;IACzD,IAAW,yBAAyB,CAAC,KAAK,gCAAA,EAEzC;gBAGS,gBAAgB,EAAE,gBAAgB;IAG5C;;;OAGG;IACU,wBAAwB,CAAC,OAAO,EAAE,iBAAiB,EAAE,MAAM,CAAC,EAAE,IAAI,CAAC,wBAAwB,EAAE,SAAS,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAqCpI,wDAAwD;IAC3C,wBAAwB,CAAC,OAAO,EAAE,iBAAiB,EAAE,MAAM,CAAC,EAAE,IAAI,CAAC,wBAAwB,EAAE,SAAS,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAIpI;;;OAGG;IACH,OAAO,CAAC,aAAa;IAYrB;;;;;;;;;;;OAWG;YACW,aAAa;IAY3B;;;;;;;;OAQG;IACH,OAAO,CAAC,UAAU;IASlB;;;OAGG;IACH,OAAO,CAAC,WAAW;IAgBnB;;;OAGG;IACU,uBAAuB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAa9E,uDAAuD;IAC1C,uBAAuB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9E;;OAEG;IACU,wBAAwB,CAAC,QAAQ,EAAE,gBAAgB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAclF,wDAAwD;IAC3C,wBAAwB,CAAC,QAAQ,EAAE,gBAAgB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlF;;;OAGG;YACW,iBAAiB;yCArNpB,uBAAuB;6CAAvB,uBAAuB;CAsOnC"}
|
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
import { Injectable, EventEmitter } from '@angular/core';
|
|
2
|
+
import { BuildMailtoURL } from '@memberjunction/ai-core-plus';
|
|
2
3
|
import * as i0 from "@angular/core";
|
|
3
4
|
import * as i1 from "./data-cache.service";
|
|
4
5
|
/**
|
|
5
6
|
* Service for handling UI commands from agents.
|
|
6
7
|
*
|
|
7
|
-
* Generic commands (open:url
|
|
8
|
-
* App-specific commands (open:resource
|
|
8
|
+
* Generic commands (open:url, and compose:email while it fits in a mailto: URL) are handled
|
|
9
|
+
* directly by this service. App-specific commands (open:resource, and the compose:email
|
|
10
|
+
* over-length fallback) are emitted for the host application to handle.
|
|
9
11
|
*/
|
|
10
12
|
export class UICommandHandlerService {
|
|
11
13
|
dataCacheService;
|
|
12
14
|
/**
|
|
13
15
|
* Event emitted when an actionable command requires host-app handling.
|
|
14
|
-
*
|
|
16
|
+
*
|
|
17
|
+
* open:resource is always emitted. compose:email is emitted ONLY as a fallback, when the draft
|
|
18
|
+
* is too long for a mailto: URL and the host must open the draft artifact instead; a draft
|
|
19
|
+
* within the limit is handled here and never reaches the host. On that fallback the host also
|
|
20
|
+
* owns telling the user why their mail client did not open, because only the host knows whether
|
|
21
|
+
* a draft artifact opened (see {@link ActionableCommandRequest.DraftCopiedToClipboard}).
|
|
22
|
+
* open:url is always handled here.
|
|
15
23
|
*/
|
|
16
24
|
ActionableCommandRequested = new EventEmitter();
|
|
17
25
|
/** @deprecated Use {@link ActionableCommandRequested}. */
|
|
@@ -45,16 +53,36 @@ export class UICommandHandlerService {
|
|
|
45
53
|
async ExecuteActionableCommand(command, origin) {
|
|
46
54
|
if (command.type === 'open:url') {
|
|
47
55
|
this.handleOpenUrl(command);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const request = {
|
|
59
|
+
command,
|
|
60
|
+
conversationId: origin?.conversationId ?? null,
|
|
61
|
+
conversationDetailId: origin?.conversationDetailId ?? null
|
|
62
|
+
};
|
|
63
|
+
if (command.type === 'compose:email') {
|
|
64
|
+
const { url, withinLimit } = BuildMailtoURL(command);
|
|
65
|
+
if (withinLimit) {
|
|
66
|
+
this.openMailto(url);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
// Too long for a mailto: URL. We do NOT open it: a mail client past its limit does not
|
|
70
|
+
// refuse the URL, it opens a draft with the body SILENTLY TRUNCATED and the user sends half
|
|
71
|
+
// a message without noticing. Falls through to the host, which opens the full draft
|
|
72
|
+
// artifact and tells the user what happened.
|
|
73
|
+
request.DraftCopiedToClipboard = await this.copyDraftBody(command);
|
|
74
|
+
}
|
|
75
|
+
// open:resource (and the compose:email fallback above) require app-specific navigation.
|
|
76
|
+
// compose:email is logged by TYPE ONLY: its body is free-text member correspondence, and the
|
|
77
|
+
// whole command object would otherwise land in the browser console and any console-forwarding
|
|
78
|
+
// telemetry.
|
|
79
|
+
if (command.type === 'compose:email') {
|
|
80
|
+
console.log('📤 Emitting actionable command for host app:', command.type);
|
|
48
81
|
}
|
|
49
82
|
else {
|
|
50
|
-
// open:resource requires app-specific navigation — emit for host to handle
|
|
51
83
|
console.log('📤 Emitting actionable command for host app:', command);
|
|
52
|
-
this.ActionableCommandRequested.emit({
|
|
53
|
-
command,
|
|
54
|
-
conversationId: origin?.conversationId ?? null,
|
|
55
|
-
conversationDetailId: origin?.conversationDetailId ?? null
|
|
56
|
-
});
|
|
57
84
|
}
|
|
85
|
+
this.ActionableCommandRequested.emit(request);
|
|
58
86
|
}
|
|
59
87
|
/** @deprecated Use {@link ExecuteActionableCommand}. */
|
|
60
88
|
async executeActionableCommand(command, origin) {
|
|
@@ -75,6 +103,47 @@ export class UICommandHandlerService {
|
|
|
75
103
|
window.open(url, target, target === '_blank' ? 'noopener,noreferrer' : undefined);
|
|
76
104
|
}
|
|
77
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Best-effort copy of an over-length draft's body, so the text is not lost even when the host
|
|
108
|
+
* cannot open the draft artifact.
|
|
109
|
+
*
|
|
110
|
+
* Never rejects, so a clipboard that is unavailable (plain HTTP) or denied by permissions policy
|
|
111
|
+
* cannot stop the fallback. Nothing on the click path awaits before writeText is called, so it
|
|
112
|
+
* still runs inside the click's user activation; the fallback then awaits the outcome only so
|
|
113
|
+
* the host's notice can say whether the copy happened.
|
|
114
|
+
*
|
|
115
|
+
* @returns true only when the body is on the clipboard, so the host never claims a copy that
|
|
116
|
+
* did not happen.
|
|
117
|
+
*/
|
|
118
|
+
async copyDraftBody(command) {
|
|
119
|
+
if (!command.body || !navigator.clipboard) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
await navigator.clipboard.writeText(command.body);
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Open the user's own mail client with a compose:email draft's fields pre-filled, via a
|
|
132
|
+
* synthesized anchor click.
|
|
133
|
+
*
|
|
134
|
+
* NOTHING IS SENT HERE. The agent drafted; the user sends. This only opens a compose window.
|
|
135
|
+
*
|
|
136
|
+
* Deliberately not window.open: Chrome treats window.open with a non-http scheme as a popup and
|
|
137
|
+
* strands an about:blank tab behind the compose window.
|
|
138
|
+
*/
|
|
139
|
+
openMailto(url) {
|
|
140
|
+
const a = document.createElement('a');
|
|
141
|
+
a.href = url;
|
|
142
|
+
a.style.display = 'none';
|
|
143
|
+
document.body.appendChild(a);
|
|
144
|
+
a.click();
|
|
145
|
+
document.body.removeChild(a);
|
|
146
|
+
}
|
|
78
147
|
/**
|
|
79
148
|
* Open a data URI by converting it to a Blob URL.
|
|
80
149
|
* Chrome blocks window.open('data:...') for security, so we create a Blob URL instead.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui-command-handler.service.js","sourceRoot":"","sources":["../../../src/lib/services/ui-command-handler.service.ts"],"names":[],"mappings":"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","sourcesContent":["import { Injectable, EventEmitter } from '@angular/core';\nimport { ActionableCommand, AutomaticCommand, RefreshDataCommand, OpenURLCommand } from '@memberjunction/ai-core-plus';\nimport { DataCacheService } from './data-cache.service';\n\nexport interface ActionableCommandRequest {\n command: ActionableCommand;\n conversationId?: string | null;\n conversationDetailId?: string | null;\n}\n\n/**\n * Service for handling UI commands from agents.\n *\n * Generic commands (open:url) are handled directly by this service.\n * App-specific commands (open:resource) are emitted for the host application to handle.\n */\n@Injectable({\n providedIn: 'root'\n})\nexport class UICommandHandlerService {\n /**\n * Event emitted when an actionable command requires host-app handling.\n * Currently only open:resource commands are emitted — open:url is handled directly.\n */\n public ActionableCommandRequested = new EventEmitter<ActionableCommandRequest>();\n\n /** @deprecated Use {@link ActionableCommandRequested}. */\n public get actionableCommandRequested() {\n return this.ActionableCommandRequested;\n }\n /** @deprecated Use {@link ActionableCommandRequested}. */\n public set actionableCommandRequested(value) {\n this.ActionableCommandRequested = value;\n }\n\n /**\n * Event emitted when an automatic command should be executed\n * Host application should subscribe to this and handle the command appropriately\n */\n public AutomaticCommandRequested = new EventEmitter<AutomaticCommand>();\n\n /** @deprecated Use {@link AutomaticCommandRequested}. */\n public get automaticCommandRequested() {\n return this.AutomaticCommandRequested;\n }\n /** @deprecated Use {@link AutomaticCommandRequested}. */\n public set automaticCommandRequested(value) {\n this.AutomaticCommandRequested = value;\n }\n\n constructor(\n private dataCacheService: DataCacheService\n ) {}\n\n /**\n * Execute an actionable command (triggered by user clicking a button).\n * Generic commands like open:url are handled directly; others are emitted to the host.\n */\n public async ExecuteActionableCommand(command: ActionableCommand, origin?: Omit<ActionableCommandRequest, 'command'>): Promise<void> {\n if (command.type === 'open:url') {\n this.handleOpenUrl(command);\n } else {\n // open:resource requires app-specific navigation — emit for host to handle\n console.log('📤 Emitting actionable command for host app:', command);\n this.ActionableCommandRequested.emit({\n command,\n conversationId: origin?.conversationId ?? null,\n conversationDetailId: origin?.conversationDetailId ?? null\n });\n }\n }\n\n /** @deprecated Use {@link ExecuteActionableCommand}. */\n public async executeActionableCommand(command: ActionableCommand, origin?: Omit<ActionableCommandRequest, 'command'>): Promise<void> {\n return this.ExecuteActionableCommand(command, origin);\n }\n\n /**\n * Handle open:url commands directly by opening the URL in a browser tab.\n * Data URIs are converted to Blob URLs because Chrome blocks window.open with data: URIs.\n */\n private handleOpenUrl(command: OpenURLCommand): void {\n const url = command.url;\n const newTab = command.newTab !== false;\n\n if (url.startsWith('data:')) {\n this.openDataUri(url);\n } else {\n const target = newTab ? '_blank' : '_self';\n window.open(url, target, target === '_blank' ? 'noopener,noreferrer' : undefined);\n }\n }\n\n /**\n * Open a data URI by converting it to a Blob URL.\n * Chrome blocks window.open('data:...') for security, so we create a Blob URL instead.\n */\n private openDataUri(dataUri: string): void {\n const [header, base64Data] = dataUri.split(',');\n const mimeMatch = header.match(/data:([^;]+)/);\n const mimeType = mimeMatch ? mimeMatch[1] : 'application/octet-stream';\n\n const byteString = atob(base64Data);\n const byteArray = new Uint8Array(byteString.length);\n for (let i = 0; i < byteString.length; i++) {\n byteArray[i] = byteString.charCodeAt(i);\n }\n\n const blob = new Blob([byteArray], { type: mimeType });\n const blobUrl = URL.createObjectURL(blob);\n window.open(blobUrl, '_blank');\n }\n\n /**\n * Execute an automatic command (runs immediately without user interaction)\n * Special handling: refresh:data commands execute locally, others emit to host\n */\n public async ExecuteAutomaticCommand(command: AutomaticCommand): Promise<void> {\n console.log('Executing automatic command:', command);\n\n if (command.type === 'refresh:data') {\n // Handle data refresh locally as it's a generic operation\n await this.handleRefreshData(command);\n } else {\n // Emit other automatic commands (like notifications) for host to handle\n console.log('📤 Emitting automatic command for host app:', command);\n this.AutomaticCommandRequested.emit(command);\n }\n }\n\n /** @deprecated Use {@link ExecuteAutomaticCommand}. */\n public async executeAutomaticCommand(command: AutomaticCommand): Promise<void> {\n return this.ExecuteAutomaticCommand(command);\n }\n\n /**\n * Execute all automatic commands from an agent result\n */\n public async ExecuteAutomaticCommands(commands: AutomaticCommand[]): Promise<void> {\n if (!commands || commands.length === 0) {\n return;\n }\n\n for (const command of commands) {\n try {\n await this.ExecuteAutomaticCommand(command);\n } catch (error) {\n console.error('Error executing automatic command:', command, error);\n }\n }\n }\n\n /** @deprecated Use {@link ExecuteAutomaticCommands}. */\n public async executeAutomaticCommands(commands: AutomaticCommand[]): Promise<void> {\n return this.ExecuteAutomaticCommands(commands);\n }\n\n /**\n * Handle refreshing data (entity data or caches)\n * This is handled locally as it's a generic operation that doesn't require host app knowledge\n */\n private async handleRefreshData(command: RefreshDataCommand): Promise<void> {\n const { scope, entityNames, cacheName } = command;\n\n if (scope === 'entity' && entityNames && entityNames.length > 0) {\n // Refresh specific entity data\n for (const entityName of entityNames) {\n await this.dataCacheService.refreshEntity(entityName);\n }\n console.log('Refreshed entity data:', entityNames);\n } else if (scope === 'cache' && cacheName) {\n // Refresh a specific cache\n await this.dataCacheService.refreshCache(cacheName);\n console.log('Refreshed cache:', cacheName);\n } else {\n console.warn('Invalid refresh:data command parameters:', command);\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ui-command-handler.service.js","sourceRoot":"","sources":["../../../src/lib/services/ui-command-handler.service.ts"],"names":[],"mappings":"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","sourcesContent":["import { Injectable, EventEmitter } from '@angular/core';\nimport {\n ActionableCommand,\n AutomaticCommand,\n RefreshDataCommand,\n OpenURLCommand,\n ComposeEmailCommand,\n BuildMailtoURL\n} from '@memberjunction/ai-core-plus';\nimport { DataCacheService } from './data-cache.service';\n\nexport interface ActionableCommandRequest {\n command: ActionableCommand;\n conversationId?: string | null;\n conversationDetailId?: string | null;\n /**\n * Set only on the compose:email over-length fallback: whether the draft body is now on the\n * user's clipboard. The copy overwrites whatever the user had copied, so the host must say it\n * happened, and must not claim it when this is false (no body, clipboard unavailable over plain\n * HTTP, or denied by permissions policy).\n */\n DraftCopiedToClipboard?: boolean;\n}\n\n/**\n * Service for handling UI commands from agents.\n *\n * Generic commands (open:url, and compose:email while it fits in a mailto: URL) are handled\n * directly by this service. App-specific commands (open:resource, and the compose:email\n * over-length fallback) are emitted for the host application to handle.\n */\n@Injectable({\n providedIn: 'root'\n})\nexport class UICommandHandlerService {\n /**\n * Event emitted when an actionable command requires host-app handling.\n *\n * open:resource is always emitted. compose:email is emitted ONLY as a fallback, when the draft\n * is too long for a mailto: URL and the host must open the draft artifact instead; a draft\n * within the limit is handled here and never reaches the host. On that fallback the host also\n * owns telling the user why their mail client did not open, because only the host knows whether\n * a draft artifact opened (see {@link ActionableCommandRequest.DraftCopiedToClipboard}).\n * open:url is always handled here.\n */\n public ActionableCommandRequested = new EventEmitter<ActionableCommandRequest>();\n\n /** @deprecated Use {@link ActionableCommandRequested}. */\n public get actionableCommandRequested() {\n return this.ActionableCommandRequested;\n }\n /** @deprecated Use {@link ActionableCommandRequested}. */\n public set actionableCommandRequested(value) {\n this.ActionableCommandRequested = value;\n }\n\n /**\n * Event emitted when an automatic command should be executed\n * Host application should subscribe to this and handle the command appropriately\n */\n public AutomaticCommandRequested = new EventEmitter<AutomaticCommand>();\n\n /** @deprecated Use {@link AutomaticCommandRequested}. */\n public get automaticCommandRequested() {\n return this.AutomaticCommandRequested;\n }\n /** @deprecated Use {@link AutomaticCommandRequested}. */\n public set automaticCommandRequested(value) {\n this.AutomaticCommandRequested = value;\n }\n\n constructor(\n private dataCacheService: DataCacheService\n ) {}\n\n /**\n * Execute an actionable command (triggered by user clicking a button).\n * Generic commands like open:url are handled directly; others are emitted to the host.\n */\n public async ExecuteActionableCommand(command: ActionableCommand, origin?: Omit<ActionableCommandRequest, 'command'>): Promise<void> {\n if (command.type === 'open:url') {\n this.handleOpenUrl(command);\n return;\n }\n\n const request: ActionableCommandRequest = {\n command,\n conversationId: origin?.conversationId ?? null,\n conversationDetailId: origin?.conversationDetailId ?? null\n };\n\n if (command.type === 'compose:email') {\n const { url, withinLimit } = BuildMailtoURL(command);\n if (withinLimit) {\n this.openMailto(url);\n return;\n }\n // Too long for a mailto: URL. We do NOT open it: a mail client past its limit does not\n // refuse the URL, it opens a draft with the body SILENTLY TRUNCATED and the user sends half\n // a message without noticing. Falls through to the host, which opens the full draft\n // artifact and tells the user what happened.\n request.DraftCopiedToClipboard = await this.copyDraftBody(command);\n }\n\n // open:resource (and the compose:email fallback above) require app-specific navigation.\n // compose:email is logged by TYPE ONLY: its body is free-text member correspondence, and the\n // whole command object would otherwise land in the browser console and any console-forwarding\n // telemetry.\n if (command.type === 'compose:email') {\n console.log('📤 Emitting actionable command for host app:', command.type);\n } else {\n console.log('📤 Emitting actionable command for host app:', command);\n }\n this.ActionableCommandRequested.emit(request);\n }\n\n /** @deprecated Use {@link ExecuteActionableCommand}. */\n public async executeActionableCommand(command: ActionableCommand, origin?: Omit<ActionableCommandRequest, 'command'>): Promise<void> {\n return this.ExecuteActionableCommand(command, origin);\n }\n\n /**\n * Handle open:url commands directly by opening the URL in a browser tab.\n * Data URIs are converted to Blob URLs because Chrome blocks window.open with data: URIs.\n */\n private handleOpenUrl(command: OpenURLCommand): void {\n const url = command.url;\n const newTab = command.newTab !== false;\n\n if (url.startsWith('data:')) {\n this.openDataUri(url);\n } else {\n const target = newTab ? '_blank' : '_self';\n window.open(url, target, target === '_blank' ? 'noopener,noreferrer' : undefined);\n }\n }\n\n /**\n * Best-effort copy of an over-length draft's body, so the text is not lost even when the host\n * cannot open the draft artifact.\n *\n * Never rejects, so a clipboard that is unavailable (plain HTTP) or denied by permissions policy\n * cannot stop the fallback. Nothing on the click path awaits before writeText is called, so it\n * still runs inside the click's user activation; the fallback then awaits the outcome only so\n * the host's notice can say whether the copy happened.\n *\n * @returns true only when the body is on the clipboard, so the host never claims a copy that\n * did not happen.\n */\n private async copyDraftBody(command: ComposeEmailCommand): Promise<boolean> {\n if (!command.body || !navigator.clipboard) {\n return false;\n }\n try {\n await navigator.clipboard.writeText(command.body);\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * Open the user's own mail client with a compose:email draft's fields pre-filled, via a\n * synthesized anchor click.\n *\n * NOTHING IS SENT HERE. The agent drafted; the user sends. This only opens a compose window.\n *\n * Deliberately not window.open: Chrome treats window.open with a non-http scheme as a popup and\n * strands an about:blank tab behind the compose window.\n */\n private openMailto(url: string): void {\n const a = document.createElement('a');\n a.href = url;\n a.style.display = 'none';\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n }\n\n /**\n * Open a data URI by converting it to a Blob URL.\n * Chrome blocks window.open('data:...') for security, so we create a Blob URL instead.\n */\n private openDataUri(dataUri: string): void {\n const [header, base64Data] = dataUri.split(',');\n const mimeMatch = header.match(/data:([^;]+)/);\n const mimeType = mimeMatch ? mimeMatch[1] : 'application/octet-stream';\n\n const byteString = atob(base64Data);\n const byteArray = new Uint8Array(byteString.length);\n for (let i = 0; i < byteString.length; i++) {\n byteArray[i] = byteString.charCodeAt(i);\n }\n\n const blob = new Blob([byteArray], { type: mimeType });\n const blobUrl = URL.createObjectURL(blob);\n window.open(blobUrl, '_blank');\n }\n\n /**\n * Execute an automatic command (runs immediately without user interaction)\n * Special handling: refresh:data commands execute locally, others emit to host\n */\n public async ExecuteAutomaticCommand(command: AutomaticCommand): Promise<void> {\n console.log('Executing automatic command:', command);\n\n if (command.type === 'refresh:data') {\n // Handle data refresh locally as it's a generic operation\n await this.handleRefreshData(command);\n } else {\n // Emit other automatic commands (like notifications) for host to handle\n console.log('📤 Emitting automatic command for host app:', command);\n this.AutomaticCommandRequested.emit(command);\n }\n }\n\n /** @deprecated Use {@link ExecuteAutomaticCommand}. */\n public async executeAutomaticCommand(command: AutomaticCommand): Promise<void> {\n return this.ExecuteAutomaticCommand(command);\n }\n\n /**\n * Execute all automatic commands from an agent result\n */\n public async ExecuteAutomaticCommands(commands: AutomaticCommand[]): Promise<void> {\n if (!commands || commands.length === 0) {\n return;\n }\n\n for (const command of commands) {\n try {\n await this.ExecuteAutomaticCommand(command);\n } catch (error) {\n console.error('Error executing automatic command:', command, error);\n }\n }\n }\n\n /** @deprecated Use {@link ExecuteAutomaticCommands}. */\n public async executeAutomaticCommands(commands: AutomaticCommand[]): Promise<void> {\n return this.ExecuteAutomaticCommands(commands);\n }\n\n /**\n * Handle refreshing data (entity data or caches)\n * This is handled locally as it's a generic operation that doesn't require host app knowledge\n */\n private async handleRefreshData(command: RefreshDataCommand): Promise<void> {\n const { scope, entityNames, cacheName } = command;\n\n if (scope === 'entity' && entityNames && entityNames.length > 0) {\n // Refresh specific entity data\n for (const entityName of entityNames) {\n await this.dataCacheService.refreshEntity(entityName);\n }\n console.log('Refreshed entity data:', entityNames);\n } else if (scope === 'cache' && cacheName) {\n // Refresh a specific cache\n await this.dataCacheService.refreshCache(cacheName);\n console.log('Refreshed cache:', cacheName);\n } else {\n console.warn('Invalid refresh:data command parameters:', command);\n }\n }\n}\n"]}
|
|
@@ -12,6 +12,11 @@ export interface AgentTurnCandidates {
|
|
|
12
12
|
MentionedAgentIds: readonly string[];
|
|
13
13
|
/** The last agent that answered in the conversation, other than the conversation manager. */
|
|
14
14
|
ContinuityAgentId: string | null;
|
|
15
|
+
/**
|
|
16
|
+
* True when a routing decision put `ContinuityAgentId` there in place of the last agent that
|
|
17
|
+
* answered. The turn is then labelled `DecisionRouted` instead of `Continuity`.
|
|
18
|
+
*/
|
|
19
|
+
ContinuityDecisionRouted?: boolean;
|
|
15
20
|
/** The agent pinned on the conversation. */
|
|
16
21
|
ConversationDefaultAgentId: string | null;
|
|
17
22
|
/** The host's `DefaultAgentId` input. */
|
|
@@ -26,17 +31,13 @@ export interface AgentTurnRules {
|
|
|
26
31
|
/** The agents that may answer. Null allows every agent. */
|
|
27
32
|
AllowedAgentIDs: readonly string[] | null;
|
|
28
33
|
}
|
|
29
|
-
/**
|
|
30
|
-
* True when the agent may take a turn. A null or undefined list allows every agent; an empty
|
|
31
|
-
* list allows none.
|
|
32
|
-
*/
|
|
33
|
-
export declare function IsAgentAllowed(agentId: string | null | undefined, allowedAgentIDs: readonly string[] | null | undefined): boolean;
|
|
34
34
|
/**
|
|
35
35
|
* Picks the agent and route for one message, or null when no turn starts.
|
|
36
36
|
*
|
|
37
37
|
* The order is MJ's: the first tagged agent the host allows, then (under `Always` only) the last
|
|
38
38
|
* agent that answered, the conversation's pinned agent, the host's default agent, and the
|
|
39
|
-
* conversation manager. A candidate the host doesn't allow is skipped.
|
|
39
|
+
* conversation manager. A candidate the host doesn't allow is skipped. A continuity candidate that
|
|
40
|
+
* a routing decision chose (`ContinuityDecisionRouted`) takes the `DecisionRouted` label.
|
|
40
41
|
*
|
|
41
42
|
* A continuity or default candidate that `isKnownAgent` doesn't recognise falls back to the
|
|
42
43
|
* conversation manager, as MJ always has, rather than to the next route. A tagged agent is taken
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-turn-routing.d.ts","sourceRoot":"","sources":["../../../src/lib/utils/agent-turn-routing.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAkB,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAElG,2FAA2F;AAC3F,MAAM,WAAW,mBAAmB;IAChC,yDAAyD;IACzD,iBAAiB,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,6FAA6F;IAC7F,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,4CAA4C;IAC5C,0BAA0B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,yCAAyC;IACzC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,uCAAuC;IACvC,0BAA0B,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7C;AAED,qCAAqC;AACrC,MAAM,WAAW,cAAc;IAC3B,oCAAoC;IACpC,SAAS,EAAE,cAAc,CAAC;IAC1B,2DAA2D;IAC3D,eAAe,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;CAC7C;AAED
|
|
1
|
+
{"version":3,"file":"agent-turn-routing.d.ts","sourceRoot":"","sources":["../../../src/lib/utils/agent-turn-routing.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAkB,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAElG,2FAA2F;AAC3F,MAAM,WAAW,mBAAmB;IAChC,yDAAyD;IACzD,iBAAiB,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,6FAA6F;IAC7F,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC;;;OAGG;IACH,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,4CAA4C;IAC5C,0BAA0B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,yCAAyC;IACzC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,uCAAuC;IACvC,0BAA0B,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7C;AAED,qCAAqC;AACrC,MAAM,WAAW,cAAc;IAC3B,oCAAoC;IACpC,SAAS,EAAE,cAAc,CAAC;IAC1B,2DAA2D;IAC3D,eAAe,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;CAC7C;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAC5B,UAAU,EAAE,mBAAmB,EAC/B,KAAK,EAAE,cAAc,EACrB,YAAY,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,GAC3C,eAAe,GAAG,IAAI,CAsBxB;AAUD,kFAAkF;AAClF,UAAU,aAAa;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,CAAC;CACnD;AAED;;;;;;;;;GASG;AACH,wBAAgB,6BAA6B,CACzC,KAAK,EAAE,aAAa,CAAC,aAAa,CAAC,EACnC,eAAe,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,EACzC,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,GACrD,MAAM,EAAE,CAYV"}
|
|
@@ -5,26 +5,14 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @module @memberjunction/ng-conversations
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
9
|
-
/**
|
|
10
|
-
* True when the agent may take a turn. A null or undefined list allows every agent; an empty
|
|
11
|
-
* list allows none.
|
|
12
|
-
*/
|
|
13
|
-
export function IsAgentAllowed(agentId, allowedAgentIDs) {
|
|
14
|
-
if (!agentId) {
|
|
15
|
-
return false;
|
|
16
|
-
}
|
|
17
|
-
if (allowedAgentIDs == null) {
|
|
18
|
-
return true;
|
|
19
|
-
}
|
|
20
|
-
return allowedAgentIDs.some(id => UUIDsEqual(id, agentId));
|
|
21
|
-
}
|
|
8
|
+
import { IsAgentAllowed } from '@memberjunction/ai-core-plus';
|
|
22
9
|
/**
|
|
23
10
|
* Picks the agent and route for one message, or null when no turn starts.
|
|
24
11
|
*
|
|
25
12
|
* The order is MJ's: the first tagged agent the host allows, then (under `Always` only) the last
|
|
26
13
|
* agent that answered, the conversation's pinned agent, the host's default agent, and the
|
|
27
|
-
* conversation manager. A candidate the host doesn't allow is skipped.
|
|
14
|
+
* conversation manager. A candidate the host doesn't allow is skipped. A continuity candidate that
|
|
15
|
+
* a routing decision chose (`ContinuityDecisionRouted`) takes the `DecisionRouted` label.
|
|
28
16
|
*
|
|
29
17
|
* A continuity or default candidate that `isKnownAgent` doesn't recognise falls back to the
|
|
30
18
|
* conversation manager, as MJ always has, rather than to the next route. A tagged agent is taken
|
|
@@ -43,7 +31,7 @@ export function ResolveAgentTurn(candidates, rules, isKnownAgent) {
|
|
|
43
31
|
return null;
|
|
44
32
|
}
|
|
45
33
|
const implicitRoutes = [
|
|
46
|
-
['Continuity', candidates.ContinuityAgentId],
|
|
34
|
+
[candidates.ContinuityDecisionRouted ? 'DecisionRouted' : 'Continuity', candidates.ContinuityAgentId],
|
|
47
35
|
['ConversationDefault', candidates.ConversationDefaultAgentId],
|
|
48
36
|
['HostDefault', candidates.HostDefaultAgentId],
|
|
49
37
|
];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-turn-routing.js","sourceRoot":"","sources":["../../../src/lib/utils/agent-turn-routing.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"agent-turn-routing.js","sourceRoot":"","sources":["../../../src/lib/utils/agent-turn-routing.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @fileoverview Pure routing rules for an agent turn: which agent answers a message, and whether\n * any does. Kept free of Angular and of I/O so the whole matrix (reply mode × mention × allowed\n * list × route) is testable directly; `MessageInputComponent` supplies the candidates.\n *\n * @module @memberjunction/ng-conversations\n */\n\nimport { IsAgentAllowed } from '@memberjunction/ai-core-plus';\nimport type { AgentReplyMode, AgentTurnRoute, AgentTurnTarget } from '../models/agent-turn.model';\n\n/** The agent each route would use for one message, before the host's rules are applied. */\nexport interface AgentTurnCandidates {\n /** Agents the message tags, in the order they appear. */\n MentionedAgentIds: readonly string[];\n /** The last agent that answered in the conversation, other than the conversation manager. */\n ContinuityAgentId: string | null;\n /**\n * True when a routing decision put `ContinuityAgentId` there in place of the last agent that\n * answered. The turn is then labelled `DecisionRouted` instead of `Continuity`.\n */\n ContinuityDecisionRouted?: boolean;\n /** The agent pinned on the conversation. */\n ConversationDefaultAgentId: string | null;\n /** The host's `DefaultAgentId` input. */\n HostDefaultAgentId: string | null;\n /** MJ's conversation manager agent. */\n ConversationManagerAgentId: string | null;\n}\n\n/** The host's rules for the chat. */\nexport interface AgentTurnRules {\n /** When a message starts a turn. */\n ReplyMode: AgentReplyMode;\n /** The agents that may answer. Null allows every agent. */\n AllowedAgentIDs: readonly string[] | null;\n}\n\n/**\n * Picks the agent and route for one message, or null when no turn starts.\n *\n * The order is MJ's: the first tagged agent the host allows, then (under `Always` only) the last\n * agent that answered, the conversation's pinned agent, the host's default agent, and the\n * conversation manager. A candidate the host doesn't allow is skipped. A continuity candidate that\n * a routing decision chose (`ContinuityDecisionRouted`) takes the `DecisionRouted` label.\n *\n * A continuity or default candidate that `isKnownAgent` doesn't recognise falls back to the\n * conversation manager, as MJ always has, rather than to the next route. A tagged agent is taken\n * as tagged.\n *\n * @param candidates The agent each route would use.\n * @param rules The host's reply mode and allowed list.\n * @param isKnownAgent True when the agent exists in the client's agent catalog.\n */\nexport function ResolveAgentTurn(\n candidates: AgentTurnCandidates,\n rules: AgentTurnRules,\n isKnownAgent: (agentId: string) => boolean\n): AgentTurnTarget | null {\n const mentioned = candidates.MentionedAgentIds.find(id => IsAgentAllowed(id, rules.AllowedAgentIDs));\n if (mentioned) {\n return { AgentId: mentioned, Route: 'Mention' };\n }\n if (rules.ReplyMode === 'MentionOnly') {\n return null;\n }\n\n const implicitRoutes: ReadonlyArray<[AgentTurnRoute, string | null]> = [\n [candidates.ContinuityDecisionRouted ? 'DecisionRouted' : 'Continuity', candidates.ContinuityAgentId],\n ['ConversationDefault', candidates.ConversationDefaultAgentId],\n ['HostDefault', candidates.HostDefaultAgentId],\n ];\n for (const [route, agentId] of implicitRoutes) {\n if (agentId && IsAgentAllowed(agentId, rules.AllowedAgentIDs)) {\n return isKnownAgent(agentId)\n ? { AgentId: agentId, Route: route }\n : conversationManagerTarget(candidates, rules);\n }\n }\n return conversationManagerTarget(candidates, rules);\n}\n\n/** The conversation manager's turn, when there is one and the host allows it. */\nfunction conversationManagerTarget(candidates: AgentTurnCandidates, rules: AgentTurnRules): AgentTurnTarget | null {\n const agentId = candidates.ConversationManagerAgentId;\n return agentId && IsAgentAllowed(agentId, rules.AllowedAgentIDs)\n ? { AgentId: agentId, Route: 'ConversationManager' }\n : null;\n}\n\n/** A task-graph step that runs an agent, as the conversation manager emits it. */\ninterface AgentTaskStep {\n kind: string;\n configuration?: { agentName?: string } | object;\n}\n\n/**\n * The agent names in a task graph that the host doesn't allow. The conversation manager can\n * answer with a multi-step workflow instead of a single delegation; each `Agent` step names the\n * agent that runs it, so the allowed list has to hold there too.\n *\n * @param steps The graph's tasks.\n * @param allowedAgentIDs The host's allowed list. Null allows every agent.\n * @param resolveAgentId Maps an agent name to its ID, or null when no such agent exists.\n * @returns The disallowed names, each once, in the order they appear. Empty when all are allowed.\n */\nexport function FindDisallowedTaskGraphAgents(\n steps: ReadonlyArray<AgentTaskStep>,\n allowedAgentIDs: readonly string[] | null,\n resolveAgentId: (agentName: string) => string | null\n): string[] {\n if (allowedAgentIDs == null) {\n return [];\n }\n const disallowed: string[] = [];\n for (const step of steps) {\n const agentName = step.kind === 'Agent' ? agentNameOf(step.configuration) : null;\n if (agentName && !IsAgentAllowed(resolveAgentId(agentName), allowedAgentIDs) && !disallowed.includes(agentName)) {\n disallowed.push(agentName);\n }\n }\n return disallowed;\n}\n\n/** The `agentName` of an Agent step's configuration, when it has one. */\nfunction agentNameOf(configuration: AgentTaskStep['configuration']): string | null {\n if (configuration && 'agentName' in configuration && typeof configuration.agentName === 'string') {\n return configuration.agentName;\n }\n return null;\n}\n"]}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Decision routing for an unmentioned message (plan Tasks 3.9 and 3.2): one typed
|
|
3
|
+
* decision that may send the message to a different agent in the conversation than the last one
|
|
4
|
+
* that answered, and may name the artifact version the message modifies.
|
|
5
|
+
*
|
|
6
|
+
* It runs before {@link ResolveAgentTurn}, which stays pure. When the answer is confident, the
|
|
7
|
+
* caller replaces the continuity candidate with {@link ApplyRoutingDecision}; otherwise nothing
|
|
8
|
+
* changes. Every failure keeps today's routing. The chat turns it on with `EnableDecisionRouting`,
|
|
9
|
+
* which is off by default.
|
|
10
|
+
*
|
|
11
|
+
* The decision itself (its input, questions and state, and how its answers are read) is built in
|
|
12
|
+
* `@memberjunction/ai-core-plus` (`conversation-routing-decision.ts`), so the Decision Eval harness
|
|
13
|
+
* measures exactly what the chat sends. What stays here is the call and what the chat does with
|
|
14
|
+
* the outcome. Import the builders and their types from `@memberjunction/ai-core-plus` directly.
|
|
15
|
+
*
|
|
16
|
+
* Kept free of Angular so the whole path, with the decision call mocked, is testable directly;
|
|
17
|
+
* `MessageInputComponent` supplies the conversation and the call.
|
|
18
|
+
*
|
|
19
|
+
* @module @memberjunction/ng-conversations
|
|
20
|
+
*/
|
|
21
|
+
import { type RoutingDecisionInput, type RoutingDecisionOutcome } from '@memberjunction/ai-core-plus';
|
|
22
|
+
import type { RunDecisionParams, RunDecisionResult } from '@memberjunction/graphql-dataprovider';
|
|
23
|
+
import type { AgentReplyMode } from '../models/agent-turn.model';
|
|
24
|
+
import type { AgentTurnCandidates } from './agent-turn-routing';
|
|
25
|
+
/** Runs a decision on the server: `ConversationAgentService.RunDecision` in the chat. */
|
|
26
|
+
export type RoutingDecisionRunner = (params: RunDecisionParams) => Promise<RunDecisionResult>;
|
|
27
|
+
/** What decides whether a message gets a routing decision at all. */
|
|
28
|
+
export interface RoutingDecisionGate {
|
|
29
|
+
/** The chat's `EnableDecisionRouting` input. */
|
|
30
|
+
Enabled: boolean;
|
|
31
|
+
/** The chat's reply mode. */
|
|
32
|
+
ReplyMode: AgentReplyMode;
|
|
33
|
+
/** Agents the message tags. */
|
|
34
|
+
MentionedAgentIds: readonly string[];
|
|
35
|
+
/** The message, as saved. */
|
|
36
|
+
Message: string;
|
|
37
|
+
/** The last agent that answered, other than the conversation manager. */
|
|
38
|
+
ContinuityAgentId: string | null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* True when a message should get a routing decision: routing is on, the reply mode is `Always`,
|
|
42
|
+
* some agent has answered before, and the message tags no agent and is not a form response. A
|
|
43
|
+
* form response always goes back to the agent that asked for it. Every other message keeps
|
|
44
|
+
* today's routing, with no call.
|
|
45
|
+
*/
|
|
46
|
+
export declare function ShouldRunRoutingDecision(gate: RoutingDecisionGate): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Asks one routing decision and reads its answer. It fails toward continuity: an error, no answer
|
|
49
|
+
* within `timeoutMs` (the call is abandoned), or an unsure answer keeps today's routing. Never
|
|
50
|
+
* throws.
|
|
51
|
+
*
|
|
52
|
+
* @param input What to decide. Check it with `CanAskRoutingDecision` first.
|
|
53
|
+
* @param runDecision Runs the decision on the server.
|
|
54
|
+
* @param timeoutMs How long to wait. Defaults to `DECISION_ROUTING_TIMEOUT_MS`.
|
|
55
|
+
*/
|
|
56
|
+
export declare function RunRoutingDecision(input: RoutingDecisionInput, runDecision: RoutingDecisionRunner, timeoutMs?: number): Promise<RoutingDecisionOutcome>;
|
|
57
|
+
/**
|
|
58
|
+
* The candidates with the decision applied: a routed agent replaces continuity and is labelled
|
|
59
|
+
* `DecisionRouted`, and "someone else" clears continuity. Any other outcome, or none, leaves the
|
|
60
|
+
* candidates as they are.
|
|
61
|
+
*/
|
|
62
|
+
export declare function ApplyRoutingDecision(candidates: AgentTurnCandidates, outcome: RoutingDecisionOutcome | null): AgentTurnCandidates;
|
|
63
|
+
/**
|
|
64
|
+
* The artifact version to hand a turn's agent: the decision's target, when it is one of that
|
|
65
|
+
* agent's versions. Null otherwise, including after a redirect to another agent.
|
|
66
|
+
*/
|
|
67
|
+
export declare function ArtifactVersionForTurn(outcome: RoutingDecisionOutcome | null, agentId: string): string | null;
|
|
68
|
+
//# sourceMappingURL=decision-routing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decision-routing.d.ts","sourceRoot":"","sources":["../../../src/lib/utils/decision-routing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAOH,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC9B,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAEjG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEhE,yFAAyF;AACzF,MAAM,MAAM,qBAAqB,GAAG,CAAC,MAAM,EAAE,iBAAiB,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAE9F,qEAAqE;AACrE,MAAM,WAAW,mBAAmB;IAChC,gDAAgD;IAChD,OAAO,EAAE,OAAO,CAAC;IACjB,6BAA6B;IAC7B,SAAS,EAAE,cAAc,CAAC;IAC1B,+BAA+B;IAC/B,iBAAiB,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,6BAA6B;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,yEAAyE;IACzE,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAM3E;AAED;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CACpC,KAAK,EAAE,oBAAoB,EAC3B,WAAW,EAAE,qBAAqB,EAClC,SAAS,GAAE,MAAoC,GAChD,OAAO,CAAC,sBAAsB,CAAC,CAUjC;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAChC,UAAU,EAAE,mBAAmB,EAC/B,OAAO,EAAE,sBAAsB,GAAG,IAAI,GACvC,mBAAmB,CAQrB;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,sBAAsB,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAG7G"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Decision routing for an unmentioned message (plan Tasks 3.9 and 3.2): one typed
|
|
3
|
+
* decision that may send the message to a different agent in the conversation than the last one
|
|
4
|
+
* that answered, and may name the artifact version the message modifies.
|
|
5
|
+
*
|
|
6
|
+
* It runs before {@link ResolveAgentTurn}, which stays pure. When the answer is confident, the
|
|
7
|
+
* caller replaces the continuity candidate with {@link ApplyRoutingDecision}; otherwise nothing
|
|
8
|
+
* changes. Every failure keeps today's routing. The chat turns it on with `EnableDecisionRouting`,
|
|
9
|
+
* which is off by default.
|
|
10
|
+
*
|
|
11
|
+
* The decision itself (its input, questions and state, and how its answers are read) is built in
|
|
12
|
+
* `@memberjunction/ai-core-plus` (`conversation-routing-decision.ts`), so the Decision Eval harness
|
|
13
|
+
* measures exactly what the chat sends. What stays here is the call and what the chat does with
|
|
14
|
+
* the outcome. Import the builders and their types from `@memberjunction/ai-core-plus` directly.
|
|
15
|
+
*
|
|
16
|
+
* Kept free of Angular so the whole path, with the decision call mocked, is testable directly;
|
|
17
|
+
* `MessageInputComponent` supplies the conversation and the call.
|
|
18
|
+
*
|
|
19
|
+
* @module @memberjunction/ng-conversations
|
|
20
|
+
*/
|
|
21
|
+
import { BuildRoutingQuestions, BuildRoutingState, ConversationUtility, DECISION_ROUTING_TIMEOUT_MS, InterpretRoutingAnswers, KeptContinuityOutcome } from '@memberjunction/ai-core-plus';
|
|
22
|
+
import { UUIDsEqual } from '@memberjunction/global';
|
|
23
|
+
/**
|
|
24
|
+
* True when a message should get a routing decision: routing is on, the reply mode is `Always`,
|
|
25
|
+
* some agent has answered before, and the message tags no agent and is not a form response. A
|
|
26
|
+
* form response always goes back to the agent that asked for it. Every other message keeps
|
|
27
|
+
* today's routing, with no call.
|
|
28
|
+
*/
|
|
29
|
+
export function ShouldRunRoutingDecision(gate) {
|
|
30
|
+
return gate.Enabled
|
|
31
|
+
&& gate.ReplyMode === 'Always'
|
|
32
|
+
&& !!gate.ContinuityAgentId
|
|
33
|
+
&& gate.MentionedAgentIds.length === 0
|
|
34
|
+
&& !ConversationUtility.ContainsFormResponse(gate.Message);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Asks one routing decision and reads its answer. It fails toward continuity: an error, no answer
|
|
38
|
+
* within `timeoutMs` (the call is abandoned), or an unsure answer keeps today's routing. Never
|
|
39
|
+
* throws.
|
|
40
|
+
*
|
|
41
|
+
* @param input What to decide. Check it with `CanAskRoutingDecision` first.
|
|
42
|
+
* @param runDecision Runs the decision on the server.
|
|
43
|
+
* @param timeoutMs How long to wait. Defaults to `DECISION_ROUTING_TIMEOUT_MS`.
|
|
44
|
+
*/
|
|
45
|
+
export async function RunRoutingDecision(input, runDecision, timeoutMs = DECISION_ROUTING_TIMEOUT_MS) {
|
|
46
|
+
const params = {
|
|
47
|
+
State: BuildRoutingState(input),
|
|
48
|
+
Questions: BuildRoutingQuestions(input),
|
|
49
|
+
TimeoutMS: timeoutMs
|
|
50
|
+
};
|
|
51
|
+
const result = await runWithDeadline(() => runDecision(params), timeoutMs);
|
|
52
|
+
return result
|
|
53
|
+
? InterpretRoutingAnswers(input, result)
|
|
54
|
+
: KeptContinuityOutcome(`no answer within ${timeoutMs} ms`);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The candidates with the decision applied: a routed agent replaces continuity and is labelled
|
|
58
|
+
* `DecisionRouted`, and "someone else" clears continuity. Any other outcome, or none, leaves the
|
|
59
|
+
* candidates as they are.
|
|
60
|
+
*/
|
|
61
|
+
export function ApplyRoutingDecision(candidates, outcome) {
|
|
62
|
+
if (outcome?.Verdict === 'Routed' && outcome.RoutedAgentId) {
|
|
63
|
+
return { ...candidates, ContinuityAgentId: outcome.RoutedAgentId, ContinuityDecisionRouted: true };
|
|
64
|
+
}
|
|
65
|
+
if (outcome?.Verdict === 'SomeoneElse') {
|
|
66
|
+
return { ...candidates, ContinuityAgentId: null };
|
|
67
|
+
}
|
|
68
|
+
return candidates;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The artifact version to hand a turn's agent: the decision's target, when it is one of that
|
|
72
|
+
* agent's versions. Null otherwise, including after a redirect to another agent.
|
|
73
|
+
*/
|
|
74
|
+
export function ArtifactVersionForTurn(outcome, agentId) {
|
|
75
|
+
const target = outcome?.TargetArtifact;
|
|
76
|
+
return target && UUIDsEqual(target.AgentId, agentId) ? target.ArtifactVersionId : null;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Runs the call, or gives up on it after `timeoutMs`. Resolves with the result, a failed result
|
|
80
|
+
* when the call throws, or null when it didn't answer in time. A late answer is ignored.
|
|
81
|
+
*/
|
|
82
|
+
async function runWithDeadline(call, timeoutMs) {
|
|
83
|
+
let timer;
|
|
84
|
+
const deadline = new Promise(resolve => {
|
|
85
|
+
timer = setTimeout(() => resolve(null), timeoutMs);
|
|
86
|
+
});
|
|
87
|
+
const answer = Promise.resolve()
|
|
88
|
+
.then(call)
|
|
89
|
+
.catch((error) => ({
|
|
90
|
+
Success: false,
|
|
91
|
+
ErrorMessage: error instanceof Error ? error.message : String(error),
|
|
92
|
+
Answers: {}
|
|
93
|
+
}));
|
|
94
|
+
try {
|
|
95
|
+
return await Promise.race([answer, deadline]);
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=decision-routing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decision-routing.js","sourceRoot":"","sources":["../../../src/lib/utils/decision-routing.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @fileoverview Decision routing for an unmentioned message (plan Tasks 3.9 and 3.2): one typed\n * decision that may send the message to a different agent in the conversation than the last one\n * that answered, and may name the artifact version the message modifies.\n *\n * It runs before {@link ResolveAgentTurn}, which stays pure. When the answer is confident, the\n * caller replaces the continuity candidate with {@link ApplyRoutingDecision}; otherwise nothing\n * changes. Every failure keeps today's routing. The chat turns it on with `EnableDecisionRouting`,\n * which is off by default.\n *\n * The decision itself (its input, questions and state, and how its answers are read) is built in\n * `@memberjunction/ai-core-plus` (`conversation-routing-decision.ts`), so the Decision Eval harness\n * measures exactly what the chat sends. What stays here is the call and what the chat does with\n * the outcome. Import the builders and their types from `@memberjunction/ai-core-plus` directly.\n *\n * Kept free of Angular so the whole path, with the decision call mocked, is testable directly;\n * `MessageInputComponent` supplies the conversation and the call.\n *\n * @module @memberjunction/ng-conversations\n */\n\nimport {\n BuildRoutingQuestions,\n BuildRoutingState,\n ConversationUtility,\n DECISION_ROUTING_TIMEOUT_MS,\n InterpretRoutingAnswers,\n KeptContinuityOutcome,\n type RoutingDecisionInput,\n type RoutingDecisionOutcome\n} from '@memberjunction/ai-core-plus';\nimport type { RunDecisionParams, RunDecisionResult } from '@memberjunction/graphql-dataprovider';\nimport { UUIDsEqual } from '@memberjunction/global';\nimport type { AgentReplyMode } from '../models/agent-turn.model';\nimport type { AgentTurnCandidates } from './agent-turn-routing';\n\n/** Runs a decision on the server: `ConversationAgentService.RunDecision` in the chat. */\nexport type RoutingDecisionRunner = (params: RunDecisionParams) => Promise<RunDecisionResult>;\n\n/** What decides whether a message gets a routing decision at all. */\nexport interface RoutingDecisionGate {\n /** The chat's `EnableDecisionRouting` input. */\n Enabled: boolean;\n /** The chat's reply mode. */\n ReplyMode: AgentReplyMode;\n /** Agents the message tags. */\n MentionedAgentIds: readonly string[];\n /** The message, as saved. */\n Message: string;\n /** The last agent that answered, other than the conversation manager. */\n ContinuityAgentId: string | null;\n}\n\n/**\n * True when a message should get a routing decision: routing is on, the reply mode is `Always`,\n * some agent has answered before, and the message tags no agent and is not a form response. A\n * form response always goes back to the agent that asked for it. Every other message keeps\n * today's routing, with no call.\n */\nexport function ShouldRunRoutingDecision(gate: RoutingDecisionGate): boolean {\n return gate.Enabled\n && gate.ReplyMode === 'Always'\n && !!gate.ContinuityAgentId\n && gate.MentionedAgentIds.length === 0\n && !ConversationUtility.ContainsFormResponse(gate.Message);\n}\n\n/**\n * Asks one routing decision and reads its answer. It fails toward continuity: an error, no answer\n * within `timeoutMs` (the call is abandoned), or an unsure answer keeps today's routing. Never\n * throws.\n *\n * @param input What to decide. Check it with `CanAskRoutingDecision` first.\n * @param runDecision Runs the decision on the server.\n * @param timeoutMs How long to wait. Defaults to `DECISION_ROUTING_TIMEOUT_MS`.\n */\nexport async function RunRoutingDecision(\n input: RoutingDecisionInput,\n runDecision: RoutingDecisionRunner,\n timeoutMs: number = DECISION_ROUTING_TIMEOUT_MS\n): Promise<RoutingDecisionOutcome> {\n const params: RunDecisionParams = {\n State: BuildRoutingState(input),\n Questions: BuildRoutingQuestions(input),\n TimeoutMS: timeoutMs\n };\n const result = await runWithDeadline(() => runDecision(params), timeoutMs);\n return result\n ? InterpretRoutingAnswers(input, result)\n : KeptContinuityOutcome(`no answer within ${timeoutMs} ms`);\n}\n\n/**\n * The candidates with the decision applied: a routed agent replaces continuity and is labelled\n * `DecisionRouted`, and \"someone else\" clears continuity. Any other outcome, or none, leaves the\n * candidates as they are.\n */\nexport function ApplyRoutingDecision(\n candidates: AgentTurnCandidates,\n outcome: RoutingDecisionOutcome | null\n): AgentTurnCandidates {\n if (outcome?.Verdict === 'Routed' && outcome.RoutedAgentId) {\n return { ...candidates, ContinuityAgentId: outcome.RoutedAgentId, ContinuityDecisionRouted: true };\n }\n if (outcome?.Verdict === 'SomeoneElse') {\n return { ...candidates, ContinuityAgentId: null };\n }\n return candidates;\n}\n\n/**\n * The artifact version to hand a turn's agent: the decision's target, when it is one of that\n * agent's versions. Null otherwise, including after a redirect to another agent.\n */\nexport function ArtifactVersionForTurn(outcome: RoutingDecisionOutcome | null, agentId: string): string | null {\n const target = outcome?.TargetArtifact;\n return target && UUIDsEqual(target.AgentId, agentId) ? target.ArtifactVersionId : null;\n}\n\n/**\n * Runs the call, or gives up on it after `timeoutMs`. Resolves with the result, a failed result\n * when the call throws, or null when it didn't answer in time. A late answer is ignored.\n */\nasync function runWithDeadline(\n call: () => Promise<RunDecisionResult>,\n timeoutMs: number\n): Promise<RunDecisionResult | null> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const deadline = new Promise<null>(resolve => {\n timer = setTimeout(() => resolve(null), timeoutMs);\n });\n const answer = Promise.resolve()\n .then(call)\n .catch((error: unknown): RunDecisionResult => ({\n Success: false,\n ErrorMessage: error instanceof Error ? error.message : String(error),\n Answers: {}\n }));\n try {\n return await Promise.race([answer, deadline]);\n } finally {\n clearTimeout(timer);\n }\n}\n"]}
|
package/dist/public-api.d.ts
CHANGED
|
@@ -5,9 +5,11 @@ export * from './lib/models/lazy-artifact-info';
|
|
|
5
5
|
export * from './lib/models/navigation-request.model';
|
|
6
6
|
export * from './lib/models/agent-turn.model';
|
|
7
7
|
export * from './lib/utils/agent-turn-routing';
|
|
8
|
+
export * from './lib/utils/decision-routing';
|
|
8
9
|
export * from './lib/services/data-cache.service';
|
|
9
10
|
export * from './lib/services/artifact-state.service';
|
|
10
11
|
export * from './lib/services/agent-state.service';
|
|
12
|
+
export * from './lib/services/conversation-liveness-dom.service';
|
|
11
13
|
export * from './lib/services/conversation-agent.service';
|
|
12
14
|
export * from './lib/services/active-tasks.service';
|
|
13
15
|
export * from './lib/services/conversation-streaming.service';
|