@cmdoss/memwal-sdk 0.6.2 → 0.8.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.
Files changed (247) hide show
  1. package/ARCHITECTURE.md +547 -547
  2. package/BENCHMARKS.md +238 -238
  3. package/README.md +310 -181
  4. package/dist/ai-sdk/tools.d.ts +2 -2
  5. package/dist/ai-sdk/tools.js +2 -2
  6. package/dist/client/ClientMemoryManager.js +2 -2
  7. package/dist/client/ClientMemoryManager.js.map +1 -1
  8. package/dist/client/PersonalDataWallet.d.ts.map +1 -1
  9. package/dist/client/SimplePDWClient.d.ts +29 -1
  10. package/dist/client/SimplePDWClient.d.ts.map +1 -1
  11. package/dist/client/SimplePDWClient.js +45 -13
  12. package/dist/client/SimplePDWClient.js.map +1 -1
  13. package/dist/client/namespaces/EmbeddingsNamespace.d.ts +1 -1
  14. package/dist/client/namespaces/EmbeddingsNamespace.js +1 -1
  15. package/dist/client/namespaces/MemoryNamespace.d.ts +31 -0
  16. package/dist/client/namespaces/MemoryNamespace.d.ts.map +1 -1
  17. package/dist/client/namespaces/MemoryNamespace.js +272 -39
  18. package/dist/client/namespaces/MemoryNamespace.js.map +1 -1
  19. package/dist/client/namespaces/consolidated/AINamespace.d.ts +2 -2
  20. package/dist/client/namespaces/consolidated/AINamespace.js +2 -2
  21. package/dist/client/namespaces/consolidated/BlockchainNamespace.d.ts +12 -2
  22. package/dist/client/namespaces/consolidated/BlockchainNamespace.d.ts.map +1 -1
  23. package/dist/client/namespaces/consolidated/BlockchainNamespace.js +62 -4
  24. package/dist/client/namespaces/consolidated/BlockchainNamespace.js.map +1 -1
  25. package/dist/client/namespaces/consolidated/StorageNamespace.d.ts +67 -2
  26. package/dist/client/namespaces/consolidated/StorageNamespace.d.ts.map +1 -1
  27. package/dist/client/namespaces/consolidated/StorageNamespace.js +549 -16
  28. package/dist/client/namespaces/consolidated/StorageNamespace.js.map +1 -1
  29. package/dist/config/ConfigurationHelper.js +61 -61
  30. package/dist/config/defaults.js +2 -2
  31. package/dist/config/defaults.js.map +1 -1
  32. package/dist/graph/GraphService.js +21 -21
  33. package/dist/graph/GraphService.js.map +1 -1
  34. package/dist/index.d.ts +3 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +3 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/infrastructure/seal/EncryptionService.d.ts +9 -5
  39. package/dist/infrastructure/seal/EncryptionService.d.ts.map +1 -1
  40. package/dist/infrastructure/seal/EncryptionService.js +37 -15
  41. package/dist/infrastructure/seal/EncryptionService.js.map +1 -1
  42. package/dist/infrastructure/seal/SealService.d.ts +13 -5
  43. package/dist/infrastructure/seal/SealService.d.ts.map +1 -1
  44. package/dist/infrastructure/seal/SealService.js +36 -34
  45. package/dist/infrastructure/seal/SealService.js.map +1 -1
  46. package/dist/langchain/createPDWRAG.js +30 -30
  47. package/dist/retrieval/MemoryDecryptionPipeline.d.ts.map +1 -1
  48. package/dist/retrieval/MemoryDecryptionPipeline.js +2 -1
  49. package/dist/retrieval/MemoryDecryptionPipeline.js.map +1 -1
  50. package/dist/retrieval/MemoryRetrievalService.d.ts +31 -0
  51. package/dist/retrieval/MemoryRetrievalService.d.ts.map +1 -1
  52. package/dist/retrieval/MemoryRetrievalService.js +44 -4
  53. package/dist/retrieval/MemoryRetrievalService.js.map +1 -1
  54. package/dist/services/CapabilityService.d.ts.map +1 -1
  55. package/dist/services/CapabilityService.js +30 -14
  56. package/dist/services/CapabilityService.js.map +1 -1
  57. package/dist/services/CrossContextPermissionService.d.ts.map +1 -1
  58. package/dist/services/CrossContextPermissionService.js +9 -7
  59. package/dist/services/CrossContextPermissionService.js.map +1 -1
  60. package/dist/services/EmbeddingService.d.ts +28 -1
  61. package/dist/services/EmbeddingService.d.ts.map +1 -1
  62. package/dist/services/EmbeddingService.js +54 -0
  63. package/dist/services/EmbeddingService.js.map +1 -1
  64. package/dist/services/EncryptionService.d.ts.map +1 -1
  65. package/dist/services/EncryptionService.js +6 -5
  66. package/dist/services/EncryptionService.js.map +1 -1
  67. package/dist/services/GeminiAIService.js +309 -309
  68. package/dist/services/IndexManager.d.ts +5 -1
  69. package/dist/services/IndexManager.d.ts.map +1 -1
  70. package/dist/services/IndexManager.js +17 -40
  71. package/dist/services/IndexManager.js.map +1 -1
  72. package/dist/services/QueryService.js +1 -1
  73. package/dist/services/QueryService.js.map +1 -1
  74. package/dist/services/StorageService.d.ts +11 -0
  75. package/dist/services/StorageService.d.ts.map +1 -1
  76. package/dist/services/StorageService.js +73 -10
  77. package/dist/services/StorageService.js.map +1 -1
  78. package/dist/services/TransactionService.d.ts +20 -0
  79. package/dist/services/TransactionService.d.ts.map +1 -1
  80. package/dist/services/TransactionService.js +43 -0
  81. package/dist/services/TransactionService.js.map +1 -1
  82. package/dist/services/ViewService.js +2 -2
  83. package/dist/services/ViewService.js.map +1 -1
  84. package/dist/services/storage/QuiltBatchManager.d.ts +101 -1
  85. package/dist/services/storage/QuiltBatchManager.d.ts.map +1 -1
  86. package/dist/services/storage/QuiltBatchManager.js +410 -20
  87. package/dist/services/storage/QuiltBatchManager.js.map +1 -1
  88. package/dist/services/storage/index.d.ts +1 -1
  89. package/dist/services/storage/index.d.ts.map +1 -1
  90. package/dist/services/storage/index.js.map +1 -1
  91. package/dist/utils/LRUCache.d.ts +106 -0
  92. package/dist/utils/LRUCache.d.ts.map +1 -0
  93. package/dist/utils/LRUCache.js +281 -0
  94. package/dist/utils/LRUCache.js.map +1 -0
  95. package/dist/utils/index.d.ts +1 -0
  96. package/dist/utils/index.d.ts.map +1 -1
  97. package/dist/utils/index.js +2 -0
  98. package/dist/utils/index.js.map +1 -1
  99. package/dist/utils/memoryIndexOnChain.d.ts +212 -0
  100. package/dist/utils/memoryIndexOnChain.d.ts.map +1 -0
  101. package/dist/utils/memoryIndexOnChain.js +312 -0
  102. package/dist/utils/memoryIndexOnChain.js.map +1 -0
  103. package/dist/utils/rebuildIndexNode.d.ts +29 -0
  104. package/dist/utils/rebuildIndexNode.d.ts.map +1 -1
  105. package/dist/utils/rebuildIndexNode.js +366 -98
  106. package/dist/utils/rebuildIndexNode.js.map +1 -1
  107. package/dist/vector/HnswWasmService.d.ts +20 -5
  108. package/dist/vector/HnswWasmService.d.ts.map +1 -1
  109. package/dist/vector/HnswWasmService.js +73 -40
  110. package/dist/vector/HnswWasmService.js.map +1 -1
  111. package/dist/vector/IHnswService.d.ts +10 -1
  112. package/dist/vector/IHnswService.d.ts.map +1 -1
  113. package/dist/vector/IHnswService.js.map +1 -1
  114. package/dist/vector/NodeHnswService.d.ts +16 -0
  115. package/dist/vector/NodeHnswService.d.ts.map +1 -1
  116. package/dist/vector/NodeHnswService.js +84 -5
  117. package/dist/vector/NodeHnswService.js.map +1 -1
  118. package/dist/vector/createHnswService.d.ts +1 -1
  119. package/dist/vector/createHnswService.js +1 -1
  120. package/dist/vector/index.d.ts +1 -1
  121. package/dist/vector/index.js +1 -1
  122. package/package.json +157 -157
  123. package/src/access/PermissionService.ts +635 -635
  124. package/src/aggregation/AggregationService.ts +389 -389
  125. package/src/ai-sdk/PDWVectorStore.ts +715 -715
  126. package/src/ai-sdk/index.ts +65 -65
  127. package/src/ai-sdk/tools.ts +460 -460
  128. package/src/ai-sdk/types.ts +404 -404
  129. package/src/batch/BatchManager.ts +597 -597
  130. package/src/batch/BatchingService.ts +429 -429
  131. package/src/batch/MemoryProcessingCache.ts +492 -492
  132. package/src/batch/index.ts +30 -30
  133. package/src/browser.ts +200 -200
  134. package/src/client/ClientMemoryManager.ts +987 -987
  135. package/src/client/PersonalDataWallet.ts +345 -345
  136. package/src/client/SimplePDWClient.ts +1289 -1222
  137. package/src/client/factory.ts +154 -154
  138. package/src/client/namespaces/AnalyticsNamespace.ts +377 -377
  139. package/src/client/namespaces/BatchNamespace.ts +356 -356
  140. package/src/client/namespaces/CacheNamespace.ts +123 -123
  141. package/src/client/namespaces/CapabilityNamespace.ts +217 -217
  142. package/src/client/namespaces/ClassifyNamespace.ts +169 -169
  143. package/src/client/namespaces/ContextNamespace.ts +297 -297
  144. package/src/client/namespaces/EmbeddingsNamespace.ts +99 -99
  145. package/src/client/namespaces/EncryptionNamespace.ts +221 -221
  146. package/src/client/namespaces/GraphNamespace.ts +468 -468
  147. package/src/client/namespaces/IndexNamespace.ts +361 -361
  148. package/src/client/namespaces/MemoryNamespace.ts +1422 -1135
  149. package/src/client/namespaces/PermissionsNamespace.ts +254 -254
  150. package/src/client/namespaces/PipelineNamespace.ts +220 -220
  151. package/src/client/namespaces/SearchNamespace.ts +1049 -1049
  152. package/src/client/namespaces/StorageNamespace.ts +458 -458
  153. package/src/client/namespaces/TxNamespace.ts +260 -260
  154. package/src/client/namespaces/WalletNamespace.ts +243 -243
  155. package/src/client/namespaces/consolidated/AINamespace.ts +449 -449
  156. package/src/client/namespaces/consolidated/BlockchainNamespace.ts +607 -546
  157. package/src/client/namespaces/consolidated/SecurityNamespace.ts +648 -648
  158. package/src/client/namespaces/consolidated/StorageNamespace.ts +1141 -497
  159. package/src/client/namespaces/consolidated/index.ts +39 -39
  160. package/src/client/signers/KeypairSigner.ts +108 -108
  161. package/src/client/signers/UnifiedSigner.ts +110 -110
  162. package/src/client/signers/WalletAdapterSigner.ts +159 -159
  163. package/src/client/signers/index.ts +26 -26
  164. package/src/config/ConfigurationHelper.ts +412 -412
  165. package/src/config/defaults.ts +51 -51
  166. package/src/config/index.ts +8 -8
  167. package/src/config/validation.ts +70 -70
  168. package/src/core/index.ts +14 -14
  169. package/src/core/interfaces/IService.ts +307 -307
  170. package/src/core/interfaces/index.ts +8 -8
  171. package/src/core/types/capability.ts +297 -297
  172. package/src/core/types/index.ts +870 -870
  173. package/src/core/types/wallet.ts +270 -270
  174. package/src/core/types.ts +9 -9
  175. package/src/core/wallet.ts +222 -222
  176. package/src/embedding/index.ts +19 -19
  177. package/src/embedding/types.ts +357 -357
  178. package/src/errors/index.ts +602 -602
  179. package/src/errors/recovery.ts +461 -461
  180. package/src/errors/validation.ts +567 -567
  181. package/src/generated/pdw/capability.ts +319 -319
  182. package/src/graph/GraphService.ts +887 -887
  183. package/src/graph/KnowledgeGraphManager.ts +728 -728
  184. package/src/graph/index.ts +25 -25
  185. package/src/index.ts +498 -474
  186. package/src/infrastructure/index.ts +22 -22
  187. package/src/infrastructure/seal/EncryptionService.ts +628 -603
  188. package/src/infrastructure/seal/SealService.ts +613 -615
  189. package/src/infrastructure/seal/index.ts +9 -9
  190. package/src/infrastructure/sui/BlockchainManager.ts +627 -627
  191. package/src/infrastructure/sui/SuiService.ts +888 -888
  192. package/src/infrastructure/sui/index.ts +9 -9
  193. package/src/infrastructure/walrus/StorageManager.ts +604 -604
  194. package/src/infrastructure/walrus/WalrusStorageService.ts +612 -612
  195. package/src/infrastructure/walrus/index.ts +9 -9
  196. package/src/langchain/PDWEmbeddings.ts +145 -145
  197. package/src/langchain/PDWVectorStore.ts +456 -456
  198. package/src/langchain/createPDWRAG.ts +303 -303
  199. package/src/langchain/index.ts +47 -47
  200. package/src/permissions/ConsentRepository.browser.ts +249 -249
  201. package/src/permissions/ConsentRepository.ts +364 -364
  202. package/src/pipeline/MemoryPipeline.ts +862 -862
  203. package/src/pipeline/PipelineManager.ts +683 -683
  204. package/src/pipeline/index.ts +26 -26
  205. package/src/retrieval/AdvancedSearchService.ts +629 -629
  206. package/src/retrieval/MemoryAnalyticsService.ts +711 -711
  207. package/src/retrieval/MemoryDecryptionPipeline.ts +825 -824
  208. package/src/retrieval/MemoryRetrievalService.ts +904 -830
  209. package/src/retrieval/index.ts +42 -42
  210. package/src/services/BatchService.ts +352 -352
  211. package/src/services/CapabilityService.ts +464 -448
  212. package/src/services/ClassifierService.ts +465 -465
  213. package/src/services/CrossContextPermissionService.ts +486 -484
  214. package/src/services/EmbeddingService.ts +771 -706
  215. package/src/services/EncryptionService.ts +712 -711
  216. package/src/services/GeminiAIService.ts +753 -753
  217. package/src/services/IndexManager.ts +977 -1004
  218. package/src/services/MemoryIndexService.ts +1003 -1003
  219. package/src/services/MemoryService.ts +369 -369
  220. package/src/services/QueryService.ts +890 -890
  221. package/src/services/StorageService.ts +1182 -1111
  222. package/src/services/TransactionService.ts +838 -790
  223. package/src/services/VectorService.ts +462 -462
  224. package/src/services/ViewService.ts +484 -484
  225. package/src/services/index.ts +25 -25
  226. package/src/services/storage/BlobAttributesManager.ts +333 -333
  227. package/src/services/storage/KnowledgeGraphManager.ts +425 -425
  228. package/src/services/storage/MemorySearchManager.ts +387 -387
  229. package/src/services/storage/QuiltBatchManager.ts +1130 -660
  230. package/src/services/storage/WalrusMetadataManager.ts +268 -268
  231. package/src/services/storage/WalrusStorageManager.ts +287 -287
  232. package/src/services/storage/index.ts +57 -52
  233. package/src/types/index.ts +13 -13
  234. package/src/utils/LRUCache.ts +378 -0
  235. package/src/utils/index.ts +76 -68
  236. package/src/utils/memoryIndexOnChain.ts +507 -0
  237. package/src/utils/rebuildIndex.ts +290 -290
  238. package/src/utils/rebuildIndexNode.ts +771 -424
  239. package/src/vector/BrowserHnswIndexService.ts +758 -758
  240. package/src/vector/HnswWasmService.ts +731 -679
  241. package/src/vector/IHnswService.ts +233 -224
  242. package/src/vector/NodeHnswService.ts +833 -735
  243. package/src/vector/VectorManager.ts +478 -478
  244. package/src/vector/createHnswService.ts +135 -135
  245. package/src/vector/index.ts +56 -56
  246. package/src/wallet/ContextWalletService.ts +656 -656
  247. package/src/wallet/MainWalletService.ts +317 -317
