@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.
- package/ARCHITECTURE.md +547 -547
- package/BENCHMARKS.md +238 -238
- package/README.md +310 -181
- package/dist/ai-sdk/tools.d.ts +2 -2
- package/dist/ai-sdk/tools.js +2 -2
- package/dist/client/ClientMemoryManager.js +2 -2
- package/dist/client/ClientMemoryManager.js.map +1 -1
- package/dist/client/PersonalDataWallet.d.ts.map +1 -1
- package/dist/client/SimplePDWClient.d.ts +29 -1
- package/dist/client/SimplePDWClient.d.ts.map +1 -1
- package/dist/client/SimplePDWClient.js +45 -13
- package/dist/client/SimplePDWClient.js.map +1 -1
- package/dist/client/namespaces/EmbeddingsNamespace.d.ts +1 -1
- package/dist/client/namespaces/EmbeddingsNamespace.js +1 -1
- package/dist/client/namespaces/MemoryNamespace.d.ts +31 -0
- package/dist/client/namespaces/MemoryNamespace.d.ts.map +1 -1
- package/dist/client/namespaces/MemoryNamespace.js +272 -39
- package/dist/client/namespaces/MemoryNamespace.js.map +1 -1
- package/dist/client/namespaces/consolidated/AINamespace.d.ts +2 -2
- package/dist/client/namespaces/consolidated/AINamespace.js +2 -2
- package/dist/client/namespaces/consolidated/BlockchainNamespace.d.ts +12 -2
- package/dist/client/namespaces/consolidated/BlockchainNamespace.d.ts.map +1 -1
- package/dist/client/namespaces/consolidated/BlockchainNamespace.js +62 -4
- package/dist/client/namespaces/consolidated/BlockchainNamespace.js.map +1 -1
- package/dist/client/namespaces/consolidated/StorageNamespace.d.ts +67 -2
- package/dist/client/namespaces/consolidated/StorageNamespace.d.ts.map +1 -1
- package/dist/client/namespaces/consolidated/StorageNamespace.js +549 -16
- package/dist/client/namespaces/consolidated/StorageNamespace.js.map +1 -1
- package/dist/config/ConfigurationHelper.js +61 -61
- package/dist/config/defaults.js +2 -2
- package/dist/config/defaults.js.map +1 -1
- package/dist/graph/GraphService.js +21 -21
- package/dist/graph/GraphService.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/seal/EncryptionService.d.ts +9 -5
- package/dist/infrastructure/seal/EncryptionService.d.ts.map +1 -1
- package/dist/infrastructure/seal/EncryptionService.js +37 -15
- package/dist/infrastructure/seal/EncryptionService.js.map +1 -1
- package/dist/infrastructure/seal/SealService.d.ts +13 -5
- package/dist/infrastructure/seal/SealService.d.ts.map +1 -1
- package/dist/infrastructure/seal/SealService.js +36 -34
- package/dist/infrastructure/seal/SealService.js.map +1 -1
- package/dist/langchain/createPDWRAG.js +30 -30
- package/dist/retrieval/MemoryDecryptionPipeline.d.ts.map +1 -1
- package/dist/retrieval/MemoryDecryptionPipeline.js +2 -1
- package/dist/retrieval/MemoryDecryptionPipeline.js.map +1 -1
- package/dist/retrieval/MemoryRetrievalService.d.ts +31 -0
- package/dist/retrieval/MemoryRetrievalService.d.ts.map +1 -1
- package/dist/retrieval/MemoryRetrievalService.js +44 -4
- package/dist/retrieval/MemoryRetrievalService.js.map +1 -1
- package/dist/services/CapabilityService.d.ts.map +1 -1
- package/dist/services/CapabilityService.js +30 -14
- package/dist/services/CapabilityService.js.map +1 -1
- package/dist/services/CrossContextPermissionService.d.ts.map +1 -1
- package/dist/services/CrossContextPermissionService.js +9 -7
- package/dist/services/CrossContextPermissionService.js.map +1 -1
- package/dist/services/EmbeddingService.d.ts +28 -1
- package/dist/services/EmbeddingService.d.ts.map +1 -1
- package/dist/services/EmbeddingService.js +54 -0
- package/dist/services/EmbeddingService.js.map +1 -1
- package/dist/services/EncryptionService.d.ts.map +1 -1
- package/dist/services/EncryptionService.js +6 -5
- package/dist/services/EncryptionService.js.map +1 -1
- package/dist/services/GeminiAIService.js +309 -309
- package/dist/services/IndexManager.d.ts +5 -1
- package/dist/services/IndexManager.d.ts.map +1 -1
- package/dist/services/IndexManager.js +17 -40
- package/dist/services/IndexManager.js.map +1 -1
- package/dist/services/QueryService.js +1 -1
- package/dist/services/QueryService.js.map +1 -1
- package/dist/services/StorageService.d.ts +11 -0
- package/dist/services/StorageService.d.ts.map +1 -1
- package/dist/services/StorageService.js +73 -10
- package/dist/services/StorageService.js.map +1 -1
- package/dist/services/TransactionService.d.ts +20 -0
- package/dist/services/TransactionService.d.ts.map +1 -1
- package/dist/services/TransactionService.js +43 -0
- package/dist/services/TransactionService.js.map +1 -1
- package/dist/services/ViewService.js +2 -2
- package/dist/services/ViewService.js.map +1 -1
- package/dist/services/storage/QuiltBatchManager.d.ts +101 -1
- package/dist/services/storage/QuiltBatchManager.d.ts.map +1 -1
- package/dist/services/storage/QuiltBatchManager.js +410 -20
- package/dist/services/storage/QuiltBatchManager.js.map +1 -1
- package/dist/services/storage/index.d.ts +1 -1
- package/dist/services/storage/index.d.ts.map +1 -1
- package/dist/services/storage/index.js.map +1 -1
- package/dist/utils/LRUCache.d.ts +106 -0
- package/dist/utils/LRUCache.d.ts.map +1 -0
- package/dist/utils/LRUCache.js +281 -0
- package/dist/utils/LRUCache.js.map +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/memoryIndexOnChain.d.ts +212 -0
- package/dist/utils/memoryIndexOnChain.d.ts.map +1 -0
- package/dist/utils/memoryIndexOnChain.js +312 -0
- package/dist/utils/memoryIndexOnChain.js.map +1 -0
- package/dist/utils/rebuildIndexNode.d.ts +29 -0
- package/dist/utils/rebuildIndexNode.d.ts.map +1 -1
- package/dist/utils/rebuildIndexNode.js +366 -98
- package/dist/utils/rebuildIndexNode.js.map +1 -1
- package/dist/vector/HnswWasmService.d.ts +20 -5
- package/dist/vector/HnswWasmService.d.ts.map +1 -1
- package/dist/vector/HnswWasmService.js +73 -40
- package/dist/vector/HnswWasmService.js.map +1 -1
- package/dist/vector/IHnswService.d.ts +10 -1
- package/dist/vector/IHnswService.d.ts.map +1 -1
- package/dist/vector/IHnswService.js.map +1 -1
- package/dist/vector/NodeHnswService.d.ts +16 -0
- package/dist/vector/NodeHnswService.d.ts.map +1 -1
- package/dist/vector/NodeHnswService.js +84 -5
- package/dist/vector/NodeHnswService.js.map +1 -1
- package/dist/vector/createHnswService.d.ts +1 -1
- package/dist/vector/createHnswService.js +1 -1
- package/dist/vector/index.d.ts +1 -1
- package/dist/vector/index.js +1 -1
- package/package.json +157 -157
- package/src/access/PermissionService.ts +635 -635
- package/src/aggregation/AggregationService.ts +389 -389
- package/src/ai-sdk/PDWVectorStore.ts +715 -715
- package/src/ai-sdk/index.ts +65 -65
- package/src/ai-sdk/tools.ts +460 -460
- package/src/ai-sdk/types.ts +404 -404
- package/src/batch/BatchManager.ts +597 -597
- package/src/batch/BatchingService.ts +429 -429
- package/src/batch/MemoryProcessingCache.ts +492 -492
- package/src/batch/index.ts +30 -30
- package/src/browser.ts +200 -200
- package/src/client/ClientMemoryManager.ts +987 -987
- package/src/client/PersonalDataWallet.ts +345 -345
- package/src/client/SimplePDWClient.ts +1289 -1222
- package/src/client/factory.ts +154 -154
- package/src/client/namespaces/AnalyticsNamespace.ts +377 -377
- package/src/client/namespaces/BatchNamespace.ts +356 -356
- package/src/client/namespaces/CacheNamespace.ts +123 -123
- package/src/client/namespaces/CapabilityNamespace.ts +217 -217
- package/src/client/namespaces/ClassifyNamespace.ts +169 -169
- package/src/client/namespaces/ContextNamespace.ts +297 -297
- package/src/client/namespaces/EmbeddingsNamespace.ts +99 -99
- package/src/client/namespaces/EncryptionNamespace.ts +221 -221
- package/src/client/namespaces/GraphNamespace.ts +468 -468
- package/src/client/namespaces/IndexNamespace.ts +361 -361
- package/src/client/namespaces/MemoryNamespace.ts +1422 -1135
- package/src/client/namespaces/PermissionsNamespace.ts +254 -254
- package/src/client/namespaces/PipelineNamespace.ts +220 -220
- package/src/client/namespaces/SearchNamespace.ts +1049 -1049
- package/src/client/namespaces/StorageNamespace.ts +458 -458
- package/src/client/namespaces/TxNamespace.ts +260 -260
- package/src/client/namespaces/WalletNamespace.ts +243 -243
- package/src/client/namespaces/consolidated/AINamespace.ts +449 -449
- package/src/client/namespaces/consolidated/BlockchainNamespace.ts +607 -546
- package/src/client/namespaces/consolidated/SecurityNamespace.ts +648 -648
- package/src/client/namespaces/consolidated/StorageNamespace.ts +1141 -497
- package/src/client/namespaces/consolidated/index.ts +39 -39
- package/src/client/signers/KeypairSigner.ts +108 -108
- package/src/client/signers/UnifiedSigner.ts +110 -110
- package/src/client/signers/WalletAdapterSigner.ts +159 -159
- package/src/client/signers/index.ts +26 -26
- package/src/config/ConfigurationHelper.ts +412 -412
- package/src/config/defaults.ts +51 -51
- package/src/config/index.ts +8 -8
- package/src/config/validation.ts +70 -70
- package/src/core/index.ts +14 -14
- package/src/core/interfaces/IService.ts +307 -307
- package/src/core/interfaces/index.ts +8 -8
- package/src/core/types/capability.ts +297 -297
- package/src/core/types/index.ts +870 -870
- package/src/core/types/wallet.ts +270 -270
- package/src/core/types.ts +9 -9
- package/src/core/wallet.ts +222 -222
- package/src/embedding/index.ts +19 -19
- package/src/embedding/types.ts +357 -357
- package/src/errors/index.ts +602 -602
- package/src/errors/recovery.ts +461 -461
- package/src/errors/validation.ts +567 -567
- package/src/generated/pdw/capability.ts +319 -319
- package/src/graph/GraphService.ts +887 -887
- package/src/graph/KnowledgeGraphManager.ts +728 -728
- package/src/graph/index.ts +25 -25
- package/src/index.ts +498 -474
- package/src/infrastructure/index.ts +22 -22
- package/src/infrastructure/seal/EncryptionService.ts +628 -603
- package/src/infrastructure/seal/SealService.ts +613 -615
- package/src/infrastructure/seal/index.ts +9 -9
- package/src/infrastructure/sui/BlockchainManager.ts +627 -627
- package/src/infrastructure/sui/SuiService.ts +888 -888
- package/src/infrastructure/sui/index.ts +9 -9
- package/src/infrastructure/walrus/StorageManager.ts +604 -604
- package/src/infrastructure/walrus/WalrusStorageService.ts +612 -612
- package/src/infrastructure/walrus/index.ts +9 -9
- package/src/langchain/PDWEmbeddings.ts +145 -145
- package/src/langchain/PDWVectorStore.ts +456 -456
- package/src/langchain/createPDWRAG.ts +303 -303
- package/src/langchain/index.ts +47 -47
- package/src/permissions/ConsentRepository.browser.ts +249 -249
- package/src/permissions/ConsentRepository.ts +364 -364
- package/src/pipeline/MemoryPipeline.ts +862 -862
- package/src/pipeline/PipelineManager.ts +683 -683
- package/src/pipeline/index.ts +26 -26
- package/src/retrieval/AdvancedSearchService.ts +629 -629
- package/src/retrieval/MemoryAnalyticsService.ts +711 -711
- package/src/retrieval/MemoryDecryptionPipeline.ts +825 -824
- package/src/retrieval/MemoryRetrievalService.ts +904 -830
- package/src/retrieval/index.ts +42 -42
- package/src/services/BatchService.ts +352 -352
- package/src/services/CapabilityService.ts +464 -448
- package/src/services/ClassifierService.ts +465 -465
- package/src/services/CrossContextPermissionService.ts +486 -484
- package/src/services/EmbeddingService.ts +771 -706
- package/src/services/EncryptionService.ts +712 -711
- package/src/services/GeminiAIService.ts +753 -753
- package/src/services/IndexManager.ts +977 -1004
- package/src/services/MemoryIndexService.ts +1003 -1003
- package/src/services/MemoryService.ts +369 -369
- package/src/services/QueryService.ts +890 -890
- package/src/services/StorageService.ts +1182 -1111
- package/src/services/TransactionService.ts +838 -790
- package/src/services/VectorService.ts +462 -462
- package/src/services/ViewService.ts +484 -484
- package/src/services/index.ts +25 -25
- package/src/services/storage/BlobAttributesManager.ts +333 -333
- package/src/services/storage/KnowledgeGraphManager.ts +425 -425
- package/src/services/storage/MemorySearchManager.ts +387 -387
- package/src/services/storage/QuiltBatchManager.ts +1130 -660
- package/src/services/storage/WalrusMetadataManager.ts +268 -268
- package/src/services/storage/WalrusStorageManager.ts +287 -287
- package/src/services/storage/index.ts +57 -52
- package/src/types/index.ts +13 -13
- package/src/utils/LRUCache.ts +378 -0
- package/src/utils/index.ts +76 -68
- package/src/utils/memoryIndexOnChain.ts +507 -0
- package/src/utils/rebuildIndex.ts +290 -290
- package/src/utils/rebuildIndexNode.ts +771 -424
- package/src/vector/BrowserHnswIndexService.ts +758 -758
- package/src/vector/HnswWasmService.ts +731 -679
- package/src/vector/IHnswService.ts +233 -224
- package/src/vector/NodeHnswService.ts +833 -735
- package/src/vector/VectorManager.ts +478 -478
- package/src/vector/createHnswService.ts +135 -135
- package/src/vector/index.ts +56 -56
- package/src/wallet/ContextWalletService.ts +656 -656
- 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;
|