@stacksjs/ai 0.70.55 → 0.70.56
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 +10 -7
- package/src/agents/claude/index.ts +0 -310
- package/src/agents/index.ts +0 -7
- package/src/buddy.ts +0 -619
- package/src/drivers/anthropic/index.ts +0 -430
- package/src/drivers/claude-agent-sdk/index.ts +0 -370
- package/src/drivers/index.ts +0 -14
- package/src/drivers/ollama/index.ts +0 -514
- package/src/drivers/openai/index.ts +0 -529
- package/src/image.ts +0 -607
- package/src/index.ts +0 -53
- package/src/mcp.ts +0 -658
- package/src/personalization.ts +0 -490
- package/src/search.ts +0 -555
- package/src/text.ts +0 -79
- package/src/types.ts +0 -229
- package/src/utils/client-bedrock-runtime.ts +0 -51
- package/src/utils/client-bedrock.ts +0 -75
- package/src/utils/model-access.ts +0 -32
- package/src/utils/retry.ts +0 -124
- package/src/utils/tokens.ts +0 -159
- package/src/utils/usage.ts +0 -98
- package/src/utils/vision.ts +0 -119
- /package/dist/{src/agents → agents}/claude/index.d.ts +0 -0
- /package/dist/{src/agents → agents}/index.d.ts +0 -0
- /package/dist/{src/buddy.d.ts → buddy.d.ts} +0 -0
- /package/dist/{src/drivers → drivers}/anthropic/index.d.ts +0 -0
- /package/dist/{src/drivers → drivers}/claude-agent-sdk/index.d.ts +0 -0
- /package/dist/{src/drivers → drivers}/index.d.ts +0 -0
- /package/dist/{src/drivers → drivers}/ollama/index.d.ts +0 -0
- /package/dist/{src/drivers → drivers}/openai/index.d.ts +0 -0
- /package/dist/{src/image.d.ts → image.d.ts} +0 -0
- /package/dist/{src/index.d.ts → index.d.ts} +0 -0
- /package/dist/{src/index.js → index.js} +0 -0
- /package/dist/{src/mcp.d.ts → mcp.d.ts} +0 -0
- /package/dist/{src/personalization.d.ts → personalization.d.ts} +0 -0
- /package/dist/{src/search.d.ts → search.d.ts} +0 -0
- /package/dist/{src/text.d.ts → text.d.ts} +0 -0
- /package/dist/{src/types.d.ts → types.d.ts} +0 -0
- /package/dist/{src/utils → utils}/client-bedrock-runtime.d.ts +0 -0
- /package/dist/{src/utils → utils}/client-bedrock.d.ts +0 -0
- /package/dist/{src/utils → utils}/model-access.d.ts +0 -0
- /package/dist/{src/utils → utils}/retry.d.ts +0 -0
- /package/dist/{src/utils → utils}/tokens.d.ts +0 -0
- /package/dist/{src/utils → utils}/usage.d.ts +0 -0
- /package/dist/{src/utils → utils}/vision.d.ts +0 -0
package/src/personalization.ts
DELETED
|
@@ -1,490 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AI Personalization Module
|
|
3
|
-
*
|
|
4
|
-
* Provides AI-powered personalization features including content recommendations,
|
|
5
|
-
* user profiling, sentiment analysis, content classification, and summarization.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { AIResult } from './types'
|
|
9
|
-
|
|
10
|
-
// ============================================================================
|
|
11
|
-
// Types
|
|
12
|
-
// ============================================================================
|
|
13
|
-
|
|
14
|
-
export interface UserProfile {
|
|
15
|
-
id: string
|
|
16
|
-
preferences: Record<string, number>
|
|
17
|
-
interactions: Interaction[]
|
|
18
|
-
segments: string[]
|
|
19
|
-
metadata?: Record<string, unknown>
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface Interaction {
|
|
23
|
-
type: 'view' | 'click' | 'purchase' | 'like' | 'dislike' | 'share' | 'bookmark' | 'custom'
|
|
24
|
-
itemId: string
|
|
25
|
-
timestamp: number
|
|
26
|
-
weight?: number
|
|
27
|
-
metadata?: Record<string, unknown>
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface ContentItem {
|
|
31
|
-
id: string
|
|
32
|
-
content: string
|
|
33
|
-
category?: string
|
|
34
|
-
tags?: string[]
|
|
35
|
-
metadata?: Record<string, unknown>
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface RecommendationOptions {
|
|
39
|
-
provider?: 'anthropic' | 'openai' | 'ollama'
|
|
40
|
-
model?: string
|
|
41
|
-
maxTokens?: number
|
|
42
|
-
temperature?: number
|
|
43
|
-
limit?: number
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface RecommendationResult {
|
|
47
|
-
recommendations: Array<{
|
|
48
|
-
itemId: string
|
|
49
|
-
score: number
|
|
50
|
-
reason: string
|
|
51
|
-
}>
|
|
52
|
-
model: string
|
|
53
|
-
provider: string
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export interface SentimentResult {
|
|
57
|
-
sentiment: 'positive' | 'negative' | 'neutral' | 'mixed'
|
|
58
|
-
score: number
|
|
59
|
-
confidence: number
|
|
60
|
-
aspects?: Array<{
|
|
61
|
-
aspect: string
|
|
62
|
-
sentiment: 'positive' | 'negative' | 'neutral'
|
|
63
|
-
score: number
|
|
64
|
-
}>
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export interface ClassificationResult {
|
|
68
|
-
label: string
|
|
69
|
-
confidence: number
|
|
70
|
-
allLabels: Array<{
|
|
71
|
-
label: string
|
|
72
|
-
confidence: number
|
|
73
|
-
}>
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export interface SummaryOptions {
|
|
77
|
-
provider?: 'anthropic' | 'openai' | 'ollama'
|
|
78
|
-
model?: string
|
|
79
|
-
maxLength?: number
|
|
80
|
-
style?: 'concise' | 'detailed' | 'bullet-points'
|
|
81
|
-
language?: string
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// ============================================================================
|
|
85
|
-
// Sentiment Analysis
|
|
86
|
-
// ============================================================================
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Analyze the sentiment of text using AI.
|
|
90
|
-
*/
|
|
91
|
-
export async function analyzeSentiment(
|
|
92
|
-
text: string,
|
|
93
|
-
options: {
|
|
94
|
-
provider?: 'anthropic' | 'openai' | 'ollama'
|
|
95
|
-
model?: string
|
|
96
|
-
aspects?: string[]
|
|
97
|
-
} = {},
|
|
98
|
-
): Promise<SentimentResult> {
|
|
99
|
-
const { provider = 'anthropic', aspects } = options
|
|
100
|
-
|
|
101
|
-
const aspectInstruction = aspects
|
|
102
|
-
? `\nAlso analyze sentiment for these specific aspects: ${aspects.join(', ')}`
|
|
103
|
-
: ''
|
|
104
|
-
|
|
105
|
-
const systemPrompt = `You are a sentiment analysis expert. Analyze the sentiment of the given text and respond with ONLY valid JSON in this exact format:
|
|
106
|
-
{
|
|
107
|
-
"sentiment": "positive" | "negative" | "neutral" | "mixed",
|
|
108
|
-
"score": <number from -1.0 to 1.0>,
|
|
109
|
-
"confidence": <number from 0.0 to 1.0>${aspects ? `,
|
|
110
|
-
"aspects": [{"aspect": "<name>", "sentiment": "positive" | "negative" | "neutral", "score": <number>}]` : ''}
|
|
111
|
-
}${aspectInstruction}`
|
|
112
|
-
|
|
113
|
-
const result = await callProvider(provider, systemPrompt, text, options.model)
|
|
114
|
-
|
|
115
|
-
try {
|
|
116
|
-
return JSON.parse(result.content) as SentimentResult
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
// Fallback: extract sentiment from text response
|
|
120
|
-
const lower = result.content.toLowerCase()
|
|
121
|
-
const isPositive = lower.includes('positive')
|
|
122
|
-
const isNegative = lower.includes('negative')
|
|
123
|
-
return {
|
|
124
|
-
sentiment: isPositive && isNegative ? 'mixed' : isPositive ? 'positive' : isNegative ? 'negative' : 'neutral',
|
|
125
|
-
score: isPositive ? 0.5 : isNegative ? -0.5 : 0,
|
|
126
|
-
confidence: 0.5,
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// ============================================================================
|
|
132
|
-
// Text Classification
|
|
133
|
-
// ============================================================================
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Classify text into one of the provided labels.
|
|
137
|
-
*/
|
|
138
|
-
export async function classifyText(
|
|
139
|
-
text: string,
|
|
140
|
-
labels: string[],
|
|
141
|
-
options: {
|
|
142
|
-
provider?: 'anthropic' | 'openai' | 'ollama'
|
|
143
|
-
model?: string
|
|
144
|
-
multiLabel?: boolean
|
|
145
|
-
} = {},
|
|
146
|
-
): Promise<ClassificationResult> {
|
|
147
|
-
const { provider = 'anthropic', multiLabel = false } = options
|
|
148
|
-
|
|
149
|
-
const systemPrompt = `You are a text classification expert. Classify the given text into ${multiLabel ? 'one or more of' : 'exactly one of'} these categories: ${labels.join(', ')}.
|
|
150
|
-
|
|
151
|
-
Respond with ONLY valid JSON in this exact format:
|
|
152
|
-
{
|
|
153
|
-
"label": "<primary label>",
|
|
154
|
-
"confidence": <number from 0.0 to 1.0>,
|
|
155
|
-
"allLabels": [{"label": "<label>", "confidence": <number>}]
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
Include ALL provided labels in allLabels with their confidence scores, sorted by confidence descending.`
|
|
159
|
-
|
|
160
|
-
const result = await callProvider(provider, systemPrompt, text, options.model)
|
|
161
|
-
|
|
162
|
-
try {
|
|
163
|
-
return JSON.parse(result.content) as ClassificationResult
|
|
164
|
-
}
|
|
165
|
-
catch {
|
|
166
|
-
return {
|
|
167
|
-
label: labels[0],
|
|
168
|
-
confidence: 0.5,
|
|
169
|
-
allLabels: labels.map(l => ({ label: l, confidence: 1 / labels.length })),
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// ============================================================================
|
|
175
|
-
// Smart Summarization
|
|
176
|
-
// ============================================================================
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Generate an intelligent summary of text.
|
|
180
|
-
*
|
|
181
|
-
* Not exported as a bare top-level identifier because `text.ts` already
|
|
182
|
-
* exposes `summarize`, and `index.ts` re-exports both via `export *`.
|
|
183
|
-
* Reach this multi-provider variant through the `personalization`
|
|
184
|
-
* namespace export below: `personalization.summarize(...)`.
|
|
185
|
-
*/
|
|
186
|
-
async function summarize(
|
|
187
|
-
text: string,
|
|
188
|
-
options: SummaryOptions = {},
|
|
189
|
-
): Promise<AIResult> {
|
|
190
|
-
const {
|
|
191
|
-
provider = 'anthropic',
|
|
192
|
-
style = 'concise',
|
|
193
|
-
maxLength,
|
|
194
|
-
language,
|
|
195
|
-
} = options
|
|
196
|
-
|
|
197
|
-
let styleInstruction: string
|
|
198
|
-
switch (style) {
|
|
199
|
-
case 'bullet-points':
|
|
200
|
-
styleInstruction = 'Use bullet points to organize the key points.'
|
|
201
|
-
break
|
|
202
|
-
case 'detailed':
|
|
203
|
-
styleInstruction = 'Provide a detailed summary covering all major points.'
|
|
204
|
-
break
|
|
205
|
-
default:
|
|
206
|
-
styleInstruction = 'Be concise and focus on the most important points.'
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const lengthInstruction = maxLength ? ` Keep the summary under ${maxLength} words.` : ''
|
|
210
|
-
const languageInstruction = language ? ` Write the summary in ${language}.` : ''
|
|
211
|
-
|
|
212
|
-
const systemPrompt = `You are an expert summarizer. ${styleInstruction}${lengthInstruction}${languageInstruction}`
|
|
213
|
-
|
|
214
|
-
return callProvider(provider, systemPrompt, `Summarize the following text:\n\n${text}`, options.model)
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// ============================================================================
|
|
218
|
-
// Content Recommendations
|
|
219
|
-
// ============================================================================
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Generate personalized content recommendations based on user profile and available items.
|
|
223
|
-
*/
|
|
224
|
-
export async function recommend(
|
|
225
|
-
profile: UserProfile,
|
|
226
|
-
items: ContentItem[],
|
|
227
|
-
options: RecommendationOptions = {},
|
|
228
|
-
): Promise<RecommendationResult> {
|
|
229
|
-
const { provider = 'anthropic', limit = 5 } = options
|
|
230
|
-
|
|
231
|
-
const profileSummary = buildProfileSummary(profile)
|
|
232
|
-
const itemsList = items.map(item =>
|
|
233
|
-
`ID: ${item.id} | Category: ${item.category || 'none'} | Tags: ${item.tags?.join(', ') || 'none'} | Content: ${item.content.slice(0, 200)}`,
|
|
234
|
-
).join('\n')
|
|
235
|
-
|
|
236
|
-
const systemPrompt = `You are a recommendation engine. Based on the user profile, recommend the most relevant items. Respond with ONLY valid JSON:
|
|
237
|
-
{
|
|
238
|
-
"recommendations": [
|
|
239
|
-
{"itemId": "<id>", "score": <0.0-1.0>, "reason": "<brief reason>"}
|
|
240
|
-
]
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
Return at most ${limit} recommendations, sorted by relevance score descending.`
|
|
244
|
-
|
|
245
|
-
const prompt = `User Profile:\n${profileSummary}\n\nAvailable Items:\n${itemsList}`
|
|
246
|
-
const result = await callProvider(provider, systemPrompt, prompt, options.model)
|
|
247
|
-
|
|
248
|
-
try {
|
|
249
|
-
const parsed = JSON.parse(result.content) as { recommendations: Array<{ itemId: string; score: number; reason: string }> }
|
|
250
|
-
return {
|
|
251
|
-
recommendations: parsed.recommendations.slice(0, limit),
|
|
252
|
-
model: result.model,
|
|
253
|
-
provider: provider,
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
catch {
|
|
257
|
-
return {
|
|
258
|
-
recommendations: [],
|
|
259
|
-
model: result.model,
|
|
260
|
-
provider: provider,
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function buildProfileSummary(profile: UserProfile): string {
|
|
266
|
-
const topPreferences = Object.entries(profile.preferences)
|
|
267
|
-
.sort(([, a], [, b]) => b - a)
|
|
268
|
-
.slice(0, 10)
|
|
269
|
-
.map(([key, value]) => `${key}: ${value.toFixed(2)}`)
|
|
270
|
-
.join(', ')
|
|
271
|
-
|
|
272
|
-
const recentInteractions = profile.interactions
|
|
273
|
-
.sort((a, b) => b.timestamp - a.timestamp)
|
|
274
|
-
.slice(0, 10)
|
|
275
|
-
.map(i => `${i.type} on ${i.itemId}`)
|
|
276
|
-
.join(', ')
|
|
277
|
-
|
|
278
|
-
return `Segments: ${profile.segments.join(', ')}
|
|
279
|
-
Top Preferences: ${topPreferences || 'none'}
|
|
280
|
-
Recent Interactions: ${recentInteractions || 'none'}`
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// ============================================================================
|
|
284
|
-
// User Profile Building
|
|
285
|
-
// ============================================================================
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Create a new empty user profile.
|
|
289
|
-
*/
|
|
290
|
-
export function createProfile(id: string, segments: string[] = []): UserProfile {
|
|
291
|
-
return {
|
|
292
|
-
id,
|
|
293
|
-
preferences: {},
|
|
294
|
-
interactions: [],
|
|
295
|
-
segments,
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Record a user interaction and update preferences.
|
|
301
|
-
*/
|
|
302
|
-
export function recordInteraction(profile: UserProfile, interaction: Interaction): UserProfile {
|
|
303
|
-
profile.interactions.push(interaction)
|
|
304
|
-
|
|
305
|
-
// Update preference weights based on interaction type
|
|
306
|
-
const weights: Record<string, number> = {
|
|
307
|
-
view: 0.1,
|
|
308
|
-
click: 0.3,
|
|
309
|
-
like: 0.5,
|
|
310
|
-
share: 0.6,
|
|
311
|
-
bookmark: 0.7,
|
|
312
|
-
purchase: 1.0,
|
|
313
|
-
dislike: -0.5,
|
|
314
|
-
custom: interaction.weight || 0.3,
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const weight = weights[interaction.type] || 0.1
|
|
318
|
-
const key = interaction.itemId
|
|
319
|
-
profile.preferences[key] = (profile.preferences[key] || 0) + weight
|
|
320
|
-
|
|
321
|
-
return profile
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Extract keywords/topics from user interactions using AI.
|
|
326
|
-
*/
|
|
327
|
-
export async function extractUserInterests(
|
|
328
|
-
profile: UserProfile,
|
|
329
|
-
items: ContentItem[],
|
|
330
|
-
options: { provider?: 'anthropic' | 'openai' | 'ollama'; model?: string } = {},
|
|
331
|
-
): Promise<string[]> {
|
|
332
|
-
const { provider = 'anthropic' } = options
|
|
333
|
-
|
|
334
|
-
const interactedItemIds = new Set(profile.interactions.map(i => i.itemId))
|
|
335
|
-
const interactedItems = items.filter(item => interactedItemIds.has(item.id))
|
|
336
|
-
|
|
337
|
-
if (interactedItems.length === 0) return profile.segments
|
|
338
|
-
|
|
339
|
-
const contentSample = interactedItems
|
|
340
|
-
.slice(0, 20)
|
|
341
|
-
.map(item => item.content.slice(0, 200))
|
|
342
|
-
.join('\n---\n')
|
|
343
|
-
|
|
344
|
-
const systemPrompt = `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.`
|
|
345
|
-
|
|
346
|
-
const result = await callProvider(provider, systemPrompt, contentSample, options.model)
|
|
347
|
-
|
|
348
|
-
try {
|
|
349
|
-
return JSON.parse(result.content) as string[]
|
|
350
|
-
}
|
|
351
|
-
catch {
|
|
352
|
-
return profile.segments
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// ============================================================================
|
|
357
|
-
// Provider Helper
|
|
358
|
-
// ============================================================================
|
|
359
|
-
|
|
360
|
-
async function callProvider(
|
|
361
|
-
provider: string,
|
|
362
|
-
systemPrompt: string,
|
|
363
|
-
userMessage: string,
|
|
364
|
-
model?: string,
|
|
365
|
-
): Promise<AIResult> {
|
|
366
|
-
if (provider === 'anthropic') {
|
|
367
|
-
const apiKey = process.env.ANTHROPIC_API_KEY
|
|
368
|
-
if (!apiKey) throw new Error('ANTHROPIC_API_KEY required.')
|
|
369
|
-
|
|
370
|
-
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
|
371
|
-
method: 'POST',
|
|
372
|
-
headers: {
|
|
373
|
-
'Content-Type': 'application/json',
|
|
374
|
-
'x-api-key': apiKey,
|
|
375
|
-
'anthropic-version': '2023-06-01',
|
|
376
|
-
},
|
|
377
|
-
body: JSON.stringify({
|
|
378
|
-
model: model || 'claude-sonnet-4-20250514',
|
|
379
|
-
max_tokens: 4096,
|
|
380
|
-
system: systemPrompt,
|
|
381
|
-
messages: [{ role: 'user', content: userMessage }],
|
|
382
|
-
}),
|
|
383
|
-
})
|
|
384
|
-
|
|
385
|
-
if (!response.ok) {
|
|
386
|
-
const error = await response.text()
|
|
387
|
-
throw new Error(`Claude API error: ${error}`)
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
const data = (await response.json()) as any
|
|
391
|
-
return {
|
|
392
|
-
content: data.content[0].text,
|
|
393
|
-
model: data.model,
|
|
394
|
-
usage: {
|
|
395
|
-
promptTokens: data.usage?.input_tokens || 0,
|
|
396
|
-
completionTokens: data.usage?.output_tokens || 0,
|
|
397
|
-
totalTokens: (data.usage?.input_tokens || 0) + (data.usage?.output_tokens || 0),
|
|
398
|
-
},
|
|
399
|
-
finishReason: data.stop_reason,
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
if (provider === 'openai') {
|
|
404
|
-
const apiKey = process.env.OPENAI_API_KEY
|
|
405
|
-
if (!apiKey) throw new Error('OPENAI_API_KEY required.')
|
|
406
|
-
|
|
407
|
-
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
408
|
-
method: 'POST',
|
|
409
|
-
headers: {
|
|
410
|
-
'Content-Type': 'application/json',
|
|
411
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
412
|
-
},
|
|
413
|
-
body: JSON.stringify({
|
|
414
|
-
model: model || 'gpt-4o',
|
|
415
|
-
max_tokens: 4096,
|
|
416
|
-
messages: [
|
|
417
|
-
{ role: 'system', content: systemPrompt },
|
|
418
|
-
{ role: 'user', content: userMessage },
|
|
419
|
-
],
|
|
420
|
-
}),
|
|
421
|
-
})
|
|
422
|
-
|
|
423
|
-
if (!response.ok) {
|
|
424
|
-
const error = await response.text()
|
|
425
|
-
throw new Error(`OpenAI API error: ${error}`)
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
const data = (await response.json()) as any
|
|
429
|
-
return {
|
|
430
|
-
content: data.choices[0].message.content,
|
|
431
|
-
model: data.model,
|
|
432
|
-
usage: {
|
|
433
|
-
promptTokens: data.usage?.prompt_tokens || 0,
|
|
434
|
-
completionTokens: data.usage?.completion_tokens || 0,
|
|
435
|
-
totalTokens: data.usage?.total_tokens || 0,
|
|
436
|
-
},
|
|
437
|
-
finishReason: data.choices[0].finish_reason,
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
if (provider === 'ollama') {
|
|
442
|
-
const host = process.env.OLLAMA_HOST || 'http://localhost:11434'
|
|
443
|
-
|
|
444
|
-
const response = await fetch(`${host}/api/chat`, {
|
|
445
|
-
method: 'POST',
|
|
446
|
-
headers: { 'Content-Type': 'application/json' },
|
|
447
|
-
body: JSON.stringify({
|
|
448
|
-
model: model || 'llama3.2',
|
|
449
|
-
messages: [
|
|
450
|
-
{ role: 'system', content: systemPrompt },
|
|
451
|
-
{ role: 'user', content: userMessage },
|
|
452
|
-
],
|
|
453
|
-
stream: false,
|
|
454
|
-
}),
|
|
455
|
-
})
|
|
456
|
-
|
|
457
|
-
if (!response.ok) {
|
|
458
|
-
const error = await response.text()
|
|
459
|
-
throw new Error(`Ollama API error: ${error}`)
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
const data = (await response.json()) as any
|
|
463
|
-
return {
|
|
464
|
-
content: data.message.content,
|
|
465
|
-
model: data.model,
|
|
466
|
-
usage: {
|
|
467
|
-
promptTokens: data.prompt_eval_count || 0,
|
|
468
|
-
completionTokens: data.eval_count || 0,
|
|
469
|
-
totalTokens: (data.prompt_eval_count || 0) + (data.eval_count || 0),
|
|
470
|
-
},
|
|
471
|
-
finishReason: data.done_reason || 'stop',
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
throw new Error(`Provider not supported: ${provider}`)
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// ============================================================================
|
|
479
|
-
// Exports
|
|
480
|
-
// ============================================================================
|
|
481
|
-
|
|
482
|
-
export const personalization = {
|
|
483
|
-
analyzeSentiment,
|
|
484
|
-
classifyText,
|
|
485
|
-
summarize,
|
|
486
|
-
recommend,
|
|
487
|
-
createProfile,
|
|
488
|
-
recordInteraction,
|
|
489
|
-
extractUserInterests,
|
|
490
|
-
}
|