@@ -1,753 +1,753 @@
1
- /**
2
- * GeminiAIService - AI Integration via OpenRouter SDK
3
- *
4
- * Provides AI-powered text analysis capabilities using OpenRouter SDK
5
- * for entity extraction, relationship identification, and content analysis.
6
- *
7
- * Supports any model available on OpenRouter (Google Gemini, OpenAI, Anthropic, etc.)
8
- *
9
- * Refactored to use official @openrouter/sdk instead of raw fetch calls.
10
- */
11
-
12
- import { OpenRouter } from '@openrouter/sdk';
13
-
14
- export interface GeminiConfig {
15
- apiKey: string;
16
- model?: string;
17
- temperature?: number;
18
- maxTokens?: number;
19
- timeout?: number;
20
- }
21
-
22
- export interface EntityExtractionRequest {
23
- content: string;
24
- context?: string;
25
- confidenceThreshold?: number;
26
- }
27
-
28
- export interface EntityExtractionResponse {
29
- entities: Array<{
30
- id: string;
31
- label: string;
32
- type: string;
33
- confidence: number;
34
- properties?: Record<string, any>;
35
- }>;
36
- relationships: Array<{
37
- source: string;
38
- target: string;
39
- label: string;
40
- confidence: number;
41
- type?: string;
42
- }>;
43
- processingTimeMs: number;
44
- }
45
-
46
- /**
47
- * AI service for advanced text analysis and knowledge extraction
48
- * Uses OpenRouter SDK for maximum flexibility and model choice
49
- */
50
- export class GeminiAIService {
51
- private readonly apiKey: string;
52
- private readonly config: Required<GeminiConfig>;
53
- private readonly openRouterClient: OpenRouter;
54
-
55
- constructor(config: GeminiConfig) {
56
- // Resolve API key: prefer OPENROUTER_API_KEY, fallback to provided apiKey
57
- this.apiKey = process.env.OPENROUTER_API_KEY || config.apiKey;
58
-
59
- if (!this.apiKey) {
60
- throw new Error(
61
- 'API key is required. Set OPENROUTER_API_KEY environment variable or provide apiKey in config.'
62
- );
63
- }
64
-
65
- this.config = {
66
- model: config.model || process.env.AI_CHAT_MODEL || 'google/gemini-2.5-flash',
67
- temperature: config.temperature || 0.1,
68
- maxTokens: config.maxTokens || 4096,
69
- timeout: config.timeout || 30000,
70
- apiKey: this.apiKey
71
- };
72
-
73
- // Initialize OpenRouter SDK client
74
- this.openRouterClient = new OpenRouter({
75
- apiKey: this.apiKey
76
- });
77
-
78
- console.log(`✅ GeminiAIService initialized with OpenRouter SDK (${this.config.model})`);
79
- }
80
-
81
- /**
82
- * Call OpenRouter Chat Completions API using SDK
83
- */
84
- private async callOpenRouter(prompt: string): Promise<string> {
85
- try {
86
- const result = await this.openRouterClient.chat.send({
87
- model: this.config.model,
88
- messages: [
89
- { role: 'user', content: prompt }
90
- ],
91
- temperature: this.config.temperature,
92
- maxTokens: this.config.maxTokens,
93
- stream: false
94
- });
95
-
96
- if (!result.choices || !result.choices[0] || !result.choices[0].message) {
97
- throw new Error('Invalid response from OpenRouter API');
98
- }
99
-
100
- // Handle content which can be string or array
101
- const content = result.choices[0].message.content;
102
- if (typeof content === 'string') {
103
- return content;
104
- }
105
- if (Array.isArray(content)) {
106
- // Extract text from content items
107
- return content
108
- .filter((item: any) => item.type === 'text')
109
- .map((item: any) => item.text || '')
110
- .join('');
111
- }
112
- return '';
113
- } catch (error) {
114
- if (error instanceof Error) {
115
- throw new Error(`OpenRouter API error: ${error.message}`);
116
- }
117
- throw error;
118
- }
119
- }
120
-
121
- /**
122
- * Extract entities and relationships from text using AI
123
- */
124
- async extractEntitiesAndRelationships(request: EntityExtractionRequest): Promise<EntityExtractionResponse> {
125
- const startTime = Date.now();
126
-
127
- try {
128
- // Validate input: return empty result for empty/whitespace-only content
129
- const trimmedContent = request.content?.trim();
130
- if (!trimmedContent || trimmedContent.length === 0) {
131
- return {
132
- entities: [],
133
- relationships: [],
134
- processingTimeMs: Date.now() - startTime
135
- };
136
- }
137
-
138
- const prompt = this.buildExtractionPrompt(request.content, request.context);
139
- const text = await this.callOpenRouter(prompt);
140
-
141
- const parsed = this.parseExtractionResponse(text);
142
- const processingTimeMs = Date.now() - startTime;
143
-
144
- return {
145
- entities: parsed.entities,
146
- relationships: parsed.relationships,
147
- processingTimeMs
148
- };
149
-
150
- } catch (error) {
151
- console.error('AI extraction failed:', error);
152
-
153
- // Return empty result with processing time on error
154
- return {
155
- entities: [],
156
- relationships: [],
157
- processingTimeMs: Date.now() - startTime
158
- };
159
- }
160
- }
161
-
162
- /**
163
- * Extract entities and relationships from multiple texts in batch
164
- */
165
- async extractBatch(requests: EntityExtractionRequest[]): Promise<EntityExtractionResponse[]> {
166
- const results: EntityExtractionResponse[] = [];
167
-
168
- // Process in batches to avoid rate limiting
169
- const batchSize = 3;
170
- for (let i = 0; i < requests.length; i += batchSize) {
171
- const batch = requests.slice(i, i + batchSize);
172
- const batchPromises = batch.map(request => this.extractEntitiesAndRelationships(request));
173
- const batchResults = await Promise.all(batchPromises);
174
- results.push(...batchResults);
175
-
176
- // Small delay between batches to respect rate limits
177
- if (i + batchSize < requests.length) {
178
- await this.delay(500);
179
- }
180
- }
181
-
182
- return results;
183
- }
184
-
185
- /**
186
- * Analyze text content for categorization and sentiment
187
- */
188
- async analyzeContent(content: string): Promise<{
189
- categories: string[];
190
- sentiment: 'positive' | 'negative' | 'neutral';
191
- topics: string[];
192
- confidence: number;
193
- }> {
194
- try {
195
- const prompt = `
196
- Analyze the following text and provide a JSON response with:
197
- - "categories": array of relevant categories (max 3)
198
- - "sentiment": "positive", "negative", or "neutral"
199
- - "topics": array of main topics/themes (max 5)
200
- - "confidence": overall analysis confidence (0.0-1.0)
201
-
202
- TEXT: ${content}
203
-
204
- JSON:`;
205
-
206
- const text = await this.callOpenRouter(prompt);
207
- return this.parseAnalysisResponse(text);
208
-
209
- } catch (error) {
210
- console.error('Content analysis failed:', error);
211
- return {
212
- categories: [],
213
- sentiment: 'neutral',
214
- topics: [],
215
- confidence: 0
216
- };
217
- }
218
- }
219
-
220
- // ==================== PRIVATE METHODS ====================
221
-
222
- private buildExtractionPrompt(content: string, context?: string): string {
223
- const contextSection = context ? `\nCONTEXT: ${context}\n` : '';
224
-
225
- return `
226
- You are a knowledge graph extraction system for a Personal Data Wallet application. Your task is to extract meaningful entities and relationships from personal memories, notes, and user statements.
227
-
228
- ## CRITICAL RULE: User Entity
229
- ALWAYS include a "user" entity to represent the person who wrote this memory:
230
- {
231
- "id": "user",
232
- "label": "User",
233
- "type": "person",
234
- "confidence": 1.0
235
- }
236
- This "user" entity should be the source or target of relationships describing personal preferences, attributes, or experiences.
237
-
238
- ## Entity Types (Comprehensive List)
239
-
240
- ### People & Social
241
- - **person**: Individual people, including the user themselves
242
- - Examples: "user", "john_doe", "my_mother", "boss"
243
- - Properties: name, role, relationship_to_user
244
-
245
- ### Organizations & Groups
246
- - **organization**: Companies, institutions, teams, communities
247
- - Examples: "google", "harvard_university", "local_gym"
248
- - Properties: industry, size, location
249
-
250
- ### Locations & Places
251
- - **location**: Geographic places, addresses, venues
252
- - Examples: "ho_chi_minh_city", "vietnam", "my_office", "central_park"
253
- - Properties: type (city/country/venue), coordinates
254
-
255
- ### Food & Dining
256
- - **food**: Foods, dishes, cuisines, beverages, ingredients
257
- - Examples: "spaghetti", "vietnamese_cuisine", "coffee", "chocolate"
258
- - Properties: cuisine_type, meal_type, dietary_info
259
- - **restaurant**: Eating establishments
260
- - Examples: "starbucks", "local_pho_shop"
261
- - Properties: cuisine, price_range
262
-
263
- ### Preferences & Interests
264
- - **preference**: General likes, dislikes, favorites
265
- - Examples: "blue_color", "morning_routine", "minimalist_style"
266
- - Properties: sentiment (positive/negative/neutral), intensity (1-10)
267
- - **hobby**: Recreational activities, pastimes
268
- - Examples: "playing_guitar", "photography", "hiking", "gaming"
269
- - Properties: frequency, skill_level
270
- - **interest**: Topics of curiosity or passion
271
- - Examples: "artificial_intelligence", "history", "cooking"
272
- - Properties: depth (casual/moderate/deep)
273
-
274
- ### Skills & Abilities
275
- - **skill**: Technical or soft skills, expertise areas
276
- - Examples: "python_programming", "public_speaking", "cooking"
277
- - Properties: proficiency (beginner/intermediate/expert)
278
- - **language**: Languages known or being learned
279
- - Examples: "english", "vietnamese", "japanese"
280
- - Properties: proficiency, native (true/false)
281
-
282
- ### Objects & Possessions
283
- - **object**: Physical items, products, tools
284
- - Examples: "macbook_pro", "my_car", "guitar"
285
- - Properties: brand, model, acquisition_date
286
- - **digital_product**: Software, apps, digital services
287
- - Examples: "spotify", "notion", "chatgpt"
288
- - Properties: category, usage_frequency
289
-
290
- ### Time & Events
291
- - **event**: Occasions, milestones, meetings
292
- - Examples: "birthday_2024", "job_interview", "vacation_trip"
293
- - Properties: date, duration, importance
294
- - **routine**: Regular activities or habits
295
- - Examples: "morning_workout", "weekly_meeting", "daily_meditation"
296
- - Properties: frequency, time_of_day
297
-
298
- ### Abstract & Conceptual
299
- - **concept**: Ideas, topics, abstract things
300
- - Examples: "work_life_balance", "productivity", "happiness"
301
- - **goal**: Objectives, aspirations, plans
302
- - Examples: "learn_japanese", "run_marathon", "save_money"
303
- - Properties: deadline, priority, status
304
- - **emotion**: Feelings, moods, emotional states
305
- - Examples: "happiness", "stress", "excitement"
306
- - Properties: intensity, trigger
307
-
308
- ### Health & Wellness
309
- - **health_condition**: Medical conditions, allergies
310
- - Examples: "lactose_intolerance", "migraine", "allergy_to_peanuts"
311
- - **medication**: Medicines, supplements
312
- - Examples: "vitamin_d", "aspirin"
313
- - **exercise**: Physical activities for health
314
- - Examples: "running", "yoga", "weight_training"
315
-
316
- ### Media & Entertainment
317
- - **music**: Songs, artists, genres, albums
318
- - Examples: "jazz_music", "beatles", "classical_piano"
319
- - **movie**: Films, TV shows, documentaries
320
- - Examples: "inception", "game_of_thrones"
321
- - **book**: Books, authors, genres
322
- - Examples: "atomic_habits", "fiction_genre"
323
- - **game**: Video games, board games
324
- - Examples: "chess", "minecraft"
325
-
326
- ## Relationship Types (Comprehensive List)
327
-
328
- ### Preference Relationships (source: usually "user")
329
- - **loves**: Strong positive preference (intensity 9-10)
330
- - **likes**: Moderate positive preference (intensity 6-8)
331
- - **enjoys**: Positive experience with something
332
- - **prefers**: Comparative preference
333
- - **favorite**: Top choice in a category
334
- - **interested_in**: Curiosity or engagement
335
- - **dislikes**: Moderate negative preference
336
- - **hates**: Strong negative preference (intensity 9-10)
337
- - **avoids**: Intentionally stays away from
338
- - **allergic_to**: Medical/physical aversion
339
-
340
- ### Affiliation Relationships
341
- - **works_at**: Employment relationship
342
- - **studies_at**: Educational institution
343
- - **member_of**: Group membership
344
- - **belongs_to**: General affiliation
345
- - **founded**: Created an organization
346
- - **leads**: Leadership role
347
-
348
- ### Location Relationships
349
- - **lives_in**: Current residence
350
- - **from**: Origin/hometown
351
- - **located_in**: Physical location
352
- - **visited**: Past travel
353
- - **wants_to_visit**: Travel aspiration
354
-
355
- ### Social Relationships
356
- - **knows**: Acquaintance
357
- - **friends_with**: Friendship
358
- - **family_of**: Family relationship (specify: parent, sibling, child, spouse)
359
- - **works_with**: Professional relationship
360
- - **mentored_by**: Learning relationship
361
-
362
- ### Skill & Knowledge Relationships
363
- - **has_skill**: Possesses ability
364
- - **expert_in**: High proficiency
365
- - **learning**: Currently acquiring
366
- - **wants_to_learn**: Aspiration to learn
367
- - **teaches**: Instructing others
368
- - **certified_in**: Formal qualification
369
-
370
- ### Possession & Usage
371
- - **owns**: Ownership
372
- - **uses**: Regular usage
373
- - **wants**: Desire to acquire
374
- - **recommends**: Positive endorsement
375
-
376
- ### Temporal Relationships
377
- - **started_on**: Beginning date
378
- - **ended_on**: Ending date
379
- - **scheduled_for**: Future event
380
- - **happens_during**: Temporal context
381
-
382
- ### Causal & Descriptive
383
- - **causes**: Causal relationship
384
- - **related_to**: General association
385
- - **part_of**: Component relationship
386
- - **similar_to**: Similarity
387
- - **opposite_of**: Contrast
388
-
389
- ## Output Format
390
-
391
- Return ONLY valid JSON with this structure:
392
- {
393
- "entities": [
394
- {
395
- "id": "snake_case_identifier",
396
- "label": "Human Readable Name",
397
- "type": "entity_type_from_list_above",
398
- "confidence": 0.0-1.0,
399
- "properties": { "optional": "attributes" }
400
- }
401
- ],
402
- "relationships": [
403
- {
404
- "source": "source_entity_id",
405
- "target": "target_entity_id",
406
- "label": "relationship_type_from_list_above",
407
- "confidence": 0.0-1.0,
408
- "type": "optional_category"
409
- }
410
- ]
411
- }
412
-
413
- ## Examples
414
-
415
- ### Example 1: Food Preference
416
- Input: "i love spaghetti"
417
- Output:
418
- {
419
- "entities": [
420
- {"id": "user", "label": "User", "type": "person", "confidence": 1.0},
421
- {"id": "spaghetti", "label": "Spaghetti", "type": "food", "confidence": 0.95, "properties": {"cuisine": "italian", "meal_type": "main_course"}}
422
- ],
423
- "relationships": [
424
- {"source": "user", "target": "spaghetti", "label": "loves", "confidence": 0.95, "type": "preference"}
425
- ]
426
- }
427
-
428
- ### Example 2: Multiple Preferences
429
- Input: "i like hamburgers but hate vegetables"
430
- Output:
431
- {
432
- "entities": [
433
- {"id": "user", "label": "User", "type": "person", "confidence": 1.0},
434
- {"id": "hamburgers", "label": "Hamburgers", "type": "food", "confidence": 0.95},
435
- {"id": "vegetables", "label": "Vegetables", "type": "food", "confidence": 0.95}
436
- ],
437
- "relationships": [
438
- {"source": "user", "target": "hamburgers", "label": "likes", "confidence": 0.9, "type": "preference"},
439
- {"source": "user", "target": "vegetables", "label": "dislikes", "confidence": 0.9, "type": "preference"}
440
- ]
441
- }
442
-
443
- ### Example 3: Work Information
444
- Input: "i work at Google as a software engineer in Mountain View"
445
- Output:
446
- {
447
- "entities": [
448
- {"id": "user", "label": "User", "type": "person", "confidence": 1.0, "properties": {"role": "software_engineer"}},
449
- {"id": "google", "label": "Google", "type": "organization", "confidence": 0.98, "properties": {"industry": "technology"}},
450
- {"id": "software_engineer", "label": "Software Engineer", "type": "skill", "confidence": 0.9},
451
- {"id": "mountain_view", "label": "Mountain View", "type": "location", "confidence": 0.95, "properties": {"type": "city"}}
452
- ],
453
- "relationships": [
454
- {"source": "user", "target": "google", "label": "works_at", "confidence": 0.98, "type": "affiliation"},
455
- {"source": "user", "target": "software_engineer", "label": "has_skill", "confidence": 0.9, "type": "skill"},
456
- {"source": "google", "target": "mountain_view", "label": "located_in", "confidence": 0.85, "type": "location"}
457
- ]
458
- }
459
-
460
- ### Example 4: Personal Life
461
- Input: "my name is Aaron and I live in Ho Chi Minh City with my wife"
462
- Output:
463
- {
464
- "entities": [
465
- {"id": "user", "label": "Aaron", "type": "person", "confidence": 1.0, "properties": {"name": "Aaron"}},
466
- {"id": "ho_chi_minh_city", "label": "Ho Chi Minh City", "type": "location", "confidence": 0.98, "properties": {"type": "city", "country": "Vietnam"}},
467
- {"id": "wife", "label": "Wife", "type": "person", "confidence": 0.9, "properties": {"relationship": "spouse"}}
468
- ],
469
- "relationships": [
470
- {"source": "user", "target": "ho_chi_minh_city", "label": "lives_in", "confidence": 0.98, "type": "location"},
471
- {"source": "user", "target": "wife", "label": "family_of", "confidence": 0.95, "type": "social", "properties": {"relationship_type": "spouse"}}
472
- ]
473
- }
474
-
475
- ### Example 5: Hobbies and Interests
476
- Input: "i enjoy playing guitar and listening to jazz music on weekends"
477
- Output:
478
- {
479
- "entities": [
480
- {"id": "user", "label": "User", "type": "person", "confidence": 1.0},
481
- {"id": "playing_guitar", "label": "Playing Guitar", "type": "hobby", "confidence": 0.95},
482
- {"id": "guitar", "label": "Guitar", "type": "object", "confidence": 0.9, "properties": {"category": "musical_instrument"}},
483
- {"id": "jazz_music", "label": "Jazz Music", "type": "music", "confidence": 0.95, "properties": {"genre": "jazz"}},
484
- {"id": "weekends", "label": "Weekends", "type": "routine", "confidence": 0.8, "properties": {"frequency": "weekly"}}
485
- ],
486
- "relationships": [
487
- {"source": "user", "target": "playing_guitar", "label": "enjoys", "confidence": 0.95, "type": "hobby"},
488
- {"source": "playing_guitar", "target": "guitar", "label": "uses", "confidence": 0.9, "type": "activity"},
489
- {"source": "user", "target": "jazz_music", "label": "enjoys", "confidence": 0.9, "type": "preference"},
490
- {"source": "playing_guitar", "target": "weekends", "label": "happens_during", "confidence": 0.8, "type": "temporal"}
491
- ]
492
- }
493
-
494
- ## Guidelines
495
-
496
- 1. **Always include "user" entity** for personal statements (first-person: "I", "my", "me")
497
- 2. **Extract implicit information**: "i'm a doctor" implies medical skill
498
- 3. **Handle negations properly**: "don't like" = dislikes relationship
499
- 4. **Capture intensity**: "love" vs "like" vs "enjoy" = different confidence/intensity
500
- 5. **Include relevant properties**: cuisine type for food, location type for places
501
- 6. **Create bidirectional relationships when applicable**: if A works_at B, B employs A
502
- 7. **Minimum confidence threshold**: 0.5 for entities, 0.5 for relationships
503
- 8. **Be comprehensive**: Extract ALL meaningful entities, even from short texts
504
- 9. **Handle multilingual content**: Vietnamese, English, etc.
505
- 10. **Infer entity types from context**: "spaghetti" = food, "Python" = skill/language based on context
506
- ${contextSection}
507
- ## TEXT TO ANALYZE:
508
- ${content}
509
-
510
- ## JSON OUTPUT:`;
511
- }
512
-
513
- private parseExtractionResponse(response: string): { entities: any[]; relationships: any[] } {
514
- try {
515
- // Clean up the response text (remove markdown formatting if present)
516
- let cleanResponse = response.trim();
517
- if (cleanResponse.startsWith('```json')) {
518
- cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '');
519
- } else if (cleanResponse.startsWith('```')) {
520
- cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '');
521
- }
522
-
523
- const parsed = JSON.parse(cleanResponse);
524
-
525
- if (!parsed.entities || !Array.isArray(parsed.entities)) {
526
- throw new Error('Invalid entities format');
527
- }
528
- if (!parsed.relationships || !Array.isArray(parsed.relationships)) {
529
- throw new Error('Invalid relationships format');
530
- }
531
-
532
- // Validate and sanitize entities
533
- const entities = parsed.entities
534
- .filter((e: any) => e.id && e.label && e.type)
535
- .map((e: any) => ({
536
- id: this.sanitizeId(e.id),
537
- label: e.label.trim(),
538
- type: e.type.toLowerCase(),
539
- confidence: Math.max(0, Math.min(1, e.confidence || 0.7)),
540
- properties: e.properties || {}
541
- }));
542
-
543
- // Create entity ID map for relationship validation
544
- const entityIds = new Set(entities.map((e: any) => e.id));
545
-
546
- // Validate and sanitize relationships
547
- const relationships = parsed.relationships
548
- .filter((r: any) => r.source && r.target && r.label)
549
- .filter((r: any) => entityIds.has(this.sanitizeId(r.source)) && entityIds.has(this.sanitizeId(r.target)))
550
- .map((r: any) => ({
551
- source: this.sanitizeId(r.source),
552
- target: this.sanitizeId(r.target),
553
- label: r.label.trim(),
554
- confidence: Math.max(0, Math.min(1, r.confidence || 0.7)),
555
- type: r.type || 'general'
556
- }));
557
-
558
- return { entities, relationships };
559
-
560
- } catch (error) {
561
- // Only log detailed errors in development mode
562
- if (process.env.NODE_ENV === 'development') {
563
- console.error('Failed to parse AI response:', error);
564
- console.error('Raw response:', response);
565
- }
566
- return { entities: [], relationships: [] };
567
- }
568
- }
569
-
570
- private parseAnalysisResponse(response: string): any {
571
- try {
572
- let cleanResponse = response.trim();
573
- if (cleanResponse.startsWith('```json')) {
574
- cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '');
575
- }
576
-
577
- const parsed = JSON.parse(cleanResponse);
578
-
579
- return {
580
- categories: parsed.categories || [],
581
- sentiment: parsed.sentiment || 'neutral',
582
- topics: parsed.topics || [],
583
- confidence: Math.max(0, Math.min(1, parsed.confidence || 0.5))
584
- };
585
-
586
- } catch (error) {
587
- console.error('Failed to parse analysis response:', error);
588
- return {
589
- categories: [],
590
- sentiment: 'neutral',
591
- topics: [],
592
- confidence: 0
593
- };
594
- }
595
- }
596
-
597
- private sanitizeId(id: string): string {
598
- return id
599
- .toLowerCase()
600
- .replace(/[^a-z0-9_]/g, '_')
601
- .replace(/_+/g, '_')
602
- .replace(/^_|_$/g, '');
603
- }
604
-
605
- private delay(ms: number): Promise<void> {
606
- return new Promise(resolve => setTimeout(resolve, ms));
607
- }
608
-
609
- /**
610
- * Extract rich metadata from content for memory creation
611
- * Returns importance (1-10), topic, and summary
612
- */
613
- async extractRichMetadata(content: string, categoryHint?: string): Promise<{
614
- importance: number;
615
- topic: string;
616
- summary: string;
617
- category: string;
618
- }> {
619
- try {
620
- const prompt = `
621
- Analyze the following text and extract rich metadata in JSON format:
622
- - "importance": relevance/importance score from 1-10 (1=trivial, 10=critical)
623
- - "topic": concise topic/title (max 100 chars)
624
- - "summary": brief summary (max 200 chars)
625
- - "category": best-fit category (personal, work, education, health, finance, travel, family, hobbies, goals, ideas)
626
-
627
- Consider:
628
- - Importance: How valuable is this information for future recall?
629
- - Topic: What's the main subject or theme?
630
- - Summary: Key points in 1-2 sentences
631
- ${categoryHint ? `- Prefer category: ${categoryHint}` : ''}
632
-
633
- TEXT: ${content}
634
-
635
- JSON:`;
636
-
637
- const text = await this.callOpenRouter(prompt);
638
- return this.parseRichMetadataResponse(text, content, categoryHint);
639
-
640
- } catch (error) {
641
- console.error('Rich metadata extraction failed:', error);
642
- return this.getFallbackMetadata(content, categoryHint);
643
- }
644
- }
645
-
646
- /**
647
- * Extract metadata for multiple contents in batch (with rate limiting)
648
- */
649
- async extractRichMetadataBatch(
650
- contents: Array<{ content: string; category?: string }>
651
- ): Promise<Array<{ importance: number; topic: string; summary: string; category: string }>> {
652
- const results: Array<{ importance: number; topic: string; summary: string; category: string }> = [];
653
-
654
- // Process in batches to avoid rate limiting
655
- const batchSize = 3;
656
- for (let i = 0; i < contents.length; i += batchSize) {
657
- const batch = contents.slice(i, i + batchSize);
658
- const batchPromises = batch.map(item =>
659
- this.extractRichMetadata(item.content, item.category)
660
- );
661
- const batchResults = await Promise.all(batchPromises);
662
- results.push(...batchResults);
663
-
664
- // Small delay between batches to respect rate limits
665
- if (i + batchSize < contents.length) {
666
- await this.delay(500);
667
- }
668
- }
669
-
670
- return results;
671
- }
672
-
673
- private parseRichMetadataResponse(response: string, content: string, categoryHint?: string): {
674
- importance: number;
675
- topic: string;
676
- summary: string;
677
- category: string;
678
- } {
679
- try {
680
- let cleanResponse = response.trim();
681
- if (cleanResponse.startsWith('```json')) {
682
- cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '');
683
- } else if (cleanResponse.startsWith('```')) {
684
- cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '');
685
- }
686
-
687
- const parsed = JSON.parse(cleanResponse);
688
-
689
- return {
690
- importance: Math.max(1, Math.min(10, parsed.importance || 5)),
691
- topic: (parsed.topic || this.extractTopicFallback(content)).substring(0, 100),
692
- summary: (parsed.summary || content.substring(0, 200)).substring(0, 200),
693
- category: parsed.category || categoryHint || 'personal'
694
- };
695
-
696
- } catch (error) {
697
- console.error('Failed to parse rich metadata response:', error);
698
- return this.getFallbackMetadata(content, categoryHint);
699
- }
700
- }
701
-
702
- private getFallbackMetadata(content: string, categoryHint?: string): {
703
- importance: number;
704
- topic: string;
705
- summary: string;
706
- category: string;
707
- } {
708
- return {
709
- importance: 5,
710
- topic: this.extractTopicFallback(content),
711
- summary: content.substring(0, 200) + (content.length > 200 ? '...' : ''),
712
- category: categoryHint || 'personal'
713
- };
714
- }
715
-
716
- private extractTopicFallback(text: string): string {
717
- // Try to get first sentence
718
- const firstSentence = text.match(/^[^.!?]+[.!?]/);
719
- if (firstSentence) {
720
- return firstSentence[0].trim().substring(0, 100);
721
- }
722
-
723
- // Fallback: first 50 characters
724
- return text.substring(0, 50).trim() + (text.length > 50 ? '...' : '');
725
- }
726
-
727
- /**
728
- * Check if the service is properly configured and can make API calls
729
- */
730
- async testConnection(): Promise<boolean> {
731
- try {
732
- const text = await this.callOpenRouter('Test connection. Respond with only "OK".');
733
- return text.includes('OK');
734
- } catch {
735
- return false;
736
- }
737
- }
738
-
739
- /**
740
- * Get service configuration (without sensitive data)
741
- */
742
- getConfig(): Omit<Required<GeminiConfig>, 'apiKey'> & { apiKeyConfigured: boolean } {
743
- return {
744
- model: this.config.model,
745
- temperature: this.config.temperature,
746
- maxTokens: this.config.maxTokens,
747
- timeout: this.config.timeout,
748
- apiKeyConfigured: !!this.apiKey
749
- };
750
- }
751
- }
752
-
753
- export default GeminiAIService;
1
+ /**
2
+ * GeminiAIService - AI Integration via OpenRouter SDK
3
+ *
4
+ * Provides AI-powered text analysis capabilities using OpenRouter SDK
5
+ * for entity extraction, relationship identification, and content analysis.
6
+ *
7
+ * Supports any model available on OpenRouter (Google Gemini, OpenAI, Anthropic, etc.)
8
+ *
9
+ * Refactored to use official @openrouter/sdk instead of raw fetch calls.
10
+ */
11
+
12
+ import { OpenRouter } from '@openrouter/sdk';
13
+
14
+ export interface GeminiConfig {
15
+ apiKey: string;
16
+ model?: string;
17
+ temperature?: number;
18
+ maxTokens?: number;
19
+ timeout?: number;
20
+ }
21
+
22
+ export interface EntityExtractionRequest {
23
+ content: string;
24
+ context?: string;
25
+ confidenceThreshold?: number;
26
+ }
27
+
28
+ export interface EntityExtractionResponse {
29
+ entities: Array<{
30
+ id: string;
31
+ label: string;
32
+ type: string;
33
+ confidence: number;
34
+ properties?: Record<string, any>;
35
+ }>;
36
+ relationships: Array<{
37
+ source: string;
38
+ target: string;
39
+ label: string;
40
+ confidence: number;
41
+ type?: string;
42
+ }>;
43
+ processingTimeMs: number;
44
+ }
45
+
46
+ /**
47
+ * AI service for advanced text analysis and knowledge extraction
48
+ * Uses OpenRouter SDK for maximum flexibility and model choice
49
+ */
50
+ export class GeminiAIService {
51
+ private readonly apiKey: string;
52
+ private readonly config: Required<GeminiConfig>;
53
+ private readonly openRouterClient: OpenRouter;
54
+
55
+ constructor(config: GeminiConfig) {
56
+ // Resolve API key: prefer OPENROUTER_API_KEY, fallback to provided apiKey
57
+ this.apiKey = process.env.OPENROUTER_API_KEY || config.apiKey;
58
+
59
+ if (!this.apiKey) {
60
+ throw new Error(
61
+ 'API key is required. Set OPENROUTER_API_KEY environment variable or provide apiKey in config.'
62
+ );
63
+ }
64
+
65
+ this.config = {
66
+ model: config.model || process.env.AI_CHAT_MODEL || 'google/gemini-2.5-flash',
67
+ temperature: config.temperature || 0.1,
68
+ maxTokens: config.maxTokens || 4096,
69
+ timeout: config.timeout || 30000,
70
+ apiKey: this.apiKey
71
+ };
72
+
73
+ // Initialize OpenRouter SDK client
74
+ this.openRouterClient = new OpenRouter({
75
+ apiKey: this.apiKey
76
+ });
77
+
78
+ console.log(`✅ GeminiAIService initialized with OpenRouter SDK (${this.config.model})`);
79
+ }
80
+
81
+ /**
82
+ * Call OpenRouter Chat Completions API using SDK
83
+ */
84
+ private async callOpenRouter(prompt: string): Promise<string> {
85
+ try {
86
+ const result = await this.openRouterClient.chat.send({
87
+ model: this.config.model,
88
+ messages: [
89
+ { role: 'user', content: prompt }
90
+ ],
91
+ temperature: this.config.temperature,
92
+ maxTokens: this.config.maxTokens,
93
+ stream: false
94
+ });
95
+
96
+ if (!result.choices || !result.choices[0] || !result.choices[0].message) {
97
+ throw new Error('Invalid response from OpenRouter API');
98
+ }
99
+
100
+ // Handle content which can be string or array
101
+ const content = result.choices[0].message.content;
102
+ if (typeof content === 'string') {
103
+ return content;
104
+ }
105
+ if (Array.isArray(content)) {
106
+ // Extract text from content items
107
+ return content
108
+ .filter((item: any) => item.type === 'text')
109
+ .map((item: any) => item.text || '')
110
+ .join('');
111
+ }
112
+ return '';
113
+ } catch (error) {
114
+ if (error instanceof Error) {
115
+ throw new Error(`OpenRouter API error: ${error.message}`);
116
+ }
117
+ throw error;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Extract entities and relationships from text using AI
123
+ */
124
+ async extractEntitiesAndRelationships(request: EntityExtractionRequest): Promise<EntityExtractionResponse> {
125
+ const startTime = Date.now();
126
+
127
+ try {
128
+ // Validate input: return empty result for empty/whitespace-only content
129
+ const trimmedContent = request.content?.trim();
130
+ if (!trimmedContent || trimmedContent.length === 0) {
131
+ return {
132
+ entities: [],
133
+ relationships: [],
134
+ processingTimeMs: Date.now() - startTime
135
+ };
136
+ }
137
+
138
+ const prompt = this.buildExtractionPrompt(request.content, request.context);
139
+ const text = await this.callOpenRouter(prompt);
140
+
141
+ const parsed = this.parseExtractionResponse(text);
142
+ const processingTimeMs = Date.now() - startTime;
143
+
144
+ return {
145
+ entities: parsed.entities,
146
+ relationships: parsed.relationships,
147
+ processingTimeMs
148
+ };
149
+
150
+ } catch (error) {
151
+ console.error('AI extraction failed:', error);
152
+
153
+ // Return empty result with processing time on error
154
+ return {
155
+ entities: [],
156
+ relationships: [],
157
+ processingTimeMs: Date.now() - startTime
158
+ };
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Extract entities and relationships from multiple texts in batch
164
+ */
165
+ async extractBatch(requests: EntityExtractionRequest[]): Promise<EntityExtractionResponse[]> {
166
+ const results: EntityExtractionResponse[] = [];
167
+
168
+ // Process in batches to avoid rate limiting
169
+ const batchSize = 3;
170
+ for (let i = 0; i < requests.length; i += batchSize) {
171
+ const batch = requests.slice(i, i + batchSize);
172
+ const batchPromises = batch.map(request => this.extractEntitiesAndRelationships(request));
173
+ const batchResults = await Promise.all(batchPromises);
174
+ results.push(...batchResults);
175
+
176
+ // Small delay between batches to respect rate limits
177
+ if (i + batchSize < requests.length) {
178
+ await this.delay(500);
179
+ }
180
+ }
181
+
182
+ return results;
183
+ }
184
+
185
+ /**
186
+ * Analyze text content for categorization and sentiment
187
+ */
188
+ async analyzeContent(content: string): Promise<{
189
+ categories: string[];
190
+ sentiment: 'positive' | 'negative' | 'neutral';
191
+ topics: string[];
192
+ confidence: number;
193
+ }> {
194
+ try {
195
+ const prompt = `
196
+ Analyze the following text and provide a JSON response with:
197
+ - "categories": array of relevant categories (max 3)
198
+ - "sentiment": "positive", "negative", or "neutral"
199
+ - "topics": array of main topics/themes (max 5)
200
+ - "confidence": overall analysis confidence (0.0-1.0)
201
+
202
+ TEXT: ${content}
203
+
204
+ JSON:`;
205
+
206
+ const text = await this.callOpenRouter(prompt);
207
+ return this.parseAnalysisResponse(text);
208
+
209
+ } catch (error) {
210
+ console.error('Content analysis failed:', error);
211
+ return {
212
+ categories: [],
213
+ sentiment: 'neutral',
214
+ topics: [],
215
+ confidence: 0
216
+ };
217
+ }
218
+ }
219
+
220
+ // ==================== PRIVATE METHODS ====================
221
+
222
+ private buildExtractionPrompt(content: string, context?: string): string {
223
+ const contextSection = context ? `\nCONTEXT: ${context}\n` : '';
224
+
225
+ return `
226
+ You are a knowledge graph extraction system for a Personal Data Wallet application. Your task is to extract meaningful entities and relationships from personal memories, notes, and user statements.
227
+
228
+ ## CRITICAL RULE: User Entity
229
+ ALWAYS include a "user" entity to represent the person who wrote this memory:
230
+ {
231
+ "id": "user",
232
+ "label": "User",
233
+ "type": "person",
234
+ "confidence": 1.0
235
+ }
236
+ This "user" entity should be the source or target of relationships describing personal preferences, attributes, or experiences.
237
+
238
+ ## Entity Types (Comprehensive List)
239
+
240
+ ### People & Social
241
+ - **person**: Individual people, including the user themselves
242
+ - Examples: "user", "john_doe", "my_mother", "boss"
243
+ - Properties: name, role, relationship_to_user
244
+
245
+ ### Organizations & Groups
246
+ - **organization**: Companies, institutions, teams, communities
247
+ - Examples: "google", "harvard_university", "local_gym"
248
+ - Properties: industry, size, location
249
+
250
+ ### Locations & Places
251
+ - **location**: Geographic places, addresses, venues
252
+ - Examples: "ho_chi_minh_city", "vietnam", "my_office", "central_park"
253
+ - Properties: type (city/country/venue), coordinates
254
+
255
+ ### Food & Dining
256
+ - **food**: Foods, dishes, cuisines, beverages, ingredients
257
+ - Examples: "spaghetti", "vietnamese_cuisine", "coffee", "chocolate"
258
+ - Properties: cuisine_type, meal_type, dietary_info
259
+ - **restaurant**: Eating establishments
260
+ - Examples: "starbucks", "local_pho_shop"
261
+ - Properties: cuisine, price_range
262
+
263
+ ### Preferences & Interests
264
+ - **preference**: General likes, dislikes, favorites
265
+ - Examples: "blue_color", "morning_routine", "minimalist_style"
266
+ - Properties: sentiment (positive/negative/neutral), intensity (1-10)
267
+ - **hobby**: Recreational activities, pastimes
268
+ - Examples: "playing_guitar", "photography", "hiking", "gaming"
269
+ - Properties: frequency, skill_level
270
+ - **interest**: Topics of curiosity or passion
271
+ - Examples: "artificial_intelligence", "history", "cooking"
272
+ - Properties: depth (casual/moderate/deep)
273
+
274
+ ### Skills & Abilities
275
+ - **skill**: Technical or soft skills, expertise areas
276
+ - Examples: "python_programming", "public_speaking", "cooking"
277
+ - Properties: proficiency (beginner/intermediate/expert)
278
+ - **language**: Languages known or being learned
279
+ - Examples: "english", "vietnamese", "japanese"
280
+ - Properties: proficiency, native (true/false)
281
+
282
+ ### Objects & Possessions
283
+ - **object**: Physical items, products, tools
284
+ - Examples: "macbook_pro", "my_car", "guitar"
285
+ - Properties: brand, model, acquisition_date
286
+ - **digital_product**: Software, apps, digital services
287
+ - Examples: "spotify", "notion", "chatgpt"
288
+ - Properties: category, usage_frequency
289
+
290
+ ### Time & Events
291
+ - **event**: Occasions, milestones, meetings
292
+ - Examples: "birthday_2024", "job_interview", "vacation_trip"
293
+ - Properties: date, duration, importance
294
+ - **routine**: Regular activities or habits
295
+ - Examples: "morning_workout", "weekly_meeting", "daily_meditation"
296
+ - Properties: frequency, time_of_day
297
+
298
+ ### Abstract & Conceptual
299
+ - **concept**: Ideas, topics, abstract things
300
+ - Examples: "work_life_balance", "productivity", "happiness"
301
+ - **goal**: Objectives, aspirations, plans
302
+ - Examples: "learn_japanese", "run_marathon", "save_money"
303
+ - Properties: deadline, priority, status
304
+ - **emotion**: Feelings, moods, emotional states
305
+ - Examples: "happiness", "stress", "excitement"
306
+ - Properties: intensity, trigger
307
+
308
+ ### Health & Wellness
309
+ - **health_condition**: Medical conditions, allergies
310
+ - Examples: "lactose_intolerance", "migraine", "allergy_to_peanuts"
311
+ - **medication**: Medicines, supplements
312
+ - Examples: "vitamin_d", "aspirin"
313
+ - **exercise**: Physical activities for health
314
+ - Examples: "running", "yoga", "weight_training"
315
+
316
+ ### Media & Entertainment
317
+ - **music**: Songs, artists, genres, albums
318
+ - Examples: "jazz_music", "beatles", "classical_piano"
319
+ - **movie**: Films, TV shows, documentaries
320
+ - Examples: "inception", "game_of_thrones"
321
+ - **book**: Books, authors, genres
322
+ - Examples: "atomic_habits", "fiction_genre"
323
+ - **game**: Video games, board games
324
+ - Examples: "chess", "minecraft"
325
+
326
+ ## Relationship Types (Comprehensive List)
327
+
328
+ ### Preference Relationships (source: usually "user")
329
+ - **loves**: Strong positive preference (intensity 9-10)
330
+ - **likes**: Moderate positive preference (intensity 6-8)
331
+ - **enjoys**: Positive experience with something
332
+ - **prefers**: Comparative preference
333
+ - **favorite**: Top choice in a category
334
+ - **interested_in**: Curiosity or engagement
335
+ - **dislikes**: Moderate negative preference
336
+ - **hates**: Strong negative preference (intensity 9-10)
337
+ - **avoids**: Intentionally stays away from
338
+ - **allergic_to**: Medical/physical aversion
339
+
340
+ ### Affiliation Relationships
341
+ - **works_at**: Employment relationship
342
+ - **studies_at**: Educational institution
343
+ - **member_of**: Group membership
344
+ - **belongs_to**: General affiliation
345
+ - **founded**: Created an organization
346
+ - **leads**: Leadership role
347
+
348
+ ### Location Relationships
349
+ - **lives_in**: Current residence
350
+ - **from**: Origin/hometown
351
+ - **located_in**: Physical location
352
+ - **visited**: Past travel
353
+ - **wants_to_visit**: Travel aspiration
354
+
355
+ ### Social Relationships
356
+ - **knows**: Acquaintance
357
+ - **friends_with**: Friendship
358
+ - **family_of**: Family relationship (specify: parent, sibling, child, spouse)
359
+ - **works_with**: Professional relationship
360
+ - **mentored_by**: Learning relationship
361
+
362
+ ### Skill & Knowledge Relationships
363
+ - **has_skill**: Possesses ability
364
+ - **expert_in**: High proficiency
365
+ - **learning**: Currently acquiring
366
+ - **wants_to_learn**: Aspiration to learn
367
+ - **teaches**: Instructing others
368
+ - **certified_in**: Formal qualification
369
+
370
+ ### Possession & Usage
371
+ - **owns**: Ownership
372
+ - **uses**: Regular usage
373
+ - **wants**: Desire to acquire
374
+ - **recommends**: Positive endorsement
375
+
376
+ ### Temporal Relationships
377
+ - **started_on**: Beginning date
378
+ - **ended_on**: Ending date
379
+ - **scheduled_for**: Future event
380
+ - **happens_during**: Temporal context
381
+
382
+ ### Causal & Descriptive
383
+ - **causes**: Causal relationship
384
+ - **related_to**: General association
385
+ - **part_of**: Component relationship
386
+ - **similar_to**: Similarity
387
+ - **opposite_of**: Contrast
388
+
389
+ ## Output Format
390
+
391
+ Return ONLY valid JSON with this structure:
392
+ {
393
+ "entities": [
394
+ {
395
+ "id": "snake_case_identifier",
396
+ "label": "Human Readable Name",
397
+ "type": "entity_type_from_list_above",
398
+ "confidence": 0.0-1.0,
399
+ "properties": { "optional": "attributes" }
400
+ }
401
+ ],
402
+ "relationships": [
403
+ {
404
+ "source": "source_entity_id",
405
+ "target": "target_entity_id",
406
+ "label": "relationship_type_from_list_above",
407
+ "confidence": 0.0-1.0,
408
+ "type": "optional_category"
409
+ }
410
+ ]
411
+ }
412
+
413
+ ## Examples
414
+
415
+ ### Example 1: Food Preference
416
+ Input: "i love spaghetti"
417
+ Output:
418
+ {
419
+ "entities": [
420
+ {"id": "user", "label": "User", "type": "person", "confidence": 1.0},
421
+ {"id": "spaghetti", "label": "Spaghetti", "type": "food", "confidence": 0.95, "properties": {"cuisine": "italian", "meal_type": "main_course"}}
422
+ ],
423
+ "relationships": [
424
+ {"source": "user", "target": "spaghetti", "label": "loves", "confidence": 0.95, "type": "preference"}
425
+ ]
426
+ }
427
+
428
+ ### Example 2: Multiple Preferences
429
+ Input: "i like hamburgers but hate vegetables"
430
+ Output:
431
+ {
432
+ "entities": [
433
+ {"id": "user", "label": "User", "type": "person", "confidence": 1.0},
434
+ {"id": "hamburgers", "label": "Hamburgers", "type": "food", "confidence": 0.95},
435
+ {"id": "vegetables", "label": "Vegetables", "type": "food", "confidence": 0.95}
436
+ ],
437
+ "relationships": [
438
+ {"source": "user", "target": "hamburgers", "label": "likes", "confidence": 0.9, "type": "preference"},
439
+ {"source": "user", "target": "vegetables", "label": "dislikes", "confidence": 0.9, "type": "preference"}
440
+ ]
441
+ }
442
+
443
+ ### Example 3: Work Information
444
+ Input: "i work at Google as a software engineer in Mountain View"
445
+ Output:
446
+ {
447
+ "entities": [
448
+ {"id": "user", "label": "User", "type": "person", "confidence": 1.0, "properties": {"role": "software_engineer"}},
449
+ {"id": "google", "label": "Google", "type": "organization", "confidence": 0.98, "properties": {"industry": "technology"}},
450
+ {"id": "software_engineer", "label": "Software Engineer", "type": "skill", "confidence": 0.9},
451
+ {"id": "mountain_view", "label": "Mountain View", "type": "location", "confidence": 0.95, "properties": {"type": "city"}}
452
+ ],
453
+ "relationships": [
454
+ {"source": "user", "target": "google", "label": "works_at", "confidence": 0.98, "type": "affiliation"},
455
+ {"source": "user", "target": "software_engineer", "label": "has_skill", "confidence": 0.9, "type": "skill"},
456
+ {"source": "google", "target": "mountain_view", "label": "located_in", "confidence": 0.85, "type": "location"}
457
+ ]
458
+ }
459
+
460
+ ### Example 4: Personal Life
461
+ Input: "my name is Aaron and I live in Ho Chi Minh City with my wife"
462
+ Output:
463
+ {
464
+ "entities": [
465
+ {"id": "user", "label": "Aaron", "type": "person", "confidence": 1.0, "properties": {"name": "Aaron"}},
466
+ {"id": "ho_chi_minh_city", "label": "Ho Chi Minh City", "type": "location", "confidence": 0.98, "properties": {"type": "city", "country": "Vietnam"}},
467
+ {"id": "wife", "label": "Wife", "type": "person", "confidence": 0.9, "properties": {"relationship": "spouse"}}
468
+ ],
469
+ "relationships": [
470
+ {"source": "user", "target": "ho_chi_minh_city", "label": "lives_in", "confidence": 0.98, "type": "location"},
471
+ {"source": "user", "target": "wife", "label": "family_of", "confidence": 0.95, "type": "social", "properties": {"relationship_type": "spouse"}}
472
+ ]
473
+ }
474
+
475
+ ### Example 5: Hobbies and Interests
476
+ Input: "i enjoy playing guitar and listening to jazz music on weekends"
477
+ Output:
478
+ {
479
+ "entities": [
480
+ {"id": "user", "label": "User", "type": "person", "confidence": 1.0},
481
+ {"id": "playing_guitar", "label": "Playing Guitar", "type": "hobby", "confidence": 0.95},
482
+ {"id": "guitar", "label": "Guitar", "type": "object", "confidence": 0.9, "properties": {"category": "musical_instrument"}},
483
+ {"id": "jazz_music", "label": "Jazz Music", "type": "music", "confidence": 0.95, "properties": {"genre": "jazz"}},
484
+ {"id": "weekends", "label": "Weekends", "type": "routine", "confidence": 0.8, "properties": {"frequency": "weekly"}}
485
+ ],
486
+ "relationships": [
487
+ {"source": "user", "target": "playing_guitar", "label": "enjoys", "confidence": 0.95, "type": "hobby"},
488
+ {"source": "playing_guitar", "target": "guitar", "label": "uses", "confidence": 0.9, "type": "activity"},
489
+ {"source": "user", "target": "jazz_music", "label": "enjoys", "confidence": 0.9, "type": "preference"},
490
+ {"source": "playing_guitar", "target": "weekends", "label": "happens_during", "confidence": 0.8, "type": "temporal"}
491
+ ]
492
+ }
493
+
494
+ ## Guidelines
495
+
496
+ 1. **Always include "user" entity** for personal statements (first-person: "I", "my", "me")
497
+ 2. **Extract implicit information**: "i'm a doctor" implies medical skill
498
+ 3. **Handle negations properly**: "don't like" = dislikes relationship
499
+ 4. **Capture intensity**: "love" vs "like" vs "enjoy" = different confidence/intensity
500
+ 5. **Include relevant properties**: cuisine type for food, location type for places
501
+ 6. **Create bidirectional relationships when applicable**: if A works_at B, B employs A
502
+ 7. **Minimum confidence threshold**: 0.5 for entities, 0.5 for relationships
503
+ 8. **Be comprehensive**: Extract ALL meaningful entities, even from short texts
504
+ 9. **Handle multilingual content**: Vietnamese, English, etc.
505
+ 10. **Infer entity types from context**: "spaghetti" = food, "Python" = skill/language based on context
506
+ ${contextSection}
507
+ ## TEXT TO ANALYZE:
508
+ ${content}
509
+
510
+ ## JSON OUTPUT:`;
511
+ }
512
+
513
+ private parseExtractionResponse(response: string): { entities: any[]; relationships: any[] } {
514
+ try {
515
+ // Clean up the response text (remove markdown formatting if present)
516
+ let cleanResponse = response.trim();
517
+ if (cleanResponse.startsWith('```json')) {
518
+ cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '');
519
+ } else if (cleanResponse.startsWith('```')) {
520
+ cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '');
521
+ }
522
+
523
+ const parsed = JSON.parse(cleanResponse);
524
+
525
+ if (!parsed.entities || !Array.isArray(parsed.entities)) {
526
+ throw new Error('Invalid entities format');
527
+ }
528
+ if (!parsed.relationships || !Array.isArray(parsed.relationships)) {
529
+ throw new Error('Invalid relationships format');
530
+ }
531
+
532
+ // Validate and sanitize entities
533
+ const entities = parsed.entities
534
+ .filter((e: any) => e.id && e.label && e.type)
535
+ .map((e: any) => ({
536
+ id: this.sanitizeId(e.id),
537
+ label: e.label.trim(),
538
+ type: e.type.toLowerCase(),
539
+ confidence: Math.max(0, Math.min(1, e.confidence || 0.7)),
540
+ properties: e.properties || {}
541
+ }));
542
+
543
+ // Create entity ID map for relationship validation
544
+ const entityIds = new Set(entities.map((e: any) => e.id));
545
+
546
+ // Validate and sanitize relationships
547
+ const relationships = parsed.relationships
548
+ .filter((r: any) => r.source && r.target && r.label)
549
+ .filter((r: any) => entityIds.has(this.sanitizeId(r.source)) && entityIds.has(this.sanitizeId(r.target)))
550
+ .map((r: any) => ({
551
+ source: this.sanitizeId(r.source),
552
+ target: this.sanitizeId(r.target),
553
+ label: r.label.trim(),
554
+ confidence: Math.max(0, Math.min(1, r.confidence || 0.7)),
555
+ type: r.type || 'general'
556
+ }));
557
+
558
+ return { entities, relationships };
559
+
560
+ } catch (error) {
561
+ // Only log detailed errors in development mode
562
+ if (process.env.NODE_ENV === 'development') {
563
+ console.error('Failed to parse AI response:', error);
564
+ console.error('Raw response:', response);
565
+ }
566
+ return { entities: [], relationships: [] };
567
+ }
568
+ }
569
+
570
+ private parseAnalysisResponse(response: string): any {
571
+ try {
572
+ let cleanResponse = response.trim();
573
+ if (cleanResponse.startsWith('```json')) {
574
+ cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '');
575
+ }
576
+
577
+ const parsed = JSON.parse(cleanResponse);
578
+
579
+ return {
580
+ categories: parsed.categories || [],
581
+ sentiment: parsed.sentiment || 'neutral',
582
+ topics: parsed.topics || [],
583
+ confidence: Math.max(0, Math.min(1, parsed.confidence || 0.5))
584
+ };
585
+
586
+ } catch (error) {
587
+ console.error('Failed to parse analysis response:', error);
588
+ return {
589
+ categories: [],
590
+ sentiment: 'neutral',
591
+ topics: [],
592
+ confidence: 0
593
+ };
594
+ }
595
+ }
596
+
597
+ private sanitizeId(id: string): string {
598
+ return id
599
+ .toLowerCase()
600
+ .replace(/[^a-z0-9_]/g, '_')
601
+ .replace(/_+/g, '_')
602
+ .replace(/^_|_$/g, '');
603
+ }
604
+
605
+ private delay(ms: number): Promise<void> {
606
+ return new Promise(resolve => setTimeout(resolve, ms));
607
+ }
608
+
609
+ /**
610
+ * Extract rich metadata from content for memory creation
611
+ * Returns importance (1-10), topic, and summary
612
+ */
613
+ async extractRichMetadata(content: string, categoryHint?: string): Promise<{
614
+ importance: number;
615
+ topic: string;
616
+ summary: string;
617
+ category: string;
618
+ }> {
619
+ try {
620
+ const prompt = `
621
+ Analyze the following text and extract rich metadata in JSON format:
622
+ - "importance": relevance/importance score from 1-10 (1=trivial, 10=critical)
623
+ - "topic": concise topic/title (max 100 chars)
624
+ - "summary": brief summary (max 200 chars)
625
+ - "category": best-fit category (personal, work, education, health, finance, travel, family, hobbies, goals, ideas)
626
+
627
+ Consider:
628
+ - Importance: How valuable is this information for future recall?
629
+ - Topic: What's the main subject or theme?
630
+ - Summary: Key points in 1-2 sentences
631
+ ${categoryHint ? `- Prefer category: ${categoryHint}` : ''}
632
+
633
+ TEXT: ${content}
634
+
635
+ JSON:`;
636
+
637
+ const text = await this.callOpenRouter(prompt);
638
+ return this.parseRichMetadataResponse(text, content, categoryHint);
639
+
640
+ } catch (error) {
641
+ console.error('Rich metadata extraction failed:', error);
642
+ return this.getFallbackMetadata(content, categoryHint);
643
+ }
644
+ }
645
+
646
+ /**
647
+ * Extract metadata for multiple contents in batch (with rate limiting)
648
+ */
649
+ async extractRichMetadataBatch(
650
+ contents: Array<{ content: string; category?: string }>
651
+ ): Promise<Array<{ importance: number; topic: string; summary: string; category: string }>> {
652
+ const results: Array<{ importance: number; topic: string; summary: string; category: string }> = [];
653
+
654
+ // Process in batches to avoid rate limiting
655
+ const batchSize = 3;
656
+ for (let i = 0; i < contents.length; i += batchSize) {
657
+ const batch = contents.slice(i, i + batchSize);
658
+ const batchPromises = batch.map(item =>
659
+ this.extractRichMetadata(item.content, item.category)
660
+ );
661
+ const batchResults = await Promise.all(batchPromises);
662
+ results.push(...batchResults);
663
+
664
+ // Small delay between batches to respect rate limits
665
+ if (i + batchSize < contents.length) {
666
+ await this.delay(500);
667
+ }
668
+ }
669
+
670
+ return results;
671
+ }
672
+
673
+ private parseRichMetadataResponse(response: string, content: string, categoryHint?: string): {
674
+ importance: number;
675
+ topic: string;
676
+ summary: string;
677
+ category: string;
678
+ } {
679
+ try {
680
+ let cleanResponse = response.trim();
681
+ if (cleanResponse.startsWith('```json')) {
682
+ cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '');
683
+ } else if (cleanResponse.startsWith('```')) {
684
+ cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '');
685
+ }
686
+
687
+ const parsed = JSON.parse(cleanResponse);
688
+
689
+ return {
690
+ importance: Math.max(1, Math.min(10, parsed.importance || 5)),
691
+ topic: (parsed.topic || this.extractTopicFallback(content)).substring(0, 100),
692
+ summary: (parsed.summary || content.substring(0, 200)).substring(0, 200),
693
+ category: parsed.category || categoryHint || 'personal'
694
+ };
695
+
696
+ } catch (error) {
697
+ console.error('Failed to parse rich metadata response:', error);
698
+ return this.getFallbackMetadata(content, categoryHint);
699
+ }
700
+ }
701
+
702
+ private getFallbackMetadata(content: string, categoryHint?: string): {
703
+ importance: number;
704
+ topic: string;
705
+ summary: string;
706
+ category: string;
707
+ } {
708
+ return {
709
+ importance: 5,
710
+ topic: this.extractTopicFallback(content),
711
+ summary: content.substring(0, 200) + (content.length > 200 ? '...' : ''),
712
+ category: categoryHint || 'personal'
713
+ };
714
+ }
715
+
716
+ private extractTopicFallback(text: string): string {
717
+ // Try to get first sentence
718
+ const firstSentence = text.match(/^[^.!?]+[.!?]/);
719
+ if (firstSentence) {
720
+ return firstSentence[0].trim().substring(0, 100);
721
+ }
722
+
723
+ // Fallback: first 50 characters
724
+ return text.substring(0, 50).trim() + (text.length > 50 ? '...' : '');
725
+ }
726
+
727
+ /**
728
+ * Check if the service is properly configured and can make API calls
729
+ */
730
+ async testConnection(): Promise<boolean> {
731
+ try {
732
+ const text = await this.callOpenRouter('Test connection. Respond with only "OK".');
733
+ return text.includes('OK');
734
+ } catch {
735
+ return false;
736
+ }
737
+ }
738
+
739
+ /**
740
+ * Get service configuration (without sensitive data)
741
+ */
742
+ getConfig(): Omit<Required<GeminiConfig>, 'apiKey'> & { apiKeyConfigured: boolean } {
743
+ return {
744
+ model: this.config.model,
745
+ temperature: this.config.temperature,
746
+ maxTokens: this.config.maxTokens,
747
+ timeout: this.config.timeout,
748
+ apiKeyConfigured: !!this.apiKey
749
+ };
750
+ }
751
+ }
752
+
753
+ export default GeminiAIService;