@benchcubed/today-cli 1.0.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 (210) hide show
  1. package/README.md +296 -0
  2. package/dist/ai/bedrock-client.d.ts +1 -0
  3. package/dist/ai/bedrock-client.d.ts.map +1 -0
  4. package/dist/ai/bedrock-client.js +2 -0
  5. package/dist/ai/bedrock-client.js.map +1 -0
  6. package/dist/ai/knowledge-processor.d.ts +1 -0
  7. package/dist/ai/knowledge-processor.d.ts.map +1 -0
  8. package/dist/ai/knowledge-processor.js +2 -0
  9. package/dist/ai/knowledge-processor.js.map +1 -0
  10. package/dist/cli.d.ts +3 -0
  11. package/dist/cli.d.ts.map +1 -0
  12. package/dist/cli.js +70 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/commands/ask.d.ts +3 -0
  15. package/dist/commands/ask.d.ts.map +1 -0
  16. package/dist/commands/ask.js +217 -0
  17. package/dist/commands/ask.js.map +1 -0
  18. package/dist/commands/audit.d.ts +3 -0
  19. package/dist/commands/audit.d.ts.map +1 -0
  20. package/dist/commands/audit.js +153 -0
  21. package/dist/commands/audit.js.map +1 -0
  22. package/dist/commands/auth.d.ts +3 -0
  23. package/dist/commands/auth.d.ts.map +1 -0
  24. package/dist/commands/auth.js +114 -0
  25. package/dist/commands/auth.js.map +1 -0
  26. package/dist/commands/config.d.ts +3 -0
  27. package/dist/commands/config.d.ts.map +1 -0
  28. package/dist/commands/config.js +72 -0
  29. package/dist/commands/config.js.map +1 -0
  30. package/dist/commands/embeddings.d.ts +3 -0
  31. package/dist/commands/embeddings.d.ts.map +1 -0
  32. package/dist/commands/embeddings.js +118 -0
  33. package/dist/commands/embeddings.js.map +1 -0
  34. package/dist/commands/experts.d.ts +3 -0
  35. package/dist/commands/experts.d.ts.map +1 -0
  36. package/dist/commands/experts.js +129 -0
  37. package/dist/commands/experts.js.map +1 -0
  38. package/dist/commands/export-import.d.ts +4 -0
  39. package/dist/commands/export-import.d.ts.map +1 -0
  40. package/dist/commands/export-import.js +285 -0
  41. package/dist/commands/export-import.js.map +1 -0
  42. package/dist/commands/git.d.ts +4 -0
  43. package/dist/commands/git.d.ts.map +1 -0
  44. package/dist/commands/git.js +274 -0
  45. package/dist/commands/git.js.map +1 -0
  46. package/dist/commands/impact.d.ts +3 -0
  47. package/dist/commands/impact.d.ts.map +1 -0
  48. package/dist/commands/impact.js +121 -0
  49. package/dist/commands/impact.js.map +1 -0
  50. package/dist/commands/insights.d.ts +3 -0
  51. package/dist/commands/insights.d.ts.map +1 -0
  52. package/dist/commands/insights.js +113 -0
  53. package/dist/commands/insights.js.map +1 -0
  54. package/dist/commands/learned.d.ts +3 -0
  55. package/dist/commands/learned.d.ts.map +1 -0
  56. package/dist/commands/learned.js +355 -0
  57. package/dist/commands/learned.js.map +1 -0
  58. package/dist/commands/onboard.d.ts +3 -0
  59. package/dist/commands/onboard.d.ts.map +1 -0
  60. package/dist/commands/onboard.js +141 -0
  61. package/dist/commands/onboard.js.map +1 -0
  62. package/dist/commands/recent.d.ts +3 -0
  63. package/dist/commands/recent.d.ts.map +1 -0
  64. package/dist/commands/recent.js +83 -0
  65. package/dist/commands/recent.js.map +1 -0
  66. package/dist/commands/secrets.d.ts +3 -0
  67. package/dist/commands/secrets.d.ts.map +1 -0
  68. package/dist/commands/secrets.js +203 -0
  69. package/dist/commands/secrets.js.map +1 -0
  70. package/dist/commands/stats.d.ts +3 -0
  71. package/dist/commands/stats.d.ts.map +1 -0
  72. package/dist/commands/stats.js +120 -0
  73. package/dist/commands/stats.js.map +1 -0
  74. package/dist/config/index.d.ts +12 -0
  75. package/dist/config/index.d.ts.map +1 -0
  76. package/dist/config/index.js +46 -0
  77. package/dist/config/index.js.map +1 -0
  78. package/dist/database/migrations.d.ts +13 -0
  79. package/dist/database/migrations.d.ts.map +1 -0
  80. package/dist/database/migrations.js +190 -0
  81. package/dist/database/migrations.js.map +1 -0
  82. package/dist/database/postgresql.d.ts +92 -0
  83. package/dist/database/postgresql.d.ts.map +1 -0
  84. package/dist/database/postgresql.js +382 -0
  85. package/dist/database/postgresql.js.map +1 -0
  86. package/dist/database/sqlite.d.ts +54 -0
  87. package/dist/database/sqlite.d.ts.map +1 -0
  88. package/dist/database/sqlite.js +337 -0
  89. package/dist/database/sqlite.js.map +1 -0
  90. package/dist/lambda/admin.d.ts +3 -0
  91. package/dist/lambda/admin.d.ts.map +1 -0
  92. package/dist/lambda/admin.js +818 -0
  93. package/dist/lambda/admin.js.map +1 -0
  94. package/dist/lambda/ai-services.d.ts +6 -0
  95. package/dist/lambda/ai-services.d.ts.map +1 -0
  96. package/dist/lambda/ai-services.js +472 -0
  97. package/dist/lambda/ai-services.js.map +1 -0
  98. package/dist/lambda/analytics.d.ts +3 -0
  99. package/dist/lambda/analytics.d.ts.map +1 -0
  100. package/dist/lambda/analytics.js +481 -0
  101. package/dist/lambda/analytics.js.map +1 -0
  102. package/dist/lambda/api-router.d.ts +3 -0
  103. package/dist/lambda/api-router.d.ts.map +1 -0
  104. package/dist/lambda/api-router.js +162 -0
  105. package/dist/lambda/api-router.js.map +1 -0
  106. package/dist/lambda/custom-topics.d.ts +3 -0
  107. package/dist/lambda/custom-topics.d.ts.map +1 -0
  108. package/dist/lambda/custom-topics.js +425 -0
  109. package/dist/lambda/custom-topics.js.map +1 -0
  110. package/dist/lambda/graph-builder.d.ts +3 -0
  111. package/dist/lambda/graph-builder.d.ts.map +1 -0
  112. package/dist/lambda/graph-builder.js +442 -0
  113. package/dist/lambda/graph-builder.js.map +1 -0
  114. package/dist/lambda/knowledge-ai.d.ts +3 -0
  115. package/dist/lambda/knowledge-ai.d.ts.map +1 -0
  116. package/dist/lambda/knowledge-ai.js +849 -0
  117. package/dist/lambda/knowledge-ai.js.map +1 -0
  118. package/dist/lambda/post-confirmation.d.ts +8 -0
  119. package/dist/lambda/post-confirmation.d.ts.map +1 -0
  120. package/dist/lambda/post-confirmation.js +92 -0
  121. package/dist/lambda/post-confirmation.js.map +1 -0
  122. package/dist/lambda/teams.d.ts +3 -0
  123. package/dist/lambda/teams.d.ts.map +1 -0
  124. package/dist/lambda/teams.js +568 -0
  125. package/dist/lambda/teams.js.map +1 -0
  126. package/dist/lib/export-helpers.d.ts +5 -0
  127. package/dist/lib/export-helpers.d.ts.map +1 -0
  128. package/dist/lib/export-helpers.js +137 -0
  129. package/dist/lib/export-helpers.js.map +1 -0
  130. package/dist/lib/import-helpers.d.ts +5 -0
  131. package/dist/lib/import-helpers.d.ts.map +1 -0
  132. package/dist/lib/import-helpers.js +185 -0
  133. package/dist/lib/import-helpers.js.map +1 -0
  134. package/dist/lib/insights-helpers.d.ts +123 -0
  135. package/dist/lib/insights-helpers.d.ts.map +1 -0
  136. package/dist/lib/insights-helpers.js +374 -0
  137. package/dist/lib/insights-helpers.js.map +1 -0
  138. package/dist/lib/search-helpers.d.ts +4 -0
  139. package/dist/lib/search-helpers.d.ts.map +1 -0
  140. package/dist/lib/search-helpers.js +124 -0
  141. package/dist/lib/search-helpers.js.map +1 -0
  142. package/dist/lib/secret-helpers.d.ts +2 -0
  143. package/dist/lib/secret-helpers.d.ts.map +1 -0
  144. package/dist/lib/secret-helpers.js +85 -0
  145. package/dist/lib/secret-helpers.js.map +1 -0
  146. package/dist/lib/stats-helpers.d.ts +41 -0
  147. package/dist/lib/stats-helpers.d.ts.map +1 -0
  148. package/dist/lib/stats-helpers.js +263 -0
  149. package/dist/lib/stats-helpers.js.map +1 -0
  150. package/dist/services/aws-api.d.ts +81 -0
  151. package/dist/services/aws-api.d.ts.map +1 -0
  152. package/dist/services/aws-api.js +388 -0
  153. package/dist/services/aws-api.js.map +1 -0
  154. package/dist/services/bedrock.d.ts +83 -0
  155. package/dist/services/bedrock.d.ts.map +1 -0
  156. package/dist/services/bedrock.js +434 -0
  157. package/dist/services/bedrock.js.map +1 -0
  158. package/dist/services/commit-learnings.d.ts +25 -0
  159. package/dist/services/commit-learnings.d.ts.map +1 -0
  160. package/dist/services/commit-learnings.js +180 -0
  161. package/dist/services/commit-learnings.js.map +1 -0
  162. package/dist/services/embedding-storage.d.ts +42 -0
  163. package/dist/services/embedding-storage.d.ts.map +1 -0
  164. package/dist/services/embedding-storage.js +124 -0
  165. package/dist/services/embedding-storage.js.map +1 -0
  166. package/dist/services/expert-engine.d.ts +21 -0
  167. package/dist/services/expert-engine.d.ts.map +1 -0
  168. package/dist/services/expert-engine.js +58 -0
  169. package/dist/services/expert-engine.js.map +1 -0
  170. package/dist/services/onboarding-accelerator.d.ts +118 -0
  171. package/dist/services/onboarding-accelerator.d.ts.map +1 -0
  172. package/dist/services/onboarding-accelerator.js +403 -0
  173. package/dist/services/onboarding-accelerator.js.map +1 -0
  174. package/dist/services/secret-detection.d.ts +46 -0
  175. package/dist/services/secret-detection.d.ts.map +1 -0
  176. package/dist/services/secret-detection.js +75 -0
  177. package/dist/services/secret-detection.js.map +1 -0
  178. package/dist/services/secret-manager-simple.d.ts +51 -0
  179. package/dist/services/secret-manager-simple.d.ts.map +1 -0
  180. package/dist/services/secret-manager-simple.js +119 -0
  181. package/dist/services/secret-manager-simple.js.map +1 -0
  182. package/dist/services/secret-manager.d.ts +150 -0
  183. package/dist/services/secret-manager.d.ts.map +1 -0
  184. package/dist/services/secret-manager.js +287 -0
  185. package/dist/services/secret-manager.js.map +1 -0
  186. package/dist/services/vector-embeddings.d.ts +70 -0
  187. package/dist/services/vector-embeddings.d.ts.map +1 -0
  188. package/dist/services/vector-embeddings.js +167 -0
  189. package/dist/services/vector-embeddings.js.map +1 -0
  190. package/dist/services/vector-search.d.ts +28 -0
  191. package/dist/services/vector-search.d.ts.map +1 -0
  192. package/dist/services/vector-search.js +192 -0
  193. package/dist/services/vector-search.js.map +1 -0
  194. package/dist/types/index.d.ts +86 -0
  195. package/dist/types/index.d.ts.map +1 -0
  196. package/dist/types/index.js +4 -0
  197. package/dist/types/index.js.map +1 -0
  198. package/dist/utils/formatting.d.ts +10 -0
  199. package/dist/utils/formatting.d.ts.map +1 -0
  200. package/dist/utils/formatting.js +82 -0
  201. package/dist/utils/formatting.js.map +1 -0
  202. package/dist/utils/git.d.ts +52 -0
  203. package/dist/utils/git.d.ts.map +1 -0
  204. package/dist/utils/git.js +182 -0
  205. package/dist/utils/git.js.map +1 -0
  206. package/dist/utils/validation.d.ts +17 -0
  207. package/dist/utils/validation.d.ts.map +1 -0
  208. package/dist/utils/validation.js +187 -0
  209. package/dist/utils/validation.js.map +1 -0
  210. package/package.json +81 -0
