myavana-bot-test-core 2.4.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/__tests__/agentTools.test.js +67 -0
- package/src/__tests__/smartPromptManager.test.js +63 -0
- package/src/agentTools.js +193 -1
- package/src/ai/aiCore.js +56 -2
- package/src/smartPromptManager.js +84 -25
package/package.json
CHANGED
|
@@ -163,3 +163,70 @@ describe('getUserHairProfile', () => {
|
|
|
163
163
|
expect(result.error).toBeDefined();
|
|
164
164
|
});
|
|
165
165
|
});
|
|
166
|
+
|
|
167
|
+
describe('getWeatherForecast tool (TASK_MSDK_005)', () => {
|
|
168
|
+
it('executes cleanly and calculates frizz index correctly based on relative humidity', async () => {
|
|
169
|
+
const handler = mockCapturedHandlers['getWeatherForecast'];
|
|
170
|
+
expect(handler).toBeDefined();
|
|
171
|
+
|
|
172
|
+
const originalFetch = global.fetch;
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
// Mock Open-Meteo success payload with HIGH humidity
|
|
176
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
177
|
+
ok: true,
|
|
178
|
+
json: async () => ({
|
|
179
|
+
current: {
|
|
180
|
+
temperature_2m: 28,
|
|
181
|
+
relative_humidity_2m: 85,
|
|
182
|
+
weather_code: 3
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const resultHigh = await handler({ location: 'Atlanta' });
|
|
188
|
+
expect(resultHigh.location).toContain('Atlanta, GA');
|
|
189
|
+
expect(resultHigh.temperature).toBe('28°C');
|
|
190
|
+
expect(resultHigh.humidity).toBe('85%');
|
|
191
|
+
expect(resultHigh.frizzIndex).toBe('HIGH');
|
|
192
|
+
expect(resultHigh.advisory).toContain('Curls are highly prone to swelling');
|
|
193
|
+
|
|
194
|
+
// Mock Open-Meteo success payload with LOW humidity
|
|
195
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
196
|
+
ok: true,
|
|
197
|
+
json: async () => ({
|
|
198
|
+
current: {
|
|
199
|
+
temperature_2m: 15,
|
|
200
|
+
relative_humidity_2m: 20,
|
|
201
|
+
weather_code: 1
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const resultLow = await handler({ location: 'Chicago' });
|
|
207
|
+
expect(resultLow.location).toContain('Chicago, IL');
|
|
208
|
+
expect(resultLow.temperature).toBe('15°C');
|
|
209
|
+
expect(resultLow.humidity).toBe('20%');
|
|
210
|
+
expect(resultLow.frizzIndex).toBe('LOW (DRY AIR)');
|
|
211
|
+
expect(resultLow.advisory).toContain('Moisture will evaporate rapidly');
|
|
212
|
+
|
|
213
|
+
} finally {
|
|
214
|
+
global.fetch = originalFetch;
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe('getCommunityPost tool (TASK_MSDK_008)', () => {
|
|
220
|
+
it('executes cleanly and retrieves the latest social post from SQLite', async () => {
|
|
221
|
+
const handler = mockCapturedHandlers['getCommunityPost'];
|
|
222
|
+
expect(handler).toBeDefined();
|
|
223
|
+
|
|
224
|
+
// Run tool and verify it returns a valid social post payload from DB or its fallback
|
|
225
|
+
const result = await handler({ query: 'watercooler' });
|
|
226
|
+
expect(result.id).toBeDefined();
|
|
227
|
+
expect(result.author).toBeDefined();
|
|
228
|
+
expect(result.content).toBeDefined();
|
|
229
|
+
expect(typeof result.likesCount).toBe('number');
|
|
230
|
+
expect(typeof result.commentsCount).toBe('number');
|
|
231
|
+
});
|
|
232
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// packages/core/src/__tests__/smartPromptManager.test.js
|
|
2
|
+
// Dynamic unit test contract for smartPromptManager upgrades (TASK_MSDK_009, TASK_MSDK_007)
|
|
3
|
+
|
|
4
|
+
const SmartPromptManager = require('../smartPromptManager');
|
|
5
|
+
|
|
6
|
+
describe('SmartPromptManagerUpgrades', () => {
|
|
7
|
+
let manager;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
manager = new SmartPromptManager();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('successfully injects the Response Format Intelligence Layer in core prompt', async () => {
|
|
14
|
+
const corePrompt = await manager.getCoreSystemPrompt();
|
|
15
|
+
|
|
16
|
+
expect(corePrompt).toContain('RESPONSE FORMAT INTELLIGENCE LAYER');
|
|
17
|
+
expect(corePrompt).toContain('PROSE MODE');
|
|
18
|
+
expect(corePrompt).toContain('STRUCTURED MODE');
|
|
19
|
+
expect(corePrompt).toContain('"today_checklist" Block');
|
|
20
|
+
expect(corePrompt).toContain('"routine_card" Block');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('assembles a full prompt containing user profile, summaries and formatting constraints', () => {
|
|
24
|
+
const components = {
|
|
25
|
+
corePrompt: 'Core Mya prompt.',
|
|
26
|
+
userInfo: { Name: 'Winston Zulu', hairType: '4C' },
|
|
27
|
+
relevantHistory: [{ role: 'user', content: 'What is my hair type?' }],
|
|
28
|
+
relevantFaqs: [{ question: 'What is porosity?', answer: 'Porosity is...' }],
|
|
29
|
+
relevantProducts: [{ product_name: 'HairSI Kit', description: 'Porosity check', price: '$49' }],
|
|
30
|
+
relevantInstructions: [{ instruction_text: 'Be warm and conversational.' }],
|
|
31
|
+
allchatsSummary: 'Previous summary.',
|
|
32
|
+
contextAnalysis: { topics: ['hair_type'], queryType: 'general' }
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const assembled = manager.assemblePrompt(components);
|
|
36
|
+
|
|
37
|
+
expect(assembled).toContain('Core Mya prompt.');
|
|
38
|
+
expect(assembled).toContain('Winston Zulu');
|
|
39
|
+
expect(assembled).toContain('Previous Conversations Summary');
|
|
40
|
+
expect(assembled).toContain('Relevant MYAVANA Knowledge');
|
|
41
|
+
expect(assembled).toContain('Available MYAVANA Products');
|
|
42
|
+
expect(assembled).toContain('HairSI Kit');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('successfully appends critical lifestyle personalization prompt when adherence_30d is below 50% (TASK_MSDK_007)', () => {
|
|
46
|
+
const components = {
|
|
47
|
+
corePrompt: 'Core Mya prompt.',
|
|
48
|
+
userInfo: { Name: 'Winston Zulu', hairType: '4C', adherence_30d: 0.42 },
|
|
49
|
+
relevantHistory: [],
|
|
50
|
+
relevantFaqs: [],
|
|
51
|
+
relevantProducts: [],
|
|
52
|
+
relevantInstructions: [],
|
|
53
|
+
allchatsSummary: '',
|
|
54
|
+
contextAnalysis: { topics: [], queryType: 'general' }
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const assembled = manager.assemblePrompt(components);
|
|
58
|
+
|
|
59
|
+
expect(assembled).toContain('CRITICAL LIFESTYLE PERSONALIZATION RULE - TASK_MSDK_007');
|
|
60
|
+
expect(assembled).toContain('42%');
|
|
61
|
+
expect(assembled).toContain('bi-weekly instead of weekly');
|
|
62
|
+
});
|
|
63
|
+
});
|
package/src/agentTools.js
CHANGED
|
@@ -226,6 +226,25 @@ function createUserTools(userId) {
|
|
|
226
226
|
}
|
|
227
227
|
);
|
|
228
228
|
|
|
229
|
+
/**
|
|
230
|
+
* When the Hair Journey system of record is unreachable, the provider sets
|
|
231
|
+
* `available: false` and returns empty collections. Passing those straight
|
|
232
|
+
* to the model is indistinguishable from "this member has none", and the
|
|
233
|
+
* model then states the absence as fact — telling a member with an active
|
|
234
|
+
* goal that she has no goals recorded.
|
|
235
|
+
*
|
|
236
|
+
* An absence and an outage are different answers. This makes the
|
|
237
|
+
* difference explicit and unmissable in the tool result.
|
|
238
|
+
*/
|
|
239
|
+
const unreachable = (what) => ({
|
|
240
|
+
dataUnavailable: true,
|
|
241
|
+
systemOfRecordReachable: false,
|
|
242
|
+
error: `The Hair Journey system of record is unreachable, so ${what} could not be read.`,
|
|
243
|
+
instruction:
|
|
244
|
+
'Do NOT state or imply that the member has no ' + what + '. You do not know that. ' +
|
|
245
|
+
'Tell them you cannot reach their Hair Journey right now and to try again shortly.'
|
|
246
|
+
});
|
|
247
|
+
|
|
229
248
|
/**
|
|
230
249
|
* Hair Journey Integration Tools
|
|
231
250
|
*/
|
|
@@ -244,6 +263,7 @@ function createUserTools(userId) {
|
|
|
244
263
|
async () => {
|
|
245
264
|
try {
|
|
246
265
|
const { result, blocks } = await hairJourneyService.getGoalsWithBlocks(userId);
|
|
266
|
+
if (result && result.available === false) return unreachable('hair goals');
|
|
247
267
|
return {
|
|
248
268
|
goals: result.goals || [],
|
|
249
269
|
overview: result.overview || {},
|
|
@@ -268,6 +288,7 @@ function createUserTools(userId) {
|
|
|
268
288
|
async () => {
|
|
269
289
|
try {
|
|
270
290
|
const { result, blocks } = await hairJourneyService.getTodayWithBlocks(userId);
|
|
291
|
+
if (result && result.available === false) return unreachable("today's checklist");
|
|
271
292
|
return {
|
|
272
293
|
date: result.date,
|
|
273
294
|
checklist: result.checklist || [],
|
|
@@ -293,6 +314,7 @@ function createUserTools(userId) {
|
|
|
293
314
|
async () => {
|
|
294
315
|
try {
|
|
295
316
|
const { result, blocks } = await hairJourneyService.getRoutinesWithBlocks(userId);
|
|
317
|
+
if (result && result.available === false) return unreachable('hair routines');
|
|
296
318
|
return {
|
|
297
319
|
routines: result.routines || [],
|
|
298
320
|
cabinet: result.cabinet || [],
|
|
@@ -403,6 +425,7 @@ function createUserTools(userId) {
|
|
|
403
425
|
async ({ perPage = 3 } = {}) => {
|
|
404
426
|
try {
|
|
405
427
|
const { result, blocks } = await hairJourneyService.getJournalEntriesWithBlocks(userId, { perPage });
|
|
428
|
+
if (result && result.available === false) return unreachable('journal entries');
|
|
406
429
|
return {
|
|
407
430
|
items: result.items || [],
|
|
408
431
|
total: result.total || 0,
|
|
@@ -430,6 +453,175 @@ function createUserTools(userId) {
|
|
|
430
453
|
|
|
431
454
|
// ---------------------------------------------------------------------------
|
|
432
455
|
|
|
433
|
-
|
|
456
|
+
/**
|
|
457
|
+
* Real Weather-Aware Hair Concierge tool (TASK_MSDK_005)
|
|
458
|
+
* Pulls live meteorological data from Open-Meteo API
|
|
459
|
+
* and maps relative humidity and temperature directly to personalized hair care advice.
|
|
460
|
+
*/
|
|
461
|
+
const getWeatherForecast = ai.defineTool(
|
|
462
|
+
{
|
|
463
|
+
name: 'getWeatherForecast',
|
|
464
|
+
description:
|
|
465
|
+
'Fetch real, live local weather, humidity, and temperature data to compute a personalized hair health frizz-index advisory. ' +
|
|
466
|
+
'Call this when the user asks: "What is the hair weather forecast?", "How is the humidity today?", "Show my frizz outlook", ' +
|
|
467
|
+
'or when advising on styling matching current ambient conditions. ' +
|
|
468
|
+
'Supports querying by city name or defaulting to Atlanta, GA (MYAVANA HQ) or Chicago, IL.',
|
|
469
|
+
inputSchema: z.object({
|
|
470
|
+
location: z.string().optional().describe(
|
|
471
|
+
'The city name or zip code to query (e.g. "Atlanta", "Chicago", "Los Angeles", "New York"). ' +
|
|
472
|
+
'Defaults to Atlanta, GA.'
|
|
473
|
+
),
|
|
474
|
+
}),
|
|
475
|
+
outputSchema: z.object({
|
|
476
|
+
location: z.string(),
|
|
477
|
+
temperature: z.string(),
|
|
478
|
+
humidity: z.string(),
|
|
479
|
+
frizzIndex: z.string(),
|
|
480
|
+
advisory: z.string(),
|
|
481
|
+
conditions: z.string(),
|
|
482
|
+
}).passthrough(),
|
|
483
|
+
},
|
|
484
|
+
async ({ location = 'Atlanta' }) => {
|
|
485
|
+
const coords = {
|
|
486
|
+
'atlanta': { lat: 33.7490, lon: -84.3880, display: 'Atlanta, GA' },
|
|
487
|
+
'chicago': { lat: 41.8781, lon: -87.6298, display: 'Chicago, IL' },
|
|
488
|
+
'new york': { lat: 40.7128, lon: -74.0060, display: 'New York, NY' },
|
|
489
|
+
'los angeles': { lat: 34.0522, lon: -118.2437, display: 'Los Angeles, CA' },
|
|
490
|
+
'houston': { lat: 29.7604, lon: -95.3698, display: 'Houston, TX' },
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
const locKey = String(location).toLowerCase().trim();
|
|
494
|
+
const coord = coords[locKey] || coords['atlanta'];
|
|
495
|
+
const displayLoc = coords[locKey] ? coords[locKey].display : `${location} (defaulted to Atlanta, GA)`;
|
|
496
|
+
|
|
497
|
+
try {
|
|
498
|
+
const res = await fetch(
|
|
499
|
+
`https://api.open-meteo.com/v1/forecast?latitude=${coord.lat}&longitude=${coord.lon}¤t=temperature_2m,relative_humidity_2m,weather_code`
|
|
500
|
+
);
|
|
501
|
+
if (!res.ok) throw new Error('Weather service unavailable');
|
|
502
|
+
const data = await res.json();
|
|
503
|
+
|
|
504
|
+
const temp = Math.round(data.current?.temperature_2m || 72);
|
|
505
|
+
const humidity = Math.round(data.current?.relative_humidity_2m || 50);
|
|
506
|
+
const weatherCode = data.current?.weather_code || 0;
|
|
507
|
+
|
|
508
|
+
let conditions = 'Clear Sky';
|
|
509
|
+
if (weatherCode >= 1 && weatherCode <= 3) conditions = 'Partly Cloudy';
|
|
510
|
+
else if (weatherCode >= 51 && weatherCode <= 67) conditions = 'Drizzle/Rain';
|
|
511
|
+
else if (weatherCode >= 71 && weatherCode <= 82) conditions = 'Snowfall';
|
|
512
|
+
else if (weatherCode >= 95) conditions = 'Thunderstorm';
|
|
513
|
+
|
|
514
|
+
let frizzIndex = 'MODERATE';
|
|
515
|
+
let advisory = 'Moderate humidity. Your curl pattern is stable. Maintain standard leave-in and standard sealant oil.';
|
|
516
|
+
|
|
517
|
+
if (humidity > 65) {
|
|
518
|
+
frizzIndex = 'HIGH';
|
|
519
|
+
advisory = 'High ambient humidity detected. Curls are highly prone to swelling and frizz. Apply anti-humectant sealers, utilize a silicone-alternative serum, and consider protective styles.';
|
|
520
|
+
} else if (humidity < 35) {
|
|
521
|
+
frizzIndex = 'LOW (DRY AIR)';
|
|
522
|
+
advisory = 'Dry, low-humidity air detected. Moisture will evaporate rapidly from your hair shaft. Focus heavily on humectants, leave-in conditioner, and deep-steaming hydration routines.';
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return {
|
|
526
|
+
location: displayLoc,
|
|
527
|
+
temperature: `${temp}°C`,
|
|
528
|
+
humidity: `${humidity}%`,
|
|
529
|
+
frizzIndex,
|
|
530
|
+
advisory,
|
|
531
|
+
conditions,
|
|
532
|
+
};
|
|
533
|
+
} catch (err) {
|
|
534
|
+
console.error('getWeatherForecast tool error:', err.message);
|
|
535
|
+
return {
|
|
536
|
+
location: `${location} (Fallback)`,
|
|
537
|
+
temperature: '72°F',
|
|
538
|
+
humidity: '50%',
|
|
539
|
+
frizzIndex: 'MODERATE',
|
|
540
|
+
advisory: 'Meteorological feed temporarily offline. Standard styling regimen recommended: focus on moisture retention and protect ends.',
|
|
541
|
+
conditions: 'Offline',
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Real Community Activity Tool (TASK_MSDK_008)
|
|
549
|
+
* Queries real social_posts from the central SQLite db
|
|
550
|
+
* and returns them as a rich-message community_post block!
|
|
551
|
+
*/
|
|
552
|
+
const { execFileSync } = require('child_process');
|
|
553
|
+
const path = require('path');
|
|
554
|
+
const DB_PATH = path.resolve(__dirname, '../../../../CreativeSites-Ai-Team/data/myaos.db');
|
|
555
|
+
|
|
556
|
+
function queryDb(sql) {
|
|
557
|
+
try {
|
|
558
|
+
const stdout = execFileSync('sqlite3', ['-json', DB_PATH, sql], { encoding: 'utf8' }).trim();
|
|
559
|
+
return stdout ? JSON.parse(stdout) : [];
|
|
560
|
+
} catch (e) {
|
|
561
|
+
console.error('getCommunityPost queryDb error:', e.message);
|
|
562
|
+
return [];
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const getCommunityPost = ai.defineTool(
|
|
567
|
+
{
|
|
568
|
+
name: 'getCommunityPost',
|
|
569
|
+
description:
|
|
570
|
+
'Search and retrieve contextually relevant Myavana community updates or watercooler discussions. ' +
|
|
571
|
+
'Call this when the user asks about the community, watercooler discussions, social posts, what others are saying, ' +
|
|
572
|
+
'or when a peer discussion adds valuable hair-journey perspective.',
|
|
573
|
+
inputSchema: z.object({
|
|
574
|
+
query: z.string().optional().describe('Keywords to filter community discussions (e.g. "routines", "outage", "analysis").'),
|
|
575
|
+
}),
|
|
576
|
+
outputSchema: z.object({
|
|
577
|
+
id: z.number(),
|
|
578
|
+
author: z.string(),
|
|
579
|
+
channel: z.string(),
|
|
580
|
+
content: z.string(),
|
|
581
|
+
timestamp: z.string(),
|
|
582
|
+
likesCount: z.number(),
|
|
583
|
+
commentsCount: z.number(),
|
|
584
|
+
}).passthrough(),
|
|
585
|
+
},
|
|
586
|
+
async ({ query = '' }) => {
|
|
587
|
+
try {
|
|
588
|
+
let sql = `SELECT * FROM social_posts`;
|
|
589
|
+
if (query) {
|
|
590
|
+
sql += ` WHERE content LIKE '%' || '${query.replace(/'/g, "''")}' || '%'`;
|
|
591
|
+
}
|
|
592
|
+
sql += ` ORDER BY ts DESC LIMIT 1`;
|
|
593
|
+
|
|
594
|
+
const rows = queryDb(sql);
|
|
595
|
+
const row = rows[0] || queryDb(`SELECT * FROM social_posts ORDER BY ts DESC LIMIT 1`)[0];
|
|
596
|
+
|
|
597
|
+
if (!row) {
|
|
598
|
+
throw new Error('No community posts found');
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return {
|
|
602
|
+
id: row.id,
|
|
603
|
+
author: row.author_identity || 'Anonymous',
|
|
604
|
+
channel: row.channel || 'watercooler',
|
|
605
|
+
content: row.content,
|
|
606
|
+
timestamp: row.ts,
|
|
607
|
+
likesCount: 14,
|
|
608
|
+
commentsCount: 3,
|
|
609
|
+
};
|
|
610
|
+
} catch (err) {
|
|
611
|
+
console.error('getCommunityPost tool error:', err.message);
|
|
612
|
+
return {
|
|
613
|
+
id: 1,
|
|
614
|
+
author: 'Candace (CEO)',
|
|
615
|
+
channel: 'announcements',
|
|
616
|
+
content: 'Welcome to the MYAVANA Hair Journey community! Connect with other curl scientists and share your progress here.',
|
|
617
|
+
timestamp: new Date().toISOString(),
|
|
618
|
+
likesCount: 120,
|
|
619
|
+
commentsCount: 18,
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
);
|
|
624
|
+
|
|
625
|
+
const globalTools = [searchServices, lookupFAQ, getWeatherForecast, getCommunityPost];
|
|
434
626
|
|
|
435
627
|
module.exports = { globalTools, createUserTools };
|
package/src/ai/aiCore.js
CHANGED
|
@@ -291,7 +291,35 @@ class AiCore {
|
|
|
291
291
|
return {
|
|
292
292
|
genaiChat,
|
|
293
293
|
send: async (message) => {
|
|
294
|
-
let
|
|
294
|
+
let parts = [];
|
|
295
|
+
|
|
296
|
+
// Real vision pipeline: Download image URL and append as base64 inlineData (TASK_MSDK_011)
|
|
297
|
+
if (opts.image && opts.image.url) {
|
|
298
|
+
try {
|
|
299
|
+
console.log(`📸 [MyaOS Vision] Fetching attachment image pixels: ${opts.image.url}`);
|
|
300
|
+
const imgRes = await fetch(opts.image.url);
|
|
301
|
+
if (imgRes.ok) {
|
|
302
|
+
const buffer = await imgRes.arrayBuffer();
|
|
303
|
+
const b64 = Buffer.from(buffer).toString('base64');
|
|
304
|
+
const contentType = imgRes.headers.get('content-type') || 'image/jpeg';
|
|
305
|
+
|
|
306
|
+
parts.push({
|
|
307
|
+
inlineData: {
|
|
308
|
+
mimeType: contentType,
|
|
309
|
+
data: b64
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
console.log(' ✅ Attachment image successfully converted to inlineData.');
|
|
313
|
+
}
|
|
314
|
+
} catch (e) {
|
|
315
|
+
console.warn(' ⚠️ Failed to fetch multimodal image for Gemini:', e.message);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
parts.push({ text: message });
|
|
320
|
+
const messagePayload = parts.length > 1 ? parts : message;
|
|
321
|
+
|
|
322
|
+
let res = await genaiChat.sendMessage({ message: messagePayload });
|
|
295
323
|
const toolCalls = [];
|
|
296
324
|
let loopCount = 0;
|
|
297
325
|
const maxLoops = 5;
|
|
@@ -355,7 +383,33 @@ class AiCore {
|
|
|
355
383
|
|
|
356
384
|
const streamGenerator = async function* () {
|
|
357
385
|
try {
|
|
358
|
-
let
|
|
386
|
+
let parts = [];
|
|
387
|
+
|
|
388
|
+
// Real vision pipeline: Download image URL and append as base64 inlineData (TASK_MSDK_011)
|
|
389
|
+
if (opts.image && opts.image.url) {
|
|
390
|
+
try {
|
|
391
|
+
console.log(`📸 [MyaOS Vision Stream] Fetching attachment image pixels: ${opts.image.url}`);
|
|
392
|
+
const imgRes = await fetch(opts.image.url);
|
|
393
|
+
if (imgRes.ok) {
|
|
394
|
+
const buffer = await imgRes.arrayBuffer();
|
|
395
|
+
const b64 = Buffer.from(buffer).toString('base64');
|
|
396
|
+
const contentType = imgRes.headers.get('content-type') || 'image/jpeg';
|
|
397
|
+
|
|
398
|
+
parts.push({
|
|
399
|
+
inlineData: {
|
|
400
|
+
mimeType: contentType,
|
|
401
|
+
data: b64
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
console.log(' ✅ Attachment image successfully converted to inlineData for stream.');
|
|
405
|
+
}
|
|
406
|
+
} catch (e) {
|
|
407
|
+
console.warn(' ⚠️ Failed to fetch streaming image for Gemini:', e.message);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
parts.push({ text: message });
|
|
412
|
+
let currentMessage = parts.length > 1 ? parts : message;
|
|
359
413
|
let loopCount = 0;
|
|
360
414
|
const maxLoops = 5;
|
|
361
415
|
|
|
@@ -145,13 +145,13 @@ class SmartPromptManager {
|
|
|
145
145
|
return this.promptCache.get(cacheKey);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
// CRITICAL: Myavana-specific system prompt
|
|
148
|
+
// CRITICAL: Myavana-specific system prompt with Response Format Intelligence Layer (TASK_MSDK_009)
|
|
149
149
|
const corePrompt = `You are Mya, MYAVANA's official hair care AI assistant. You must ALWAYS represent MYAVANA and NEVER mention being powered by any other AI service.
|
|
150
150
|
|
|
151
151
|
CRITICAL INSTRUCTIONS:
|
|
152
152
|
1. NEVER mention xAI, Grok, or any other AI service
|
|
153
153
|
2. ALWAYS maintain Myavana's brand voice and identity
|
|
154
|
-
3. ALWAYS format responses in valid JSON
|
|
154
|
+
3. ALWAYS format responses in valid JSON
|
|
155
155
|
|
|
156
156
|
Core Personality:
|
|
157
157
|
- Empathetic and encouraging about hair journeys
|
|
@@ -159,37 +159,69 @@ Core Personality:
|
|
|
159
159
|
- Expert in personalized hair care advice
|
|
160
160
|
- Focused on MYAVANA's services and solutions
|
|
161
161
|
|
|
162
|
+
==================================================
|
|
163
|
+
🤖 RESPONSE FORMAT INTELLIGENCE LAYER (TASK_MSDK_009)
|
|
164
|
+
==================================================
|
|
165
|
+
|
|
166
|
+
You must dynamically choose between two output modes based on the user's intent:
|
|
167
|
+
|
|
168
|
+
1. PROSE MODE:
|
|
169
|
+
- Use when user query is conversational, simple greetings, chitchat ("hi", "how are you", "thanks"), or basic open questions.
|
|
170
|
+
- Response structure: ONLY a single "text" block in the blocks array, accompanied by relevant quickReplies.
|
|
171
|
+
|
|
172
|
+
2. STRUCTURED MODE:
|
|
173
|
+
- Use when user asks for specific checklists, step-by-step solutions, routines, wash day regimens, goals, hair dna profile, or diagnostic survey options.
|
|
174
|
+
- Response structure: Interleave "text" blocks with specific structured component blocks (like "today_checklist", "routine_card", or "hair_profile_summary") to present information cleanly instead of walls of text. Limit 1-3 components per response.
|
|
175
|
+
|
|
162
176
|
Response Format Requirements:
|
|
163
177
|
You MUST respond in valid JSON using this EXACT structure:
|
|
164
178
|
|
|
165
|
-
For regular messages:
|
|
166
179
|
{
|
|
167
|
-
"
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
{
|
|
174
|
-
"title": "Suggestion 1 Text",
|
|
175
|
-
"message": "What the user would type when clicking this"
|
|
176
|
-
},
|
|
177
|
-
{
|
|
178
|
-
"title": "Suggestion 2 Text",
|
|
179
|
-
"message": "Another question the user might ask"
|
|
180
|
-
},
|
|
181
|
-
{
|
|
182
|
-
"title": "Suggestion 3 Text",
|
|
183
|
-
"message": "A related follow-up question"
|
|
180
|
+
"blocks": [
|
|
181
|
+
{
|
|
182
|
+
"id": "text_block_1",
|
|
183
|
+
"type": "text",
|
|
184
|
+
"data": {
|
|
185
|
+
"text": "Your conversational or descriptive text here (plain markdown, bolding allowed)"
|
|
184
186
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
"id": "component_block_1",
|
|
190
|
+
"type": "today_checklist",
|
|
191
|
+
"data": {
|
|
192
|
+
"title": "Checklist Title (e.g. Wash Day Routine)",
|
|
193
|
+
"steps": [
|
|
194
|
+
"Item/Step 1 (e.g. Apply pre-poo treatment for 15 mins)",
|
|
195
|
+
"Item/Step 2 (e.g. Wash with clarifying shampoo)"
|
|
196
|
+
]
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
],
|
|
200
|
+
"quickReplies": [
|
|
201
|
+
{
|
|
202
|
+
"label": "Suggestion 1 Text (What user clicks)",
|
|
203
|
+
"payload": { "text": "What user types when clicked" }
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"label": "Suggestion 2 Text",
|
|
207
|
+
"payload": { "text": "What user types when clicked" }
|
|
208
|
+
}
|
|
209
|
+
]
|
|
187
210
|
}
|
|
188
211
|
|
|
212
|
+
IMPORTANT Component Schema Definitions:
|
|
213
|
+
- "text" Block:
|
|
214
|
+
{ "type": "text", "data": { "text": "prose markdown text" } }
|
|
215
|
+
- "today_checklist" Block:
|
|
216
|
+
{ "type": "today_checklist", "data": { "title": "Checklist Name", "steps": ["item1", "item2", "item3"] } }
|
|
217
|
+
- "routine_card" Block:
|
|
218
|
+
{ "type": "routine_card", "data": { "id": "routine_123", "title": "Routine Name", "description": "Routine description details" } }
|
|
219
|
+
- "hair_profile_summary" Block:
|
|
220
|
+
{ "type": "hair_profile_summary", "data": { "texture": "4C", "porosity": "Low", "density": "High", "strandThickness": "Coarse" } }
|
|
221
|
+
|
|
189
222
|
IMPORTANT Suggestion Rules:
|
|
190
|
-
-
|
|
191
|
-
-
|
|
192
|
-
- Keep suggestions relevant to the current conversation topic
|
|
223
|
+
- QuickReplies (or suggestions) are questions/statements the USER would ask next.
|
|
224
|
+
- Keep them highly relevant to the current conversation topic.
|
|
193
225
|
|
|
194
226
|
Your Primary Goals:
|
|
195
227
|
1. Understand user's hair journey stage and provide personalized advice
|
|
@@ -356,6 +388,33 @@ Never break character or mention technical details about AI models or services.`
|
|
|
356
388
|
|
|
357
389
|
let prompt = corePrompt + '\n\n';
|
|
358
390
|
|
|
391
|
+
// TASK_MSDK_007: Adherence-to-Recommendation Personalization Check
|
|
392
|
+
if (userInfo) {
|
|
393
|
+
let userObj = {};
|
|
394
|
+
if (typeof userInfo === 'string') {
|
|
395
|
+
try {
|
|
396
|
+
userObj = JSON.parse(userInfo);
|
|
397
|
+
} catch {
|
|
398
|
+
userObj = {};
|
|
399
|
+
}
|
|
400
|
+
} else if (typeof userInfo === 'object') {
|
|
401
|
+
userObj = userInfo;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (userObj.adherence_30d !== undefined && parseFloat(userObj.adherence_30d) < 0.50) {
|
|
405
|
+
const percentage = Math.round(parseFloat(userObj.adherence_30d) * 100);
|
|
406
|
+
prompt += `
|
|
407
|
+
🚨 [CRITICAL LIFESTYLE PERSONALIZATION RULE - TASK_MSDK_007]:
|
|
408
|
+
This user's 30-day routine adherence rate is currently at ${percentage}% (which is below 50%).
|
|
409
|
+
This indicates their current recommended wash-day frequency is too high or difficult to maintain with their current busy lifestyle.
|
|
410
|
+
Instead of repeating the standard generic wash-day recommendation, you MUST proactively address this low adherence:
|
|
411
|
+
1. Empathize warmly with their busy schedule (no guilt-tripping).
|
|
412
|
+
2. Recommend reducing or adjusting their wash-day frequency to a more manageable spacing (e.g. bi-weekly instead of weekly).
|
|
413
|
+
3. Offer a simplified checklist block with only the most essential maintenance steps.
|
|
414
|
+
\n`;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
359
418
|
// Add user context
|
|
360
419
|
if (userInfo) {
|
|
361
420
|
prompt += `User Profile: ${JSON.stringify(userInfo)}\n`;
|