@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.
- package/README.md +296 -0
- package/dist/ai/bedrock-client.d.ts +1 -0
- package/dist/ai/bedrock-client.d.ts.map +1 -0
- package/dist/ai/bedrock-client.js +2 -0
- package/dist/ai/bedrock-client.js.map +1 -0
- package/dist/ai/knowledge-processor.d.ts +1 -0
- package/dist/ai/knowledge-processor.d.ts.map +1 -0
- package/dist/ai/knowledge-processor.js +2 -0
- package/dist/ai/knowledge-processor.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +70 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/ask.d.ts +3 -0
- package/dist/commands/ask.d.ts.map +1 -0
- package/dist/commands/ask.js +217 -0
- package/dist/commands/ask.js.map +1 -0
- package/dist/commands/audit.d.ts +3 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js +153 -0
- package/dist/commands/audit.js.map +1 -0
- package/dist/commands/auth.d.ts +3 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +114 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +72 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/embeddings.d.ts +3 -0
- package/dist/commands/embeddings.d.ts.map +1 -0
- package/dist/commands/embeddings.js +118 -0
- package/dist/commands/embeddings.js.map +1 -0
- package/dist/commands/experts.d.ts +3 -0
- package/dist/commands/experts.d.ts.map +1 -0
- package/dist/commands/experts.js +129 -0
- package/dist/commands/experts.js.map +1 -0
- package/dist/commands/export-import.d.ts +4 -0
- package/dist/commands/export-import.d.ts.map +1 -0
- package/dist/commands/export-import.js +285 -0
- package/dist/commands/export-import.js.map +1 -0
- package/dist/commands/git.d.ts +4 -0
- package/dist/commands/git.d.ts.map +1 -0
- package/dist/commands/git.js +274 -0
- package/dist/commands/git.js.map +1 -0
- package/dist/commands/impact.d.ts +3 -0
- package/dist/commands/impact.d.ts.map +1 -0
- package/dist/commands/impact.js +121 -0
- package/dist/commands/impact.js.map +1 -0
- package/dist/commands/insights.d.ts +3 -0
- package/dist/commands/insights.d.ts.map +1 -0
- package/dist/commands/insights.js +113 -0
- package/dist/commands/insights.js.map +1 -0
- package/dist/commands/learned.d.ts +3 -0
- package/dist/commands/learned.d.ts.map +1 -0
- package/dist/commands/learned.js +355 -0
- package/dist/commands/learned.js.map +1 -0
- package/dist/commands/onboard.d.ts +3 -0
- package/dist/commands/onboard.d.ts.map +1 -0
- package/dist/commands/onboard.js +141 -0
- package/dist/commands/onboard.js.map +1 -0
- package/dist/commands/recent.d.ts +3 -0
- package/dist/commands/recent.d.ts.map +1 -0
- package/dist/commands/recent.js +83 -0
- package/dist/commands/recent.js.map +1 -0
- package/dist/commands/secrets.d.ts +3 -0
- package/dist/commands/secrets.d.ts.map +1 -0
- package/dist/commands/secrets.js +203 -0
- package/dist/commands/secrets.js.map +1 -0
- package/dist/commands/stats.d.ts +3 -0
- package/dist/commands/stats.d.ts.map +1 -0
- package/dist/commands/stats.js +120 -0
- package/dist/commands/stats.js.map +1 -0
- package/dist/config/index.d.ts +12 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +46 -0
- package/dist/config/index.js.map +1 -0
- package/dist/database/migrations.d.ts +13 -0
- package/dist/database/migrations.d.ts.map +1 -0
- package/dist/database/migrations.js +190 -0
- package/dist/database/migrations.js.map +1 -0
- package/dist/database/postgresql.d.ts +92 -0
- package/dist/database/postgresql.d.ts.map +1 -0
- package/dist/database/postgresql.js +382 -0
- package/dist/database/postgresql.js.map +1 -0
- package/dist/database/sqlite.d.ts +54 -0
- package/dist/database/sqlite.d.ts.map +1 -0
- package/dist/database/sqlite.js +337 -0
- package/dist/database/sqlite.js.map +1 -0
- package/dist/lambda/admin.d.ts +3 -0
- package/dist/lambda/admin.d.ts.map +1 -0
- package/dist/lambda/admin.js +818 -0
- package/dist/lambda/admin.js.map +1 -0
- package/dist/lambda/ai-services.d.ts +6 -0
- package/dist/lambda/ai-services.d.ts.map +1 -0
- package/dist/lambda/ai-services.js +472 -0
- package/dist/lambda/ai-services.js.map +1 -0
- package/dist/lambda/analytics.d.ts +3 -0
- package/dist/lambda/analytics.d.ts.map +1 -0
- package/dist/lambda/analytics.js +481 -0
- package/dist/lambda/analytics.js.map +1 -0
- package/dist/lambda/api-router.d.ts +3 -0
- package/dist/lambda/api-router.d.ts.map +1 -0
- package/dist/lambda/api-router.js +162 -0
- package/dist/lambda/api-router.js.map +1 -0
- package/dist/lambda/custom-topics.d.ts +3 -0
- package/dist/lambda/custom-topics.d.ts.map +1 -0
- package/dist/lambda/custom-topics.js +425 -0
- package/dist/lambda/custom-topics.js.map +1 -0
- package/dist/lambda/graph-builder.d.ts +3 -0
- package/dist/lambda/graph-builder.d.ts.map +1 -0
- package/dist/lambda/graph-builder.js +442 -0
- package/dist/lambda/graph-builder.js.map +1 -0
- package/dist/lambda/knowledge-ai.d.ts +3 -0
- package/dist/lambda/knowledge-ai.d.ts.map +1 -0
- package/dist/lambda/knowledge-ai.js +849 -0
- package/dist/lambda/knowledge-ai.js.map +1 -0
- package/dist/lambda/post-confirmation.d.ts +8 -0
- package/dist/lambda/post-confirmation.d.ts.map +1 -0
- package/dist/lambda/post-confirmation.js +92 -0
- package/dist/lambda/post-confirmation.js.map +1 -0
- package/dist/lambda/teams.d.ts +3 -0
- package/dist/lambda/teams.d.ts.map +1 -0
- package/dist/lambda/teams.js +568 -0
- package/dist/lambda/teams.js.map +1 -0
- package/dist/lib/export-helpers.d.ts +5 -0
- package/dist/lib/export-helpers.d.ts.map +1 -0
- package/dist/lib/export-helpers.js +137 -0
- package/dist/lib/export-helpers.js.map +1 -0
- package/dist/lib/import-helpers.d.ts +5 -0
- package/dist/lib/import-helpers.d.ts.map +1 -0
- package/dist/lib/import-helpers.js +185 -0
- package/dist/lib/import-helpers.js.map +1 -0
- package/dist/lib/insights-helpers.d.ts +123 -0
- package/dist/lib/insights-helpers.d.ts.map +1 -0
- package/dist/lib/insights-helpers.js +374 -0
- package/dist/lib/insights-helpers.js.map +1 -0
- package/dist/lib/search-helpers.d.ts +4 -0
- package/dist/lib/search-helpers.d.ts.map +1 -0
- package/dist/lib/search-helpers.js +124 -0
- package/dist/lib/search-helpers.js.map +1 -0
- package/dist/lib/secret-helpers.d.ts +2 -0
- package/dist/lib/secret-helpers.d.ts.map +1 -0
- package/dist/lib/secret-helpers.js +85 -0
- package/dist/lib/secret-helpers.js.map +1 -0
- package/dist/lib/stats-helpers.d.ts +41 -0
- package/dist/lib/stats-helpers.d.ts.map +1 -0
- package/dist/lib/stats-helpers.js +263 -0
- package/dist/lib/stats-helpers.js.map +1 -0
- package/dist/services/aws-api.d.ts +81 -0
- package/dist/services/aws-api.d.ts.map +1 -0
- package/dist/services/aws-api.js +388 -0
- package/dist/services/aws-api.js.map +1 -0
- package/dist/services/bedrock.d.ts +83 -0
- package/dist/services/bedrock.d.ts.map +1 -0
- package/dist/services/bedrock.js +434 -0
- package/dist/services/bedrock.js.map +1 -0
- package/dist/services/commit-learnings.d.ts +25 -0
- package/dist/services/commit-learnings.d.ts.map +1 -0
- package/dist/services/commit-learnings.js +180 -0
- package/dist/services/commit-learnings.js.map +1 -0
- package/dist/services/embedding-storage.d.ts +42 -0
- package/dist/services/embedding-storage.d.ts.map +1 -0
- package/dist/services/embedding-storage.js +124 -0
- package/dist/services/embedding-storage.js.map +1 -0
- package/dist/services/expert-engine.d.ts +21 -0
- package/dist/services/expert-engine.d.ts.map +1 -0
- package/dist/services/expert-engine.js +58 -0
- package/dist/services/expert-engine.js.map +1 -0
- package/dist/services/onboarding-accelerator.d.ts +118 -0
- package/dist/services/onboarding-accelerator.d.ts.map +1 -0
- package/dist/services/onboarding-accelerator.js +403 -0
- package/dist/services/onboarding-accelerator.js.map +1 -0
- package/dist/services/secret-detection.d.ts +46 -0
- package/dist/services/secret-detection.d.ts.map +1 -0
- package/dist/services/secret-detection.js +75 -0
- package/dist/services/secret-detection.js.map +1 -0
- package/dist/services/secret-manager-simple.d.ts +51 -0
- package/dist/services/secret-manager-simple.d.ts.map +1 -0
- package/dist/services/secret-manager-simple.js +119 -0
- package/dist/services/secret-manager-simple.js.map +1 -0
- package/dist/services/secret-manager.d.ts +150 -0
- package/dist/services/secret-manager.d.ts.map +1 -0
- package/dist/services/secret-manager.js +287 -0
- package/dist/services/secret-manager.js.map +1 -0
- package/dist/services/vector-embeddings.d.ts +70 -0
- package/dist/services/vector-embeddings.d.ts.map +1 -0
- package/dist/services/vector-embeddings.js +167 -0
- package/dist/services/vector-embeddings.js.map +1 -0
- package/dist/services/vector-search.d.ts +28 -0
- package/dist/services/vector-search.d.ts.map +1 -0
- package/dist/services/vector-search.js +192 -0
- package/dist/services/vector-search.js.map +1 -0
- package/dist/types/index.d.ts +86 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +4 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/formatting.d.ts +10 -0
- package/dist/utils/formatting.d.ts.map +1 -0
- package/dist/utils/formatting.js +82 -0
- package/dist/utils/formatting.js.map +1 -0
- package/dist/utils/git.d.ts +52 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +182 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/validation.d.ts +17 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +187 -0
- package/dist/utils/validation.js.map +1 -0
- 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
|