@empiricalrun/test-gen 0.74.2 → 0.76.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 (155) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/agent/base/index.d.ts +43 -0
  3. package/dist/agent/base/index.d.ts.map +1 -0
  4. package/dist/agent/base/index.js +106 -0
  5. package/dist/agent/chat/agent-loop.d.ts +7 -7
  6. package/dist/agent/chat/agent-loop.d.ts.map +1 -1
  7. package/dist/agent/chat/agent-loop.js +5 -18
  8. package/dist/agent/chat/exports.d.ts +6 -4
  9. package/dist/agent/chat/exports.d.ts.map +1 -1
  10. package/dist/agent/chat/exports.js +9 -8
  11. package/dist/agent/chat/index.d.ts +6 -10
  12. package/dist/agent/chat/index.d.ts.map +1 -1
  13. package/dist/agent/chat/index.js +130 -200
  14. package/dist/agent/chat/prompt/index.d.ts +5 -4
  15. package/dist/agent/chat/prompt/index.d.ts.map +1 -1
  16. package/dist/agent/chat/prompt/index.js +79 -68
  17. package/dist/agent/chat/state.d.ts +3 -5
  18. package/dist/agent/chat/state.d.ts.map +1 -1
  19. package/dist/agent/chat/state.js +2 -10
  20. package/dist/agent/chat/utils.d.ts +2 -4
  21. package/dist/agent/chat/utils.d.ts.map +1 -1
  22. package/dist/agent/chat/utils.js +2 -16
  23. package/dist/agent/cli.d.ts +11 -0
  24. package/dist/agent/cli.d.ts.map +1 -0
  25. package/dist/agent/cli.js +209 -0
  26. package/dist/agent/code-review/index.d.ts +7 -0
  27. package/dist/agent/code-review/index.d.ts.map +1 -0
  28. package/dist/agent/code-review/index.js +65 -0
  29. package/dist/agent/code-review/prompt.d.ts +1 -1
  30. package/dist/agent/code-review/prompt.d.ts.map +1 -1
  31. package/dist/agent/code-review/prompt.js +52 -16
  32. package/dist/agent/index.d.ts +10 -0
  33. package/dist/agent/index.d.ts.map +1 -0
  34. package/dist/agent/index.js +19 -0
  35. package/dist/agent/triage/index.d.ts +7 -0
  36. package/dist/agent/triage/index.d.ts.map +1 -0
  37. package/dist/agent/triage/index.js +102 -0
  38. package/dist/agent/video-analysis/index.d.ts +7 -0
  39. package/dist/agent/video-analysis/index.d.ts.map +1 -0
  40. package/dist/agent/video-analysis/index.js +35 -0
  41. package/dist/bin/index.js +6 -6
  42. package/dist/file-info/adapters/github/index.d.ts +2 -2
  43. package/dist/file-info/adapters/github/index.d.ts.map +1 -1
  44. package/dist/file-info/adapters/github/index.js +4 -4
  45. package/dist/file-info/adapters/github/reader.d.ts +5 -10
  46. package/dist/file-info/adapters/github/reader.d.ts.map +1 -1
  47. package/dist/file-info/adapters/github/reader.js +168 -138
  48. package/dist/tools/create-pull-request/index.d.ts.map +1 -0
  49. package/dist/tools/{definitions/commit-and-create-pr.js → create-pull-request/index.js} +30 -1
  50. package/dist/tools/create-pull-request/utils.d.ts +21 -0
  51. package/dist/tools/create-pull-request/utils.d.ts.map +1 -0
  52. package/dist/tools/create-pull-request/utils.js +83 -0
  53. package/dist/tools/definitions/extract-frames-from-video.d.ts +39 -0
  54. package/dist/tools/definitions/extract-frames-from-video.d.ts.map +1 -0
  55. package/dist/tools/definitions/extract-frames-from-video.js +60 -0
  56. package/dist/tools/definitions/fetch-video-analysis.d.ts +28 -0
  57. package/dist/tools/definitions/fetch-video-analysis.d.ts.map +1 -1
  58. package/dist/tools/definitions/fetch-video-analysis.js +39 -4
  59. package/dist/tools/definitions/rename-file.d.ts +3 -0
  60. package/dist/tools/definitions/rename-file.d.ts.map +1 -0
  61. package/dist/tools/definitions/rename-file.js +23 -0
  62. package/dist/tools/delete-file/index.d.ts.map +1 -1
  63. package/dist/tools/delete-file/index.js +13 -1
  64. package/dist/tools/executor/index.d.ts +1 -1
  65. package/dist/tools/executor/index.d.ts.map +1 -1
  66. package/dist/tools/executor/index.js +22 -7
  67. package/dist/tools/executor/utils/checkpoint.d.ts +1 -3
  68. package/dist/tools/executor/utils/checkpoint.d.ts.map +1 -1
  69. package/dist/tools/executor/utils/checkpoint.js +17 -17
  70. package/dist/tools/executor/utils/git.d.ts +9 -1
  71. package/dist/tools/executor/utils/git.d.ts.map +1 -1
  72. package/dist/tools/executor/utils/git.js +72 -2
  73. package/dist/tools/extract-frames-from-video/index.d.ts +7 -0
  74. package/dist/tools/extract-frames-from-video/index.d.ts.map +1 -0
  75. package/dist/tools/extract-frames-from-video/index.js +145 -0
  76. package/dist/tools/{fetch-image → fetch-file}/index.d.ts +2 -2
  77. package/dist/tools/fetch-file/index.d.ts.map +1 -0
  78. package/dist/tools/fetch-file/index.js +97 -0
  79. package/dist/tools/fetch-session-diff/index.d.ts +3 -0
  80. package/dist/tools/fetch-session-diff/index.d.ts.map +1 -0
  81. package/dist/tools/fetch-session-diff/index.js +46 -0
  82. package/dist/tools/fetch-video-analysis/index.d.ts +3 -3
  83. package/dist/tools/fetch-video-analysis/index.d.ts.map +1 -1
  84. package/dist/tools/fetch-video-analysis/index.js +84 -24
  85. package/dist/tools/fetch-video-analysis/open-ai.d.ts +6 -0
  86. package/dist/tools/fetch-video-analysis/open-ai.d.ts.map +1 -0
  87. package/dist/tools/fetch-video-analysis/open-ai.js +37 -0
  88. package/dist/tools/fetch-video-analysis/utils.d.ts +9 -3
  89. package/dist/tools/fetch-video-analysis/utils.d.ts.map +1 -1
  90. package/dist/tools/fetch-video-analysis/utils.js +64 -15
  91. package/dist/tools/fetch-video-analysis/video-analysis.d.ts +2 -2
  92. package/dist/tools/fetch-video-analysis/video-analysis.d.ts.map +1 -1
  93. package/dist/tools/fetch-video-analysis/video-analysis.js +24 -8
  94. package/dist/tools/file-operations/create.d.ts.map +1 -1
  95. package/dist/tools/file-operations/create.js +6 -3
  96. package/dist/tools/file-operations/insert.d.ts.map +1 -1
  97. package/dist/tools/file-operations/insert.js +6 -3
  98. package/dist/tools/file-operations/replace.d.ts.map +1 -1
  99. package/dist/tools/file-operations/replace.js +6 -3
  100. package/dist/tools/file-operations/shared/git-helper.d.ts.map +1 -1
  101. package/dist/tools/file-operations/shared/git-helper.js +1 -1
  102. package/dist/tools/file-operations/view/index.d.ts +2 -5
  103. package/dist/tools/file-operations/view/index.d.ts.map +1 -1
  104. package/dist/tools/file-operations/view/index.js +2 -22
  105. package/dist/tools/index.d.ts +28 -2
  106. package/dist/tools/index.d.ts.map +1 -1
  107. package/dist/tools/index.js +50 -22
  108. package/dist/tools/issues/update-issue.d.ts.map +1 -1
  109. package/dist/tools/issues/update-issue.js +16 -9
  110. package/dist/tools/merge-conflicts/index.js +1 -1
  111. package/dist/tools/rename-file/index.d.ts +3 -0
  112. package/dist/tools/rename-file/index.d.ts.map +1 -0
  113. package/dist/tools/rename-file/index.js +88 -0
  114. package/dist/tools/review-pull-request/index.d.ts +3 -0
  115. package/dist/tools/review-pull-request/index.d.ts.map +1 -0
  116. package/dist/tools/review-pull-request/index.js +103 -0
  117. package/dist/tools/run-test.js +2 -2
  118. package/dist/tools/test-run-fetcher/index.d.ts.map +1 -1
  119. package/dist/tools/test-run-fetcher/index.js +4 -14
  120. package/dist/tools/trace-dot-zip/index.d.ts.map +1 -1
  121. package/dist/tools/trace-dot-zip/index.js +2 -1
  122. package/dist/tools/trace-dot-zip/types.d.ts +35 -3
  123. package/dist/tools/trace-dot-zip/types.d.ts.map +1 -1
  124. package/dist/tools/trace-dot-zip/utils/network-trace.d.ts +7 -2
  125. package/dist/tools/trace-dot-zip/utils/network-trace.d.ts.map +1 -1
  126. package/dist/tools/trace-dot-zip/utils/network-trace.js +130 -10
  127. package/dist/tools/upgrade-packages/index.js +1 -1
  128. package/dist/tools/utils/urls.d.ts +5 -0
  129. package/dist/tools/utils/urls.d.ts.map +1 -0
  130. package/dist/tools/utils/urls.js +19 -0
  131. package/dist/tools/view-failed-test-run-report/index.d.ts.map +1 -1
  132. package/dist/tools/view-failed-test-run-report/index.js +3 -15
  133. package/dist/utils/file.d.ts +1 -0
  134. package/dist/utils/file.d.ts.map +1 -1
  135. package/dist/utils/file.js +45 -1
  136. package/dist/utils/index.d.ts +0 -1
  137. package/dist/utils/index.d.ts.map +1 -1
  138. package/dist/utils/index.js +1 -3
  139. package/dist/utils/local-ffmpeg-client.d.ts +27 -0
  140. package/dist/utils/local-ffmpeg-client.d.ts.map +1 -0
  141. package/dist/{tools/fetch-video-analysis → utils}/local-ffmpeg-client.js +117 -27
  142. package/package.json +5 -5
  143. package/tsconfig.tsbuildinfo +1 -1
  144. package/dist/agent/chat/utils/tool-calls.d.ts +0 -21
  145. package/dist/agent/chat/utils/tool-calls.d.ts.map +0 -1
  146. package/dist/agent/chat/utils/tool-calls.js +0 -64
  147. package/dist/tools/commit-and-create-pr/index.d.ts.map +0 -1
  148. package/dist/tools/commit-and-create-pr/index.js +0 -83
  149. package/dist/tools/definitions/commit-and-create-pr.d.ts +0 -3
  150. package/dist/tools/definitions/commit-and-create-pr.d.ts.map +0 -1
  151. package/dist/tools/fetch-image/index.d.ts.map +0 -1
  152. package/dist/tools/fetch-image/index.js +0 -63
  153. package/dist/tools/fetch-video-analysis/local-ffmpeg-client.d.ts +0 -24
  154. package/dist/tools/fetch-video-analysis/local-ffmpeg-client.d.ts.map +0 -1
  155. /package/dist/tools/{commit-and-create-pr → create-pull-request}/index.d.ts +0 -0
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.extractFramesFromVideo = void 0;
7
+ const node_crypto_1 = __importDefault(require("node:crypto"));
8
+ const fs_1 = require("fs");
9
+ const file_1 = require("../../utils/file");
10
+ const local_ffmpeg_client_1 = require("../../utils/local-ffmpeg-client");
11
+ const extract_frames_from_video_1 = require("../definitions/extract-frames-from-video");
12
+ function parseTimeToSeconds(timeString) {
13
+ const timeRegex = /^(\d{1,2}):(\d{2})$/;
14
+ const match = timeString.match(timeRegex);
15
+ if (!match || match.length < 3) {
16
+ throw new Error(`Invalid time format: ${timeString}. Expected MM:SS format (e.g., "01:32")`);
17
+ }
18
+ const minutesStr = match[1];
19
+ const secondsStr = match[2];
20
+ if (!minutesStr || !secondsStr) {
21
+ throw new Error(`Invalid time format: ${timeString}. Expected MM:SS format (e.g., "01:32")`);
22
+ }
23
+ const minutes = parseInt(minutesStr, 10);
24
+ const seconds = parseInt(secondsStr, 10);
25
+ if (seconds >= 60) {
26
+ throw new Error(`Invalid seconds: ${seconds}. Seconds must be 0-59`);
27
+ }
28
+ return minutes * 60 + seconds;
29
+ }
30
+ function getExtractFramesParams(params) {
31
+ return {
32
+ fps: params?.fps ?? 30,
33
+ threshold: params?.threshold ?? 0.001,
34
+ startTime: params?.startTime
35
+ ? parseTimeToSeconds(params.startTime)
36
+ : undefined,
37
+ duration: params?.duration
38
+ ? parseTimeToSeconds(params.duration)
39
+ : undefined,
40
+ };
41
+ }
42
+ function hashExtractParams(videoUrl, params) {
43
+ const sortedParams = Object.keys(params)
44
+ .sort()
45
+ .reduce((acc, key) => {
46
+ acc[key] = params[key];
47
+ return acc;
48
+ }, {});
49
+ const json = JSON.stringify({ videoUrl, ...sortedParams });
50
+ return node_crypto_1.default
51
+ .createHash("sha256")
52
+ .update(json)
53
+ .digest("hex")
54
+ .substring(0, 16);
55
+ }
56
+ exports.extractFramesFromVideo = {
57
+ ...extract_frames_from_video_1.extractFramesFromVideo,
58
+ execute: async ({ input, trace, }) => {
59
+ const { videoUrl } = input;
60
+ const params = getExtractFramesParams(input.params);
61
+ const videoUrlHash = hashExtractParams(videoUrl, params);
62
+ const WORKING_DIR = `./extract-frames-artifacts/${videoUrlHash}`;
63
+ // const R2_BASE_URL = `https://video-analysis.empirical.run/${videoUrlHash}`;
64
+ const extractionSpan = trace?.span({
65
+ name: "extract-frames-from-video",
66
+ input: { videoUrl, params },
67
+ });
68
+ try {
69
+ const response = await fetch(videoUrl, { method: "HEAD" });
70
+ if (!response.ok) {
71
+ return {
72
+ result: `Failed to access video: ${response.statusText}`,
73
+ isError: true,
74
+ };
75
+ }
76
+ const ffmpegClient = new local_ffmpeg_client_1.LocalFFmpegClient();
77
+ const extractionResult = await ffmpegClient.extractVideoFrames(videoUrl, WORKING_DIR, {
78
+ fps: params.fps,
79
+ threshold: params.threshold,
80
+ startTime: params.startTime,
81
+ duration: params.duration,
82
+ });
83
+ const { totalFramesCount, uniqueFrames, videoDurationSeconds } = extractionResult;
84
+ // Create response metadata
85
+ const extractFramesResponse = {
86
+ videoUrl,
87
+ videoDurationSeconds,
88
+ totalFramesCount,
89
+ extractedFramesCount: uniqueFrames.length,
90
+ timeRange: params.startTime !== undefined || params.duration !== undefined
91
+ ? {
92
+ startTime: params.startTime || 0,
93
+ duration: params.duration || 0,
94
+ endTime: (params.startTime || 0) + (params.duration || 0),
95
+ }
96
+ : undefined,
97
+ frames: uniqueFrames.map((frame) => ({
98
+ index: frame.metadata.index,
99
+ path: frame.metadata.path,
100
+ frameId: `frame_${frame.metadata.index}`,
101
+ })),
102
+ };
103
+ // Convert frames to base64 images for multimodal response
104
+ const frameImageParts = await Promise.all(uniqueFrames.map(async (frame) => {
105
+ try {
106
+ const imageBuffer = await fs_1.promises.readFile(frame.metadata.path);
107
+ const base64Data = imageBuffer.toString("base64");
108
+ return {
109
+ type: "image/png",
110
+ base64Data: base64Data,
111
+ };
112
+ }
113
+ catch (error) {
114
+ console.warn(`Failed to read frame ${frame.metadata.path}:`, error);
115
+ return null;
116
+ }
117
+ }));
118
+ await (0, file_1.safeCleanupDirectory)(WORKING_DIR, "extract-frames-tool");
119
+ // Filter out any failed frame reads
120
+ const validFrameImageParts = frameImageParts.filter((part) => part !== null);
121
+ return {
122
+ result: [
123
+ {
124
+ type: "text",
125
+ text: JSON.stringify(extractFramesResponse, null, 2),
126
+ },
127
+ ...validFrameImageParts,
128
+ ],
129
+ isError: false,
130
+ };
131
+ }
132
+ catch (error) {
133
+ console.error("Error in extractFramesFromVideo", error);
134
+ extractionSpan?.end();
135
+ const message = error instanceof Error ? error.message : String(error);
136
+ return {
137
+ result: `Error extracting frames from video: ${message}`,
138
+ isError: true,
139
+ };
140
+ }
141
+ finally {
142
+ extractionSpan?.end();
143
+ }
144
+ },
145
+ };
@@ -1,12 +1,12 @@
1
1
  import { Tool } from "@empiricalrun/shared-types";
