myavana-bot-test-core 2.3.3 → 2.4.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/index.js CHANGED
@@ -38,6 +38,7 @@ const modules = {
38
38
  getConversationHistory: safeRequire('./src/conversation', 'conversation')?.getConversationHistory,
39
39
  saveConversation: safeRequire('./src/conversation', 'conversation')?.saveConversation,
40
40
  generateConversationSummary: safeRequire('./src/conversation', 'conversation')?.generateConversationSummary,
41
+ getUserConversationsList: safeRequire('./src/conversation', 'conversation')?.getUserConversationsList,
41
42
 
42
43
  // Post-processing and hair issues
43
44
  postProcessConversation: safeRequire('./src/postResponseProcessing', 'postResponseProcessing')?.postProcessConversation,
@@ -55,6 +56,7 @@ const modules = {
55
56
  getAdditionalInstructions: safeRequire('./src/utils', 'utils')?.getAdditionalInstructions,
56
57
  getTrainingData: safeRequire('./src/utils', 'utils')?.getTrainingData,
57
58
  getChatbotWelcomeSettings: safeRequire('./src/utils', 'utils')?.getChatbotWelcomeSettings,
59
+ buildBudgetedPromptContext: safeRequire('./src/context/promptBudgetManager', 'promptBudgetManager')?.buildBudgetedPromptContext,
58
60
  };
59
61
 
60
62
  // Smart systems - Load with proper error handling and fallbacks
@@ -170,6 +172,19 @@ const enhancedModules = {
170
172
  HealthMonitor: safeRequire('./src/healthMonitor', 'HealthMonitor')
171
173
  };
172
174
 
175
+ // Hair Journey Platform Services
176
+ const hairJourneyModules = {
177
+ HairJourneyService: safeRequire('./src/hairJourney/hairJourneyService', 'hairJourneyService')?.HairJourneyService,
178
+ WordPressHairJourneyProvider: safeRequire('./src/hairJourney/wordPressHairJourneyProvider', 'wordPressHairJourneyProvider')?.WordPressHairJourneyProvider,
179
+ ExperienceContextService: safeRequire('./src/experience/experienceContextService', 'experienceContextService')?.ExperienceContextService,
180
+ };
181
+
182
+ // Chat Protocol (NDJSON streaming, semantic blocks, response parsing)
183
+ const chatProtocolModules = {
184
+ chatProtocol: safeRequire('./src/chatProtocol', 'chatProtocol'),
185
+ ...(safeRequire('./src/chatProtocol', 'chatProtocol') || {}),
186
+ };
187
+
173
188
  // Export all modules
174
189
  module.exports = {
175
190
  ...modules,
@@ -181,6 +196,8 @@ module.exports = {
181
196
  ...agentToolModules,
182
197
  ...funnelEventModules,
183
198
  ...agentMemoryModules,
199
+ ...hairJourneyModules,
200
+ ...chatProtocolModules,
184
201
  };
185
202
 
186
203
  // Log export summary
@@ -68,6 +68,7 @@ CREATE INDEX IF NOT EXISTS idx_user_journey_events_user ON user_journey_events(u
68
68
  CREATE INDEX IF NOT EXISTS idx_ai_model_performance_date ON ai_model_performance(date_tracked);
69
69
 
70
70
  -- Views for easy analytics queries
71
+ DROP VIEW IF EXISTS daily_conversation_stats CASCADE;
71
72
  CREATE OR REPLACE VIEW daily_conversation_stats AS
72
73
  SELECT
73
74
  DATE(created_at) as date,
@@ -79,14 +80,15 @@ FROM conversation_metrics
79
80
  GROUP BY DATE(created_at)
80
81
  ORDER BY date DESC;
81
82
 
83
+ DROP VIEW IF EXISTS model_reliability_stats CASCADE;
82
84
  CREATE OR REPLACE VIEW model_reliability_stats AS
83
85
  SELECT
84
86
  model_name,
85
87
  SUM(request_count) as total_requests,
86
88
  SUM(success_count) as total_successes,
87
89
  SUM(failure_count) as total_failures,
88
- ROUND((SUM(success_count)::float / SUM(request_count)::float * 100), 2) as success_rate,
89
- ROUND(AVG(avg_response_time_ms), 2) as avg_response_time
90
+ ROUND((SUM(success_count)::numeric / NULLIF(SUM(request_count), 0)::numeric * 100), 2) as success_rate,
91
+ ROUND(AVG(avg_response_time_ms)::numeric, 2) as avg_response_time
90
92
  FROM ai_model_performance
91
93
  WHERE date_tracked >= CURRENT_DATE - INTERVAL '30 days'
92
94
  GROUP BY model_name;
@@ -131,6 +131,10 @@ CREATE TABLE IF NOT EXISTS product_recommendations (
131
131
  created_at TIMESTAMP DEFAULT NOW()
132
132
  );
133
133
 
134
+ -- Ensure compatibility with existing schemas
135
+ ALTER TABLE hair_issues ADD COLUMN IF NOT EXISTS created_at TIMESTAMP DEFAULT NOW();
136
+ ALTER TABLE product_recommendations ADD COLUMN IF NOT EXISTS created_at TIMESTAMP DEFAULT NOW();
137
+
134
138
  -- Indexes for better performance
135
139
  CREATE INDEX IF NOT EXISTS idx_error_logs_user_created ON error_logs(user_id, created_at);
136
140
  CREATE INDEX IF NOT EXISTS idx_error_logs_type_model ON error_logs(error_type, model_name);
@@ -142,6 +146,7 @@ CREATE INDEX IF NOT EXISTS idx_product_recommendations_user ON product_recommend
142
146
  CREATE INDEX IF NOT EXISTS idx_ab_assignments_experiment ON ab_test_assignments(experiment_id, assigned_variant);
143
147
 
144
148
  -- Views for analytics and reporting
149
+ DROP VIEW IF EXISTS error_recovery_stats CASCADE;
145
150
  CREATE OR REPLACE VIEW error_recovery_stats AS
146
151
  SELECT
147
152
  error_type,
@@ -150,29 +155,31 @@ SELECT
150
155
  COUNT(CASE WHEN recovery_attempted THEN 1 END) as recovery_attempts,
151
156
  COUNT(CASE WHEN recovery_successful THEN 1 END) as successful_recoveries,
152
157
  ROUND(
153
- COUNT(CASE WHEN recovery_successful THEN 1 END)::float /
154
- NULLIF(COUNT(CASE WHEN recovery_attempted THEN 1 END), 0) * 100, 2
158
+ (COUNT(CASE WHEN recovery_successful THEN 1 END)::numeric /
159
+ NULLIF(COUNT(CASE WHEN recovery_attempted THEN 1 END), 0)::numeric * 100), 2
155
160
  ) as recovery_success_rate,
156
161
  DATE(created_at) as error_date
157
162
  FROM error_logs
158
163
  GROUP BY error_type, model_name, DATE(created_at)
159
164
  ORDER BY total_errors DESC;
160
165
 
166
+ DROP VIEW IF EXISTS personalization_effectiveness CASCADE;
161
167
  CREATE OR REPLACE VIEW personalization_effectiveness AS
162
168
  SELECT
163
169
  persona_assigned,
164
170
  COUNT(*) as total_interactions,
165
- AVG(user_satisfaction_score) as avg_satisfaction,
171
+ ROUND(AVG(user_satisfaction_score)::numeric, 2) as avg_satisfaction,
166
172
  COUNT(CASE WHEN user_satisfaction_score >= 4 THEN 1 END) as satisfied_users,
167
173
  ROUND(
168
- COUNT(CASE WHEN user_satisfaction_score >= 4 THEN 1 END)::float /
169
- COUNT(*) * 100, 2
174
+ (COUNT(CASE WHEN user_satisfaction_score >= 4 THEN 1 END)::numeric /
175
+ NULLIF(COUNT(*), 0)::numeric * 100), 2
170
176
  ) as satisfaction_rate
171
177
  FROM personalization_metrics
172
178
  WHERE persona_assigned IS NOT NULL
173
179
  GROUP BY persona_assigned
174
180
  ORDER BY avg_satisfaction DESC;
175
181
 
182
+ DROP VIEW IF EXISTS proactive_engagement_performance CASCADE;
176
183
  CREATE OR REPLACE VIEW proactive_engagement_performance AS
177
184
  SELECT
178
185
  c.campaign_name,
@@ -180,10 +187,10 @@ SELECT
180
187
  COUNT(h.id) as messages_sent,
181
188
  COUNT(CASE WHEN h.delivery_status = 'responded' THEN 1 END) as responses_received,
182
189
  ROUND(
183
- COUNT(CASE WHEN h.delivery_status = 'responded' THEN 1 END)::float /
184
- COUNT(h.id) * 100, 2
190
+ (COUNT(CASE WHEN h.delivery_status = 'responded' THEN 1 END)::numeric /
191
+ NULLIF(COUNT(h.id), 0)::numeric * 100), 2
185
192
  ) as response_rate,
186
- AVG(h.engagement_score) as avg_engagement_score
193
+ ROUND(AVG(h.engagement_score)::numeric, 2) as avg_engagement_score
187
194
  FROM proactive_campaigns c
188
195
  LEFT JOIN proactive_engagement_history h ON c.id = h.campaign_id
189
196
  WHERE c.active = true
@@ -0,0 +1,18 @@
1
+ -- Migration 008: Platform Identity Links
2
+ -- Maps internal canonical myavana_user_id to external provider identities
3
+ -- (wordpress, kommunicate, mobile, widget_anon)
4
+
5
+ CREATE TABLE IF NOT EXISTS identity_links (
6
+ id SERIAL PRIMARY KEY,
7
+ myavana_user_id VARCHAR(255) NOT NULL,
8
+ provider VARCHAR(50) NOT NULL, -- 'wordpress', 'kommunicate', 'widget_anon', 'mobile'
9
+ external_id VARCHAR(255) NOT NULL,
10
+ metadata JSONB DEFAULT '{}',
11
+ verified_at TIMESTAMP,
12
+ created_at TIMESTAMP DEFAULT NOW(),
13
+ updated_at TIMESTAMP DEFAULT NOW(),
14
+ UNIQUE(provider, external_id)
15
+ );
16
+
17
+ CREATE INDEX IF NOT EXISTS idx_identity_links_lookup ON identity_links(provider, external_id);
18
+ CREATE INDEX IF NOT EXISTS idx_identity_links_canonical ON identity_links(myavana_user_id);
@@ -145,6 +145,7 @@ BEGIN
145
145
  CREATE INDEX IF NOT EXISTS idx_ai_model_performance_date ON ai_model_performance(date_tracked);
146
146
 
147
147
  -- Views
148
+ DROP VIEW IF EXISTS daily_conversation_stats CASCADE;
148
149
  CREATE OR REPLACE VIEW daily_conversation_stats AS
149
150
  SELECT
150
151
  DATE(created_at) as date,
@@ -156,14 +157,15 @@ BEGIN
156
157
  GROUP BY DATE(created_at)
157
158
  ORDER BY date DESC;
158
159
 
160
+ DROP VIEW IF EXISTS model_reliability_stats CASCADE;
159
161
  CREATE OR REPLACE VIEW model_reliability_stats AS
160
162
  SELECT
161
163
  model_name,
162
164
  SUM(request_count) as total_requests,
163
165
  SUM(success_count) as total_successes,
164
166
  SUM(failure_count) as total_failures,
165
- ROUND((SUM(success_count)::float / SUM(request_count)::float * 100), 2) as success_rate,
166
- ROUND(AVG(avg_response_time_ms), 2) as avg_response_time
167
+ ROUND((SUM(success_count)::numeric / NULLIF(SUM(request_count), 0)::numeric * 100), 2) as success_rate,
168
+ ROUND(AVG(avg_response_time_ms)::numeric, 2) as avg_response_time
167
169
  FROM ai_model_performance
168
170
  WHERE date_tracked >= CURRENT_DATE - INTERVAL '30 days'
169
171
  GROUP BY model_name;
@@ -265,6 +267,8 @@ BEGIN
265
267
  resolved_at TIMESTAMP
266
268
  );
267
269
 
270
+ ALTER TABLE hair_issues ADD COLUMN IF NOT EXISTS created_at TIMESTAMP DEFAULT NOW();
271
+
268
272
  -- Indexes
269
273
  CREATE INDEX IF NOT EXISTS idx_error_logs_user_created ON error_logs(user_id, created_at);
270
274
  CREATE INDEX IF NOT EXISTS idx_error_logs_type_model ON error_logs(error_type, model_name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myavana-bot-test-core",
3
- "version": "2.3.3",
3
+ "version": "2.4.0",
4
4
  "description": "Shared bot functionality with enhanced features",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -11,13 +11,10 @@
11
11
  "lint:fix": "eslint src/ --fix"
12
12
  },
13
13
  "dependencies": {
14
- "@genkit-ai/google-genai": "1.28.0",
15
- "@genkit-ai/googleai": "1.28.0",
16
14
  "@google/genai": "1.39.0",
17
15
  "dotenv": "^16.4.7",
18
16
  "express-rate-limit": "^7.1.5",
19
17
  "express-slow-down": "^2.0.1",
20
- "genkit": "1.28.0",
21
18
  "isomorphic-dompurify": "^2.9.0",
22
19
  "joi": "^17.11.0",
23
20
  "node-cache": "^5.1.2",
@@ -0,0 +1,68 @@
1
+ const { Client } = require('pg');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ require('dotenv').config({ path: path.join(__dirname, '../../myavana/.env') });
5
+
6
+ async function run() {
7
+ const client = new Client({
8
+ host: process.env.DB_HOST,
9
+ user: process.env.DB_USER,
10
+ password: process.env.DB_PASSWORD,
11
+ database: process.env.DB_NAME,
12
+ port: parseInt(process.env.DB_PORT, 10) || 5432
13
+ });
14
+
15
+ await client.connect();
16
+ console.log('Connected to PostgreSQL database at', process.env.DB_HOST);
17
+
18
+ // Ensure schema_migrations table exists
19
+ await client.query(`
20
+ CREATE TABLE IF NOT EXISTS schema_migrations (
21
+ id SERIAL PRIMARY KEY,
22
+ migration_name VARCHAR(255) NOT NULL UNIQUE,
23
+ applied_at TIMESTAMP DEFAULT NOW()
24
+ );
25
+ `);
26
+
27
+ const migrationsToRun = [
28
+ '001_add_analytics_tables.sql',
29
+ '002_add_advanced_features_tables.sql',
30
+ '008_platform_identity.sql'
31
+ ];
32
+
33
+ for (const file of migrationsToRun) {
34
+ const migrationName = path.basename(file, '.sql');
35
+ const checkRes = await client.query('SELECT 1 FROM schema_migrations WHERE migration_name = $1', [migrationName]);
36
+ if (checkRes.rows.length > 0) {
37
+ console.log(`Migration ${migrationName} already marked as applied, running idempotently...`);
38
+ } else {
39
+ console.log(`Applying migration ${migrationName}...`);
40
+ }
41
+
42
+ const filePath = path.join(__dirname, '../migrations', file);
43
+ const sql = fs.readFileSync(filePath, 'utf8');
44
+
45
+ await client.query('BEGIN');
46
+ try {
47
+ await client.query(sql);
48
+ await client.query(
49
+ `INSERT INTO schema_migrations (migration_name) VALUES ($1) ON CONFLICT (migration_name) DO NOTHING`,
50
+ [migrationName]
51
+ );
52
+ await client.query('COMMIT');
53
+ console.log(`✅ ${migrationName} successfully applied!`);
54
+ } catch (err) {
55
+ await client.query('ROLLBACK');
56
+ console.error(`❌ Migration ${migrationName} failed:`, err.message);
57
+ throw err;
58
+ }
59
+ }
60
+
61
+ await client.end();
62
+ console.log('🏁 All requested migrations applied successfully!');
63
+ }
64
+
65
+ run().catch((err) => {
66
+ console.error('Fatal error during migration execution:', err);
67
+ process.exit(1);
68
+ });
@@ -7,18 +7,15 @@ jest.mock('../database', () => ({
7
7
  redisClient: { get: mockRedisGet, set: mockRedisSet },
8
8
  }));
9
9
 
10
- jest.mock('../ai', () => ({}));
11
- jest.mock('@genkit-ai/google-genai', () => ({
12
- googleAI: { model: jest.fn(() => 'mocked-model') },
13
- }));
14
- jest.mock('../config', () => ({ genkitApiKey: 'test-key' }));
15
-
16
10
  const mockCacheCreate = jest.fn();
17
- jest.mock('@google/genai', () => ({
18
- GoogleGenAI: jest.fn().mockImplementation(() => ({
19
- caches: { create: mockCacheCreate },
20
- })),
11
+ jest.mock('../ai', () => ({
12
+ client: {
13
+ caches: {
14
+ create: mockCacheCreate,
15
+ },
16
+ },
21
17
  }));
18
+ jest.mock('../config', () => ({ genkitApiKey: 'test-key' }));
22
19
 
23
20
  const CacheManager = require('../cacheManager');
24
21
 
@@ -0,0 +1,162 @@
1
+ const { HairJourneyService } = require('../hairJourney/hairJourneyService');
2
+ const { BlockType, ActionType } = require('../chatProtocol');
3
+
4
+ describe('HairJourneyService', () => {
5
+ let service;
6
+
7
+ beforeEach(() => {
8
+ service = new HairJourneyService();
9
+ });
10
+
11
+ test('buildGoalCardBlock returns valid GOAL_CARD block', () => {
12
+ const goal = {
13
+ id: 'goal_test_1',
14
+ title: 'Grow 4 inches',
15
+ category: 'Length',
16
+ progressPercent: 50,
17
+ targetMetric: '4 Inches',
18
+ targetDate: 'Dec 2026'
19
+ };
20
+
21
+ const block = service.buildGoalCardBlock(goal);
22
+ expect(block.type).toBe(BlockType.GOAL_CARD);
23
+ expect(block.data.title).toBe('Grow 4 inches');
24
+ expect(block.data.progressPercent).toBe(50);
25
+ expect(block.actions.length).toBeGreaterThan(0);
26
+ });
27
+
28
+ test('buildTodayChecklistBlock returns valid TODAY_CHECKLIST block', () => {
29
+ const todayData = {
30
+ date: '2026-09-04',
31
+ streakDays: 5,
32
+ checklist: [
33
+ { id: 's1', title: 'Hydration Mist', completed: true },
34
+ { id: 's2', title: 'Deep Condition', completed: false }
35
+ ]
36
+ };
37
+
38
+ const block = service.buildTodayChecklistBlock(todayData);
39
+ expect(block.type).toBe(BlockType.TODAY_CHECKLIST);
40
+ expect(block.data.streakDays).toBe(5);
41
+ expect(block.data.steps.length).toBe(2);
42
+ expect(block.data.steps[0].completed).toBe(true);
43
+ });
44
+
45
+ test('buildJournalEntryBlock returns valid JOURNAL_ENTRY block with UPLOAD_PHOTO action', () => {
46
+ const entry = {
47
+ id: 123,
48
+ title: 'Wash Day Success',
49
+ notes: 'Hair felt deeply moisturized and shiny',
50
+ moistureLevel: 5,
51
+ scalpState: 'Healthy',
52
+ photos: ['https://example.com/photo1.jpg']
53
+ };
54
+
55
+ const block = service.buildJournalEntryBlock(entry);
56
+ expect(block.type).toBe(BlockType.JOURNAL_ENTRY);
57
+ expect(block.data.title).toBe('Wash Day Success');
58
+ expect(block.data.moistureLevel).toBe(5);
59
+ expect(block.data.photos.length).toBe(1);
60
+
61
+ const uploadAction = block.actions.find(a => a.type === ActionType.UPLOAD_PHOTO);
62
+ expect(uploadAction).toBeDefined();
63
+ expect(uploadAction.payload.entryId).toBe(123);
64
+ });
65
+
66
+ test('buildHairProfileBlock preserves nulls and does not fabricate Type 4 or Normal porosity', () => {
67
+ const emptyProfile = {
68
+ hairType: null,
69
+ porosity: null,
70
+ hairConcerns: []
71
+ };
72
+ const block = service.buildHairProfileBlock(emptyProfile);
73
+ expect(block).toBeDefined();
74
+ expect(block.data.hairType).toBeNull();
75
+ expect(block.data.porosity).toBeNull();
76
+ expect(block.data.regimen).toBeNull();
77
+ });
78
+
79
+ test('buildHairProfileBlock returns null when profile is unavailable', () => {
80
+ const unavailableProfile = { available: false, error: 'API offline' };
81
+ const block = service.buildHairProfileBlock(unavailableProfile);
82
+ expect(block).toBeNull();
83
+ });
84
+
85
+ test('honest unavailable handling when backend is offline', async () => {
86
+ // Service with offline/unreachable provider
87
+ const offlineProvider = {
88
+ getGoals: jest.fn().mockResolvedValue({ available: false, error: 'offline', goals: [] }),
89
+ getTodayData: jest.fn().mockResolvedValue({ available: false, error: 'offline', checklist: [] }),
90
+ getJournalEntries: jest.fn().mockResolvedValue({ available: false, error: 'offline', items: [] }),
91
+ getProfile: jest.fn().mockResolvedValue({ available: false, error: 'offline' })
92
+ };
93
+ const offlineService = new HairJourneyService({ provider: offlineProvider });
94
+
95
+ const goalsRes = await offlineService.getGoalsWithBlocks('user_123');
96
+ expect(goalsRes.result.available).toBe(false);
97
+ expect(goalsRes.blocks).toEqual([]);
98
+
99
+ const todayRes = await offlineService.getTodayWithBlocks('user_123');
100
+ expect(todayRes.result.available).toBe(false);
101
+ expect(todayRes.blocks).toEqual([]);
102
+
103
+ const profileRes = await offlineService.getProfileWithBlocks('user_123');
104
+ expect(profileRes.result.available).toBe(false);
105
+ expect(profileRes.blocks).toEqual([]);
106
+ });
107
+
108
+ test('getGoalsWithBlocks returns goals and blocks when provider has authentic data', async () => {
109
+ const mockProvider = {
110
+ getGoals: jest.fn().mockResolvedValue({
111
+ available: true,
112
+ goals: [
113
+ { id: 'g1', title: 'Retain 4 Inches Length', progressPercent: 68, targetMetric: '14.5 in', targetDate: '2026-12-15' }
114
+ ]
115
+ })
116
+ };
117
+ const activeService = new HairJourneyService({ provider: mockProvider });
118
+ const { result, blocks } = await activeService.getGoalsWithBlocks('test_user');
119
+ expect(result.available).toBe(true);
120
+ expect(blocks.length).toBe(1);
121
+ expect(blocks[0].type).toBe(BlockType.GOAL_CARD);
122
+ expect(blocks[0].data.title).toBe('Retain 4 Inches Length');
123
+ expect(blocks[0].data.progressPercent).toBe(68);
124
+ });
125
+
126
+ test('getTodayWithBlocks returns today checklist block when provider has authentic data', async () => {
127
+ const mockProvider = {
128
+ getTodayData: jest.fn().mockResolvedValue({
129
+ available: true,
130
+ streakDays: 9,
131
+ checklist: [{ id: 's1', title: 'Morning Hydration', completed: true }]
132
+ })
133
+ };
134
+ const activeService = new HairJourneyService({ provider: mockProvider });
135
+ const { result, blocks } = await activeService.getTodayWithBlocks('test_user');
136
+ expect(result.available).toBe(true);
137
+ expect(blocks.length).toBe(1);
138
+ expect(blocks[0].type).toBe(BlockType.TODAY_CHECKLIST);
139
+ expect(blocks[0].data.streakDays).toBe(9);
140
+ });
141
+
142
+ test('getProfileWithBlocks returns authentic hair profile without fabrication', async () => {
143
+ const mockProvider = {
144
+ getProfile: jest.fn().mockResolvedValue({
145
+ available: true,
146
+ hairType: '4A',
147
+ porosity: 'Medium',
148
+ elasticity: 'Medium',
149
+ strandDiameter: 'Medium',
150
+ hairConcerns: ['Moisture Retention']
151
+ })
152
+ };
153
+ const activeService = new HairJourneyService({ provider: mockProvider });
154
+ const { result, blocks } = await activeService.getProfileWithBlocks('test_user');
155
+ expect(result.available).toBe(true);
156
+ expect(result.hairType).toBe('4A');
157
+ expect(result.porosity).toBe('Medium');
158
+ expect(blocks.length).toBe(1);
159
+ expect(blocks[0].data.hairType).toBe('4A');
160
+ expect(blocks[0].data.porosity).toBe('Medium');
161
+ });
162
+ });
@@ -4,14 +4,12 @@
4
4
  // the existing AWS/GCP infra. Extraction follows the same LLM-structured-extraction
5
5
  // pattern already used by postProcessConversation/classifyAndSavePersona.
6
6
  const ai = require('./ai');
7
- const { googleAI } = require('@genkit-ai/google-genai');
8
7
  const { pgClient } = require('./database');
9
8
 
10
- const EMBEDDER = googleAI.embedder('gemini-embedding-001'); // 768-dim output, matches agent_memories.embedding
11
9
  const DUPLICATE_DISTANCE_THRESHOLD = 0.15; // cosine distance below this = treat as the same memory
12
10
 
13
11
  async function embedText(text) {
14
- const [{ embedding }] = await ai.embed({ embedder: EMBEDDER, content: text });
12
+ const [{ embedding }] = await ai.embed({ embedder: 'gemini-embedding-001', content: text });
15
13
  return `[${embedding.join(',')}]`;
16
14
  }
17
15
 
@@ -44,7 +42,7 @@ const extractAndStoreMemories = async (userId, conversationId, chatHistory) => {
44
42
  if (!latestUserTurn?.content) return;
45
43
 
46
44
  const chat = ai.chat({
47
- model: googleAI.model('gemini-3.6-flash'),
45
+ model: 'gemini-2.5-flash',
48
46
  config: { temperature: 0.3 },
49
47
  });
50
48
  const { output } = await chat.send(extractionPrompt(chatHistory, latestUserTurn.content));