@@ -0,0 +1,849 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handler = void 0;
4
+ const client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
5
+ const client_s3_1 = require("@aws-sdk/client-s3");
6
+ const client_bedrock_runtime_1 = require("@aws-sdk/client-bedrock-runtime");
7
+ const client_cognito_identity_provider_1 = require("@aws-sdk/client-cognito-identity-provider");
8
+ const util_dynamodb_1 = require("@aws-sdk/util-dynamodb");
9
+ // Initialize AWS clients
10
+ const dynamoClient = new client_dynamodb_1.DynamoDBClient({});
11
+ const bedrockClient = new client_bedrock_runtime_1.BedrockRuntimeClient({});
12
+ const s3Client = new client_s3_1.S3Client({});
13
+ const cognitoClient = new client_cognito_identity_provider_1.CognitoIdentityProviderClient({});
14
+ const userNameCache = new Map();
15
+ const EMBEDDING_MODEL = 'amazon.titan-embed-text-v2:0';
16
+ const RELEVANCE_THRESHOLD = 0.3;
17
+ const MAX_SEARCH_RESULTS = 10;
18
+ const MIN_CONFIDENCE = 0.1;
19
+ const MAX_CONFIDENCE = 0.95;
20
+ const RECENCY_DECAY_DAYS = 90;
21
+ const MILLISECONDS_PER_DAY = 1000 * 60 * 60 * 24; // Milliseconds in a day
22
+ // Scoring weights for hybrid search (intentBoost is applied as additional bonus)
23
+ const SCORE_WEIGHTS = {
24
+ semantic: 0.5, // 50% weight for semantic similarity
25
+ keyword: 0.3, // 30% weight for keyword matching
26
+ topic: 0.1, // 10% weight for topic overlap
27
+ recency: 0.1, // 10% weight for recency (total = 1.0)
28
+ intentBoost: 0.1 // Additional boost applied when intent matches (not part of base scoring)
29
+ };
30
+ // Intent-based content matching keywords
31
+ const INTENT_KEYWORDS = {
32
+ 'debug': 'error',
33
+ 'how-to': 'step',
34
+ 'configure': 'config'
35
+ };
36
+ // Helper function to decode JWT token (without verification for simplicity)
37
+ function decodeJWT(token) {
38
+ try {
39
+ const parts = token.split('.');
40
+ if (parts.length !== 3)
41
+ return null;
42
+ const payload = Buffer.from(parts[1], 'base64').toString('utf8');
43
+ return JSON.parse(payload);
44
+ }
45
+ catch (error) {
46
+ console.error('Failed to decode JWT:', error);
47
+ return null;
48
+ }
49
+ }
50
+ // Fetch user name from Cognito with caching
51
+ async function getUserName(userId) {
52
+ // Check cache first
53
+ if (userNameCache.has(userId)) {
54
+ return userNameCache.get(userId);
55
+ }
56
+ try {
57
+ const response = await cognitoClient.send(new client_cognito_identity_provider_1.AdminGetUserCommand({
58
+ UserPoolId: process.env.USER_POOL_ID,
59
+ Username: userId,
60
+ }));
61
+ // Use the username from Cognito
62
+ const userName = response.Username || userId;
63
+ userNameCache.set(userId, userName);
64
+ return userName;
65
+ }
66
+ catch (error) {
67
+ console.warn(`Failed to fetch user name for ${userId}:`, error);
68
+ userNameCache.set(userId, userId);
69
+ return userId;
70
+ }
71
+ }
72
+ // Enrich entries with usernames
73
+ async function enrichEntriesWithUsernames(entries) {
74
+ return Promise.all(entries.map(async (entry) => {
75
+ const username = await getUserName(entry.authorId);
76
+ return {
77
+ ...entry,
78
+ username
79
+ };
80
+ }));
81
+ }
82
+ // Extract user info from Authorization header
83
+ function getUserFromAuth(event) {
84
+ const authHeader = event.headers?.Authorization || event.headers?.authorization;
85
+ if (!authHeader)
86
+ return null;
87
+ const token = authHeader.replace('Bearer ', '');
88
+ const decoded = decodeJWT(token);
89
+ if (!decoded || !decoded.sub)
90
+ return null;
91
+ return {
92
+ userId: decoded.sub,
93
+ teamId: decoded['custom:team_id'] || 'default'
94
+ };
95
+ }
96
+ // ========== VECTOR EMBEDDING HELPER FUNCTIONS ==========
97
+ /**
98
+ * Generate vector embedding using Amazon Bedrock Titan Embeddings
99
+ * @param text - Text to generate embedding for
100
+ * @returns Vector embedding (1024 dimensions for Titan v2)
101
+ */
102
+ async function generateEmbedding(text) {
103
+ try {
104
+ const preprocessedText = text
105
+ .trim()
106
+ .replace(/\s+/g, ' ')
107
+ .substring(0, 8000); // Limit to 8000 chars for cost optimization
108
+ const response = await bedrockClient.send(new client_bedrock_runtime_1.InvokeModelCommand({
109
+ modelId: EMBEDDING_MODEL,
110
+ contentType: 'application/json',
111
+ body: JSON.stringify({
112
+ inputText: preprocessedText,
113
+ dimensions: 1024 // Specify output dimensions for Titan v2
114
+ }),
115
+ }));
116
+ const responseBody = JSON.parse(new TextDecoder().decode(response.body));
117
+ // Validate response has embedding
118
+ if (!responseBody.embedding || !Array.isArray(responseBody.embedding)) {
119
+ console.warn('Invalid embedding response format:', responseBody);
120
+ return [];
121
+ }
122
+ return responseBody.embedding;
123
+ }
124
+ catch (error) {
125
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
126
+ console.warn('Failed to generate embedding:', errorMessage, error);
127
+ return []; // Return empty array on failure
128
+ }
129
+ }
130
+ /**
131
+ * Calculate cosine similarity between two vectors
132
+ * @param a - First vector
133
+ * @param b - Second vector
134
+ * @returns Similarity score (0 to 1)
135
+ */
136
+ function cosineSimilarity(a, b) {
137
+ // Validate inputs
138
+ if (!a || !b || !Array.isArray(a) || !Array.isArray(b)) {
139
+ return 0;
140
+ }
141
+ if (a.length !== b.length || a.length === 0) {
142
+ return 0;
143
+ }
144
+ const dotProduct = a.reduce((sum, val, i) => sum + val * b[i], 0);
145
+ const magnitudeA = Math.sqrt(a.reduce((sum, val) => sum + val * val, 0));
146
+ const magnitudeB = Math.sqrt(b.reduce((sum, val) => sum + val * val, 0));
147
+ if (magnitudeA === 0 || magnitudeB === 0) {
148
+ return 0;
149
+ }
150
+ return dotProduct / (magnitudeA * magnitudeB);
151
+ }
152
+ // ========== END HELPER FUNCTIONS ==========
153
+ /**
154
+ * Enhance user query using Claude Haiku to extract intent and expand terms
155
+ * @param question - Original user question
156
+ * @returns Enhanced query metadata
157
+ */
158
+ async function enhanceQuery(question) {
159
+ try {
160
+ const prompt = `Analyze this developer question and extract structured information:
161
+
162
+ Question: "${question}"
163
+
164
+ Provide a JSON response with:
165
+ 1. intent: Primary intent (one of: "how-to", "debug", "explain", "find-example", "configure")
166
+ 2. keywords: 3-5 key technical terms
167
+ 3. expandedTerms: 3-5 related or synonym terms
168
+ 4. topics: 2-4 technology topics
169
+
170
+ Example format:
171
+ {
172
+ "intent": "debug",
173
+ "keywords": ["authentication", "api", "errors"],
174
+ "expandedTerms": ["auth", "authorization", "access-denied", "401", "403"],
175
+ "topics": ["api-gateway", "lambda", "security"]
176
+ }
177
+
178
+ Respond ONLY with valid JSON, no other text.`;
179
+ const response = await bedrockClient.send(new client_bedrock_runtime_1.InvokeModelCommand({
180
+ modelId: 'anthropic.claude-3-haiku-20240307-v1:0',
181
+ contentType: 'application/json',
182
+ body: JSON.stringify({
183
+ anthropic_version: 'bedrock-2023-05-31',
184
+ max_tokens: 300,
185
+ messages: [
186
+ {
187
+ role: 'user',
188
+ content: prompt,
189
+ },
190
+ ],
191
+ }),
192
+ }));
193
+ const responseBody = JSON.parse(new TextDecoder().decode(response.body));
194
+ // Validate response structure
195
+ if (!responseBody.content || !Array.isArray(responseBody.content) || responseBody.content.length === 0) {
196
+ console.warn('Invalid query enhancement response format:', responseBody);
197
+ const words = question.toLowerCase().split(/\s+/).filter(w => w.length > 3);
198
+ return {
199
+ intent: 'general',
200
+ keywords: words.slice(0, 5),
201
+ expandedTerms: [],
202
+ topics: [],
203
+ };
204
+ }
205
+ // Parse AI response with error handling
206
+ let analysis;
207
+ try {
208
+ analysis = JSON.parse(responseBody.content[0].text);
209
+ }
210
+ catch (parseError) {
211
+ console.warn('Failed to parse AI query enhancement JSON:', parseError);
212
+ const words = question.toLowerCase().split(/\s+/).filter(w => w.length > 3);
213
+ return {
214
+ intent: 'general',
215
+ keywords: words.slice(0, 5),
216
+ expandedTerms: [],
217
+ topics: [],
218
+ };
219
+ }
220
+ return {
221
+ intent: analysis.intent || 'general',
222
+ keywords: analysis.keywords || [],
223
+ expandedTerms: analysis.expandedTerms || [],
224
+ topics: analysis.topics || [],
225
+ };
226
+ }
227
+ catch (error) {
228
+ console.warn('Query enhancement failed, using fallback:', error);
229
+ // Fallback to simple keyword extraction
230
+ const words = question.toLowerCase().split(/\s+/).filter(w => w.length > 3);
231
+ return {
232
+ intent: 'general',
233
+ keywords: words.slice(0, 5),
234
+ expandedTerms: [],
235
+ topics: [],
236
+ };
237
+ }
238
+ }
239
+ const handler = async (event) => {
240
+ const headers = {
241
+ 'Content-Type': 'application/json',
242
+ 'Access-Control-Allow-Origin': '*',
243
+ 'Access-Control-Allow-Headers': 'Content-Type,Authorization',
244
+ 'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',
245
+ };
246
+ try {
247
+ const { httpMethod, path, body, queryStringParameters } = event;
248
+ // Extract user info from Authorization header
249
+ const userInfo = getUserFromAuth(event);
250
+ if (!userInfo) {
251
+ return {
252
+ statusCode: 401,
253
+ headers,
254
+ body: JSON.stringify({ error: 'Unauthorized' }),
255
+ };
256
+ }
257
+ const { userId, teamId } = userInfo;
258
+ switch (httpMethod) {
259
+ case 'POST':
260
+ if (path.endsWith('/knowledge')) {
261
+ return await captureKnowledge(JSON.parse(body || '{}'), userId, teamId);
262
+ }
263
+ break;
264
+ case 'GET':
265
+ if (path.endsWith('/knowledge')) {
266
+ const query = queryStringParameters?.q;
267
+ if (query) {
268
+ return await processQuestion(query, userId, teamId);
269
+ }
270
+ else {
271
+ // List all knowledge entries
272
+ return await listKnowledgeEntries(userId, teamId);
273
+ }
274
+ }
275
+ if (path.endsWith('/experts')) {
276
+ const topic = queryStringParameters?.topic || '';
277
+ return await findExperts(topic, userId, teamId);
278
+ }
279
+ break;
280
+ default:
281
+ return {
282
+ statusCode: 405,
283
+ headers,
284
+ body: JSON.stringify({ error: 'Method not allowed' }),
285
+ };
286
+ }
287
+ return {
288
+ statusCode: 404,
289
+ headers,
290
+ body: JSON.stringify({ error: 'Not found' }),
291
+ };
292
+ }
293
+ catch (error) {
294
+ console.error('Lambda error:', error);
295
+ return {
296
+ statusCode: 500,
297
+ headers,
298
+ body: JSON.stringify({
299
+ error: 'Internal server error',
300
+ message: error instanceof Error ? error.message : 'Unknown error'
301
+ }),
302
+ };
303
+ }
304
+ };
305
+ exports.handler = handler;
306
+ /**
307
+ * Check entry content against default custom topics
308
+ * @param content - Entry content to check
309
+ * @param teamId - Team ID
310
+ * @returns Array of matching custom topic IDs
311
+ */
312
+ async function checkAgainstDefaultTopics(content, teamId) {
313
+ try {
314
+ // Get all default custom topics for the team
315
+ const response = await dynamoClient.send(new client_dynamodb_1.QueryCommand({
316
+ TableName: process.env.KNOWLEDGE_GRAPH_TABLE,
317
+ KeyConditionExpression: 'team_id = :teamId AND begins_with(relationship_id, :prefix)',
318
+ ExpressionAttributeValues: {
319
+ ':teamId': { S: teamId },
320
+ ':prefix': { S: 'custom-topic-' },
321
+ },
322
+ }));
323
+ if (!response.Items || response.Items.length === 0) {
324
+ return [];
325
+ }
326
+ const matchingTopics = [];
327
+ const contentLower = content.toLowerCase();
328
+ for (const item of response.Items) {
329
+ const unmarshalled = (0, util_dynamodb_1.unmarshall)(item);
330
+ // Only check default topics
331
+ if (unmarshalled.is_default !== true) {
332
+ continue;
333
+ }
334
+ const topicData = JSON.parse(unmarshalled.topic_data || '{}');
335
+ const topicName = topicData.name || '';
336
+ const topicDescription = topicData.description || '';
337
+ // Improved keyword matching with word boundary checks
338
+ const matchesName = topicName && new RegExp(`\\b${topicName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i').test(content);
339
+ const matchesDescription = topicDescription && new RegExp(`\\b${topicDescription.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i').test(content);
340
+ if (matchesName || matchesDescription) {
341
+ matchingTopics.push(topicData.id);
342
+ }
343
+ }
344
+ return matchingTopics;
345
+ }
346
+ catch (error) {
347
+ console.error('Error checking against default topics:', error);
348
+ return [];
349
+ }
350
+ }
351
+ async function captureKnowledge(entry, userId, teamId) {
352
+ const headers = {
353
+ 'Content-Type': 'application/json',
354
+ 'Access-Control-Allow-Origin': '*',
355
+ };
356
+ try {
357
+ // Generate unique ID
358
+ const entryId = `${teamId}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
359
+ // Parallelize AI enhancement, embedding generation, and default topic checking
360
+ console.log('Generating AI analysis, vector embedding, and checking default topics in parallel...');
361
+ const [enhancedEntry, embedding, matchingCustomTopics] = await Promise.all([
362
+ enhanceWithAI(entry.content || '', userId, teamId),
363
+ generateEmbedding(entry.content || ''),
364
+ checkAgainstDefaultTopics(entry.content || '', teamId)
365
+ ]);
366
+ console.log(`AI analysis complete. Embedding generated: ${embedding.length} dimensions. Matched ${matchingCustomTopics.length} default topics.`);
367
+ const learningEntry = {
368
+ id: entryId,
369
+ content: entry.content || '',
370
+ authorId: userId,
371
+ teamId,
372
+ createdAt: new Date().toISOString(),
373
+ aiTopics: enhancedEntry.extractedTopics,
374
+ customTopics: matchingCustomTopics, // Add matched custom topics
375
+ expertiseLevel: enhancedEntry.expertiseLevel,
376
+ impactScore: enhancedEntry.potentialImpact,
377
+ helpfulnessRating: 0,
378
+ timeSavedMinutes: 0,
379
+ viewCount: 0,
380
+ shareCount: 0,
381
+ questionsAnswered: 0,
382
+ onboardingRelevance: enhancedEntry.onboardingRelevance,
383
+ embedding, // Store embedding for semantic search
384
+ };
385
+ // Store in DynamoDB knowledge graph
386
+ const putItemParams = {
387
+ TableName: process.env.KNOWLEDGE_GRAPH_TABLE,
388
+ Item: {
389
+ team_id: { S: teamId },
390
+ relationship_id: { S: `entry-${entryId}` },
391
+ user_id: { S: userId },
392
+ entry_data: { S: JSON.stringify(learningEntry) },
393
+ created_at: { S: learningEntry.createdAt },
394
+ topics: { SS: learningEntry.aiTopics },
395
+ author_id: { S: userId },
396
+ expertise_score: { N: learningEntry.impactScore.toString() },
397
+ },
398
+ };
399
+ // Store embedding in S3 and save reference in DynamoDB
400
+ if (embedding && embedding.length > 0 && putItemParams.Item) {
401
+ // Validate embedding values are finite numbers
402
+ const validEmbedding = embedding.every(v => Number.isFinite(v));
403
+ if (validEmbedding) {
404
+ try {
405
+ const embeddingKey = `embeddings/${teamId}/${entryId}.json`;
406
+ const embeddingsBucket = process.env.EMBEDDINGS_BUCKET || `${process.env.PROJECT_NAME || 'today'}-embeddings`;
407
+ // Store embedding in S3 as compact JSON
408
+ await s3Client.send(new client_s3_1.PutObjectCommand({
409
+ Bucket: embeddingsBucket,
410
+ Key: embeddingKey,
411
+ Body: JSON.stringify(embedding),
412
+ ContentType: 'application/json',
413
+ ServerSideEncryption: 'AES256',
414
+ Metadata: {
415
+ entryId,
416
+ teamId,
417
+ dimensions: embedding.length.toString(),
418
+ createdAt: learningEntry.createdAt,
419
+ }
420
+ }));
421
+ // Store only the S3 key in DynamoDB (much smaller than 1024 numbers)
422
+ putItemParams.Item.embedding_s3_key = { S: embeddingKey };
423
+ console.log(`Embedding stored in S3: s3://${embeddingsBucket}/${embeddingKey}`);
424
+ }
425
+ catch (s3Error) {
426
+ console.error('Failed to store embedding in S3:', s3Error);
427
+ // Continue without embedding if S3 fails
428
+ }
429
+ }
430
+ else {
431
+ console.warn('Embedding contains invalid values (NaN or Infinity), skipping storage');
432
+ }
433
+ }
434
+ await dynamoClient.send(new client_dynamodb_1.PutItemCommand(putItemParams));
435
+ // Create relationships for AI-extracted topics
436
+ for (const topic of learningEntry.aiTopics) {
437
+ await dynamoClient.send(new client_dynamodb_1.PutItemCommand({
438
+ TableName: process.env.KNOWLEDGE_GRAPH_TABLE,
439
+ Item: {
440
+ team_id: { S: teamId },
441
+ relationship_id: { S: `topic-${topic}-${entryId}` },
442
+ topic: { S: topic },
443
+ entry_id: { S: entryId },
444
+ author_id: { S: userId },
445
+ expertise_score: { N: learningEntry.impactScore.toString() },
446
+ created_at: { S: learningEntry.createdAt },
447
+ },
448
+ }));
449
+ }
450
+ // Create relationships for custom topics
451
+ for (const customTopicId of matchingCustomTopics) {
452
+ await dynamoClient.send(new client_dynamodb_1.PutItemCommand({
453
+ TableName: process.env.KNOWLEDGE_GRAPH_TABLE,
454
+ Item: {
455
+ team_id: { S: teamId },
456
+ relationship_id: { S: `custom-topic-entry-${customTopicId}-${entryId}` },
457
+ topic_id: { S: customTopicId },
458
+ entry_id: { S: entryId },
459
+ created_at: { S: learningEntry.createdAt },
460
+ },
461
+ }));
462
+ }
463
+ return {
464
+ statusCode: 201,
465
+ headers,
466
+ body: JSON.stringify({
467
+ success: true,
468
+ entry: learningEntry,
469
+ message: 'Knowledge entry created successfully',
470
+ }),
471
+ };
472
+ }
473
+ catch (error) {
474
+ console.error('Error capturing knowledge:', error);
475
+ return {
476
+ statusCode: 500,
477
+ headers,
478
+ body: JSON.stringify({
479
+ error: 'Failed to capture knowledge',
480
+ message: error instanceof Error ? error.message : 'Unknown error',
481
+ }),
482
+ };
483
+ }
484
+ }
485
+ async function listKnowledgeEntries(userId, teamId) {
486
+ const headers = {
487
+ 'Content-Type': 'application/json',
488
+ 'Access-Control-Allow-Origin': '*',
489
+ };
490
+ try {
491
+ const queryParams = {
492
+ TableName: process.env.KNOWLEDGE_GRAPH_TABLE,
493
+ IndexName: 'team-user-index',
494
+ KeyConditionExpression: 'team_id = :teamId AND user_id = :userId',
495
+ FilterExpression: 'begins_with(relationship_id, :prefix)',
496
+ ExpressionAttributeValues: {
497
+ ':teamId': { S: teamId },
498
+ ':userId': { S: userId },
499
+ ':prefix': { S: 'entry-' },
500
+ },
501
+ };
502
+ const result = await dynamoClient.send(new client_dynamodb_1.QueryCommand(queryParams));
503
+ const entries = (result.Items || []).map((item) => {
504
+ // Parse entry_data JSON
505
+ const entryData = item.entry_data?.S ? JSON.parse(item.entry_data.S) : {};
506
+ return {
507
+ ...entryData,
508
+ // Include S3 key for lazy-loading embeddings (instead of embedding_vector)
509
+ embedding_s3_key: item.embedding_s3_key?.S,
510
+ // Keep backward compatibility with old entries that have embedding_vector
511
+ embedding_vector: item.embedding_vector,
512
+ };
513
+ });
514
+ // Enrich entries with usernames from Cognito
515
+ const enrichedEntries = await enrichEntriesWithUsernames(entries);
516
+ return {
517
+ statusCode: 200,
518
+ headers,
519
+ body: JSON.stringify({
520
+ success: true,
521
+ data: {
522
+ entries: enrichedEntries,
523
+ total: enrichedEntries.length,
524
+ },
525
+ }),
526
+ };
527
+ }
528
+ catch (error) {
529
+ console.error('Error listing knowledge entries:', error);
530
+ return {
531
+ statusCode: 500,
532
+ headers,
533
+ body: JSON.stringify({
534
+ error: 'Failed to list knowledge entries',
535
+ message: error instanceof Error ? error.message : 'Unknown error',
536
+ }),
537
+ };
538
+ }
539
+ }
540
+ async function processQuestion(question, _userId, _teamId) {
541
+ const headers = {
542
+ 'Content-Type': 'application/json',
543
+ 'Access-Control-Allow-Origin': '*',
544
+ };
545
+ try {
546
+ console.log(`Processing question: "${question}" for user: ${_userId}, team: ${_teamId}`);
547
+ // Query knowledge entries from team
548
+ const queryParams = {
549
+ TableName: process.env.KNOWLEDGE_GRAPH_TABLE,
550
+ KeyConditionExpression: 'team_id = :teamId AND begins_with(relationship_id, :prefix)',
551
+ ExpressionAttributeValues: {
552
+ ':teamId': { S: _teamId },
553
+ ':prefix': { S: 'entry-' },
554
+ },
555
+ Limit: 50, // Limit for cost optimization
556
+ };
557
+ console.log('DynamoDB Query params:', JSON.stringify(queryParams, null, 2));
558
+ const queryResult = await dynamoClient.send(new client_dynamodb_1.QueryCommand(queryParams));
559
+ console.log(`DynamoDB Query result: ${queryResult.Items?.length || 0} items found`);
560
+ const entries = [];
561
+ if (queryResult.Items) {
562
+ for (const item of queryResult.Items) {
563
+ console.log('Processing item:', JSON.stringify(item, null, 2));
564
+ if (item.entry_data?.S) {
565
+ try {
566
+ entries.push(JSON.parse(item.entry_data.S));
567
+ }
568
+ catch (e) {
569
+ console.warn('Failed to parse entry data:', e);
570
+ }
571
+ }
572
+ }
573
+ }
574
+ console.log(`Parsed ${entries.length} entries successfully`);
575
+ // Enrich entries with usernames from Cognito
576
+ const enrichedEntries = await enrichEntriesWithUsernames(entries);
577
+ // If no question provided, return all entries (for stats/impact commands)
578
+ if (!question || question.trim() === '') {
579
+ console.log('No question provided, returning all entries');
580
+ return {
581
+ statusCode: 200,
582
+ headers,
583
+ body: JSON.stringify({
584
+ success: true,
585
+ entries: enrichedEntries.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()),
586
+ }),
587
+ };
588
+ }
589
+ // Use AI to analyze question and match entries
590
+ const answer = await analyzeQuestionWithAI(question, enrichedEntries, _teamId);
591
+ // Enrich answer's relevant entries with usernames (in case they weren't already enriched)
592
+ if (answer.relevantEntries) {
593
+ answer.relevantEntries = await enrichEntriesWithUsernames(answer.relevantEntries);
594
+ }
595
+ return {
596
+ statusCode: 200,
597
+ headers,
598
+ body: JSON.stringify({
599
+ success: true,
600
+ answer,
601
+ totalEntries: enrichedEntries.length,
602
+ }),
603
+ };
604
+ }
605
+ catch (error) {
606
+ console.error('Error processing question:', error);
607
+ return {
608
+ statusCode: 500,
609
+ headers,
610
+ body: JSON.stringify({
611
+ error: 'Failed to process question',
612
+ message: error instanceof Error ? error.message : 'Unknown error',
613
+ details: error instanceof Error ? error.stack : 'No stack trace',
614
+ }),
615
+ };
616
+ }
617
+ }
618
+ async function findExperts(topic, _userId, _teamId) {
619
+ const headers = {
620
+ 'Content-Type': 'application/json',
621
+ 'Access-Control-Allow-Origin': '*',
622
+ };
623
+ try {
624
+ // Query by topic using GSI
625
+ const queryResult = await dynamoClient.send(new client_dynamodb_1.QueryCommand({
626
+ TableName: process.env.KNOWLEDGE_GRAPH_TABLE,
627
+ IndexName: 'topic-expertise-index',
628
+ KeyConditionExpression: 'topic = :topic',
629
+ ExpressionAttributeValues: {
630
+ ':topic': { S: topic },
631
+ },
632
+ ScanIndexForward: false, // Sort by expertise_score descending
633
+ Limit: 10,
634
+ }));
635
+ const experts = [];
636
+ const expertMap = new Map();
637
+ if (queryResult.Items) {
638
+ for (const item of queryResult.Items) {
639
+ const authorId = item.author_id?.S;
640
+ const entryId = item.entry_id?.S;
641
+ const score = parseFloat(item.expertise_score?.N || '0');
642
+ if (authorId && entryId) {
643
+ if (!expertMap.has(authorId)) {
644
+ expertMap.set(authorId, { score: 0, entries: [] });
645
+ }
646
+ const expert = expertMap.get(authorId);
647
+ expert.score += score;
648
+ expert.entries.push(entryId);
649
+ }
650
+ }
651
+ }
652
+ // Convert to expert matches - fetch actual usernames from Cognito
653
+ for (const [authorId, data] of expertMap.entries()) {
654
+ const userName = await getUserName(authorId);
655
+ experts.push({
656
+ userId: authorId,
657
+ name: userName,
658
+ relevantEntries: data.entries,
659
+ expertiseScore: data.score,
660
+ lastActive: new Date().toISOString(), // In real app, track actual activity
661
+ });
662
+ }
663
+ // Sort by expertise score
664
+ experts.sort((a, b) => b.expertiseScore - a.expertiseScore);
665
+ return {
666
+ statusCode: 200,
667
+ headers,
668
+ body: JSON.stringify({
669
+ success: true,
670
+ experts: experts.slice(0, 5), // Top 5 experts
671
+ topic,
672
+ }),
673
+ };
674
+ }
675
+ catch (error) {
676
+ console.error('Error finding experts:', error);
677
+ return {
678
+ statusCode: 500,
679
+ headers,
680
+ body: JSON.stringify({
681
+ error: 'Failed to find experts',
682
+ message: error instanceof Error ? error.message : 'Unknown error',
683
+ }),
684
+ };
685
+ }
686
+ }
687
+ async function enhanceWithAI(content, _userId, _teamId) {
688
+ try {
689
+ const prompt = `Analyze this developer learning entry and extract:
690
+ 1. Key topics/technologies (max 5, lowercase, no spaces)
691
+ 2. Expertise level (beginner/intermediate/expert)
692
+ 3. Potential impact score (1-10)
693
+ 4. Onboarding relevance (true/false)
694
+
695
+ Content: "${content}"
696
+
697
+ Respond in JSON format:
698
+ {
699
+ "topics": ["topic1", "topic2"],
700
+ "expertiseLevel": "intermediate",
701
+ "impactScore": 7,
702
+ "onboardingRelevance": true
703
+ }`;
704
+ const response = await bedrockClient.send(new client_bedrock_runtime_1.InvokeModelCommand({
705
+ modelId: 'anthropic.claude-3-haiku-20240307-v1:0',
706
+ contentType: 'application/json',
707
+ body: JSON.stringify({
708
+ anthropic_version: 'bedrock-2023-05-31',
709
+ max_tokens: 200,
710
+ messages: [
711
+ {
712
+ role: 'user',
713
+ content: prompt,
714
+ },
715
+ ],
716
+ }),
717
+ }));
718
+ const responseBody = JSON.parse(new TextDecoder().decode(response.body));
719
+ const aiResponse = JSON.parse(responseBody.content[0].text);
720
+ return {
721
+ extractedTopics: aiResponse.topics || ['general'],
722
+ expertiseLevel: aiResponse.expertiseLevel || 'intermediate',
723
+ potentialImpact: aiResponse.impactScore || 5,
724
+ onboardingRelevance: aiResponse.onboardingRelevance || false,
725
+ };
726
+ }
727
+ catch (error) {
728
+ console.warn('AI enhancement failed, using defaults:', error);
729
+ // Fallback to simple analysis
730
+ const topics = extractTopicsFromContent(content);
731
+ return {
732
+ extractedTopics: topics,
733
+ expertiseLevel: 'intermediate',
734
+ potentialImpact: 5,
735
+ onboardingRelevance: false,
736
+ };
737
+ }
738
+ }
739
+ async function analyzeQuestionWithAI(question, entries, _teamId) {
740
+ try {
741
+ console.log(`Analyzing question with semantic search: "${question}"`);
742
+ // Step 1: Enhance query using Claude Haiku for better understanding
743
+ const queryAnalysis = await enhanceQuery(question);
744
+ console.log(`Query intent: ${queryAnalysis.intent}, Keywords: ${queryAnalysis.keywords.join(', ')}`);
745
+ // Step 2: Generate embedding for the question
746
+ const questionEmbedding = await generateEmbedding(question);
747
+ console.log(`Question embedding generated: ${questionEmbedding.length} dimensions`);
748
+ const scoredEntries = entries.map(entry => {
749
+ // Semantic similarity (if embedding available)
750
+ let semanticScore = 0;
751
+ if (questionEmbedding.length > 0 && entry.embedding && entry.embedding.length > 0) {
752
+ semanticScore = cosineSimilarity(questionEmbedding, entry.embedding);
753
+ }
754
+ // Enhanced keyword matching score using expanded terms from query analysis
755
+ const contentLower = entry.content.toLowerCase();
756
+ const allSearchTerms = [
757
+ ...queryAnalysis.keywords,
758
+ ...queryAnalysis.expandedTerms,
759
+ ...question.toLowerCase().split(/\s+/).filter(w => w.length > 3)
760
+ ];
761
+ const matchedTerms = allSearchTerms.filter(term => contentLower.includes(term.toLowerCase()));
762
+ const keywordScore = allSearchTerms.length > 0 ? matchedTerms.length / allSearchTerms.length : 0;
763
+ // Topic overlap score (boosted if matches extracted topics from query)
764
+ let topicScore = 0;
765
+ const queryTopics = [...queryAnalysis.topics, ...queryAnalysis.keywords];
766
+ for (const entryTopic of entry.aiTopics) {
767
+ if (queryTopics.some(qt => entryTopic.toLowerCase().includes(qt.toLowerCase()) ||
768
+ qt.toLowerCase().includes(entryTopic.toLowerCase()))) {
769
+ topicScore = 1;
770
+ break;
771
+ }
772
+ }
773
+ // Recency score (normalize to 0-1 range)
774
+ const ageInDays = (Date.now() - new Date(entry.createdAt).getTime()) / MILLISECONDS_PER_DAY;
775
+ const recencyScore = Math.max(0, 1 - (ageInDays / RECENCY_DECAY_DAYS));
776
+ // Intent-based score adjustment
777
+ let intentBoost = 0;
778
+ const contentLowerForIntent = entry.content.toLowerCase();
779
+ const intentKeyword = INTENT_KEYWORDS[queryAnalysis.intent];
780
+ if (intentKeyword && contentLowerForIntent.includes(intentKeyword)) {
781
+ intentBoost = SCORE_WEIGHTS.intentBoost;
782
+ }
783
+ // Hybrid scoring using configured weights
784
+ const totalScore = (semanticScore * SCORE_WEIGHTS.semantic) +
785
+ (keywordScore * SCORE_WEIGHTS.keyword) +
786
+ (topicScore * SCORE_WEIGHTS.topic) +
787
+ (recencyScore * SCORE_WEIGHTS.recency) +
788
+ intentBoost;
789
+ return {
790
+ entry,
791
+ semanticScore,
792
+ keywordScore,
793
+ topicScore,
794
+ recencyScore,
795
+ totalScore
796
+ };
797
+ });
798
+ // Step 4: Filter and sort by total score
799
+ const relevantEntries = scoredEntries
800
+ .filter(item => item.totalScore >= RELEVANCE_THRESHOLD)
801
+ .sort((a, b) => b.totalScore - a.totalScore)
802
+ .slice(0, MAX_SEARCH_RESULTS);
803
+ console.log(`Found ${relevantEntries.length} relevant entries out of ${entries.length} total`);
804
+ // Step 5: Extract related topics from relevant entries
805
+ const relatedTopics = Array.from(new Set(relevantEntries.flatMap(item => item.entry.aiTopics))).slice(0, 5);
806
+ // Step 6: Calculate confidence based on top result score
807
+ const confidence = relevantEntries.length > 0 ?
808
+ Math.min(MAX_CONFIDENCE, relevantEntries[0].totalScore) : MIN_CONFIDENCE;
809
+ const topScore = relevantEntries.length > 0 ? relevantEntries[0].totalScore.toFixed(2) : 'N/A';
810
+ console.log(`Confidence: ${confidence.toFixed(2)}, Top score: ${topScore}, Intent: ${queryAnalysis.intent}`);
811
+ return {
812
+ relevantEntries: relevantEntries.map(item => item.entry),
813
+ confidence,
814
+ suggestedExperts: [], // Will be populated by separate expert query
815
+ relatedTopics,
816
+ timeSaved: relevantEntries.length * 5, // Estimate 5 minutes saved per relevant entry
817
+ };
818
+ }
819
+ catch (error) {
820
+ console.warn('Semantic search failed, using fallback:', error);
821
+ // Fallback to simple keyword matching
822
+ const questionLower = question.toLowerCase();
823
+ const relevantEntries = entries
824
+ .filter(entry => {
825
+ const contentLower = entry.content.toLowerCase();
826
+ return entry.aiTopics.some(topic => questionLower.includes(topic) || contentLower.includes(questionLower));
827
+ })
828
+ .sort((a, b) => b.impactScore - a.impactScore)
829
+ .slice(0, 10);
830
+ return {
831
+ relevantEntries,
832
+ confidence: relevantEntries.length > 0 ? 0.5 : 0.1,
833
+ suggestedExperts: [],
834
+ relatedTopics: Array.from(new Set(relevantEntries.flatMap(e => e.aiTopics))).slice(0, 5),
835
+ timeSaved: relevantEntries.length * 5,
836
+ };
837
+ }
838
+ }
839
+ function extractTopicsFromContent(content) {
840
+ const commonTech = [
841
+ 'javascript', 'typescript', 'python', 'java', 'react', 'node',
842
+ 'aws', 'docker', 'kubernetes', 'git', 'sql', 'api', 'rest',
843
+ 'graphql', 'mongodb', 'postgresql', 'redis', 'lambda', 'dynamodb', 'smartagent',
844
+ ];
845
+ const contentLower = content.toLowerCase();
846
+ const foundTopics = commonTech.filter(tech => contentLower.includes(tech));
847
+ return foundTopics.length > 0 ? foundTopics.slice(0, 3) : ['general'];
848
+ }
849
+ //# sourceMappingURL=knowledge-ai.js.map