@amplitude/ai 0.2.1 → 0.3.1
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/.claude/commands/instrument-with-amplitude-ai.md +12 -0
- package/.cursor/skills/instrument-with-amplitude-ai/SKILL.md +4 -42
- package/AGENTS.md +86 -28
- package/README.md +190 -111
- package/amplitude-ai.md +463 -0
- package/bin/amplitude-ai-doctor.mjs +0 -0
- package/bin/amplitude-ai-instrument.mjs +0 -0
- package/bin/amplitude-ai-mcp.mjs +0 -0
- package/bin/amplitude-ai-register-catalog.mjs +0 -0
- package/bin/amplitude-ai-status.mjs +0 -0
- package/bin/amplitude-ai.mjs +14 -5
- package/data/agent_event_catalog.json +52 -2
- package/dist/bound-agent.d.ts +18 -14
- package/dist/bound-agent.d.ts.map +1 -1
- package/dist/bound-agent.js +4 -1
- package/dist/bound-agent.js.map +1 -1
- package/dist/cli/status.d.ts.map +1 -1
- package/dist/cli/status.js +31 -19
- package/dist/cli/status.js.map +1 -1
- package/dist/client.d.ts +14 -14
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +38 -0
- package/dist/client.js.map +1 -1
- package/dist/context.d.ts +5 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +4 -0
- package/dist/context.js.map +1 -1
- package/dist/core/constants.d.ts +3 -1
- package/dist/core/constants.d.ts.map +1 -1
- package/dist/core/constants.js +3 -1
- package/dist/core/constants.js.map +1 -1
- package/dist/core/tracking.d.ts +12 -2
- package/dist/core/tracking.d.ts.map +1 -1
- package/dist/core/tracking.js +13 -2
- package/dist/core/tracking.js.map +1 -1
- package/dist/decorators.d.ts +1 -1
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +4 -3
- package/dist/decorators.js.map +1 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.js +5 -2
- package/dist/mcp/contract.d.ts +4 -0
- package/dist/mcp/contract.d.ts.map +1 -1
- package/dist/mcp/contract.js +6 -2
- package/dist/mcp/contract.js.map +1 -1
- package/dist/mcp/generate-verify-test.d.ts +7 -0
- package/dist/mcp/generate-verify-test.d.ts.map +1 -0
- package/dist/mcp/generate-verify-test.js +33 -0
- package/dist/mcp/generate-verify-test.js.map +1 -0
- package/dist/mcp/index.d.ts +2 -1
- package/dist/mcp/index.js +2 -1
- package/dist/mcp/instrument-file.d.ts +14 -0
- package/dist/mcp/instrument-file.d.ts.map +1 -0
- package/dist/mcp/instrument-file.js +136 -0
- package/dist/mcp/instrument-file.js.map +1 -0
- package/dist/mcp/scan-project.d.ts +52 -0
- package/dist/mcp/scan-project.d.ts.map +1 -0
- package/dist/mcp/scan-project.js +309 -0
- package/dist/mcp/scan-project.js.map +1 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +79 -4
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/validate-file.d.ts +4 -0
- package/dist/mcp/validate-file.d.ts.map +1 -1
- package/dist/mcp/validate-file.js +559 -11
- package/dist/mcp/validate-file.js.map +1 -1
- package/dist/middleware.js +2 -1
- package/dist/middleware.js.map +1 -1
- package/dist/node_modules/.pnpm/acorn-typescript@1.4.13_acorn@8.16.0/node_modules/acorn-typescript/lib/index.js +2389 -0
- package/dist/node_modules/.pnpm/acorn-typescript@1.4.13_acorn@8.16.0/node_modules/acorn-typescript/lib/index.js.map +1 -0
- package/dist/node_modules/.pnpm/acorn@8.16.0/node_modules/acorn/dist/acorn.js +5128 -0
- package/dist/node_modules/.pnpm/acorn@8.16.0/node_modules/acorn/dist/acorn.js.map +1 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js +1 -1
- package/dist/propagation.d.ts.map +1 -1
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +1 -0
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/base.d.ts +2 -1
- package/dist/providers/base.d.ts.map +1 -1
- package/dist/providers/base.js +4 -0
- package/dist/providers/base.js.map +1 -1
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/mistral.d.ts.map +1 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +2 -0
- package/dist/providers/openai.js.map +1 -1
- package/dist/serverless.d.ts +19 -0
- package/dist/serverless.d.ts.map +1 -0
- package/dist/serverless.js +35 -0
- package/dist/serverless.js.map +1 -0
- package/dist/session.d.ts +24 -8
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +20 -1
- package/dist/session.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/llms-full.txt +353 -69
- package/llms.txt +6 -2
- package/mcp.schema.json +7 -3
- package/package.json +10 -5
- package/bin/amplitude-ai-init.mjs +0 -27
- package/dist/cli/init.d.ts +0 -14
- package/dist/cli/init.d.ts.map +0 -1
- package/dist/cli/init.js +0 -40
- package/dist/cli/init.js.map +0 -1
|
@@ -1,5 +1,451 @@
|
|
|
1
|
+
import { require_acorn } from "../node_modules/.pnpm/acorn@8.16.0/node_modules/acorn/dist/acorn.js";
|
|
2
|
+
import { require_lib } from "../node_modules/.pnpm/acorn-typescript@1.4.13_acorn@8.16.0/node_modules/acorn-typescript/lib/index.js";
|
|
3
|
+
|
|
1
4
|
//#region src/mcp/validate-file.ts
|
|
2
|
-
const
|
|
5
|
+
const LLM_METHOD_CHAINS = [
|
|
6
|
+
{
|
|
7
|
+
chain: [
|
|
8
|
+
"chat",
|
|
9
|
+
"completions",
|
|
10
|
+
"create"
|
|
11
|
+
],
|
|
12
|
+
provider: "openai",
|
|
13
|
+
api: "chat.completions.create"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
chain: [
|
|
17
|
+
"chat",
|
|
18
|
+
"completions",
|
|
19
|
+
"parse"
|
|
20
|
+
],
|
|
21
|
+
provider: "openai",
|
|
22
|
+
api: "chat.completions.parse"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
chain: ["responses", "create"],
|
|
26
|
+
provider: "openai",
|
|
27
|
+
api: "responses.create"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
chain: ["messages", "create"],
|
|
31
|
+
provider: "anthropic",
|
|
32
|
+
api: "messages.create"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
chain: ["generateContent"],
|
|
36
|
+
provider: "gemini",
|
|
37
|
+
api: "generateContent",
|
|
38
|
+
singleMethodHints: [
|
|
39
|
+
"gemini",
|
|
40
|
+
"google",
|
|
41
|
+
"generative",
|
|
42
|
+
"genai"
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
chain: ["converse"],
|
|
47
|
+
provider: "bedrock",
|
|
48
|
+
api: "converse",
|
|
49
|
+
singleMethodHints: [
|
|
50
|
+
"bedrock",
|
|
51
|
+
"boto",
|
|
52
|
+
"runtime"
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
chain: ["chat", "complete"],
|
|
57
|
+
provider: "mistral",
|
|
58
|
+
api: "chat.complete"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
chain: ["getChatCompletions"],
|
|
62
|
+
provider: "azure-openai",
|
|
63
|
+
api: "getChatCompletions",
|
|
64
|
+
singleMethodHints: ["azure", "openai"]
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
chain: ["chat"],
|
|
68
|
+
provider: "cohere",
|
|
69
|
+
api: "chat",
|
|
70
|
+
singleMethodHints: ["cohere", "CohereClient"]
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
chain: ["generate"],
|
|
74
|
+
provider: "cohere",
|
|
75
|
+
api: "generate",
|
|
76
|
+
singleMethodHints: ["cohere", "CohereClient"]
|
|
77
|
+
}
|
|
78
|
+
];
|
|
79
|
+
const VERCEL_AI_SDK_FUNCTIONS = new Set([
|
|
80
|
+
"streamText",
|
|
81
|
+
"generateText",
|
|
82
|
+
"streamObject",
|
|
83
|
+
"generateObject"
|
|
84
|
+
]);
|
|
85
|
+
const JS_KEYWORDS = new Set([
|
|
86
|
+
"if",
|
|
87
|
+
"else",
|
|
88
|
+
"for",
|
|
89
|
+
"while",
|
|
90
|
+
"do",
|
|
91
|
+
"switch",
|
|
92
|
+
"case",
|
|
93
|
+
"try",
|
|
94
|
+
"catch",
|
|
95
|
+
"finally",
|
|
96
|
+
"return",
|
|
97
|
+
"throw",
|
|
98
|
+
"new",
|
|
99
|
+
"delete",
|
|
100
|
+
"typeof",
|
|
101
|
+
"void",
|
|
102
|
+
"in",
|
|
103
|
+
"of",
|
|
104
|
+
"with",
|
|
105
|
+
"class",
|
|
106
|
+
"extends",
|
|
107
|
+
"super",
|
|
108
|
+
"import",
|
|
109
|
+
"export",
|
|
110
|
+
"default",
|
|
111
|
+
"break",
|
|
112
|
+
"continue",
|
|
113
|
+
"debugger",
|
|
114
|
+
"yield",
|
|
115
|
+
"await"
|
|
116
|
+
]);
|
|
117
|
+
const PROVIDER_CONSTRUCTOR_NAMES = new Set([
|
|
118
|
+
"OpenAI",
|
|
119
|
+
"Anthropic",
|
|
120
|
+
"Gemini",
|
|
121
|
+
"AzureOpenAI",
|
|
122
|
+
"Bedrock",
|
|
123
|
+
"Mistral",
|
|
124
|
+
"GoogleGenerativeAI",
|
|
125
|
+
"GoogleGenAI",
|
|
126
|
+
"CohereClient"
|
|
127
|
+
]);
|
|
128
|
+
function extractMethodChain(node) {
|
|
129
|
+
const parts = [];
|
|
130
|
+
let current = node;
|
|
131
|
+
while (current.type === "MemberExpression") {
|
|
132
|
+
const mem = current;
|
|
133
|
+
if (!mem.computed && mem.property.type === "Identifier") parts.push(mem.property.name);
|
|
134
|
+
current = mem.object;
|
|
135
|
+
}
|
|
136
|
+
parts.reverse();
|
|
137
|
+
return {
|
|
138
|
+
chain: parts,
|
|
139
|
+
receiver: current.type === "Identifier" ? current.name : null
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function chainEndsWith(chain, pattern) {
|
|
143
|
+
if (chain.length < pattern.length) return false;
|
|
144
|
+
const offset = chain.length - pattern.length;
|
|
145
|
+
return pattern.every((p, i) => chain[offset + i] === p);
|
|
146
|
+
}
|
|
147
|
+
function hasSingleMethodHint(chain, receiver, hints) {
|
|
148
|
+
const tokens = [...chain.slice(0, -1)];
|
|
149
|
+
if (receiver) tokens.push(receiver);
|
|
150
|
+
return tokens.some((t) => hints.some((h) => t.toLowerCase().includes(h.toLowerCase())));
|
|
151
|
+
}
|
|
152
|
+
function getPropertyValue(obj, key) {
|
|
153
|
+
for (const prop of obj.properties) {
|
|
154
|
+
if (prop.type !== "Property") continue;
|
|
155
|
+
const p = prop;
|
|
156
|
+
if (p.key.type === "Identifier" && p.key.name === key) return p.value;
|
|
157
|
+
if (p.key.type === "Literal" && p.key.value === key) return p.value;
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
function hasKeywordArg(args, keyword) {
|
|
162
|
+
for (const arg of args) if (arg.type === "ObjectExpression") {
|
|
163
|
+
if (getPropertyValue(arg, keyword)) return true;
|
|
164
|
+
}
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
function extractCodeContext(sourceLines, lineNum, radius = 4) {
|
|
168
|
+
const idx = lineNum - 1;
|
|
169
|
+
const start = Math.max(0, idx - radius);
|
|
170
|
+
const end = Math.min(sourceLines.length - 1, idx + radius);
|
|
171
|
+
return sourceLines.slice(start, end + 1).map((l, i) => {
|
|
172
|
+
const ln = start + i + 1;
|
|
173
|
+
return `${ln === lineNum ? ">>>" : " "} ${ln}: ${l}`;
|
|
174
|
+
}).join("\n");
|
|
175
|
+
}
|
|
176
|
+
function walkAST(node, callback, ancestors = []) {
|
|
177
|
+
callback(node, ancestors);
|
|
178
|
+
const next = [...ancestors, node];
|
|
179
|
+
for (const key of Object.keys(node)) {
|
|
180
|
+
if (key === "type" || key === "start" || key === "end" || key === "loc") continue;
|
|
181
|
+
const val = node[key];
|
|
182
|
+
if (val && typeof val === "object") {
|
|
183
|
+
if (Array.isArray(val)) {
|
|
184
|
+
for (const item of val) if (item && typeof item === "object" && "type" in item) walkAST(item, callback, next);
|
|
185
|
+
} else if ("type" in val) walkAST(val, callback, next);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function findContainingFunctionAST(ancestors) {
|
|
190
|
+
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
191
|
+
const node = ancestors[i];
|
|
192
|
+
if (!node) continue;
|
|
193
|
+
if (node.type === "FunctionDeclaration") {
|
|
194
|
+
const name = node.id?.name;
|
|
195
|
+
if (name && !JS_KEYWORDS.has(name)) return name;
|
|
196
|
+
}
|
|
197
|
+
if (node.type === "FunctionExpression") {
|
|
198
|
+
const fn = node;
|
|
199
|
+
if (fn.id?.name && !JS_KEYWORDS.has(fn.id.name)) return fn.id.name;
|
|
200
|
+
}
|
|
201
|
+
if (node.type === "VariableDeclarator") {
|
|
202
|
+
const decl = node;
|
|
203
|
+
if (decl.id.type === "Identifier" && decl.init && (decl.init.type === "ArrowFunctionExpression" || decl.init.type === "FunctionExpression")) {
|
|
204
|
+
const name = decl.id.name;
|
|
205
|
+
if (!JS_KEYWORDS.has(name)) return name;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (node.type === "MethodDefinition" || node.type === "PropertyDefinition") {
|
|
209
|
+
const key = node.key;
|
|
210
|
+
if (key?.type === "Identifier") {
|
|
211
|
+
const name = key.name;
|
|
212
|
+
if (!JS_KEYWORDS.has(name)) return name;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
function analyzeWithAST(source, sourceLines) {
|
|
219
|
+
let parse = null;
|
|
220
|
+
try {
|
|
221
|
+
const acorn = require_acorn();
|
|
222
|
+
const tsModule = require_lib();
|
|
223
|
+
const tsPlugin = typeof tsModule === "function" ? tsModule : tsModule.tsPlugin ?? tsModule.default;
|
|
224
|
+
if (typeof tsPlugin !== "function") return null;
|
|
225
|
+
parse = (src, opts) => acorn.Parser.extend(tsPlugin()).parse(src, opts);
|
|
226
|
+
} catch {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
let tree;
|
|
230
|
+
try {
|
|
231
|
+
if (!parse) return null;
|
|
232
|
+
tree = parse(source, {
|
|
233
|
+
sourceType: "module",
|
|
234
|
+
ecmaVersion: "latest",
|
|
235
|
+
locations: true,
|
|
236
|
+
allowImportExportEverywhere: true,
|
|
237
|
+
allowReturnOutsideFunction: true
|
|
238
|
+
});
|
|
239
|
+
} catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
const root = tree;
|
|
243
|
+
const hasPatch = /\bpatch\s*\(\s*\{/.test(source);
|
|
244
|
+
const hasSessionContext = /\.session\s*\(/.test(source) || /\bsession\.run\s*\(/.test(source) || /\.runAs\s*\(/.test(source) || /\bcreateAmplitudeAIMiddleware\s*\(/.test(source);
|
|
245
|
+
const hasAmplitudeImport = /from\s+['"]@amplitude\/ai['"]/.test(source) || /require\s*\(\s*['"]@amplitude\/ai['"]\s*\)/.test(source) || /\bAmplitudeAI\b/.test(source);
|
|
246
|
+
const wrappedClients = /* @__PURE__ */ new Set();
|
|
247
|
+
const localImportedBindings = /* @__PURE__ */ new Set();
|
|
248
|
+
const localImportedFunctions = /* @__PURE__ */ new Set();
|
|
249
|
+
const isLocalSpecifier = (spec) => spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("@/") || spec.startsWith("~/");
|
|
250
|
+
walkAST(root, (node) => {
|
|
251
|
+
if (node.type === "ImportDeclaration") {
|
|
252
|
+
const decl = node;
|
|
253
|
+
const source$1 = decl.source;
|
|
254
|
+
if (!source$1 || source$1.type !== "Literal") return;
|
|
255
|
+
const specifier = source$1.value;
|
|
256
|
+
if (typeof specifier !== "string" || !isLocalSpecifier(specifier)) return;
|
|
257
|
+
const specifiers = decl.specifiers;
|
|
258
|
+
if (!specifiers) return;
|
|
259
|
+
for (const s of specifiers) {
|
|
260
|
+
const local = s.local;
|
|
261
|
+
if (local?.type === "Identifier") {
|
|
262
|
+
const name = local.name;
|
|
263
|
+
localImportedBindings.add(name);
|
|
264
|
+
localImportedFunctions.add(name);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (node.type === "NewExpression") {
|
|
269
|
+
const ne = node;
|
|
270
|
+
const calleeName = ne.callee.type === "Identifier" ? ne.callee.name : null;
|
|
271
|
+
if (calleeName && PROVIDER_CONSTRUCTOR_NAMES.has(calleeName)) {
|
|
272
|
+
if (hasKeywordArg(ne.arguments, "amplitude")) {}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
walkAST(root, (node) => {
|
|
277
|
+
if (node.type === "VariableDeclarator") {
|
|
278
|
+
const decl = node;
|
|
279
|
+
if (decl.id.type !== "Identifier" || !decl.init) return;
|
|
280
|
+
const varName = decl.id.name;
|
|
281
|
+
if (decl.init.type === "NewExpression") {
|
|
282
|
+
const ne = decl.init;
|
|
283
|
+
const calleeName = ne.callee.type === "Identifier" ? ne.callee.name : null;
|
|
284
|
+
if (calleeName && PROVIDER_CONSTRUCTOR_NAMES.has(calleeName) && hasKeywordArg(ne.arguments, "amplitude")) wrappedClients.add(varName);
|
|
285
|
+
}
|
|
286
|
+
if (decl.init.type === "CallExpression") {
|
|
287
|
+
const ce = decl.init;
|
|
288
|
+
if ((ce.callee.type === "Identifier" && ce.callee.name === "wrap" || ce.callee.type === "MemberExpression" && !ce.callee.computed && ce.callee.property.type === "Identifier" && ce.callee.property.name === "wrap") && hasAmplitudeImport) wrappedClients.add(varName);
|
|
289
|
+
if (hasAmplitudeImport && ce.callee.type === "Identifier") {
|
|
290
|
+
const fnName = ce.callee.name;
|
|
291
|
+
if (localImportedFunctions.has(fnName)) localImportedBindings.add(varName);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
const callSites = [];
|
|
297
|
+
const assistantsApiRe = /\.beta\.(?:threads|assistants)\./;
|
|
298
|
+
const isReceiverInstrumented = (receiver) => {
|
|
299
|
+
if (hasPatch) return true;
|
|
300
|
+
if (receiver !== null && wrappedClients.has(receiver)) return true;
|
|
301
|
+
if (hasAmplitudeImport && hasSessionContext && receiver !== null && localImportedBindings.has(receiver)) return true;
|
|
302
|
+
return false;
|
|
303
|
+
};
|
|
304
|
+
walkAST(root, (node, ancestors) => {
|
|
305
|
+
if (node.type !== "CallExpression") return;
|
|
306
|
+
const ce = node;
|
|
307
|
+
const lineNum = node.loc?.start.line ?? 0;
|
|
308
|
+
if (lineNum === 0) return;
|
|
309
|
+
if (ce.callee.type === "Identifier") {
|
|
310
|
+
const fnName = ce.callee.name;
|
|
311
|
+
if (VERCEL_AI_SDK_FUNCTIONS.has(fnName)) callSites.push({
|
|
312
|
+
line: lineNum,
|
|
313
|
+
provider: "vercel-ai-sdk",
|
|
314
|
+
api: fnName,
|
|
315
|
+
instrumented: hasPatch,
|
|
316
|
+
containing_function: findContainingFunctionAST(ancestors),
|
|
317
|
+
code_context: extractCodeContext(sourceLines, lineNum)
|
|
318
|
+
});
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (ce.callee.type === "MemberExpression") {
|
|
322
|
+
const mem = ce.callee;
|
|
323
|
+
if (!mem.computed && mem.property.type === "Identifier" && mem.property.name === "send" && ce.arguments.length > 0 && ce.arguments[0]?.type === "NewExpression") {
|
|
324
|
+
const newExpr = ce.arguments[0];
|
|
325
|
+
const cmdName = newExpr.callee.type === "Identifier" ? newExpr.callee.name : null;
|
|
326
|
+
if (cmdName === "InvokeModelCommand" || cmdName === "InvokeModelWithResponseStreamCommand") {
|
|
327
|
+
const receiver = mem.object.type === "Identifier" ? mem.object.name : null;
|
|
328
|
+
callSites.push({
|
|
329
|
+
line: lineNum,
|
|
330
|
+
provider: "bedrock",
|
|
331
|
+
api: cmdName === "InvokeModelWithResponseStreamCommand" ? "invokeModelWithResponseStream" : "invokeModel",
|
|
332
|
+
instrumented: isReceiverInstrumented(receiver),
|
|
333
|
+
containing_function: findContainingFunctionAST(ancestors),
|
|
334
|
+
code_context: extractCodeContext(sourceLines, lineNum)
|
|
335
|
+
});
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (cmdName === "ConverseCommand" || cmdName === "ConverseStreamCommand") {
|
|
339
|
+
const receiver = mem.object.type === "Identifier" ? mem.object.name : null;
|
|
340
|
+
callSites.push({
|
|
341
|
+
line: lineNum,
|
|
342
|
+
provider: "bedrock",
|
|
343
|
+
api: cmdName === "ConverseStreamCommand" ? "converseStream" : "converse",
|
|
344
|
+
instrumented: isReceiverInstrumented(receiver),
|
|
345
|
+
containing_function: findContainingFunctionAST(ancestors),
|
|
346
|
+
code_context: extractCodeContext(sourceLines, lineNum)
|
|
347
|
+
});
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (ce.callee.type === "MemberExpression") {
|
|
353
|
+
const { chain, receiver } = extractMethodChain(ce.callee);
|
|
354
|
+
for (const pattern of LLM_METHOD_CHAINS) {
|
|
355
|
+
if (!chainEndsWith(chain, pattern.chain)) continue;
|
|
356
|
+
if (pattern.chain.length === 1 && pattern.singleMethodHints) {
|
|
357
|
+
if (!hasSingleMethodHint(chain, receiver, pattern.singleMethodHints)) continue;
|
|
358
|
+
}
|
|
359
|
+
let effectiveProvider = pattern.provider;
|
|
360
|
+
let effectiveApi = pattern.api;
|
|
361
|
+
if (pattern.provider === "anthropic") {
|
|
362
|
+
const lineText = sourceLines[lineNum - 1] ?? "";
|
|
363
|
+
if (assistantsApiRe.test(lineText) || chain.some((p) => p === "beta" || p === "threads")) {
|
|
364
|
+
effectiveProvider = "openai-assistants";
|
|
365
|
+
effectiveApi = "beta.threads.messages.create";
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
callSites.push({
|
|
369
|
+
line: lineNum,
|
|
370
|
+
provider: effectiveProvider,
|
|
371
|
+
api: effectiveApi,
|
|
372
|
+
instrumented: isReceiverInstrumented(receiver),
|
|
373
|
+
containing_function: findContainingFunctionAST(ancestors),
|
|
374
|
+
code_context: extractCodeContext(sourceLines, lineNum)
|
|
375
|
+
});
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
const toolDefinitions = [];
|
|
381
|
+
walkAST(root, (node) => {
|
|
382
|
+
if (node.type !== "ObjectExpression") return;
|
|
383
|
+
const obj = node;
|
|
384
|
+
const typeVal = getPropertyValue(obj, "type");
|
|
385
|
+
if (!typeVal || typeVal.type !== "Literal" || typeVal.value !== "function") return;
|
|
386
|
+
const fnVal = getPropertyValue(obj, "function");
|
|
387
|
+
if (!fnVal || fnVal.type !== "ObjectExpression") return;
|
|
388
|
+
const nameVal = getPropertyValue(fnVal, "name");
|
|
389
|
+
if (nameVal?.type === "Literal" && typeof nameVal.value === "string") {
|
|
390
|
+
const name = nameVal.value;
|
|
391
|
+
if (!toolDefinitions.includes(name)) toolDefinitions.push(name);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
walkAST(root, (node) => {
|
|
395
|
+
if (node.type !== "ObjectExpression") return;
|
|
396
|
+
const obj = node;
|
|
397
|
+
const nameVal = getPropertyValue(obj, "name");
|
|
398
|
+
if (!nameVal || nameVal.type !== "Literal" || typeof nameVal.value !== "string") return;
|
|
399
|
+
const hasInputSchema = getPropertyValue(obj, "input_schema") !== null;
|
|
400
|
+
const hasParameters = getPropertyValue(obj, "parameters") !== null;
|
|
401
|
+
if (!hasInputSchema && !hasParameters) return;
|
|
402
|
+
const name = nameVal.value;
|
|
403
|
+
if (!toolDefinitions.includes(name)) toolDefinitions.push(name);
|
|
404
|
+
});
|
|
405
|
+
const functionDefinitions = [];
|
|
406
|
+
walkAST(root, (node) => {
|
|
407
|
+
if (node.type === "FunctionDeclaration") {
|
|
408
|
+
const name = node.id?.name;
|
|
409
|
+
if (name && !functionDefinitions.includes(name)) functionDefinitions.push(name);
|
|
410
|
+
}
|
|
411
|
+
if (node.type === "VariableDeclarator") {
|
|
412
|
+
const decl = node;
|
|
413
|
+
if (decl.id.type === "Identifier" && decl.init && (decl.init.type === "ArrowFunctionExpression" || decl.init.type === "FunctionExpression")) {
|
|
414
|
+
const name = decl.id.name;
|
|
415
|
+
if (!functionDefinitions.includes(name)) functionDefinitions.push(name);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
const uninstrumentedSites = callSites.filter((s) => !s.instrumented);
|
|
420
|
+
let suggestions;
|
|
421
|
+
if (uninstrumentedSites.length > 0) suggestions = [
|
|
422
|
+
"Now: instrument provider calls with wrapper/swap import (for example, `new OpenAI({ amplitude: ai, apiKey })`).",
|
|
423
|
+
"Next: add session lineage with `const session = ai.agent(...).session(...)` and wrap calls in `session.run(...)`.",
|
|
424
|
+
"Why: session context unlocks session-level scoring, enrichments, and reliable product-to-AI funnels.",
|
|
425
|
+
"Content tiers: choose `contentMode` (`full`, `metadata_only`, or `customer_enriched`) and prefer `redactPii: true` when using `full`.",
|
|
426
|
+
"Fallback: use `patch({ amplitudeAI: ai })` for migration speed, then graduate to wrapper + session context."
|
|
427
|
+
];
|
|
428
|
+
else if (callSites.length > 0 && !hasSessionContext) suggestions = [
|
|
429
|
+
"Tracking is present, but session context is missing.",
|
|
430
|
+
"Now: add `const session = ai.agent(...).session(...)` and execute LLM calls inside `session.run(...)` (or use middleware).",
|
|
431
|
+
"Why: without session lineage you lose high-value outcomes like session enrichments, scoring, and dependable session funnels.",
|
|
432
|
+
"Content tiers: set `contentMode` intentionally and keep `redactPii: true` when using `full`."
|
|
433
|
+
];
|
|
434
|
+
else if (callSites.length > 0) suggestions = ["File appears instrumented with session lineage.", "For privacy-by-default, set `contentMode` intentionally and enable `redactPii: true` when using `full`."];
|
|
435
|
+
else suggestions = ["No supported LLM call sites detected in this file.", "If this file should emit AI telemetry, add wrapped provider calls and session context."];
|
|
436
|
+
return {
|
|
437
|
+
total_call_sites: callSites.length,
|
|
438
|
+
instrumented: callSites.length - uninstrumentedSites.length,
|
|
439
|
+
uninstrumented: uninstrumentedSites.length,
|
|
440
|
+
has_amplitude_import: hasAmplitudeImport,
|
|
441
|
+
has_session_context: hasSessionContext,
|
|
442
|
+
call_sites: callSites,
|
|
443
|
+
suggestions,
|
|
444
|
+
tool_definitions: toolDefinitions,
|
|
445
|
+
function_definitions: functionDefinitions
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
const regexLlmPatterns = [
|
|
3
449
|
{
|
|
4
450
|
pattern: /\.chat\.completions\.create\s*\(/g,
|
|
5
451
|
receiverRe: /(\w+)(?:\.\w+)*\.chat\.completions\.create\s*\(/,
|
|
@@ -41,11 +487,59 @@ const llmPatterns = [
|
|
|
41
487
|
receiverRe: /(\w+)(?:\.\w+)*\.send\s*\(/,
|
|
42
488
|
provider: "bedrock",
|
|
43
489
|
api: "converse"
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
pattern: /\.send\s*\(\s*new\s+InvokeModelWithResponseStreamCommand\s*\(/g,
|
|
493
|
+
receiverRe: /(\w+)(?:\.\w+)*\.send\s*\(/,
|
|
494
|
+
provider: "bedrock",
|
|
495
|
+
api: "invokeModelWithResponseStream"
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
pattern: /\.send\s*\(\s*new\s+ConverseStreamCommand\s*\(/g,
|
|
499
|
+
receiverRe: /(\w+)(?:\.\w+)*\.send\s*\(/,
|
|
500
|
+
provider: "bedrock",
|
|
501
|
+
api: "converseStream"
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
pattern: /\.chat\.complete\s*\(/g,
|
|
505
|
+
receiverRe: /(\w+)(?:\.\w+)*\.chat\.complete\s*\(/,
|
|
506
|
+
provider: "mistral",
|
|
507
|
+
api: "chat.complete"
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
pattern: /\.getChatCompletions\s*\(/g,
|
|
511
|
+
receiverRe: /(\w+)(?:\.\w+)*\.getChatCompletions\s*\(/,
|
|
512
|
+
provider: "azure-openai",
|
|
513
|
+
api: "getChatCompletions"
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
pattern: /\bstreamText\s*\(/g,
|
|
517
|
+
receiverRe: null,
|
|
518
|
+
provider: "vercel-ai-sdk",
|
|
519
|
+
api: "streamText"
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
pattern: /\bgenerateText\s*\(/g,
|
|
523
|
+
receiverRe: null,
|
|
524
|
+
provider: "vercel-ai-sdk",
|
|
525
|
+
api: "generateText"
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
pattern: /\bstreamObject\s*\(/g,
|
|
529
|
+
receiverRe: null,
|
|
530
|
+
provider: "vercel-ai-sdk",
|
|
531
|
+
api: "streamObject"
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
pattern: /\bgenerateObject\s*\(/g,
|
|
535
|
+
receiverRe: null,
|
|
536
|
+
provider: "vercel-ai-sdk",
|
|
537
|
+
api: "generateObject"
|
|
44
538
|
}
|
|
45
539
|
];
|
|
46
|
-
function
|
|
540
|
+
function findWrappedConstructorsRegex(source) {
|
|
47
541
|
const result = /* @__PURE__ */ new Set();
|
|
48
|
-
for (const m of source.matchAll(/(?:const|let|var)\s+(\w+)\s*=\s*new\s+(?:OpenAI|Anthropic|Gemini|AzureOpenAI|Bedrock|Mistral)\s*\(/g)) {
|
|
542
|
+
for (const m of source.matchAll(/(?:const|let|var)\s+(\w+)\s*=\s*new\s+(?:OpenAI|Anthropic|Gemini|AzureOpenAI|Bedrock|Mistral|GoogleGenerativeAI|GoogleGenAI|CohereClient)\s*\(/g)) {
|
|
49
543
|
const varName = m[1] ?? "";
|
|
50
544
|
if (!varName) continue;
|
|
51
545
|
let depth = 1;
|
|
@@ -62,33 +556,80 @@ function findWrappedConstructors(source) {
|
|
|
62
556
|
}
|
|
63
557
|
return result;
|
|
64
558
|
}
|
|
65
|
-
function
|
|
559
|
+
function findContainingFunctionRegex(lines, lineIndex) {
|
|
560
|
+
for (let i = lineIndex; i >= 0; i--) {
|
|
561
|
+
const line = lines[i] ?? "";
|
|
562
|
+
const fnDeclMatch = line.match(/(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(/);
|
|
563
|
+
if (fnDeclMatch) {
|
|
564
|
+
const name = fnDeclMatch[1] ?? "";
|
|
565
|
+
if (name && !JS_KEYWORDS.has(name)) return name;
|
|
566
|
+
}
|
|
567
|
+
const arrowMatch = line.match(/(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(/);
|
|
568
|
+
if (arrowMatch) {
|
|
569
|
+
const name = arrowMatch[1] ?? "";
|
|
570
|
+
if (name && !JS_KEYWORDS.has(name)) return name;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
function findToolDefinitionsRegex(source) {
|
|
576
|
+
const tools = [];
|
|
577
|
+
for (const m of source.matchAll(/function:\s*\{[^}]*name:\s*['"](\w+)['"]/g)) {
|
|
578
|
+
const name = m[1] ?? "";
|
|
579
|
+
if (name && !tools.includes(name)) tools.push(name);
|
|
580
|
+
}
|
|
581
|
+
for (const m of source.matchAll(/name:\s*['"](\w+)['"][^}]*input_schema\s*:/g)) {
|
|
582
|
+
const name = m[1] ?? "";
|
|
583
|
+
if (name && !tools.includes(name)) tools.push(name);
|
|
584
|
+
}
|
|
585
|
+
return tools;
|
|
586
|
+
}
|
|
587
|
+
function findFunctionDefinitionsRegex(source) {
|
|
588
|
+
const fns = [];
|
|
589
|
+
for (const m of source.matchAll(/(?:async\s+)?function\s+(\w+)|(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[^=])\s*=>/g)) {
|
|
590
|
+
const name = m[1] ?? m[2] ?? "";
|
|
591
|
+
if (name && !fns.includes(name)) fns.push(name);
|
|
592
|
+
}
|
|
593
|
+
return fns;
|
|
594
|
+
}
|
|
595
|
+
function analyzeWithRegex(source) {
|
|
66
596
|
const hasPatch = /\bpatch\s*\(\s*\{/.test(source);
|
|
67
597
|
const hasSessionContext = /\.session\s*\(/.test(source) || /\bsession\.run\s*\(/.test(source) || /\.runAs\s*\(/.test(source) || /\bcreateAmplitudeAIMiddleware\s*\(/.test(source);
|
|
68
598
|
const hasAmplitudeImport = /from\s+['"]@amplitude\/ai['"]/.test(source) || /require\s*\(\s*['"]@amplitude\/ai['"]\s*\)/.test(source) || /\bAmplitudeAI\b/.test(source);
|
|
69
|
-
const wrappedClients =
|
|
599
|
+
const wrappedClients = findWrappedConstructorsRegex(source);
|
|
70
600
|
if (hasAmplitudeImport) for (const m of source.matchAll(/(?:const|let|var)\s+(\w+)\s*=\s*(?:\w+\.)*wrap\s*\(/g)) {
|
|
71
601
|
const name = m[1] ?? "";
|
|
72
602
|
if (name) wrappedClients.add(name);
|
|
73
603
|
}
|
|
74
604
|
const callSites = [];
|
|
75
605
|
const lines = source.split("\n");
|
|
606
|
+
const assistantsApiRe = /\.beta\.(?:threads|assistants)\./;
|
|
76
607
|
for (let i = 0; i < lines.length; i++) {
|
|
77
608
|
const line = lines[i] ?? "";
|
|
78
|
-
for (const { pattern, receiverRe, provider, api } of
|
|
609
|
+
for (const { pattern, receiverRe, provider, api } of regexLlmPatterns) {
|
|
79
610
|
pattern.lastIndex = 0;
|
|
80
611
|
if (pattern.test(line)) {
|
|
81
|
-
|
|
612
|
+
let effectiveProvider = provider;
|
|
613
|
+
let effectiveApi = api;
|
|
614
|
+
if (provider === "anthropic" && assistantsApiRe.test(line)) {
|
|
615
|
+
effectiveProvider = "openai-assistants";
|
|
616
|
+
effectiveApi = "beta.threads.messages.create";
|
|
617
|
+
}
|
|
618
|
+
const receiver = (receiverRe ? line.match(receiverRe) : null)?.[1] ?? "";
|
|
82
619
|
const instrumented = hasPatch || wrappedClients.has(receiver);
|
|
83
620
|
callSites.push({
|
|
84
621
|
line: i + 1,
|
|
85
|
-
provider,
|
|
86
|
-
api,
|
|
87
|
-
instrumented
|
|
622
|
+
provider: effectiveProvider,
|
|
623
|
+
api: effectiveApi,
|
|
624
|
+
instrumented,
|
|
625
|
+
containing_function: findContainingFunctionRegex(lines, i),
|
|
626
|
+
code_context: extractCodeContext(lines, i + 1)
|
|
88
627
|
});
|
|
89
628
|
}
|
|
90
629
|
}
|
|
91
630
|
}
|
|
631
|
+
const toolDefs = findToolDefinitionsRegex(source);
|
|
632
|
+
const fnDefs = findFunctionDefinitionsRegex(source);
|
|
92
633
|
const uninstrumentedSites = callSites.filter((s) => !s.instrumented);
|
|
93
634
|
let suggestions;
|
|
94
635
|
if (uninstrumentedSites.length > 0) suggestions = [
|
|
@@ -113,9 +654,16 @@ function analyzeFileInstrumentation(source) {
|
|
|
113
654
|
has_amplitude_import: hasAmplitudeImport,
|
|
114
655
|
has_session_context: hasSessionContext,
|
|
115
656
|
call_sites: callSites,
|
|
116
|
-
suggestions
|
|
657
|
+
suggestions,
|
|
658
|
+
tool_definitions: toolDefs,
|
|
659
|
+
function_definitions: fnDefs
|
|
117
660
|
};
|
|
118
661
|
}
|
|
662
|
+
function analyzeFileInstrumentation(source) {
|
|
663
|
+
const astResult = analyzeWithAST(source, source.split("\n"));
|
|
664
|
+
if (astResult) return astResult;
|
|
665
|
+
return analyzeWithRegex(source);
|
|
666
|
+
}
|
|
119
667
|
|
|
120
668
|
//#endregion
|
|
121
669
|
export { analyzeFileInstrumentation };
|