2
2
  import { z } from "zod";
3
- declare const fetchImageSchema: z.ZodObject<{
3
+ declare const fetchFileSchema: z.ZodObject<{
4
4
  url: z.ZodString;
5
5
  }, "strip", z.ZodTypeAny, {
6
6
  url: string;
7
7
  }, {
8
8
  url: string;
9
9
  }>;
10
- export declare const fetchImageTool: Tool<z.infer<typeof fetchImageSchema>>;
10
+ export declare const fetchFileTool: Tool<z.infer<typeof fetchFileSchema>>;
11
11
  export {};
12
12
  //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/tools/fetch-file/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,IAAI,EAGL,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAUxB,QAAA,MAAM,eAAe;;;;;;EAEnB,CAAC;AAEH,eAAO,MAAM,aAAa,EAAE,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAyF/D,CAAC"}
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fetchFileTool = void 0;
4
+ const zod_1 = require("zod");
5
+ const SUPPORTED_IMAGE_TYPES = [
6
+ "image/png",
7
+ "image/jpeg",
8
+ "image/gif",
9
+ "image/webp",
10
+ ];
11
+ const SUPPORTED_TEXT_TYPES = ["text/markdown", "text/plain"];
12
+ const fetchFileSchema = zod_1.z.object({
13
+ url: zod_1.z.string(),
14
+ });
15
+ exports.fetchFileTool = {
16
+ schema: {
17
+ name: "fetchFile",
18
+ description: `Use this tool to fetch file data from any valid URL that responds with an image (PNG, JPEG, GIF, WebP) or markdown file.
19
+ For images, it returns the image in base64 format for you to view or analyze. For markdown files, it returns the text content for you to read and process.
20
+
21
+ ## Caveats
22
+ 1. This will not work to fetch markdown files from a repo due to access control issues. Use file view tools for that.
23
+
24
+ ## Scenarios to use fetchFile
25
+ 1. Understand a test report (for the runTest tool) by fetching screenshots and error context files (.md) which contain the accessibility tree of the page at the
26
+ time of test failures. Both of these are available in the attachments section of the test report from the runTest tool call.
27
+
28
+ 2. While adding new tests, if the user message contains image URLs, use this tool to fetch the images, and understand the steps that the user is looking to cover..`,
29
+ parameters: fetchFileSchema,
30
+ },
31
+ needsBrowser: false,
32
+ isInlineTool: true,
33
+ execute: async ({ input }) => {
34
+ const { url } = input;
35
+ try {
36
+ const response = await fetch(url);
37
+ if (!response.ok) {
38
+ console.error(`Failed to fetch file from ${url}: ${response.statusText}`);
39
+ return {
40
+ result: `Failed to fetch file from ${url}: ${response.statusText}`,
41
+ isError: true,
42
+ };
43
+ }
44
+ const contentType = response.headers.get("content-type");
45
+ if (!contentType) {
46
+ const errorMessage = `No content type header found. URL must return an image or markdown file.`;
47
+ return {
48
+ result: errorMessage,
49
+ isError: true,
50
+ };
51
+ }
52
+ const isImage = SUPPORTED_IMAGE_TYPES.some((type) => contentType.startsWith(type));
53
+ const isText = SUPPORTED_TEXT_TYPES.some((type) => contentType.startsWith(type));
54
+ if (!isImage && !isText) {
55
+ const errorMessage = `Invalid content type: ${contentType}. URL must return an image (PNG, JPEG, GIF, WebP) or markdown file.`;
56
+ console.error(errorMessage);
57
+ return {
58
+ result: errorMessage,
59
+ isError: true,
60
+ };
61
+ }
62
+ if (isImage) {
63
+ const buffer = await response.arrayBuffer();
64
+ const base64 = Buffer.from(buffer).toString("base64");
65
+ return {
66
+ result: [
67
+ {
68
+ type: contentType,
69
+ base64Data: base64,
70
+ },
71
+ ],
72
+ isError: false,
73
+ };
74
+ }
75
+ else {
76
+ // Handle text/markdown files
77
+ const text = await response.text();
78
+ return {
79
+ result: [
80
+ {
81
+ type: "text",
82
+ text: text,
83
+ },
84
+ ],
85
+ isError: false,
86
+ };
87
+ }
88
+ }
89
+ catch (error) {
90
+ console.error("Error fetching file", error);
91
+ return {
92
+ result: `Error fetching file: ${error}`,
93
+ isError: true,
94
+ };
95
+ }
96
+ },
97
+ };
@@ -0,0 +1,3 @@
1
+ import { Tool } from "@empiricalrun/shared-types";
2
+ export declare const fetchSessionDiffTool: Tool;
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/tools/fetch-session-diff/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAc,MAAM,4BAA4B,CAAC;AAO9D,eAAO,MAAM,oBAAoB,EAAE,IA0ClC,CAAC"}
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fetchSessionDiffTool = void 0;
4
+ const zod_1 = require("zod");
5
+ const SessionDiffInputSchema = zod_1.z.object({
6
+ url: zod_1.z.string().describe(`The URL of the chat session.`),
7
+ });
8
+ exports.fetchSessionDiffTool = {
9
+ schema: {
10
+ name: "fetchSessionDiff",
11
+ description: `Extracts the session url and returns the code diff for the session.`,
12
+ parameters: SessionDiffInputSchema,
13
+ },
14
+ needsBrowser: false,
15
+ isInlineTool: true,
16
+ execute: async ({ input, apiClient }) => {
17
+ try {
18
+ const sessionUrl = input.url;
19
+ if (!sessionUrl) {
20
+ return {
21
+ isError: true,
22
+ result: "No session URL provided",
23
+ };
24
+ }
25
+ if (!apiClient) {
26
+ throw new Error("Dashboard API client is not available.");
27
+ }
28
+ const sessionId = sessionUrl.split("/").pop();
29
+ const sessionDiffApiUrl = `/api/chat-sessions/${sessionId}/diff`;
30
+ const sessionDiff = await apiClient.request(sessionDiffApiUrl, {
31
+ method: "GET",
32
+ });
33
+ return {
34
+ isError: false,
35
+ result: JSON.stringify(sessionDiff, null, 2),
36
+ };
37
+ }
38
+ catch (error) {
39
+ console.error("Error fetching session diff", error);
40
+ return {
41
+ isError: true,
42
+ result: error instanceof Error ? error.message : String(error),
43
+ };
44
+ }
45
+ },
46
+ };
@@ -1,5 +1,5 @@
1
- import { Tool } from "@empiricalrun/shared-types";
2
- import { z } from "zod";
3
- import { videoAnalysisSchema } from "../definitions/fetch-video-analysis";
1
+ import type { Tool } from "@empiricalrun/shared-types";
2
+ import type { z } from "zod";
3
+ import { type videoAnalysisSchema } from "../definitions/fetch-video-analysis";
4
4
  export declare const fetchVideoAnalysis: Tool<z.infer<typeof videoAnalysisSchema>>;
5
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/tools/fetch-video-analysis/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAqB,MAAM,4BAA4B,CAAC;AAErE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAEL,mBAAmB,EACpB,MAAM,qCAAqC,CAAC;AAK7C,eAAO,MAAM,kBAAkB,EAAE,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAiHxE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/tools/fetch-video-analysis/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAEV,IAAI,EAKL,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAI7B,OAAO,EAEL,KAAK,mBAAmB,EACzB,MAAM,qCAAqC,CAAC;AAuC7C,eAAO,MAAM,kBAAkB,EAAE,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAsKxE,CAAC"}
@@ -4,20 +4,46 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.fetchVideoAnalysis = void 0;
7
- const crypto_1 = __importDefault(require("crypto"));
7
+ const node_crypto_1 = __importDefault(require("node:crypto"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const file_1 = require("../../utils/file");
10
+ const local_ffmpeg_client_1 = require("../../utils/local-ffmpeg-client");
8
11
  const fetch_video_analysis_1 = require("../definitions/fetch-video-analysis");
9
- const local_ffmpeg_client_1 = require("./local-ffmpeg-client");
10
12
  const utils_1 = require("./utils");
11
13
  const video_analysis_1 = require("./video-analysis");
14
+ function getVideoAnalysisParams(params) {
15
+ return {
16
+ model: params?.model || "gemini-2.5-pro",
17
+ fps: params?.fps ?? 30,
18
+ threshold: params?.threshold ?? 0.001,
19
+ featureFlag: params?.featureFlag ?? "send-all-frames",
20
+ };
21
+ }
22
+ function hashObject(obj) {
23
+ const sortedObj = Object.keys(obj)
24
+ .sort()
25
+ .reduce((acc, key) => {
26
+ acc[key] = obj[key];
27
+ return acc;
28
+ }, {});
29
+ const json = JSON.stringify(sortedObj);
30
+ return node_crypto_1.default
31
+ .createHash("sha256")
32
+ .update(json)
33
+ .digest("hex")
34
+ .substring(0, 16);
35
+ }
12
36
  exports.fetchVideoAnalysis = {
13
37
  ...fetch_video_analysis_1.fetchVideoAnalysis,
14
- execute: async ({ input, trace, }) => {
38
+ execute: async ({ input, trace, featureFlags, }) => {
15
39
  const { videoUrl } = input;
16
- const videoUrlHash = crypto_1.default
17
- .createHash("sha256")
18
- .update(videoUrl)
19
- .digest("hex")
20
- .substring(0, 16);
40
+ const params = getVideoAnalysisParams(input.params);
41
+ const videoUrlHash = hashObject({
42
+ videoUrl,
43
+ ...params,
44
+ });
45
+ const { model: selectedModel, fps: effectiveFps, threshold: effectiveThreshold, featureFlag: effectiveFeatureFlag, } = params;
46
+ const WORKING_DIR = `./video-analysis-artifacts/${videoUrlHash}`;
21
47
  const R2_BASE_URL = `https://video-analysis.empirical.run/${videoUrlHash}/`;
22
48
  const videoAnalysisSpan = trace?.span({
23
49
  name: "video-analysis",
@@ -34,36 +60,70 @@ exports.fetchVideoAnalysis = {
34
60
  }
35
61
  const processingSpan = videoAnalysisSpan?.span({
36
62
  name: "ffmpeg-processing",
37
- input: { videoUrl },
63
+ input: {
64
+ videoUrl,
65
+ fps: effectiveFps,
66
+ threshold: effectiveThreshold,
67
+ },
38
68
  });
39
69
  try {
40
- const { totalFramesCount, uniqueFramesCount, uniqueFrames } = await ffmpegClient.extractVideoFrames(videoUrl, "./temp");
70
+ const extractionResult = await ffmpegClient.extractVideoFrames(videoUrl, WORKING_DIR, {
71
+ fps: effectiveFps,
72
+ threshold: effectiveThreshold,
73
+ });
74
+ const { totalFramesCount, uniqueFrames } = extractionResult;
41
75
  processingSpan?.end({
42
76
  output: {
43
77
  totalFramesCount,
44
- uniqueFramesCount,
78
+ uniqueFramesCount: uniqueFrames.length,
45
79
  },
46
80
  });
47
81
  console.log(`[video-analysis] Analyzing ${uniqueFrames.length} frames with LLM`);
48
- const videoInfoWithoutAnalysis = {
82
+ const outputZipPath = node_path_1.default.join(WORKING_DIR, "frames.zip");
83
+ const zipUploadPromise = (0, utils_1.zipAndUploadFramesToR2)(uniqueFrames, outputZipPath, videoUrlHash).catch((error) => {
84
+ throw error; // Re-throw to maintain error in Promise.all
85
+ });
86
+ const { analysis: llmAnalysis, usage } = await (0, video_analysis_1.analyzeFramesWithLLM)(uniqueFrames, videoAnalysisSpan, selectedModel);
87
+ console.log(`[video-analysis] Finished Analyzing ${uniqueFrames.length} frames with LLM`);
88
+ let finalAnalysis = llmAnalysis;
89
+ let finalKeyFramesData = [];
90
+ if (featureFlags.includes("enableFetchVideoAnalysisToolMultiModal")) {
91
+ const { updatedAnalysis, keyFramesData } = await (0, utils_1.getUpdatedAnalysisAndToolResultImagePart)(llmAnalysis, uniqueFrames);
92
+ finalAnalysis = updatedAnalysis;
93
+ finalKeyFramesData = [...keyFramesData];
94
+ }
95
+ const videoInfo = {
49
96
  total_frames_count: totalFramesCount,
50
- unique_frames_count: uniqueFramesCount,
97
+ unique_frames_count: uniqueFrames.length,
51
98
  video_url: videoUrl,
52
- video_url_hash: videoUrlHash,
99
+ analysis_id: videoUrlHash,
53
100
  created_at: new Date().toISOString(),
101
+ params: {
102
+ fps: effectiveFps,
103
+ threshold: effectiveThreshold,
104
+ model: selectedModel,
105
+ featureFlag: effectiveFeatureFlag,
106
+ },
107
+ usage,
108
+ langfuse_trace_id: trace?.id || undefined,
109
+ frames_zip_url: `${R2_BASE_URL}frames.zip`,
110
+ analysis: finalAnalysis,
54
111
  };
55
- const framesUploadPromise = (0, utils_1.uploadFramesToR2)(videoInfoWithoutAnalysis, uniqueFrames, R2_BASE_URL);
56
- const { analysis: llmAnalysis, usage } = await (0, video_analysis_1.analyzeFramesWithLLM)(uniqueFrames.map((frame) => frame.image), videoAnalysisSpan, "gemini-2.5-pro");
57
- console.log(`[video-analysis] Finished Analyzing ${uniqueFrames.length} frames with LLM`);
58
- const videoInfo = {
59
- ...videoInfoWithoutAnalysis,
60
- llm_analysis: llmAnalysis,
61
- video_url_hash: videoUrlHash,
112
+ await Promise.all([
113
+ zipUploadPromise,
114
+ (0, utils_1.uploadSummaryToR2)(videoInfo, R2_BASE_URL),
115
+ ]);
116
+ await (0, file_1.safeCleanupDirectory)(WORKING_DIR, "video-analysis-cleanup");
117
+ const toolResult = {
118
+ video_url: videoUrl,
119
+ analysis_id: videoUrlHash,
120
+ analysis: finalAnalysis,
62
121
  };
63
- const uniqueFramesWithUrls = await framesUploadPromise;
64
- await (0, utils_1.uploadAnalysisToR2)(videoInfo, uniqueFramesWithUrls, R2_BASE_URL);
65
122
  return {
66
- result: JSON.stringify(videoInfo, null, 2),
123
+ result: [
124
+ { type: "text", text: JSON.stringify(toolResult, null, 2) },
125
+ ...finalKeyFramesData,
126
+ ],
67
127
  isError: false,
68
128
  usage,
69
129
  };
@@ -0,0 +1,6 @@
1
+ export declare function analyzeImages({ systemPrompt, imageBase64Array, userPrompt, }: {
2
+ systemPrompt: string;
3
+ imageBase64Array: string[];
4
+ userPrompt: string;
5
+ }): Promise<string>;
6
+ //# sourceMappingURL=open-ai.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open-ai.d.ts","sourceRoot":"","sources":["../../../src/tools/fetch-video-analysis/open-ai.ts"],"names":[],"mappings":"AAOA,wBAAsB,aAAa,CAAC,EAClC,YAAY,EACZ,gBAAgB,EAChB,UAAU,GACX,EAAE;IACD,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;CACpB,mBA+BA"}
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.analyzeImages = analyzeImages;
7
+ const openai_1 = __importDefault(require("openai"));
8
+ const openai = new openai_1.default({
9
+ apiKey: ``,
10
+ });
11
+ async function analyzeImages({ systemPrompt, imageBase64Array, userPrompt, }) {
12
+ const response = await openai.responses.create({
13
+ model: "gpt-4.1-2025-04-14",
14
+ input: [
15
+ {
16
+ role: "system",
17
+ content: systemPrompt,
18
+ },
19
+ {
20
+ role: "user",
21
+ content: [
22
+ {
23
+ type: "input_text",
24
+ text: userPrompt,
25
+ },
26
+ ...imageBase64Array.map((base64) => ({
27
+ detail: "auto",
28
+ type: "input_image",
29
+ image_url: `data:image/png;base64,${base64}`,
30
+ })),
31
+ ],
32
+ },
33
+ ],
34
+ });
35
+ console.log(response.output_text);
36
+ return response.output_text;
37
+ }
@@ -1,10 +1,16 @@
1
- import { UniqueFrameInfo, VideoAnalysisInfo } from "@empiricalrun/shared-types";
2
- export declare const uploadFramesToR2: (videoInfo: Omit<VideoAnalysisInfo, "llm_analysis">, frames: {
1
+ import { type FileMap } from "@empiricalrun/r2-uploader";
2
+ import { ToolResultImagePart, UniqueFrameInfo, UniqueFrameWithMetadata, VideoAnalysisSummary } from "@empiricalrun/shared-types";
3
+ export declare const uploadFramesToR2: (videoInfo: Omit<VideoAnalysisSummary, "analysis">, frames: {
3
4
  metadata: {
4
5
  index: number;
5
6
  path: string;
6
7
  };
7
8
  image: string;
8
9
  }[], r2BaseUrl: string) => Promise<UniqueFrameInfo[]>;
9
- export declare const uploadAnalysisToR2: (videoInfo: VideoAnalysisInfo, uniqueFrames: UniqueFrameInfo[], r2BaseUrl: string) => Promise<string>;
10
+ export declare const uploadSummaryToR2: (videoInfo: VideoAnalysisSummary, r2BaseUrl: string) => Promise<string>;
11
+ export declare const zipAndUploadFramesToR2: (uniqueFrames: UniqueFrameWithMetadata[], outputZipPath: string, videoUrlHash: string) => Promise<FileMap>;
12
+ export declare const getUpdatedAnalysisAndToolResultImagePart: (llmAnalysis: string, uniqueFrames: UniqueFrameWithMetadata[]) => Promise<{
13
+ updatedAnalysis: string;
14
+ keyFramesData: ToolResultImagePart[];
15
+ }>;
10
16
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/tools/fetch-video-analysis/utils.ts"],"names":[],"mappings":"AACA,OAAO,EACL,eAAe,EACf,iBAAiB,EAElB,MAAM,4BAA4B,CAAC;AAmCpC,eAAO,MAAM,gBAAgB,GAC3B,WAAW,IAAI,CAAC,iBAAiB,EAAE,cAAc,CAAC,EAClD,QAAQ;IACN,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1C,KAAK,EAAE,MAAM,CAAC;CACf,EAAE,EACH,WAAW,MAAM,KAChB,OAAO,CAAC,eAAe,EAAE,CAqB3B,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAC7B,WAAW,iBAAiB,EAC5B,cAAc,eAAe,EAAE,EAC/B,WAAW,MAAM,KAChB,OAAO,CAAC,MAAM,CAwChB,CAAC"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/tools/fetch-video-analysis/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EAGb,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,uBAAuB,EACvB,oBAAoB,EACrB,MAAM,4BAA4B,CAAC;AAuCpC,eAAO,MAAM,gBAAgB,GAC3B,WAAW,IAAI,CAAC,oBAAoB,EAAE,UAAU,CAAC,EACjD,QAAQ;IACN,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1C,KAAK,EAAE,MAAM,CAAC;CACf,EAAE,EACH,WAAW,MAAM,KAChB,OAAO,CAAC,eAAe,EAAE,CAqB3B,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC5B,WAAW,oBAAoB,EAC/B,WAAW,MAAM,KAChB,OAAO,CAAC,MAAM,CAsBhB,CAAC;AAEF,eAAO,MAAM,sBAAsB,GACjC,cAAc,uBAAuB,EAAE,EACvC,eAAe,MAAM,EACrB,cAAc,MAAM,KACnB,OAAO,CAAC,OAAO,CAsBjB,CAAC;AAEF,eAAO,MAAM,wCAAwC,GACnD,aAAa,MAAM,EACnB,cAAc,uBAAuB,EAAE,KACtC,OAAO,CAAC;IACT,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,mBAAmB,EAAE,CAAC;CACtC,CA2CA,CAAC"}
@@ -1,7 +1,13 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.uploadAnalysisToR2 = exports.uploadFramesToR2 = void 0;
6
+ exports.getUpdatedAnalysisAndToolResultImagePart = exports.zipAndUploadFramesToR2 = exports.uploadSummaryToR2 = exports.uploadFramesToR2 = void 0;
4
7
  const r2_uploader_1 = require("@empiricalrun/r2-uploader");
8
+ const child_process_1 = require("child_process");
9
+ const fs_1 = require("fs");
10
+ const path_1 = __importDefault(require("path"));
5
11
  const uploadFilesToR2 = async (files, videoUrlHash) => {
6
12
  const label = `video-analysis-upload:${videoUrlHash}`;
7
13
  const start = Date.now();
@@ -25,7 +31,7 @@ const uploadFilesToR2 = async (files, videoUrlHash) => {
25
31
  }
26
32
  };
27
33
  const uploadFramesToR2 = async (videoInfo, frames, r2BaseUrl) => {
28
- const { video_url_hash: videoUrlHash } = videoInfo;
34
+ const { analysis_id: videoUrlHash } = videoInfo;
29
35
  const frameFiles = frames.map((f) => {
30
36
  const fileName = `frame_${f.metadata.index}_${videoUrlHash}.png`;
31
37
  const buffer = Buffer.from(f.image, "base64");
@@ -41,21 +47,12 @@ const uploadFramesToR2 = async (videoInfo, frames, r2BaseUrl) => {
41
47
  }));
42
48
  };
43
49
  exports.uploadFramesToR2 = uploadFramesToR2;
44
- const uploadAnalysisToR2 = async (videoInfo, uniqueFrames, r2BaseUrl) => {
50
+ const uploadSummaryToR2 = async (videoInfo, r2BaseUrl) => {
45
51
  try {
46
- const { video_url_hash: videoUrlHash, total_frames_count, unique_frames_count, video_url, llm_analysis, created_at, } = videoInfo;
47
- const summary = {
48
- total_frames_count,
49
- unique_frames_count,
50
- video_url,
51
- llm_analysis,
52
- video_url_hash: videoUrlHash,
53
- uniqueFrames,
54
- created_at,
55
- };
52
+ const { analysis_id: videoUrlHash } = videoInfo;
56
53
  const filesToUpload = [
57
54
  {
58
- buffer: Buffer.from(JSON.stringify(summary, null, 2)),
55
+ buffer: Buffer.from(JSON.stringify(videoInfo, null, 2)),
59
56
  fileName: "summary.json",
60
57
  mimeType: "application/json",
61
58
  },
@@ -69,4 +66,56 @@ const uploadAnalysisToR2 = async (videoInfo, uniqueFrames, r2BaseUrl) => {
69
66
  throw error;
70
67
  }
71
68
  };
72
- exports.uploadAnalysisToR2 = uploadAnalysisToR2;
69
+ exports.uploadSummaryToR2 = uploadSummaryToR2;
70
+ const zipAndUploadFramesToR2 = async (uniqueFrames, outputZipPath, videoUrlHash) => {
71
+ const filePaths = uniqueFrames.map((u) => u.metadata.path);
72
+ await new Promise((resolve, reject) => {
73
+ (0, child_process_1.execFile)("zip", ["-0", "-j", outputZipPath, ...filePaths], (err) => {
74
+ if (err)
75
+ return reject(err);
76
+ resolve();
77
+ });
78
+ });
79
+ const tempUploadDir = path_1.default.dirname(outputZipPath);
80
+ console.log(`[zip-upload] Uploading zip file: ${outputZipPath} to video-analysis/${videoUrlHash}/`);
81
+ return await (0, r2_uploader_1.uploadDirectory)({
82
+ sourceDir: tempUploadDir,
83
+ fileList: [outputZipPath],
84
+ destinationDir: videoUrlHash,
85
+ uploadBucket: "video-analysis",
86
+ });
87
+ };
88
+ exports.zipAndUploadFramesToR2 = zipAndUploadFramesToR2;
89
+ const getUpdatedAnalysisAndToolResultImagePart = async (llmAnalysis, uniqueFrames) => {
90
+ const keyFramesMatch = llmAnalysis.match(/KEY FRAMES:\s*(.+)$/);
91
+ const updatedAnalysis = keyFramesMatch
92
+ ? llmAnalysis.replace(/\n\nKEY FRAMES:\s*(.+)$/, "")
93
+ : llmAnalysis;
94
+ const keyFrameIndices = keyFramesMatch?.[1]
95
+ ?.match(/<frame_(\d+)>/g)
96
+ ?.map((match) => parseInt(match.match(/<frame_(\d+)>/)?.[1] || "0", 10)) || [];
97
+ const frameDataMap = new Map();
98
+ await Promise.all(uniqueFrames.map(async (frame) => {
99
+ try {
100
+ const fileBuffer = await fs_1.promises.readFile(frame.metadata.path);
101
+ const base64Data = fileBuffer.toString("base64");
102
+ frameDataMap.set(frame.metadata.index, base64Data);
103
+ }
104
+ catch (error) {
105
+ console.error(`Failed to read frame file ${frame.metadata.path}:`, error);
106
+ }
107
+ }));
108
+ const keyFramesData = keyFrameIndices
109
+ .map((frameIndex) => {
110
+ const base64Data = frameDataMap.get(frameIndex);
111
+ return base64Data
112
+ ? {
113
+ type: "image/png",
114
+ base64Data: base64Data,
115
+ }
116
+ : null;
117
+ })
118
+ .filter(Boolean);
119
+ return { updatedAnalysis, keyFramesData };
120
+ };
121
+ exports.getUpdatedAnalysisAndToolResultImagePart = getUpdatedAnalysisAndToolResultImagePart;