@stacksjs/ai 0.70.87 → 0.70.90
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/dist/agents/claude/index.js +197 -0
- package/dist/agents/index.js +1 -0
- package/dist/buddy.js +393 -0
- package/dist/drivers/anthropic/index.js +276 -0
- package/dist/drivers/claude-agent-sdk/index.js +198 -0
- package/dist/drivers/index.js +4 -0
- package/dist/drivers/ollama/index.js +332 -0
- package/dist/drivers/openai/index.js +351 -0
- package/dist/image.js +375 -0
- package/dist/index.js +16 -175
- package/dist/mcp.js +361 -0
- package/dist/personalization.js +244 -0
- package/dist/search.js +316 -0
- package/dist/text.js +51 -0
- package/dist/types.js +0 -0
- package/dist/utils/client-bedrock-runtime.js +17 -0
- package/dist/utils/client-bedrock.js +20 -0
- package/dist/utils/model-access.js +21 -0
- package/dist/utils/retry.js +39 -0
- package/dist/utils/tokens.js +59 -0
- package/dist/utils/usage.js +27 -0
- package/dist/utils/vision.js +54 -0
- package/package.json +1 -1
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
export class MCPClient {
|
|
2
|
+
config;
|
|
3
|
+
process = null;
|
|
4
|
+
requestId = 0;
|
|
5
|
+
pendingRequests = new Map;
|
|
6
|
+
tools = [];
|
|
7
|
+
resources = [];
|
|
8
|
+
prompts = [];
|
|
9
|
+
initialized = !1;
|
|
10
|
+
buffer = "";
|
|
11
|
+
constructor(config) {
|
|
12
|
+
this.config = config;
|
|
13
|
+
}
|
|
14
|
+
async connect() {
|
|
15
|
+
if (this.config.transport.type === "stdio")
|
|
16
|
+
await this.connectStdio();
|
|
17
|
+
else if (this.config.transport.type === "sse" || this.config.transport.type === "streamable-http")
|
|
18
|
+
await this.connectHTTP();
|
|
19
|
+
else
|
|
20
|
+
throw Error(`Unsupported transport type: ${this.config.transport.type}`);
|
|
21
|
+
this.initialized = !0;
|
|
22
|
+
}
|
|
23
|
+
async connectStdio() {
|
|
24
|
+
const transport = this.config.transport, { spawn } = await import("bun");
|
|
25
|
+
this.process = spawn([transport.command, ...transport.args || []], {
|
|
26
|
+
stdin: "pipe",
|
|
27
|
+
stdout: "pipe",
|
|
28
|
+
stderr: "pipe",
|
|
29
|
+
env: { ...process.env, ...transport.env }
|
|
30
|
+
});
|
|
31
|
+
this.readStdout();
|
|
32
|
+
await this.sendRequest("initialize", {
|
|
33
|
+
protocolVersion: "2024-11-05",
|
|
34
|
+
capabilities: {},
|
|
35
|
+
clientInfo: { name: "stacks-ai", version: "1.0.0" }
|
|
36
|
+
});
|
|
37
|
+
await this.sendNotification("notifications/initialized", {});
|
|
38
|
+
await this.discoverCapabilities();
|
|
39
|
+
}
|
|
40
|
+
async connectHTTP() {
|
|
41
|
+
const transport = this.config.transport, response = await fetch(transport.url, {
|
|
42
|
+
method: "POST",
|
|
43
|
+
headers: {
|
|
44
|
+
"Content-Type": "application/json",
|
|
45
|
+
...transport.headers
|
|
46
|
+
},
|
|
47
|
+
body: JSON.stringify({
|
|
48
|
+
jsonrpc: "2.0",
|
|
49
|
+
id: this.nextId(),
|
|
50
|
+
method: "initialize",
|
|
51
|
+
params: {
|
|
52
|
+
protocolVersion: "2024-11-05",
|
|
53
|
+
capabilities: {},
|
|
54
|
+
clientInfo: { name: "stacks-ai", version: "1.0.0" }
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
});
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
const error = await response.text();
|
|
60
|
+
throw Error(`MCP HTTP connection error: ${error}`);
|
|
61
|
+
}
|
|
62
|
+
await this.discoverCapabilities();
|
|
63
|
+
}
|
|
64
|
+
async readStdout() {
|
|
65
|
+
if (!this.process)
|
|
66
|
+
return;
|
|
67
|
+
const reader = this.process.stdout?.getReader();
|
|
68
|
+
if (!reader)
|
|
69
|
+
return;
|
|
70
|
+
const decoder = new TextDecoder;
|
|
71
|
+
try {
|
|
72
|
+
while (!0) {
|
|
73
|
+
const { done, value } = await reader.read();
|
|
74
|
+
if (done)
|
|
75
|
+
break;
|
|
76
|
+
this.buffer += decoder.decode(value, { stream: !0 });
|
|
77
|
+
this.processBuffer();
|
|
78
|
+
}
|
|
79
|
+
} catch {}
|
|
80
|
+
}
|
|
81
|
+
processBuffer() {
|
|
82
|
+
const lines = this.buffer.split(`
|
|
83
|
+
`);
|
|
84
|
+
this.buffer = lines.pop() || "";
|
|
85
|
+
for (const line of lines) {
|
|
86
|
+
if (!line.trim())
|
|
87
|
+
continue;
|
|
88
|
+
try {
|
|
89
|
+
const message = JSON.parse(line);
|
|
90
|
+
if (message.id !== void 0 && this.pendingRequests.has(message.id)) {
|
|
91
|
+
const pending = this.pendingRequests.get(message.id);
|
|
92
|
+
this.pendingRequests.delete(message.id);
|
|
93
|
+
if (message.error)
|
|
94
|
+
pending.reject(Error(message.error.message || "MCP error"));
|
|
95
|
+
else
|
|
96
|
+
pending.resolve(message.result);
|
|
97
|
+
}
|
|
98
|
+
} catch {}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
nextId() {
|
|
102
|
+
return ++this.requestId;
|
|
103
|
+
}
|
|
104
|
+
async sendRequest(method, params = {}) {
|
|
105
|
+
const id = this.nextId(), message = JSON.stringify({
|
|
106
|
+
jsonrpc: "2.0",
|
|
107
|
+
id,
|
|
108
|
+
method,
|
|
109
|
+
params
|
|
110
|
+
}) + `
|
|
111
|
+
`;
|
|
112
|
+
if (this.config.transport.type === "stdio" && this.process) {
|
|
113
|
+
const writer = this.process.stdin?.getWriter();
|
|
114
|
+
if (writer) {
|
|
115
|
+
await writer.write(new TextEncoder().encode(message));
|
|
116
|
+
writer.releaseLock();
|
|
117
|
+
}
|
|
118
|
+
return new Promise((resolve, reject) => {
|
|
119
|
+
const timeout = setTimeout(() => {
|
|
120
|
+
this.pendingRequests.delete(id);
|
|
121
|
+
reject(Error(`MCP request timeout: ${method}`));
|
|
122
|
+
}, 30000);
|
|
123
|
+
this.pendingRequests.set(id, {
|
|
124
|
+
resolve: (value) => {
|
|
125
|
+
clearTimeout(timeout);
|
|
126
|
+
resolve(value);
|
|
127
|
+
},
|
|
128
|
+
reject: (error) => {
|
|
129
|
+
clearTimeout(timeout);
|
|
130
|
+
reject(error);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (this.config.transport.type === "sse" || this.config.transport.type === "streamable-http") {
|
|
136
|
+
const transport = this.config.transport, response = await fetch(transport.url, {
|
|
137
|
+
method: "POST",
|
|
138
|
+
headers: {
|
|
139
|
+
"Content-Type": "application/json",
|
|
140
|
+
...transport.headers
|
|
141
|
+
},
|
|
142
|
+
body: message
|
|
143
|
+
});
|
|
144
|
+
if (!response.ok) {
|
|
145
|
+
const error = await response.text();
|
|
146
|
+
throw Error(`MCP request error: ${error}`);
|
|
147
|
+
}
|
|
148
|
+
const result = await response.json();
|
|
149
|
+
if (result.error)
|
|
150
|
+
throw Error(result.error.message || "MCP error");
|
|
151
|
+
return result.result;
|
|
152
|
+
}
|
|
153
|
+
throw Error("No transport available");
|
|
154
|
+
}
|
|
155
|
+
async sendNotification(method, params = {}) {
|
|
156
|
+
const message = JSON.stringify({
|
|
157
|
+
jsonrpc: "2.0",
|
|
158
|
+
method,
|
|
159
|
+
params
|
|
160
|
+
}) + `
|
|
161
|
+
`;
|
|
162
|
+
if (this.config.transport.type === "stdio" && this.process) {
|
|
163
|
+
const writer = this.process.stdin?.getWriter();
|
|
164
|
+
if (writer) {
|
|
165
|
+
await writer.write(new TextEncoder().encode(message));
|
|
166
|
+
writer.releaseLock();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async discoverCapabilities() {
|
|
171
|
+
const capabilities = this.config.capabilities || { tools: !0, resources: !0, prompts: !0 };
|
|
172
|
+
if (capabilities.tools !== !1)
|
|
173
|
+
try {
|
|
174
|
+
const result = await this.sendRequest("tools/list");
|
|
175
|
+
this.tools = result?.tools || [];
|
|
176
|
+
} catch {
|
|
177
|
+
this.tools = [];
|
|
178
|
+
}
|
|
179
|
+
if (capabilities.resources !== !1)
|
|
180
|
+
try {
|
|
181
|
+
const result = await this.sendRequest("resources/list");
|
|
182
|
+
this.resources = result?.resources || [];
|
|
183
|
+
} catch {
|
|
184
|
+
this.resources = [];
|
|
185
|
+
}
|
|
186
|
+
if (capabilities.prompts !== !1)
|
|
187
|
+
try {
|
|
188
|
+
const result = await this.sendRequest("prompts/list");
|
|
189
|
+
this.prompts = result?.prompts || [];
|
|
190
|
+
} catch {
|
|
191
|
+
this.prompts = [];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
listTools() {
|
|
195
|
+
return this.tools;
|
|
196
|
+
}
|
|
197
|
+
listResources() {
|
|
198
|
+
return this.resources;
|
|
199
|
+
}
|
|
200
|
+
listPrompts() {
|
|
201
|
+
return this.prompts;
|
|
202
|
+
}
|
|
203
|
+
async callTool(name, args = {}) {
|
|
204
|
+
if (!this.initialized)
|
|
205
|
+
throw Error("MCP client not connected. Call connect() first.");
|
|
206
|
+
return await this.sendRequest("tools/call", { name, arguments: args });
|
|
207
|
+
}
|
|
208
|
+
async readResource(uri) {
|
|
209
|
+
if (!this.initialized)
|
|
210
|
+
throw Error("MCP client not connected. Call connect() first.");
|
|
211
|
+
const contents = (await this.sendRequest("resources/read", { uri }))?.contents?.[0];
|
|
212
|
+
return {
|
|
213
|
+
uri: contents?.uri || uri,
|
|
214
|
+
mimeType: contents?.mimeType,
|
|
215
|
+
text: contents?.text,
|
|
216
|
+
blob: contents?.blob
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
async getPrompt(name, args = {}) {
|
|
220
|
+
if (!this.initialized)
|
|
221
|
+
throw Error("MCP client not connected. Call connect() first.");
|
|
222
|
+
return this.sendRequest("prompts/get", { name, arguments: args });
|
|
223
|
+
}
|
|
224
|
+
toAnthropicTools() {
|
|
225
|
+
return this.tools.map((tool) => ({
|
|
226
|
+
name: tool.name,
|
|
227
|
+
description: tool.description,
|
|
228
|
+
input_schema: tool.inputSchema
|
|
229
|
+
}));
|
|
230
|
+
}
|
|
231
|
+
toOpenAITools() {
|
|
232
|
+
return this.tools.map((tool) => ({
|
|
233
|
+
type: "function",
|
|
234
|
+
function: {
|
|
235
|
+
name: tool.name,
|
|
236
|
+
description: tool.description,
|
|
237
|
+
parameters: tool.inputSchema
|
|
238
|
+
}
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
241
|
+
async disconnect() {
|
|
242
|
+
if (this.process) {
|
|
243
|
+
this.process.kill();
|
|
244
|
+
this.process = null;
|
|
245
|
+
}
|
|
246
|
+
for (const pending of this.pendingRequests.values())
|
|
247
|
+
try {
|
|
248
|
+
pending.reject(Error("MCP client disconnected"));
|
|
249
|
+
} catch {}
|
|
250
|
+
this.pendingRequests.clear();
|
|
251
|
+
this.initialized = !1;
|
|
252
|
+
this.tools = [];
|
|
253
|
+
this.resources = [];
|
|
254
|
+
this.prompts = [];
|
|
255
|
+
}
|
|
256
|
+
get isConnected() {
|
|
257
|
+
return this.initialized;
|
|
258
|
+
}
|
|
259
|
+
get serverName() {
|
|
260
|
+
return this.config.name;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export class MCPManager {
|
|
265
|
+
clients = new Map;
|
|
266
|
+
async addServer(config) {
|
|
267
|
+
if (this.clients.has(config.name))
|
|
268
|
+
throw Error(`MCP server already registered: ${config.name}`);
|
|
269
|
+
const client = new MCPClient(config);
|
|
270
|
+
await client.connect();
|
|
271
|
+
this.clients.set(config.name, client);
|
|
272
|
+
return client;
|
|
273
|
+
}
|
|
274
|
+
async removeServer(name) {
|
|
275
|
+
const client = this.clients.get(name);
|
|
276
|
+
if (client) {
|
|
277
|
+
await client.disconnect();
|
|
278
|
+
this.clients.delete(name);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
getServer(name) {
|
|
282
|
+
return this.clients.get(name);
|
|
283
|
+
}
|
|
284
|
+
listServers() {
|
|
285
|
+
return Array.from(this.clients.keys());
|
|
286
|
+
}
|
|
287
|
+
getAllTools() {
|
|
288
|
+
const allTools = [];
|
|
289
|
+
for (const [name, client] of this.clients)
|
|
290
|
+
for (const tool of client.listTools())
|
|
291
|
+
allTools.push({ ...tool, serverName: name });
|
|
292
|
+
return allTools;
|
|
293
|
+
}
|
|
294
|
+
async callTool(toolPath, args = {}) {
|
|
295
|
+
if (toolPath.includes("/")) {
|
|
296
|
+
const [serverName, toolName] = toolPath.split("/", 2);
|
|
297
|
+
if (!serverName || !toolName)
|
|
298
|
+
throw Error(`Invalid MCP tool path: ${toolPath}`);
|
|
299
|
+
const client = this.clients.get(serverName);
|
|
300
|
+
if (!client)
|
|
301
|
+
throw Error(`MCP server not found: ${serverName}`);
|
|
302
|
+
return client.callTool(toolName, args);
|
|
303
|
+
}
|
|
304
|
+
for (const [, client] of this.clients)
|
|
305
|
+
if (client.listTools().some((t) => t.name === toolPath))
|
|
306
|
+
return client.callTool(toolPath, args);
|
|
307
|
+
throw Error(`MCP tool not found: ${toolPath}`);
|
|
308
|
+
}
|
|
309
|
+
toAnthropicTools() {
|
|
310
|
+
const tools = [];
|
|
311
|
+
for (const [serverName, client] of this.clients)
|
|
312
|
+
for (const tool of client.listTools())
|
|
313
|
+
tools.push({
|
|
314
|
+
name: `${serverName}__${tool.name}`,
|
|
315
|
+
description: `[${serverName}] ${tool.description}`,
|
|
316
|
+
input_schema: tool.inputSchema
|
|
317
|
+
});
|
|
318
|
+
return tools;
|
|
319
|
+
}
|
|
320
|
+
toOpenAITools() {
|
|
321
|
+
const tools = [];
|
|
322
|
+
for (const [serverName, client] of this.clients)
|
|
323
|
+
for (const tool of client.listTools())
|
|
324
|
+
tools.push({
|
|
325
|
+
type: "function",
|
|
326
|
+
function: {
|
|
327
|
+
name: `${serverName}__${tool.name}`,
|
|
328
|
+
description: `[${serverName}] ${tool.description}`,
|
|
329
|
+
parameters: tool.inputSchema
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
return tools;
|
|
333
|
+
}
|
|
334
|
+
async disconnectAll() {
|
|
335
|
+
const disconnects = Array.from(this.clients.values()).map((c) => c.disconnect());
|
|
336
|
+
await Promise.all(disconnects);
|
|
337
|
+
this.clients.clear();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
export async function connectStdio(name, command, args, env) {
|
|
341
|
+
const client = new MCPClient({
|
|
342
|
+
name,
|
|
343
|
+
transport: { type: "stdio", command, args, env }
|
|
344
|
+
});
|
|
345
|
+
await client.connect();
|
|
346
|
+
return client;
|
|
347
|
+
}
|
|
348
|
+
export async function connectHTTP(name, url, headers) {
|
|
349
|
+
const client = new MCPClient({
|
|
350
|
+
name,
|
|
351
|
+
transport: { type: "streamable-http", url, headers }
|
|
352
|
+
});
|
|
353
|
+
await client.connect();
|
|
354
|
+
return client;
|
|
355
|
+
}
|
|
356
|
+
export const mcp = {
|
|
357
|
+
MCPClient,
|
|
358
|
+
MCPManager,
|
|
359
|
+
connectStdio,
|
|
360
|
+
connectHTTP
|
|
361
|
+
};
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
export async function analyzeSentiment(text, options = {}) {
|
|
2
|
+
const { provider = "anthropic", aspects } = options, aspectInstruction = aspects ? `
|
|
3
|
+
Also analyze sentiment for these specific aspects: ${aspects.join(", ")}` : "", systemPrompt = `You are a sentiment analysis expert. Analyze the sentiment of the given text and respond with ONLY valid JSON in this exact format:
|
|
4
|
+
{
|
|
5
|
+
"sentiment": "positive" | "negative" | "neutral" | "mixed",
|
|
6
|
+
"score": <number from -1.0 to 1.0>,
|
|
7
|
+
"confidence": <number from 0.0 to 1.0>${aspects ? `,
|
|
8
|
+
"aspects": [{"aspect": "<name>", "sentiment": "positive" | "negative" | "neutral", "score": <number>}]` : ""}
|
|
9
|
+
}${aspectInstruction}`, result = await callProvider(provider, systemPrompt, text, options.model);
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(result.content);
|
|
12
|
+
} catch {
|
|
13
|
+
const lower = result.content.toLowerCase(), isPositive = lower.includes("positive"), isNegative = lower.includes("negative");
|
|
14
|
+
return {
|
|
15
|
+
sentiment: isPositive && isNegative ? "mixed" : isPositive ? "positive" : isNegative ? "negative" : "neutral",
|
|
16
|
+
score: isPositive ? 0.5 : isNegative ? -0.5 : 0,
|
|
17
|
+
confidence: 0.5
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function classifyText(text, labels, options = {}) {
|
|
22
|
+
const { provider = "anthropic", multiLabel = !1 } = options, systemPrompt = `You are a text classification expert. Classify the given text into ${multiLabel ? "one or more of" : "exactly one of"} these categories: ${labels.join(", ")}.
|
|
23
|
+
|
|
24
|
+
Respond with ONLY valid JSON in this exact format:
|
|
25
|
+
{
|
|
26
|
+
"label": "<primary label>",
|
|
27
|
+
"confidence": <number from 0.0 to 1.0>,
|
|
28
|
+
"allLabels": [{"label": "<label>", "confidence": <number>}]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
Include ALL provided labels in allLabels with their confidence scores, sorted by confidence descending.`, result = await callProvider(provider, systemPrompt, text, options.model);
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(result.content);
|
|
34
|
+
} catch {
|
|
35
|
+
return {
|
|
36
|
+
label: labels[0] ?? "",
|
|
37
|
+
confidence: 0.5,
|
|
38
|
+
allLabels: labels.map((l) => ({ label: l, confidence: 1 / labels.length }))
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function summarize(text, options = {}) {
|
|
43
|
+
const {
|
|
44
|
+
provider = "anthropic",
|
|
45
|
+
style = "concise",
|
|
46
|
+
maxLength,
|
|
47
|
+
language
|
|
48
|
+
} = options;
|
|
49
|
+
let styleInstruction;
|
|
50
|
+
switch (style) {
|
|
51
|
+
case "bullet-points":
|
|
52
|
+
styleInstruction = "Use bullet points to organize the key points.";
|
|
53
|
+
break;
|
|
54
|
+
case "detailed":
|
|
55
|
+
styleInstruction = "Provide a detailed summary covering all major points.";
|
|
56
|
+
break;
|
|
57
|
+
default:
|
|
58
|
+
styleInstruction = "Be concise and focus on the most important points.";
|
|
59
|
+
}
|
|
60
|
+
const lengthInstruction = maxLength ? ` Keep the summary under ${maxLength} words.` : "", languageInstruction = language ? ` Write the summary in ${language}.` : "", systemPrompt = `You are an expert summarizer. ${styleInstruction}${lengthInstruction}${languageInstruction}`;
|
|
61
|
+
return callProvider(provider, systemPrompt, `Summarize the following text:
|
|
62
|
+
|
|
63
|
+
${text}`, options.model);
|
|
64
|
+
}
|
|
65
|
+
export async function recommend(profile, items, options = {}) {
|
|
66
|
+
const { provider = "anthropic", limit = 5 } = options, profileSummary = buildProfileSummary(profile), itemsList = items.map((item) => `ID: ${item.id} | Category: ${item.category || "none"} | Tags: ${item.tags?.join(", ") || "none"} | Content: ${item.content.slice(0, 200)}`).join(`
|
|
67
|
+
`), systemPrompt = `You are a recommendation engine. Based on the user profile, recommend the most relevant items. Respond with ONLY valid JSON:
|
|
68
|
+
{
|
|
69
|
+
"recommendations": [
|
|
70
|
+
{"itemId": "<id>", "score": <0.0-1.0>, "reason": "<brief reason>"}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
Return at most ${limit} recommendations, sorted by relevance score descending.`, prompt = `User Profile:
|
|
75
|
+
${profileSummary}
|
|
76
|
+
|
|
77
|
+
Available Items:
|
|
78
|
+
${itemsList}`, result = await callProvider(provider, systemPrompt, prompt, options.model);
|
|
79
|
+
try {
|
|
80
|
+
return {
|
|
81
|
+
recommendations: JSON.parse(result.content).recommendations.slice(0, limit),
|
|
82
|
+
model: result.model,
|
|
83
|
+
provider
|
|
84
|
+
};
|
|
85
|
+
} catch {
|
|
86
|
+
return {
|
|
87
|
+
recommendations: [],
|
|
88
|
+
model: result.model,
|
|
89
|
+
provider
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function buildProfileSummary(profile) {
|
|
94
|
+
const topPreferences = Object.entries(profile.preferences).sort(([, a], [, b]) => b - a).slice(0, 10).map(([key, value]) => `${key}: ${value.toFixed(2)}`).join(", "), recentInteractions = profile.interactions.sort((a, b) => b.timestamp - a.timestamp).slice(0, 10).map((i) => `${i.type} on ${i.itemId}`).join(", ");
|
|
95
|
+
return `Segments: ${profile.segments.join(", ")}
|
|
96
|
+
Top Preferences: ${topPreferences || "none"}
|
|
97
|
+
Recent Interactions: ${recentInteractions || "none"}`;
|
|
98
|
+
}
|
|
99
|
+
export function createProfile(id, segments = []) {
|
|
100
|
+
return {
|
|
101
|
+
id,
|
|
102
|
+
preferences: {},
|
|
103
|
+
interactions: [],
|
|
104
|
+
segments
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
export function recordInteraction(profile, interaction) {
|
|
108
|
+
profile.interactions.push(interaction);
|
|
109
|
+
const weight = {
|
|
110
|
+
view: 0.1,
|
|
111
|
+
click: 0.3,
|
|
112
|
+
like: 0.5,
|
|
113
|
+
share: 0.6,
|
|
114
|
+
bookmark: 0.7,
|
|
115
|
+
purchase: 1,
|
|
116
|
+
dislike: -0.5,
|
|
117
|
+
custom: interaction.weight || 0.3
|
|
118
|
+
}[interaction.type] || 0.1, key = interaction.itemId;
|
|
119
|
+
profile.preferences[key] = (profile.preferences[key] || 0) + weight;
|
|
120
|
+
return profile;
|
|
121
|
+
}
|
|
122
|
+
export async function extractUserInterests(profile, items, options = {}) {
|
|
123
|
+
const { provider = "anthropic" } = options, interactedItemIds = new Set(profile.interactions.map((i) => i.itemId)), interactedItems = items.filter((item) => interactedItemIds.has(item.id));
|
|
124
|
+
if (interactedItems.length === 0)
|
|
125
|
+
return profile.segments;
|
|
126
|
+
const contentSample = interactedItems.slice(0, 20).map((item) => item.content.slice(0, 200)).join(`
|
|
127
|
+
---
|
|
128
|
+
`), result = await callProvider(provider, `Extract the main interests/topics from the user's interaction history. Return ONLY a JSON array of strings, e.g. ["technology", "cooking", "travel"]. Maximum 10 interests.`, contentSample, options.model);
|
|
129
|
+
try {
|
|
130
|
+
return JSON.parse(result.content);
|
|
131
|
+
} catch {
|
|
132
|
+
return profile.segments;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function callProvider(provider, systemPrompt, userMessage, model) {
|
|
136
|
+
if (provider === "anthropic") {
|
|
137
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
138
|
+
if (!apiKey)
|
|
139
|
+
throw Error("ANTHROPIC_API_KEY required.");
|
|
140
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
141
|
+
method: "POST",
|
|
142
|
+
headers: {
|
|
143
|
+
"Content-Type": "application/json",
|
|
144
|
+
"x-api-key": apiKey,
|
|
145
|
+
"anthropic-version": "2023-06-01"
|
|
146
|
+
},
|
|
147
|
+
body: JSON.stringify({
|
|
148
|
+
model: model || "claude-sonnet-4-20250514",
|
|
149
|
+
max_tokens: 4096,
|
|
150
|
+
system: systemPrompt,
|
|
151
|
+
messages: [{ role: "user", content: userMessage }]
|
|
152
|
+
})
|
|
153
|
+
});
|
|
154
|
+
if (!response.ok) {
|
|
155
|
+
const error = await response.text();
|
|
156
|
+
throw Error(`Claude API error: ${error}`);
|
|
157
|
+
}
|
|
158
|
+
const data = await response.json();
|
|
159
|
+
return {
|
|
160
|
+
content: data.content[0].text,
|
|
161
|
+
model: data.model,
|
|
162
|
+
usage: {
|
|
163
|
+
promptTokens: data.usage?.input_tokens || 0,
|
|
164
|
+
completionTokens: data.usage?.output_tokens || 0,
|
|
165
|
+
totalTokens: (data.usage?.input_tokens || 0) + (data.usage?.output_tokens || 0)
|
|
166
|
+
},
|
|
167
|
+
finishReason: data.stop_reason
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (provider === "openai") {
|
|
171
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
172
|
+
if (!apiKey)
|
|
173
|
+
throw Error("OPENAI_API_KEY required.");
|
|
174
|
+
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
175
|
+
method: "POST",
|
|
176
|
+
headers: {
|
|
177
|
+
"Content-Type": "application/json",
|
|
178
|
+
Authorization: `Bearer ${apiKey}`
|
|
179
|
+
},
|
|
180
|
+
body: JSON.stringify({
|
|
181
|
+
model: model || "gpt-4o",
|
|
182
|
+
max_tokens: 4096,
|
|
183
|
+
messages: [
|
|
184
|
+
{ role: "system", content: systemPrompt },
|
|
185
|
+
{ role: "user", content: userMessage }
|
|
186
|
+
]
|
|
187
|
+
})
|
|
188
|
+
});
|
|
189
|
+
if (!response.ok) {
|
|
190
|
+
const error = await response.text();
|
|
191
|
+
throw Error(`OpenAI API error: ${error}`);
|
|
192
|
+
}
|
|
193
|
+
const data = await response.json();
|
|
194
|
+
return {
|
|
195
|
+
content: data.choices[0].message.content,
|
|
196
|
+
model: data.model,
|
|
197
|
+
usage: {
|
|
198
|
+
promptTokens: data.usage?.prompt_tokens || 0,
|
|
199
|
+
completionTokens: data.usage?.completion_tokens || 0,
|
|
200
|
+
totalTokens: data.usage?.total_tokens || 0
|
|
201
|
+
},
|
|
202
|
+
finishReason: data.choices[0].finish_reason
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
if (provider === "ollama") {
|
|
206
|
+
const host = process.env.OLLAMA_HOST || "http://localhost:11434", response = await fetch(`${host}/api/chat`, {
|
|
207
|
+
method: "POST",
|
|
208
|
+
headers: { "Content-Type": "application/json" },
|
|
209
|
+
body: JSON.stringify({
|
|
210
|
+
model: model || "llama3.2",
|
|
211
|
+
messages: [
|
|
212
|
+
{ role: "system", content: systemPrompt },
|
|
213
|
+
{ role: "user", content: userMessage }
|
|
214
|
+
],
|
|
215
|
+
stream: !1
|
|
216
|
+
})
|
|
217
|
+
});
|
|
218
|
+
if (!response.ok) {
|
|
219
|
+
const error = await response.text();
|
|
220
|
+
throw Error(`Ollama API error: ${error}`);
|
|
221
|
+
}
|
|
222
|
+
const data = await response.json();
|
|
223
|
+
return {
|
|
224
|
+
content: data.message.content,
|
|
225
|
+
model: data.model,
|
|
226
|
+
usage: {
|
|
227
|
+
promptTokens: data.prompt_eval_count || 0,
|
|
228
|
+
completionTokens: data.eval_count || 0,
|
|
229
|
+
totalTokens: (data.prompt_eval_count || 0) + (data.eval_count || 0)
|
|
230
|
+
},
|
|
231
|
+
finishReason: data.done_reason || "stop"
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
throw Error(`Provider not supported: ${provider}`);
|
|
235
|
+
}
|
|
236
|
+
export const personalization = {
|
|
237
|
+
analyzeSentiment,
|
|
238
|
+
classifyText,
|
|
239
|
+
summarize,
|
|
240
|
+
recommend,
|
|
241
|
+
createProfile,
|
|
242
|
+
recordInteraction,
|
|
243
|
+
extractUserInterests
|
|
244
|
+
};
|