@teamscala/service-boot 0.23.380 → 0.23.381
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/package.json
CHANGED
package/src/agreement-gate.ts
CHANGED
|
@@ -752,17 +752,8 @@ export const agreementGateConfig: ContentGateConfig = {
|
|
|
752
752
|
resourceKey: "agreement",
|
|
753
753
|
agentConfigSlug: "agreement-assistant-agent",
|
|
754
754
|
organisationId: session.organisationId ?? undefined,
|
|
755
|
-
title: "Agreement Assistant",
|
|
756
755
|
launchLabel: "Ask about this agreement",
|
|
757
|
-
welcomeMessage:
|
|
758
|
-
"Hi! Ask me anything about this agreement — clauses, fees, the schedules, or how signing works.",
|
|
759
756
|
placeholder: "Ask about this agreement…",
|
|
760
|
-
conversationStarters: [
|
|
761
|
-
"What are the key terms?",
|
|
762
|
-
"Summarise the fees and payment terms.",
|
|
763
|
-
"What's in Schedule A and Schedule B?",
|
|
764
|
-
"How do I sign this?",
|
|
765
|
-
],
|
|
766
757
|
});
|
|
767
758
|
|
|
768
759
|
// Old-flow only: drop the pre-placed gate block (new flow has none).
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
19
19
|
import { loadRegistryConfig } from "@teamscala/db/registry/load-config";
|
|
20
20
|
import { getServerClient } from "./page-server-orpc/server-client.ts";
|
|
21
|
+
import { loadAgentPresentation } from "./resource-chat-inject.ts";
|
|
21
22
|
import * as v from "valibot";
|
|
22
23
|
import type { BlockSchemaKey } from "@teamscala/db/registry/generated/block-schema";
|
|
23
24
|
|
|
@@ -130,6 +131,7 @@ export async function injectProposalChatLauncher(
|
|
|
130
131
|
|
|
131
132
|
try {
|
|
132
133
|
const runtimeContext = await buildProposalContext(resolved);
|
|
134
|
+
const presentation = await loadAgentPresentation(agentId);
|
|
133
135
|
(page.content as Array<Record<string, unknown>>).push({
|
|
134
136
|
type: LAUNCHER_BLOCK,
|
|
135
137
|
props: {
|
|
@@ -138,10 +140,13 @@ export async function injectProposalChatLauncher(
|
|
|
138
140
|
organisationId,
|
|
139
141
|
viewerEmail,
|
|
140
142
|
runtimeContext,
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
// Copy comes from the agent row (see loadAgentPresentation): these
|
|
144
|
+
// three were spelled here while ai_agents held blank columns for
|
|
145
|
+
// each of them.
|
|
146
|
+
primaryColor: brandPrimaryColor(resolved) || presentation.primaryColor,
|
|
147
|
+
title: presentation.title,
|
|
148
|
+
welcomeMessage: presentation.welcomeMessage,
|
|
149
|
+
conversationStarters: presentation.conversationStarters,
|
|
145
150
|
launchLabel: "Ask about this proposal",
|
|
146
151
|
},
|
|
147
152
|
});
|
|
@@ -37,6 +37,18 @@ mock.module("./page-server-orpc/server-client.ts", () => ({
|
|
|
37
37
|
mock.module("@teamscala/logger/app-loggers", () => ({
|
|
38
38
|
getAppLogger: () => ({ error: () => {} }),
|
|
39
39
|
}));
|
|
40
|
+
mock.module("@teamscala/db/prisma-registry", () => ({
|
|
41
|
+
getPrisma: () => ({
|
|
42
|
+
ai_agents: {
|
|
43
|
+
findUnique: async () => ({
|
|
44
|
+
name: "Agreement Assistant",
|
|
45
|
+
welcome_message: "Hi! Ask me anything about this agreement.",
|
|
46
|
+
conversation_starters: ["What are the key terms?", "How do I sign this?"],
|
|
47
|
+
primary_color: "#6366f1",
|
|
48
|
+
}),
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
}));
|
|
40
52
|
|
|
41
53
|
const { injectResourceChatLauncher } = await import("./resource-chat-inject.ts");
|
|
42
54
|
const { injectProposalChatLauncher } = await import("./proposal-chat-inject.ts");
|
|
@@ -60,7 +72,6 @@ describe("chat injectors — pushed block name resolves to a registered block",
|
|
|
60
72
|
await injectResourceChatLauncher(resolved, "viewer@example.com", {
|
|
61
73
|
resourceKey: "agreement",
|
|
62
74
|
agentConfigSlug: "agreement-assistant-agent",
|
|
63
|
-
title: "Agreement Assistant",
|
|
64
75
|
launchLabel: "Ask about this agreement",
|
|
65
76
|
});
|
|
66
77
|
const blocks = pushedBlocks(resolved);
|
|
@@ -81,7 +92,6 @@ describe("chat injectors — pushed block name resolves to a registered block",
|
|
|
81
92
|
await injectResourceChatLauncher(a, "viewer@example.com", {
|
|
82
93
|
resourceKey: "agreement",
|
|
83
94
|
agentConfigSlug: "agreement-assistant-agent",
|
|
84
|
-
title: "Agreement Assistant",
|
|
85
95
|
launchLabel: "Ask about this agreement",
|
|
86
96
|
});
|
|
87
97
|
const p = makeResolved("proposal");
|
|
@@ -94,4 +104,25 @@ describe("chat injectors — pushed block name resolves to a registered block",
|
|
|
94
104
|
}
|
|
95
105
|
}
|
|
96
106
|
});
|
|
107
|
+
|
|
108
|
+
test("the assistant's words come from the agent row, not the caller", async () => {
|
|
109
|
+
// THE REGRESSION THIS PINS: title, welcome message and starters used to
|
|
110
|
+
// be string literals in the injectors while ai_agents held a blank
|
|
111
|
+
// column for each. Whoever changed the row saw no change on the page.
|
|
112
|
+
const resolved = makeResolved("agreement");
|
|
113
|
+
await injectResourceChatLauncher(resolved, "viewer@example.com", {
|
|
114
|
+
resourceKey: "agreement",
|
|
115
|
+
agentConfigSlug: "agreement-assistant-agent",
|
|
116
|
+
launchLabel: "Ask about this agreement",
|
|
117
|
+
});
|
|
118
|
+
const props = (pushedBlocks(resolved)[0] as { props: Record<string, unknown> }).props;
|
|
119
|
+
expect(props.title).toBe("Agreement Assistant");
|
|
120
|
+
expect(props.welcomeMessage).toBe("Hi! Ask me anything about this agreement.");
|
|
121
|
+
expect(props.conversationStarters).toEqual([
|
|
122
|
+
"What are the key terms?",
|
|
123
|
+
"How do I sign this?",
|
|
124
|
+
]);
|
|
125
|
+
// launchLabel has no column yet, so it is still the caller's.
|
|
126
|
+
expect(props.launchLabel).toBe("Ask about this agreement");
|
|
127
|
+
});
|
|
97
128
|
});
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
24
24
|
import { loadRegistryConfig } from "@teamscala/db/registry/load-config";
|
|
25
|
+
import { getPrisma } from "@teamscala/db/prisma-registry";
|
|
25
26
|
import { getServerClient } from "./page-server-orpc/server-client.ts";
|
|
26
27
|
import * as v from "valibot";
|
|
27
28
|
import type { BlockSchemaKey } from "@teamscala/db/registry/generated/block-schema";
|
|
@@ -49,16 +50,67 @@ export interface ResourceChatOptions {
|
|
|
49
50
|
/** Org id to scope the chat. If omitted, read from resolved[resourceKey].organisation_id
|
|
50
51
|
* (proposals); agreements pass it explicitly (org isn't on the resolved record). */
|
|
51
52
|
organisationId?: string;
|
|
52
|
-
/** Overlay title-bar label (e.g. "Agreement Assistant"). */
|
|
53
|
-
title: string;
|
|
54
53
|
/** Floating button label / speech bubble (e.g. "Ask about this agreement"). */
|
|
55
54
|
launchLabel: string;
|
|
56
|
-
/** First assistant message. */
|
|
57
|
-
welcomeMessage?: string;
|
|
58
55
|
/** Input placeholder. */
|
|
59
56
|
placeholder?: string;
|
|
60
|
-
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* THE ASSISTANT'S WORDS BELONG TO THE AGENT ROW, NOT TO THIS FILE.
|
|
61
|
+
*
|
|
62
|
+
* `ai_agents` already carries a column for every one of them -- name,
|
|
63
|
+
* welcome_message, conversation_starters, primary_color -- and on 2026-08-19
|
|
64
|
+
* both live agents had welcome_message NULL and conversation_starters empty
|
|
65
|
+
* while the copy sat spelled in TypeScript here and in proposal-chat-inject.
|
|
66
|
+
* The intended home existed, sat blank, and the hardcoded copy won, so nobody
|
|
67
|
+
* could change a word the assistant says without a code change and a deploy.
|
|
68
|
+
* Two copies of one fact with the stale one winning silently is exactly
|
|
69
|
+
* `information-exists-in-one-location`.
|
|
70
|
+
*
|
|
71
|
+
* `launchLabel` and `placeholder` remain caller arguments only because no
|
|
72
|
+
* column holds them yet.
|
|
73
|
+
*/
|
|
74
|
+
export interface AgentPresentation {
|
|
75
|
+
title?: string;
|
|
76
|
+
welcomeMessage?: string;
|
|
61
77
|
conversationStarters?: string[];
|
|
78
|
+
primaryColor?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function loadAgentPresentation(agentId: string): Promise<AgentPresentation> {
|
|
82
|
+
try {
|
|
83
|
+
const agent = await getPrisma().ai_agents.findUnique({
|
|
84
|
+
where: { id: agentId },
|
|
85
|
+
select: {
|
|
86
|
+
name: true,
|
|
87
|
+
welcome_message: true,
|
|
88
|
+
conversation_starters: true,
|
|
89
|
+
primary_color: true,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
if (!agent) return {};
|
|
93
|
+
const starters = Array.isArray(agent.conversation_starters)
|
|
94
|
+
? (agent.conversation_starters as unknown[]).filter(
|
|
95
|
+
(entry): entry is string => typeof entry === "string" && entry.trim().length > 0,
|
|
96
|
+
)
|
|
97
|
+
: [];
|
|
98
|
+
return {
|
|
99
|
+
title: agent.name ?? undefined,
|
|
100
|
+
welcomeMessage: agent.welcome_message ?? undefined,
|
|
101
|
+
conversationStarters: starters.length > 0 ? starters : undefined,
|
|
102
|
+
primaryColor: agent.primary_color ?? undefined,
|
|
103
|
+
};
|
|
104
|
+
} catch (err) {
|
|
105
|
+
// FAIL SOFT, LIKE THE REST OF THE INJECTION. A presentation read that
|
|
106
|
+
// fails must not remove the bubble: the launcher carries its own
|
|
107
|
+
// default for every one of these fields, so an unreadable row costs
|
|
108
|
+
// wording, never the assistant itself.
|
|
109
|
+
getAppLogger().error(
|
|
110
|
+
`[resource-chat] agent presentation read failed (${agentId}): ${err instanceof Error ? err.message : String(err)}`,
|
|
111
|
+
);
|
|
112
|
+
return {};
|
|
113
|
+
}
|
|
62
114
|
}
|
|
63
115
|
|
|
64
116
|
/** Serialise the resource record + entire page content tree as text for the GPT. */
|
|
@@ -157,6 +209,7 @@ export async function injectResourceChatLauncher(
|
|
|
157
209
|
|
|
158
210
|
try {
|
|
159
211
|
const runtimeContext = await buildResourceContext(resolved, opts.resourceKey);
|
|
212
|
+
const presentation = await loadAgentPresentation(agentId);
|
|
160
213
|
(page.content as Array<Record<string, unknown>>).push({
|
|
161
214
|
type: LAUNCHER_BLOCK,
|
|
162
215
|
props: {
|
|
@@ -165,12 +218,14 @@ export async function injectResourceChatLauncher(
|
|
|
165
218
|
organisationId,
|
|
166
219
|
viewerEmail,
|
|
167
220
|
runtimeContext,
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
221
|
+
// The page's own brand still wins over the agent's stored colour:
|
|
222
|
+
// the bubble sits ON this document and must match it.
|
|
223
|
+
primaryColor: brandPrimaryColor(resolved) || presentation.primaryColor,
|
|
224
|
+
title: presentation.title,
|
|
225
|
+
welcomeMessage: presentation.welcomeMessage,
|
|
171
226
|
launchLabel: opts.launchLabel,
|
|
172
227
|
placeholder: opts.placeholder,
|
|
173
|
-
conversationStarters:
|
|
228
|
+
conversationStarters: presentation.conversationStarters,
|
|
174
229
|
},
|
|
175
230
|
});
|
|
176
231
|
} catch (err) {
|