donobu 5.20.0 → 5.21.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/dist/apis/FlowsAiQueriesApi.d.ts +20 -0
- package/dist/apis/FlowsAiQueriesApi.js +27 -0
- package/dist/clients/OllamaGptClient.d.ts +1 -1
- package/dist/clients/OllamaGptClient.js +19 -10
- package/dist/esm/apis/FlowsAiQueriesApi.d.ts +20 -0
- package/dist/esm/apis/FlowsAiQueriesApi.js +27 -0
- package/dist/esm/clients/OllamaGptClient.d.ts +1 -1
- package/dist/esm/clients/OllamaGptClient.js +19 -10
- package/dist/esm/exceptions/GptModelCapabilityException.d.ts +12 -0
- package/dist/esm/exceptions/GptModelCapabilityException.js +19 -0
- package/dist/esm/managers/AdminApiController.js +3 -0
- package/dist/esm/managers/DonobuFlow.d.ts +2 -0
- package/dist/esm/managers/DonobuFlow.js +39 -2
- package/dist/esm/managers/DonobuFlowsManager.d.ts +3 -0
- package/dist/esm/managers/DonobuFlowsManager.js +4 -0
- package/dist/esm/managers/FlowCatalog.d.ts +2 -0
- package/dist/esm/managers/FlowCatalog.js +19 -0
- package/dist/esm/models/AiQuery.d.ts +42 -0
- package/dist/esm/models/AiQuery.js +3 -0
- package/dist/esm/persistence/DonobuSqliteDb.js +17 -0
- package/dist/esm/persistence/flows/FlowsPersistence.d.ts +10 -0
- package/dist/esm/persistence/flows/FlowsPersistenceDonobuApi.d.ts +3 -0
- package/dist/esm/persistence/flows/FlowsPersistenceDonobuApi.js +18 -0
- package/dist/esm/persistence/flows/FlowsPersistenceSqlite.d.ts +3 -0
- package/dist/esm/persistence/flows/FlowsPersistenceSqlite.js +10 -0
- package/dist/esm/persistence/flows/FlowsPersistenceVolatile.d.ts +5 -1
- package/dist/esm/persistence/flows/FlowsPersistenceVolatile.js +23 -1
- package/dist/exceptions/GptModelCapabilityException.d.ts +12 -0
- package/dist/exceptions/GptModelCapabilityException.js +19 -0
- package/dist/managers/AdminApiController.js +3 -0
- package/dist/managers/DonobuFlow.d.ts +2 -0
- package/dist/managers/DonobuFlow.js +39 -2
- package/dist/managers/DonobuFlowsManager.d.ts +3 -0
- package/dist/managers/DonobuFlowsManager.js +4 -0
- package/dist/managers/FlowCatalog.d.ts +2 -0
- package/dist/managers/FlowCatalog.js +19 -0
- package/dist/models/AiQuery.d.ts +42 -0
- package/dist/models/AiQuery.js +3 -0
- package/dist/persistence/DonobuSqliteDb.js +17 -0
- package/dist/persistence/flows/FlowsPersistence.d.ts +10 -0
- package/dist/persistence/flows/FlowsPersistenceDonobuApi.d.ts +3 -0
- package/dist/persistence/flows/FlowsPersistenceDonobuApi.js +18 -0
- package/dist/persistence/flows/FlowsPersistenceSqlite.d.ts +3 -0
- package/dist/persistence/flows/FlowsPersistenceSqlite.js +10 -0
- package/dist/persistence/flows/FlowsPersistenceVolatile.d.ts +5 -1
- package/dist/persistence/flows/FlowsPersistenceVolatile.js +23 -1
- package/package.json +1 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Request, Response } from 'express';
|
|
2
|
+
import type { DonobuFlowsManager } from '../managers/DonobuFlowsManager';
|
|
3
|
+
/**
|
|
4
|
+
* API controller for retrieving AI query records within Donobu flows.
|
|
5
|
+
*
|
|
6
|
+
* AI queries represent the decision cycles where the AI analyzed the current
|
|
7
|
+
* page state (via clean and annotated screenshots) before deciding on the next
|
|
8
|
+
* action. Each record includes the screenshots the AI was shown, the
|
|
9
|
+
* interactable elements it could choose from, and any error that occurred
|
|
10
|
+
* during the query.
|
|
11
|
+
*/
|
|
12
|
+
export declare class FlowsAiQueriesApi {
|
|
13
|
+
private readonly donobuFlowsManager;
|
|
14
|
+
constructor(donobuFlowsManager: DonobuFlowsManager);
|
|
15
|
+
/**
|
|
16
|
+
* Retrieves all AI query records for a specific flow, ordered by query time.
|
|
17
|
+
*/
|
|
18
|
+
getAiQueries(req: Request, res: Response): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=FlowsAiQueriesApi.d.ts.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FlowsAiQueriesApi = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* API controller for retrieving AI query records within Donobu flows.
|
|
6
|
+
*
|
|
7
|
+
* AI queries represent the decision cycles where the AI analyzed the current
|
|
8
|
+
* page state (via clean and annotated screenshots) before deciding on the next
|
|
9
|
+
* action. Each record includes the screenshots the AI was shown, the
|
|
10
|
+
* interactable elements it could choose from, and any error that occurred
|
|
11
|
+
* during the query.
|
|
12
|
+
*/
|
|
13
|
+
class FlowsAiQueriesApi {
|
|
14
|
+
constructor(donobuFlowsManager) {
|
|
15
|
+
this.donobuFlowsManager = donobuFlowsManager;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Retrieves all AI query records for a specific flow, ordered by query time.
|
|
19
|
+
*/
|
|
20
|
+
async getAiQueries(req, res) {
|
|
21
|
+
const flowId = String(req.params.flowId);
|
|
22
|
+
const aiQueries = await this.donobuFlowsManager.getAiQueries(flowId);
|
|
23
|
+
res.json(aiQueries);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
exports.FlowsAiQueriesApi = FlowsAiQueriesApi;
|
|
27
|
+
//# sourceMappingURL=FlowsAiQueriesApi.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ZodType } from 'zod/v4';
|
|
2
2
|
import type { OllamaConfig } from '../models/GptConfig';
|
|
3
3
|
import type { AssistantMessage, GptMessage, ProposedToolCallsMessage, StructuredOutputMessage } from '../models/GptMessage';
|
|
4
4
|
import type { ToolOption } from './GptClient';
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.OllamaGptClient = void 0;
|
|
4
|
+
const v4_1 = require("zod/v4");
|
|
5
|
+
const GptModelCapabilityException_1 = require("../exceptions/GptModelCapabilityException");
|
|
4
6
|
const GptModelNotFoundException_1 = require("../exceptions/GptModelNotFoundException");
|
|
5
7
|
const GptPlatformNotReachableException_1 = require("../exceptions/GptPlatformNotReachableException");
|
|
6
8
|
const Logger_1 = require("../utils/Logger");
|
|
@@ -23,9 +25,15 @@ class OllamaGptClient extends GptClient_1.GptClient {
|
|
|
23
25
|
}
|
|
24
26
|
async ping(options) {
|
|
25
27
|
const signal = options?.signal ?? AbortSignal.timeout(10_000);
|
|
28
|
+
const modelName = this.config.modelName;
|
|
26
29
|
let resp;
|
|
27
30
|
try {
|
|
28
|
-
resp = await fetch(`${this.apiUrl}/api/
|
|
31
|
+
resp = await fetch(`${this.apiUrl}/api/show`, {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: { 'Content-Type': 'application/json' },
|
|
34
|
+
body: JSON.stringify({ model: modelName }),
|
|
35
|
+
signal,
|
|
36
|
+
});
|
|
29
37
|
}
|
|
30
38
|
catch (error) {
|
|
31
39
|
if (error instanceof TypeError) {
|
|
@@ -34,18 +42,19 @@ class OllamaGptClient extends GptClient_1.GptClient {
|
|
|
34
42
|
}
|
|
35
43
|
throw error;
|
|
36
44
|
}
|
|
45
|
+
if (resp.status === 404) {
|
|
46
|
+
throw new GptModelNotFoundException_1.GptModelNotFoundException(this.config.type, modelName);
|
|
47
|
+
}
|
|
37
48
|
if (!resp.ok) {
|
|
38
49
|
throw new GptPlatformNotReachableException_1.GptPlatformNotReachableException(this.config.type);
|
|
39
50
|
}
|
|
40
|
-
const data =
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (!found) {
|
|
48
|
-
throw new GptModelNotFoundException_1.GptModelNotFoundException(this.config.type, modelName);
|
|
51
|
+
const data = v4_1.z
|
|
52
|
+
.object({ capabilities: v4_1.z.array(v4_1.z.string()).default([]) })
|
|
53
|
+
.parse(await resp.json());
|
|
54
|
+
const requiredCapabilities = ['completion', 'tools', 'vision'];
|
|
55
|
+
const missingCapabilities = requiredCapabilities.filter((cap) => !data.capabilities.includes(cap));
|
|
56
|
+
if (missingCapabilities.length > 0) {
|
|
57
|
+
throw new GptModelCapabilityException_1.GptModelCapabilityException(this.config.type, modelName, missingCapabilities);
|
|
49
58
|
}
|
|
50
59
|
}
|
|
51
60
|
async getMessage(messages, options) {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Request, Response } from 'express';
|
|
2
|
+
import type { DonobuFlowsManager } from '../managers/DonobuFlowsManager';
|
|
3
|
+
/**
|
|
4
|
+
* API controller for retrieving AI query records within Donobu flows.
|
|
5
|
+
*
|
|
6
|
+
* AI queries represent the decision cycles where the AI analyzed the current
|
|
7
|
+
* page state (via clean and annotated screenshots) before deciding on the next
|
|
8
|
+
* action. Each record includes the screenshots the AI was shown, the
|
|
9
|
+
* interactable elements it could choose from, and any error that occurred
|
|
10
|
+
* during the query.
|
|
11
|
+
*/
|
|
12
|
+
export declare class FlowsAiQueriesApi {
|
|
13
|
+
private readonly donobuFlowsManager;
|
|
14
|
+
constructor(donobuFlowsManager: DonobuFlowsManager);
|
|
15
|
+
/**
|
|
16
|
+
* Retrieves all AI query records for a specific flow, ordered by query time.
|
|
17
|
+
*/
|
|
18
|
+
getAiQueries(req: Request, res: Response): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=FlowsAiQueriesApi.d.ts.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FlowsAiQueriesApi = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* API controller for retrieving AI query records within Donobu flows.
|
|
6
|
+
*
|
|
7
|
+
* AI queries represent the decision cycles where the AI analyzed the current
|
|
8
|
+
* page state (via clean and annotated screenshots) before deciding on the next
|
|
9
|
+
* action. Each record includes the screenshots the AI was shown, the
|
|
10
|
+
* interactable elements it could choose from, and any error that occurred
|
|
11
|
+
* during the query.
|
|
12
|
+
*/
|
|
13
|
+
class FlowsAiQueriesApi {
|
|
14
|
+
constructor(donobuFlowsManager) {
|
|
15
|
+
this.donobuFlowsManager = donobuFlowsManager;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Retrieves all AI query records for a specific flow, ordered by query time.
|
|
19
|
+
*/
|
|
20
|
+
async getAiQueries(req, res) {
|
|
21
|
+
const flowId = String(req.params.flowId);
|
|
22
|
+
const aiQueries = await this.donobuFlowsManager.getAiQueries(flowId);
|
|
23
|
+
res.json(aiQueries);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
exports.FlowsAiQueriesApi = FlowsAiQueriesApi;
|
|
27
|
+
//# sourceMappingURL=FlowsAiQueriesApi.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ZodType } from 'zod/v4';
|
|
2
2
|
import type { OllamaConfig } from '../models/GptConfig';
|
|
3
3
|
import type { AssistantMessage, GptMessage, ProposedToolCallsMessage, StructuredOutputMessage } from '../models/GptMessage';
|
|
4
4
|
import type { ToolOption } from './GptClient';
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.OllamaGptClient = void 0;
|
|
4
|
+
const v4_1 = require("zod/v4");
|
|
5
|
+
const GptModelCapabilityException_1 = require("../exceptions/GptModelCapabilityException");
|
|
4
6
|
const GptModelNotFoundException_1 = require("../exceptions/GptModelNotFoundException");
|
|
5
7
|
const GptPlatformNotReachableException_1 = require("../exceptions/GptPlatformNotReachableException");
|
|
6
8
|
const Logger_1 = require("../utils/Logger");
|
|
@@ -23,9 +25,15 @@ class OllamaGptClient extends GptClient_1.GptClient {
|
|
|
23
25
|
}
|
|
24
26
|
async ping(options) {
|
|
25
27
|
const signal = options?.signal ?? AbortSignal.timeout(10_000);
|
|
28
|
+
const modelName = this.config.modelName;
|
|
26
29
|
let resp;
|
|
27
30
|
try {
|
|
28
|
-
resp = await fetch(`${this.apiUrl}/api/
|
|
31
|
+
resp = await fetch(`${this.apiUrl}/api/show`, {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: { 'Content-Type': 'application/json' },
|
|
34
|
+
body: JSON.stringify({ model: modelName }),
|
|
35
|
+
signal,
|
|
36
|
+
});
|
|
29
37
|
}
|
|
30
38
|
catch (error) {
|
|
31
39
|
if (error instanceof TypeError) {
|
|
@@ -34,18 +42,19 @@ class OllamaGptClient extends GptClient_1.GptClient {
|
|
|
34
42
|
}
|
|
35
43
|
throw error;
|
|
36
44
|
}
|
|
45
|
+
if (resp.status === 404) {
|
|
46
|
+
throw new GptModelNotFoundException_1.GptModelNotFoundException(this.config.type, modelName);
|
|
47
|
+
}
|
|
37
48
|
if (!resp.ok) {
|
|
38
49
|
throw new GptPlatformNotReachableException_1.GptPlatformNotReachableException(this.config.type);
|
|
39
50
|
}
|
|
40
|
-
const data =
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (!found) {
|
|
48
|
-
throw new GptModelNotFoundException_1.GptModelNotFoundException(this.config.type, modelName);
|
|
51
|
+
const data = v4_1.z
|
|
52
|
+
.object({ capabilities: v4_1.z.array(v4_1.z.string()).default([]) })
|
|
53
|
+
.parse(await resp.json());
|
|
54
|
+
const requiredCapabilities = ['completion', 'tools', 'vision'];
|
|
55
|
+
const missingCapabilities = requiredCapabilities.filter((cap) => !data.capabilities.includes(cap));
|
|
56
|
+
if (missingCapabilities.length > 0) {
|
|
57
|
+
throw new GptModelCapabilityException_1.GptModelCapabilityException(this.config.type, modelName, missingCapabilities);
|
|
49
58
|
}
|
|
50
59
|
}
|
|
51
60
|
async getMessage(messages, options) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { DonobuException } from './DonobuException';
|
|
2
|
+
/**
|
|
3
|
+
* Thrown when a model exists but lacks required capabilities
|
|
4
|
+
* (e.g. completion, tools, or vision).
|
|
5
|
+
*/
|
|
6
|
+
export declare class GptModelCapabilityException extends DonobuException {
|
|
7
|
+
readonly platform: string;
|
|
8
|
+
readonly gptModel: string;
|
|
9
|
+
readonly missingCapabilities: string[];
|
|
10
|
+
constructor(platform: string, gptModel: string, missingCapabilities: string[]);
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=GptModelCapabilityException.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GptModelCapabilityException = void 0;
|
|
4
|
+
const DonobuException_1 = require("./DonobuException");
|
|
5
|
+
/**
|
|
6
|
+
* Thrown when a model exists but lacks required capabilities
|
|
7
|
+
* (e.g. completion, tools, or vision).
|
|
8
|
+
*/
|
|
9
|
+
class GptModelCapabilityException extends DonobuException_1.DonobuException {
|
|
10
|
+
constructor(platform, gptModel, missingCapabilities) {
|
|
11
|
+
super(`The model '${gptModel}' on '${platform}' is missing required capabilities: ${missingCapabilities.join(', ')}. ` +
|
|
12
|
+
`Please choose a model that supports completion, tools, and vision.`);
|
|
13
|
+
this.platform = platform;
|
|
14
|
+
this.gptModel = gptModel;
|
|
15
|
+
this.missingCapabilities = missingCapabilities;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.GptModelCapabilityException = GptModelCapabilityException;
|
|
19
|
+
//# sourceMappingURL=GptModelCapabilityException.js.map
|
|
@@ -9,6 +9,7 @@ const v4_1 = require("zod/v4");
|
|
|
9
9
|
const AgentsApi_1 = require("../apis/AgentsApi");
|
|
10
10
|
const AskAiApi_1 = require("../apis/AskAiApi");
|
|
11
11
|
const EnvDataApi_1 = require("../apis/EnvDataApi");
|
|
12
|
+
const FlowsAiQueriesApi_1 = require("../apis/FlowsAiQueriesApi");
|
|
12
13
|
const FlowsApi_1 = require("../apis/FlowsApi");
|
|
13
14
|
const FlowsFilesApi_1 = require("../apis/FlowsFilesApi");
|
|
14
15
|
const FlowsToolCallsApi_1 = require("../apis/FlowsToolCallsApi");
|
|
@@ -237,6 +238,7 @@ class AdminApiController {
|
|
|
237
238
|
flowsApi: new FlowsApi_1.FlowsApi(donobuStack.flowsManager),
|
|
238
239
|
flowsFilesApi: new FlowsFilesApi_1.FlowsFilesApi(donobuStack.flowsPersistenceRegistry),
|
|
239
240
|
flowsToolCallsApi: new FlowsToolCallsApi_1.FlowsToolCallsApi(donobuStack.flowsManager),
|
|
241
|
+
flowsAiQueriesApi: new FlowsAiQueriesApi_1.FlowsAiQueriesApi(donobuStack.flowsManager),
|
|
240
242
|
pingApi: new PingApi_1.PingApi(),
|
|
241
243
|
schemaApi: new SchemaApi_1.SchemaApi(),
|
|
242
244
|
targetsApi: new TargetsApi_1.TargetsApi(donobuStack.targetRuntimePlugins),
|
|
@@ -308,6 +310,7 @@ class AdminApiController {
|
|
|
308
310
|
app.get('/api/flows/:flowId/video', this.asyncHandler(apis.flowsFilesApi.getFlowVideo.bind(apis.flowsFilesApi)));
|
|
309
311
|
app.get('/api/flows/:flowId/tool-calls', this.asyncHandler(apis.flowsToolCallsApi.getToolCalls.bind(apis.flowsToolCallsApi)));
|
|
310
312
|
app.get('/api/flows/:flowId/tool-calls/:toolCallId', this.asyncHandler(apis.flowsToolCallsApi.getToolCall.bind(apis.flowsToolCallsApi)));
|
|
313
|
+
app.get('/api/flows/:flowId/ai-queries', this.asyncHandler(apis.flowsAiQueriesApi.getAiQueries.bind(apis.flowsAiQueriesApi)));
|
|
311
314
|
app.get('/api/flows/:flowId/logs', this.asyncHandler(apis.flowsApi.getFlowLogs.bind(apis.flowsApi)));
|
|
312
315
|
}
|
|
313
316
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { z } from 'zod/v4';
|
|
2
2
|
import type { GptClient } from '../clients/GptClient';
|
|
3
|
+
import type { AiQuery } from '../models/AiQuery';
|
|
3
4
|
import type { ControlPanel } from '../models/ControlPanel';
|
|
4
5
|
import type { FlowMetadata } from '../models/FlowMetadata';
|
|
5
6
|
import type { GptMessage, StructuredOutputMessage, TextItem } from '../models/GptMessage';
|
|
@@ -40,6 +41,7 @@ export declare class DonobuFlow {
|
|
|
40
41
|
private static readonly MAIN_MESSAGE_ELEMENT_LIST_MARKER;
|
|
41
42
|
static readonly USER_INTERRUPT_MARKER = "[User interruption while flow was paused, this MUST be acknowledged]";
|
|
42
43
|
inProgressToolCall: ToolCall | null;
|
|
44
|
+
readonly aiQueries: AiQuery[];
|
|
43
45
|
constructor(flowsManager: DonobuFlowsManager, envData: Record<string, string>, persistence: FlowsPersistence, gptClient: GptClient | null, toolManager: ToolManager, interactionVisualizer: InteractionVisualizer, proposedToolCalls: ProposedToolCall[], invokedToolCalls: ToolCall[], gptMessages: GptMessage[], targetInspector: TargetInspector, metadata: FlowMetadata, controlPanel: ControlPanel);
|
|
44
46
|
/**
|
|
45
47
|
* Drives the entire Donobu flow state-machine until it reaches a
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DonobuFlow = void 0;
|
|
4
4
|
exports.extractFromPage = extractFromPage;
|
|
5
|
+
const crypto_1 = require("crypto");
|
|
5
6
|
const GptPlatformInternalErrorException_1 = require("../exceptions/GptPlatformInternalErrorException");
|
|
6
7
|
const UserInterruptException_1 = require("../exceptions/UserInterruptException");
|
|
7
8
|
const FlowMetadata_1 = require("../models/FlowMetadata");
|
|
@@ -104,6 +105,7 @@ class DonobuFlow {
|
|
|
104
105
|
this.metadata = metadata;
|
|
105
106
|
this.controlPanel = controlPanel;
|
|
106
107
|
this.inProgressToolCall = null;
|
|
108
|
+
this.aiQueries = [];
|
|
107
109
|
}
|
|
108
110
|
/**
|
|
109
111
|
* Drives the entire Donobu flow state-machine until it reaches a
|
|
@@ -909,18 +911,42 @@ Message: ${dialog.message()}`;
|
|
|
909
911
|
}
|
|
910
912
|
async queryGptForProposedToolCalls() {
|
|
911
913
|
this.targetInspector.checkConnectedOrThrow();
|
|
914
|
+
// Initialise the AI query record immediately so the error handler always
|
|
915
|
+
// has a record to update — no conditional check needed.
|
|
916
|
+
let aiQuery = {
|
|
917
|
+
id: (0, crypto_1.randomUUID)(),
|
|
918
|
+
cleanScreenshotId: null,
|
|
919
|
+
annotatedScreenshotId: null,
|
|
920
|
+
interactableElements: null,
|
|
921
|
+
error: null,
|
|
922
|
+
startedAt: Date.now(),
|
|
923
|
+
completedAt: null,
|
|
924
|
+
};
|
|
925
|
+
this.aiQueries.push(aiQuery);
|
|
912
926
|
try {
|
|
913
927
|
// Discover and mark all interactable elements on the current screen/page.
|
|
914
928
|
await this.targetInspector.attributeInteractableElements();
|
|
915
929
|
// Capture clean and annotated screenshots. Each inspector implementation
|
|
916
930
|
// handles the platform-specific details (DOM injection vs server-side compositing).
|
|
917
931
|
const screenshotBytes = await this.targetInspector.takeCleanScreenshot();
|
|
918
|
-
await this.persistence.saveScreenShot(this.metadata.id, screenshotBytes);
|
|
932
|
+
const cleanScreenshotId = await this.persistence.saveScreenShot(this.metadata.id, screenshotBytes);
|
|
919
933
|
await this.targetInspector.annotateInteractableElements();
|
|
920
934
|
const annotatedScreenShotBytes = await this.targetInspector.takeAnnotatedScreenshot();
|
|
921
935
|
await this.targetInspector.removeAnnotations();
|
|
922
|
-
await this.persistence.saveScreenShot(this.metadata.id, annotatedScreenShotBytes);
|
|
936
|
+
const annotatedScreenshotId = await this.persistence.saveScreenShot(this.metadata.id, annotatedScreenShotBytes);
|
|
923
937
|
const interactableElements = await this.targetInspector.getAttributedInteractableElements();
|
|
938
|
+
// Fill in the remaining fields and persist so the frontend can display
|
|
939
|
+
// the record immediately.
|
|
940
|
+
aiQuery = {
|
|
941
|
+
...aiQuery,
|
|
942
|
+
cleanScreenshotId,
|
|
943
|
+
annotatedScreenshotId,
|
|
944
|
+
interactableElements,
|
|
945
|
+
};
|
|
946
|
+
this.aiQueries[this.aiQueries.length - 1] = aiQuery;
|
|
947
|
+
await this.persistence
|
|
948
|
+
.setAiQuery(this.metadata.id, aiQuery)
|
|
949
|
+
.catch((err) => Logger_1.appLogger.error('Failed to persist AI query record', err));
|
|
924
950
|
const mainMessage = DonobuFlow.createMainUserMessage(this.targetInspector, interactableElements);
|
|
925
951
|
// Give the LLM both the pre and post annotated screenshots. It can
|
|
926
952
|
// use the clean screenshot to decide what it wants to do, then map it to
|
|
@@ -945,9 +971,20 @@ Message: ${dialog.message()}`;
|
|
|
945
971
|
}));
|
|
946
972
|
Logger_1.appLogger.debug('LLM response:', JsonUtils_1.JsonUtils.objectToJson(proposedToolCallsMessage));
|
|
947
973
|
MiscUtils_1.MiscUtils.updateTokenCounts(proposedToolCallsMessage, this.metadata);
|
|
974
|
+
aiQuery = { ...aiQuery, completedAt: Date.now() };
|
|
975
|
+
this.aiQueries[this.aiQueries.length - 1] = aiQuery;
|
|
976
|
+
await this.persistence
|
|
977
|
+
.setAiQuery(this.metadata.id, aiQuery)
|
|
978
|
+
.catch((err) => Logger_1.appLogger.error('Failed to persist AI query completion', err));
|
|
948
979
|
return proposedToolCallsMessage;
|
|
949
980
|
}
|
|
950
981
|
catch (error) {
|
|
982
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
983
|
+
aiQuery = { ...aiQuery, error: errorMessage, completedAt: Date.now() };
|
|
984
|
+
this.aiQueries[this.aiQueries.length - 1] = aiQuery;
|
|
985
|
+
await this.persistence
|
|
986
|
+
.setAiQuery(this.metadata.id, aiQuery)
|
|
987
|
+
.catch((err) => Logger_1.appLogger.error('Failed to persist AI query error', err));
|
|
951
988
|
if (this.targetInspector.isTargetClosedError(error)) {
|
|
952
989
|
this.targetInspector.checkConnectedOrThrow();
|
|
953
990
|
}
|
|
@@ -5,6 +5,7 @@ import type { GptClient } from '../clients/GptClient';
|
|
|
5
5
|
import type { GptClientFactory } from '../clients/GptClientFactory';
|
|
6
6
|
import type { GeneratedProject } from '../codegen/CodeGenerator';
|
|
7
7
|
import type { env } from '../envVars';
|
|
8
|
+
import type { AiQuery } from '../models/AiQuery';
|
|
8
9
|
import type { BrowserStateReference } from '../models/BrowserStateFlowReference';
|
|
9
10
|
import type { BrowserStorageState } from '../models/BrowserStorageState';
|
|
10
11
|
import type { CodeGenerationOptions } from '../models/CodeGenerationOptions';
|
|
@@ -109,6 +110,8 @@ export declare class DonobuFlowsManager {
|
|
|
109
110
|
getFlowByName(flowName: string): Promise<FlowMetadata>;
|
|
110
111
|
/** Returns all the tool calls made by the given flow by ID. */
|
|
111
112
|
getToolCalls(flowId: string): Promise<ToolCall[]>;
|
|
113
|
+
/** Returns all AI query records for the given flow by ID. */
|
|
114
|
+
getAiQueries(flowId: string): Promise<AiQuery[]>;
|
|
112
115
|
/**
|
|
113
116
|
* Attempts to delete a flow by ID. If the flow is active, then
|
|
114
117
|
* `CannotDeleteRunningFlowException` is thrown. If the flow is not active,
|
|
@@ -374,6 +374,10 @@ class DonobuFlowsManager {
|
|
|
374
374
|
async getToolCalls(flowId) {
|
|
375
375
|
return this.flowCatalog.getToolCalls(flowId);
|
|
376
376
|
}
|
|
377
|
+
/** Returns all AI query records for the given flow by ID. */
|
|
378
|
+
async getAiQueries(flowId) {
|
|
379
|
+
return this.flowCatalog.getAiQueries(flowId);
|
|
380
|
+
}
|
|
377
381
|
/**
|
|
378
382
|
* Attempts to delete a flow by ID. If the flow is active, then
|
|
379
383
|
* `CannotDeleteRunningFlowException` is thrown. If the flow is not active,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AiQuery } from '../models/AiQuery';
|
|
1
2
|
import type { BrowserStorageState } from '../models/BrowserStorageState';
|
|
2
3
|
import type { DonobuDeploymentEnvironment } from '../models/DonobuDeploymentEnvironment';
|
|
3
4
|
import type { FlowMetadata, FlowsQuery } from '../models/FlowMetadata';
|
|
@@ -21,6 +22,7 @@ export declare class FlowCatalog {
|
|
|
21
22
|
getFlowById(flowId: string): Promise<FlowMetadata>;
|
|
22
23
|
getFlowByName(flowName: string): Promise<FlowMetadata>;
|
|
23
24
|
getToolCalls(flowId: string): Promise<ToolCall[]>;
|
|
25
|
+
getAiQueries(flowId: string): Promise<AiQuery[]>;
|
|
24
26
|
deleteFlow(flowId: string): Promise<void>;
|
|
25
27
|
getBrowserState(flowId: string): Promise<BrowserStorageState | null>;
|
|
26
28
|
getFlows(query: FlowsQuery): Promise<PaginatedResult<FlowMetadata>>;
|
|
@@ -71,6 +71,25 @@ class FlowCatalog {
|
|
|
71
71
|
}
|
|
72
72
|
throw FlowNotFoundException_1.FlowNotFoundException.forId(flowId);
|
|
73
73
|
}
|
|
74
|
+
async getAiQueries(flowId) {
|
|
75
|
+
if (this.isLocal()) {
|
|
76
|
+
const activeHandle = this.flowRuntime.get(flowId);
|
|
77
|
+
if (activeHandle) {
|
|
78
|
+
return [...activeHandle.donobuFlow.aiQueries].sort((a, b) => a.startedAt - b.startedAt);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (const persistence of await this.flowsPersistenceRegistry.getAll()) {
|
|
82
|
+
try {
|
|
83
|
+
return await persistence.getAiQueries(flowId);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (!(error instanceof FlowNotFoundException_1.FlowNotFoundException)) {
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
throw FlowNotFoundException_1.FlowNotFoundException.forId(flowId);
|
|
92
|
+
}
|
|
74
93
|
async deleteFlow(flowId) {
|
|
75
94
|
for (const persistence of await this.flowsPersistenceRegistry.getAll()) {
|
|
76
95
|
await persistence.deleteFlow(flowId);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { InteractableElement } from './InteractableElement';
|
|
2
|
+
/**
|
|
3
|
+
* Represents a single AI decision cycle within a flow. Created at the start of
|
|
4
|
+
* a query cycle with just `id` and `queriedAt`, then progressively filled in as
|
|
5
|
+
* screenshots are captured and elements are discovered. The `error` field is
|
|
6
|
+
* populated only if the query fails.
|
|
7
|
+
*/
|
|
8
|
+
export type AiQuery = {
|
|
9
|
+
/**
|
|
10
|
+
* Unique identifier for this AI query.
|
|
11
|
+
*/
|
|
12
|
+
readonly id: string;
|
|
13
|
+
/**
|
|
14
|
+
* The ID of the clean (un-annotated) screenshot of the page at query time.
|
|
15
|
+
* Null until the screenshot is captured.
|
|
16
|
+
*/
|
|
17
|
+
readonly cleanScreenshotId: string | null;
|
|
18
|
+
/**
|
|
19
|
+
* The ID of the annotated screenshot (with numbered element badges) sent to the AI.
|
|
20
|
+
* Null until the screenshot is captured.
|
|
21
|
+
*/
|
|
22
|
+
readonly annotatedScreenshotId: string | null;
|
|
23
|
+
/**
|
|
24
|
+
* The interactable elements that were identified and sent to the AI.
|
|
25
|
+
* Null until element discovery completes.
|
|
26
|
+
*/
|
|
27
|
+
readonly interactableElements: InteractableElement[] | null;
|
|
28
|
+
/**
|
|
29
|
+
* If the AI query failed, the error message. Null on success.
|
|
30
|
+
*/
|
|
31
|
+
readonly error: string | null;
|
|
32
|
+
/**
|
|
33
|
+
* The Unix epoch millisecond timestamp of when the query was initiated.
|
|
34
|
+
*/
|
|
35
|
+
readonly startedAt: number;
|
|
36
|
+
/**
|
|
37
|
+
* The Unix epoch millisecond timestamp of when the query completed (success
|
|
38
|
+
* or failure). Null while the query is still in progress.
|
|
39
|
+
*/
|
|
40
|
+
readonly completedAt: number | null;
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=AiQuery.d.ts.map
|
|
@@ -395,6 +395,23 @@ CREATE INDEX IF NOT EXISTS idx_test_metadata_suite_id ON test_metadata(suite_id)
|
|
|
395
395
|
|
|
396
396
|
ALTER TABLE flow_metadata ADD COLUMN test_id TEXT NULL REFERENCES test_metadata(id) ON DELETE CASCADE;
|
|
397
397
|
CREATE INDEX IF NOT EXISTS idx_flow_metadata_test_id ON flow_metadata(test_id);
|
|
398
|
+
`);
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
// Create the ai_queries table to store AI decision-cycle records (clean
|
|
403
|
+
// and annotated screenshots, interactable elements, optional error).
|
|
404
|
+
version: 11,
|
|
405
|
+
up: (db) => {
|
|
406
|
+
db.exec(`
|
|
407
|
+
CREATE TABLE IF NOT EXISTS ai_queries (
|
|
408
|
+
id TEXT PRIMARY KEY,
|
|
409
|
+
flow_id TEXT NOT NULL,
|
|
410
|
+
started_at INTEGER NOT NULL,
|
|
411
|
+
ai_query TEXT NOT NULL,
|
|
412
|
+
FOREIGN KEY (flow_id) REFERENCES flow_metadata(id) ON DELETE CASCADE
|
|
413
|
+
);
|
|
414
|
+
CREATE INDEX IF NOT EXISTS idx_ai_queries_flow_id_started_at ON ai_queries(flow_id, started_at);
|
|
398
415
|
`);
|
|
399
416
|
},
|
|
400
417
|
},
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AiQuery } from '../../models/AiQuery';
|
|
1
2
|
import type { BrowserStorageState } from '../../models/BrowserStorageState';
|
|
2
3
|
import type { FlowMetadata, FlowsQuery } from '../../models/FlowMetadata';
|
|
3
4
|
import type { PaginatedResult } from '../../models/PaginatedResult';
|
|
@@ -50,6 +51,15 @@ export interface FlowsPersistence {
|
|
|
50
51
|
* Delete a persisted tool call from a specific flow.
|
|
51
52
|
*/
|
|
52
53
|
deleteToolCall(flowId: string, toolCallId: string): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Save an AI query record for a specific flow.
|
|
56
|
+
*/
|
|
57
|
+
setAiQuery(flowId: string, aiQuery: AiQuery): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Load all AI query records for a specific flow, ordered by queriedAt.
|
|
60
|
+
* @throws FlowNotFoundException if the flow is not found
|
|
61
|
+
*/
|
|
62
|
+
getAiQueries(flowId: string): Promise<AiQuery[]>;
|
|
53
63
|
/**
|
|
54
64
|
* Set video data for a specific flow.
|
|
55
65
|
*/
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AiQuery } from '../../models/AiQuery';
|
|
1
2
|
import { type BrowserStorageState } from '../../models/BrowserStorageState';
|
|
2
3
|
import type { FlowMetadata, FlowsQuery } from '../../models/FlowMetadata';
|
|
3
4
|
import type { PaginatedResult } from '../../models/PaginatedResult';
|
|
@@ -28,6 +29,8 @@ export declare class FlowsPersistenceDonobuApi implements FlowsPersistence {
|
|
|
28
29
|
setToolCall(flowId: string, toolCall: ToolCall): Promise<void>;
|
|
29
30
|
getToolCalls(flowId: string): Promise<ToolCall[]>;
|
|
30
31
|
deleteToolCall(flowId: string, toolCallId: string): Promise<void>;
|
|
32
|
+
setAiQuery(flowId: string, aiQuery: AiQuery): Promise<void>;
|
|
33
|
+
getAiQueries(flowId: string): Promise<AiQuery[]>;
|
|
31
34
|
saveScreenShot(flowId: string, bytes: Buffer): Promise<string>;
|
|
32
35
|
getScreenShot(flowId: string, screenShotId: string): Promise<Buffer | null>;
|
|
33
36
|
setVideo(_flowId: string, _bytes: Buffer): Promise<void>;
|
|
@@ -134,6 +134,24 @@ class FlowsPersistenceDonobuApi {
|
|
|
134
134
|
throw new Error(`Failed to delete tool call: ${response.status} ${response.statusText}`);
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
|
+
// -- AI Queries ----------------------------------------------------
|
|
138
|
+
async setAiQuery(flowId, aiQuery) {
|
|
139
|
+
const response = await this.jsonRequest(`/v1/flows/${encodeURIComponent(flowId)}/ai-queries/${encodeURIComponent(aiQuery.id)}`, 'PUT', { record: aiQuery });
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
throw new Error(`Failed to set AI query: ${response.status} ${response.statusText}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async getAiQueries(flowId) {
|
|
145
|
+
const response = await this.jsonRequest(`/v1/flows/${encodeURIComponent(flowId)}/ai-queries`, 'GET');
|
|
146
|
+
if (response.status === 404) {
|
|
147
|
+
throw FlowNotFoundException_1.FlowNotFoundException.forId(flowId);
|
|
148
|
+
}
|
|
149
|
+
if (!response.ok) {
|
|
150
|
+
throw new Error(`Failed to get AI queries: ${response.status} ${response.statusText}`);
|
|
151
|
+
}
|
|
152
|
+
const body = (await response.json());
|
|
153
|
+
return body.items;
|
|
154
|
+
}
|
|
137
155
|
// -- Screenshots ---------------------------------------------------
|
|
138
156
|
async saveScreenShot(flowId, bytes) {
|
|
139
157
|
const imageType = MiscUtils_1.MiscUtils.detectImageType(bytes);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type Database from 'better-sqlite3';
|
|
2
|
+
import type { AiQuery } from '../../models/AiQuery';
|
|
2
3
|
import { type BrowserStorageState } from '../../models/BrowserStorageState';
|
|
3
4
|
import type { FlowMetadata, FlowsQuery } from '../../models/FlowMetadata';
|
|
4
5
|
import type { PaginatedResult } from '../../models/PaginatedResult';
|
|
@@ -40,6 +41,8 @@ export declare class FlowsPersistenceSqlite implements FlowsPersistence {
|
|
|
40
41
|
setToolCall(flowId: string, toolCall: ToolCall): Promise<void>;
|
|
41
42
|
getToolCalls(flowId: string): Promise<ToolCall[]>;
|
|
42
43
|
deleteToolCall(flowId: string, toolCallId: string): Promise<void>;
|
|
44
|
+
setAiQuery(flowId: string, aiQuery: AiQuery): Promise<void>;
|
|
45
|
+
getAiQueries(flowId: string): Promise<AiQuery[]>;
|
|
43
46
|
setVideo(flowId: string, bytes: Buffer): Promise<void>;
|
|
44
47
|
getVideoSegment(flowId: string, startOffset: number, length: number): Promise<VideoSegment | null>;
|
|
45
48
|
getFlowFile(flowId: string, fileId: string): Promise<Buffer | null>;
|
|
@@ -212,6 +212,16 @@ class FlowsPersistenceSqlite {
|
|
|
212
212
|
const stmt = this.db.prepare('DELETE FROM tool_calls WHERE flow_id = ? AND id = ?');
|
|
213
213
|
stmt.run(flowId, toolCallId);
|
|
214
214
|
}
|
|
215
|
+
async setAiQuery(flowId, aiQuery) {
|
|
216
|
+
const stmt = this.db.prepare('INSERT OR REPLACE INTO ai_queries (id, flow_id, started_at, ai_query) VALUES (?, ?, ?, ?)');
|
|
217
|
+
stmt.run(aiQuery.id, flowId, aiQuery.startedAt, JSON.stringify(aiQuery));
|
|
218
|
+
}
|
|
219
|
+
async getAiQueries(flowId) {
|
|
220
|
+
await this.getFlowMetadataById(flowId);
|
|
221
|
+
const stmt = this.db.prepare('SELECT ai_query FROM ai_queries WHERE flow_id = ? ORDER BY started_at');
|
|
222
|
+
const rows = stmt.all(flowId);
|
|
223
|
+
return rows.map((row) => JSON.parse(row.ai_query));
|
|
224
|
+
}
|
|
215
225
|
async setVideo(flowId, bytes) {
|
|
216
226
|
// Ensure flow exists.
|
|
217
227
|
await this.getFlowMetadataById(flowId);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AiQuery } from '../../models/AiQuery';
|
|
1
2
|
import type { BrowserStorageState } from '../../models/BrowserStorageState';
|
|
2
3
|
import type { FlowMetadata, FlowsQuery } from '../../models/FlowMetadata';
|
|
3
4
|
import type { PaginatedResult } from '../../models/PaginatedResult';
|
|
@@ -14,7 +15,8 @@ export declare class FlowsPersistenceVolatile implements FlowsPersistence {
|
|
|
14
15
|
private readonly videos;
|
|
15
16
|
private readonly files;
|
|
16
17
|
private readonly browserStates;
|
|
17
|
-
|
|
18
|
+
private readonly aiQueries;
|
|
19
|
+
constructor(flows?: Map<string, FlowMetadata>, screenshots?: Map<string, Map<string, Buffer>>, toolCalls?: Map<string, ToolCall[]>, videos?: Map<string, Buffer>, files?: Map<string, Map<string, Buffer>>, browserStates?: Map<string, BrowserStorageState>, aiQueries?: Map<string, AiQuery[]>);
|
|
18
20
|
setFlowMetadata(flowMetadata: FlowMetadata): Promise<void>;
|
|
19
21
|
getFlowMetadataById(flowId: string): Promise<FlowMetadata>;
|
|
20
22
|
getFlowMetadataByName(flowName: string): Promise<FlowMetadata>;
|
|
@@ -27,6 +29,8 @@ export declare class FlowsPersistenceVolatile implements FlowsPersistence {
|
|
|
27
29
|
setToolCall(flowId: string, toolCall: ToolCall): Promise<void>;
|
|
28
30
|
getToolCalls(flowId: string): Promise<ToolCall[]>;
|
|
29
31
|
deleteToolCall(flowId: string, toolCallId: string): Promise<void>;
|
|
32
|
+
setAiQuery(flowId: string, aiQuery: AiQuery): Promise<void>;
|
|
33
|
+
getAiQueries(flowId: string): Promise<AiQuery[]>;
|
|
30
34
|
setVideo(flowId: string, bytes: Buffer): Promise<void>;
|
|
31
35
|
getVideoSegment(flowId: string, startOffset: number, length: number): Promise<VideoSegment | null>;
|
|
32
36
|
getFlowFile(flowId: string, fileId: string): Promise<Buffer | null>;
|
|
@@ -7,13 +7,14 @@ const MiscUtils_1 = require("../../utils/MiscUtils");
|
|
|
7
7
|
* A volatile (in-memory) implementation of FlowsPersistence.
|
|
8
8
|
*/
|
|
9
9
|
class FlowsPersistenceVolatile {
|
|
10
|
-
constructor(flows = new Map(), screenshots = new Map(), toolCalls = new Map(), videos = new Map(), files = new Map(), browserStates = new Map()) {
|
|
10
|
+
constructor(flows = new Map(), screenshots = new Map(), toolCalls = new Map(), videos = new Map(), files = new Map(), browserStates = new Map(), aiQueries = new Map()) {
|
|
11
11
|
this.flows = flows;
|
|
12
12
|
this.screenshots = screenshots;
|
|
13
13
|
this.toolCalls = toolCalls;
|
|
14
14
|
this.videos = videos;
|
|
15
15
|
this.files = files;
|
|
16
16
|
this.browserStates = browserStates;
|
|
17
|
+
this.aiQueries = aiQueries;
|
|
17
18
|
}
|
|
18
19
|
async setFlowMetadata(flowMetadata) {
|
|
19
20
|
this.flows.set(flowMetadata.id, { ...flowMetadata });
|
|
@@ -126,6 +127,26 @@ class FlowsPersistenceVolatile {
|
|
|
126
127
|
this.toolCalls.delete(flowId);
|
|
127
128
|
}
|
|
128
129
|
}
|
|
130
|
+
async setAiQuery(flowId, aiQuery) {
|
|
131
|
+
if (!this.aiQueries.has(flowId)) {
|
|
132
|
+
this.aiQueries.set(flowId, []);
|
|
133
|
+
}
|
|
134
|
+
const queries = this.aiQueries.get(flowId);
|
|
135
|
+
const existingIndex = queries.findIndex((q) => q.id === aiQuery.id);
|
|
136
|
+
if (existingIndex >= 0) {
|
|
137
|
+
queries[existingIndex] = { ...aiQuery };
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
queries.push({ ...aiQuery });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async getAiQueries(flowId) {
|
|
144
|
+
if (!this.flows.has(flowId)) {
|
|
145
|
+
throw FlowNotFoundException_1.FlowNotFoundException.forId(flowId);
|
|
146
|
+
}
|
|
147
|
+
const queries = this.aiQueries.get(flowId) || [];
|
|
148
|
+
return [...queries].sort((a, b) => a.startedAt - b.startedAt);
|
|
149
|
+
}
|
|
129
150
|
async setVideo(flowId, bytes) {
|
|
130
151
|
this.videos.set(flowId, Buffer.from(bytes));
|
|
131
152
|
}
|
|
@@ -167,6 +188,7 @@ class FlowsPersistenceVolatile {
|
|
|
167
188
|
this.flows.delete(flowId);
|
|
168
189
|
this.screenshots.delete(flowId);
|
|
169
190
|
this.toolCalls.delete(flowId);
|
|
191
|
+
this.aiQueries.delete(flowId);
|
|
170
192
|
this.videos.delete(flowId);
|
|
171
193
|
this.files.delete(flowId);
|
|
172
194
|
this.browserStates.delete(flowId);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { DonobuException } from './DonobuException';
|
|
2
|
+
/**
|
|
3
|
+
* Thrown when a model exists but lacks required capabilities
|
|
4
|
+
* (e.g. completion, tools, or vision).
|
|
5
|
+
*/
|
|
6
|
+
export declare class GptModelCapabilityException extends DonobuException {
|
|
7
|
+
readonly platform: string;
|
|
8
|
+
readonly gptModel: string;
|
|
9
|
+
readonly missingCapabilities: string[];
|
|
10
|
+
constructor(platform: string, gptModel: string, missingCapabilities: string[]);
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=GptModelCapabilityException.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GptModelCapabilityException = void 0;
|
|
4
|
+
const DonobuException_1 = require("./DonobuException");
|
|
5
|
+
/**
|
|
6
|
+
* Thrown when a model exists but lacks required capabilities
|
|
7
|
+
* (e.g. completion, tools, or vision).
|
|
8
|
+
*/
|
|
9
|
+
class GptModelCapabilityException extends DonobuException_1.DonobuException {
|
|
10
|
+
constructor(platform, gptModel, missingCapabilities) {
|
|
11
|
+
super(`The model '${gptModel}' on '${platform}' is missing required capabilities: ${missingCapabilities.join(', ')}. ` +
|
|
12
|
+
`Please choose a model that supports completion, tools, and vision.`);
|
|
13
|
+
this.platform = platform;
|
|
14
|
+
this.gptModel = gptModel;
|
|
15
|
+
this.missingCapabilities = missingCapabilities;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.GptModelCapabilityException = GptModelCapabilityException;
|
|
19
|
+
//# sourceMappingURL=GptModelCapabilityException.js.map
|
|
@@ -9,6 +9,7 @@ const v4_1 = require("zod/v4");
|
|
|
9
9
|
const AgentsApi_1 = require("../apis/AgentsApi");
|
|
10
10
|
const AskAiApi_1 = require("../apis/AskAiApi");
|
|
11
11
|
const EnvDataApi_1 = require("../apis/EnvDataApi");
|
|
12
|
+
const FlowsAiQueriesApi_1 = require("../apis/FlowsAiQueriesApi");
|
|
12
13
|
const FlowsApi_1 = require("../apis/FlowsApi");
|
|
13
14
|
const FlowsFilesApi_1 = require("../apis/FlowsFilesApi");
|
|
14
15
|
const FlowsToolCallsApi_1 = require("../apis/FlowsToolCallsApi");
|
|
@@ -237,6 +238,7 @@ class AdminApiController {
|
|
|
237
238
|
flowsApi: new FlowsApi_1.FlowsApi(donobuStack.flowsManager),
|
|
238
239
|
flowsFilesApi: new FlowsFilesApi_1.FlowsFilesApi(donobuStack.flowsPersistenceRegistry),
|
|
239
240
|
flowsToolCallsApi: new FlowsToolCallsApi_1.FlowsToolCallsApi(donobuStack.flowsManager),
|
|
241
|
+
flowsAiQueriesApi: new FlowsAiQueriesApi_1.FlowsAiQueriesApi(donobuStack.flowsManager),
|
|
240
242
|
pingApi: new PingApi_1.PingApi(),
|
|
241
243
|
schemaApi: new SchemaApi_1.SchemaApi(),
|
|
242
244
|
targetsApi: new TargetsApi_1.TargetsApi(donobuStack.targetRuntimePlugins),
|
|
@@ -308,6 +310,7 @@ class AdminApiController {
|
|
|
308
310
|
app.get('/api/flows/:flowId/video', this.asyncHandler(apis.flowsFilesApi.getFlowVideo.bind(apis.flowsFilesApi)));
|
|
309
311
|
app.get('/api/flows/:flowId/tool-calls', this.asyncHandler(apis.flowsToolCallsApi.getToolCalls.bind(apis.flowsToolCallsApi)));
|
|
310
312
|
app.get('/api/flows/:flowId/tool-calls/:toolCallId', this.asyncHandler(apis.flowsToolCallsApi.getToolCall.bind(apis.flowsToolCallsApi)));
|
|
313
|
+
app.get('/api/flows/:flowId/ai-queries', this.asyncHandler(apis.flowsAiQueriesApi.getAiQueries.bind(apis.flowsAiQueriesApi)));
|
|
311
314
|
app.get('/api/flows/:flowId/logs', this.asyncHandler(apis.flowsApi.getFlowLogs.bind(apis.flowsApi)));
|
|
312
315
|
}
|
|
313
316
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { z } from 'zod/v4';
|
|
2
2
|
import type { GptClient } from '../clients/GptClient';
|
|
3
|
+
import type { AiQuery } from '../models/AiQuery';
|
|
3
4
|
import type { ControlPanel } from '../models/ControlPanel';
|
|
4
5
|
import type { FlowMetadata } from '../models/FlowMetadata';
|
|
5
6
|
import type { GptMessage, StructuredOutputMessage, TextItem } from '../models/GptMessage';
|
|
@@ -40,6 +41,7 @@ export declare class DonobuFlow {
|
|
|
40
41
|
private static readonly MAIN_MESSAGE_ELEMENT_LIST_MARKER;
|
|
41
42
|
static readonly USER_INTERRUPT_MARKER = "[User interruption while flow was paused, this MUST be acknowledged]";
|
|
42
43
|
inProgressToolCall: ToolCall | null;
|
|
44
|
+
readonly aiQueries: AiQuery[];
|
|
43
45
|
constructor(flowsManager: DonobuFlowsManager, envData: Record<string, string>, persistence: FlowsPersistence, gptClient: GptClient | null, toolManager: ToolManager, interactionVisualizer: InteractionVisualizer, proposedToolCalls: ProposedToolCall[], invokedToolCalls: ToolCall[], gptMessages: GptMessage[], targetInspector: TargetInspector, metadata: FlowMetadata, controlPanel: ControlPanel);
|
|
44
46
|
/**
|
|
45
47
|
* Drives the entire Donobu flow state-machine until it reaches a
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DonobuFlow = void 0;
|
|
4
4
|
exports.extractFromPage = extractFromPage;
|
|
5
|
+
const crypto_1 = require("crypto");
|
|
5
6
|
const GptPlatformInternalErrorException_1 = require("../exceptions/GptPlatformInternalErrorException");
|
|
6
7
|
const UserInterruptException_1 = require("../exceptions/UserInterruptException");
|
|
7
8
|
const FlowMetadata_1 = require("../models/FlowMetadata");
|
|
@@ -104,6 +105,7 @@ class DonobuFlow {
|
|
|
104
105
|
this.metadata = metadata;
|
|
105
106
|
this.controlPanel = controlPanel;
|
|
106
107
|
this.inProgressToolCall = null;
|
|
108
|
+
this.aiQueries = [];
|
|
107
109
|
}
|
|
108
110
|
/**
|
|
109
111
|
* Drives the entire Donobu flow state-machine until it reaches a
|
|
@@ -909,18 +911,42 @@ Message: ${dialog.message()}`;
|
|
|
909
911
|
}
|
|
910
912
|
async queryGptForProposedToolCalls() {
|
|
911
913
|
this.targetInspector.checkConnectedOrThrow();
|
|
914
|
+
// Initialise the AI query record immediately so the error handler always
|
|
915
|
+
// has a record to update — no conditional check needed.
|
|
916
|
+
let aiQuery = {
|
|
917
|
+
id: (0, crypto_1.randomUUID)(),
|
|
918
|
+
cleanScreenshotId: null,
|
|
919
|
+
annotatedScreenshotId: null,
|
|
920
|
+
interactableElements: null,
|
|
921
|
+
error: null,
|
|
922
|
+
startedAt: Date.now(),
|
|
923
|
+
completedAt: null,
|
|
924
|
+
};
|
|
925
|
+
this.aiQueries.push(aiQuery);
|
|
912
926
|
try {
|
|
913
927
|
// Discover and mark all interactable elements on the current screen/page.
|
|
914
928
|
await this.targetInspector.attributeInteractableElements();
|
|
915
929
|
// Capture clean and annotated screenshots. Each inspector implementation
|
|
916
930
|
// handles the platform-specific details (DOM injection vs server-side compositing).
|
|
917
931
|
const screenshotBytes = await this.targetInspector.takeCleanScreenshot();
|
|
918
|
-
await this.persistence.saveScreenShot(this.metadata.id, screenshotBytes);
|
|
932
|
+
const cleanScreenshotId = await this.persistence.saveScreenShot(this.metadata.id, screenshotBytes);
|
|
919
933
|
await this.targetInspector.annotateInteractableElements();
|
|
920
934
|
const annotatedScreenShotBytes = await this.targetInspector.takeAnnotatedScreenshot();
|
|
921
935
|
await this.targetInspector.removeAnnotations();
|
|
922
|
-
await this.persistence.saveScreenShot(this.metadata.id, annotatedScreenShotBytes);
|
|
936
|
+
const annotatedScreenshotId = await this.persistence.saveScreenShot(this.metadata.id, annotatedScreenShotBytes);
|
|
923
937
|
const interactableElements = await this.targetInspector.getAttributedInteractableElements();
|
|
938
|
+
// Fill in the remaining fields and persist so the frontend can display
|
|
939
|
+
// the record immediately.
|
|
940
|
+
aiQuery = {
|
|
941
|
+
...aiQuery,
|
|
942
|
+
cleanScreenshotId,
|
|
943
|
+
annotatedScreenshotId,
|
|
944
|
+
interactableElements,
|
|
945
|
+
};
|
|
946
|
+
this.aiQueries[this.aiQueries.length - 1] = aiQuery;
|
|
947
|
+
await this.persistence
|
|
948
|
+
.setAiQuery(this.metadata.id, aiQuery)
|
|
949
|
+
.catch((err) => Logger_1.appLogger.error('Failed to persist AI query record', err));
|
|
924
950
|
const mainMessage = DonobuFlow.createMainUserMessage(this.targetInspector, interactableElements);
|
|
925
951
|
// Give the LLM both the pre and post annotated screenshots. It can
|
|
926
952
|
// use the clean screenshot to decide what it wants to do, then map it to
|
|
@@ -945,9 +971,20 @@ Message: ${dialog.message()}`;
|
|
|
945
971
|
}));
|
|
946
972
|
Logger_1.appLogger.debug('LLM response:', JsonUtils_1.JsonUtils.objectToJson(proposedToolCallsMessage));
|
|
947
973
|
MiscUtils_1.MiscUtils.updateTokenCounts(proposedToolCallsMessage, this.metadata);
|
|
974
|
+
aiQuery = { ...aiQuery, completedAt: Date.now() };
|
|
975
|
+
this.aiQueries[this.aiQueries.length - 1] = aiQuery;
|
|
976
|
+
await this.persistence
|
|
977
|
+
.setAiQuery(this.metadata.id, aiQuery)
|
|
978
|
+
.catch((err) => Logger_1.appLogger.error('Failed to persist AI query completion', err));
|
|
948
979
|
return proposedToolCallsMessage;
|
|
949
980
|
}
|
|
950
981
|
catch (error) {
|
|
982
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
983
|
+
aiQuery = { ...aiQuery, error: errorMessage, completedAt: Date.now() };
|
|
984
|
+
this.aiQueries[this.aiQueries.length - 1] = aiQuery;
|
|
985
|
+
await this.persistence
|
|
986
|
+
.setAiQuery(this.metadata.id, aiQuery)
|
|
987
|
+
.catch((err) => Logger_1.appLogger.error('Failed to persist AI query error', err));
|
|
951
988
|
if (this.targetInspector.isTargetClosedError(error)) {
|
|
952
989
|
this.targetInspector.checkConnectedOrThrow();
|
|
953
990
|
}
|
|
@@ -5,6 +5,7 @@ import type { GptClient } from '../clients/GptClient';
|
|
|
5
5
|
import type { GptClientFactory } from '../clients/GptClientFactory';
|
|
6
6
|
import type { GeneratedProject } from '../codegen/CodeGenerator';
|
|
7
7
|
import type { env } from '../envVars';
|
|
8
|
+
import type { AiQuery } from '../models/AiQuery';
|
|
8
9
|
import type { BrowserStateReference } from '../models/BrowserStateFlowReference';
|
|
9
10
|
import type { BrowserStorageState } from '../models/BrowserStorageState';
|
|
10
11
|
import type { CodeGenerationOptions } from '../models/CodeGenerationOptions';
|
|
@@ -109,6 +110,8 @@ export declare class DonobuFlowsManager {
|
|
|
109
110
|
getFlowByName(flowName: string): Promise<FlowMetadata>;
|
|
110
111
|
/** Returns all the tool calls made by the given flow by ID. */
|
|
111
112
|
getToolCalls(flowId: string): Promise<ToolCall[]>;
|
|
113
|
+
/** Returns all AI query records for the given flow by ID. */
|
|
114
|
+
getAiQueries(flowId: string): Promise<AiQuery[]>;
|
|
112
115
|
/**
|
|
113
116
|
* Attempts to delete a flow by ID. If the flow is active, then
|
|
114
117
|
* `CannotDeleteRunningFlowException` is thrown. If the flow is not active,
|
|
@@ -374,6 +374,10 @@ class DonobuFlowsManager {
|
|
|
374
374
|
async getToolCalls(flowId) {
|
|
375
375
|
return this.flowCatalog.getToolCalls(flowId);
|
|
376
376
|
}
|
|
377
|
+
/** Returns all AI query records for the given flow by ID. */
|
|
378
|
+
async getAiQueries(flowId) {
|
|
379
|
+
return this.flowCatalog.getAiQueries(flowId);
|
|
380
|
+
}
|
|
377
381
|
/**
|
|
378
382
|
* Attempts to delete a flow by ID. If the flow is active, then
|
|
379
383
|
* `CannotDeleteRunningFlowException` is thrown. If the flow is not active,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AiQuery } from '../models/AiQuery';
|
|
1
2
|
import type { BrowserStorageState } from '../models/BrowserStorageState';
|
|
2
3
|
import type { DonobuDeploymentEnvironment } from '../models/DonobuDeploymentEnvironment';
|
|
3
4
|
import type { FlowMetadata, FlowsQuery } from '../models/FlowMetadata';
|
|
@@ -21,6 +22,7 @@ export declare class FlowCatalog {
|
|
|
21
22
|
getFlowById(flowId: string): Promise<FlowMetadata>;
|
|
22
23
|
getFlowByName(flowName: string): Promise<FlowMetadata>;
|
|
23
24
|
getToolCalls(flowId: string): Promise<ToolCall[]>;
|
|
25
|
+
getAiQueries(flowId: string): Promise<AiQuery[]>;
|
|
24
26
|
deleteFlow(flowId: string): Promise<void>;
|
|
25
27
|
getBrowserState(flowId: string): Promise<BrowserStorageState | null>;
|
|
26
28
|
getFlows(query: FlowsQuery): Promise<PaginatedResult<FlowMetadata>>;
|
|
@@ -71,6 +71,25 @@ class FlowCatalog {
|
|
|
71
71
|
}
|
|
72
72
|
throw FlowNotFoundException_1.FlowNotFoundException.forId(flowId);
|
|
73
73
|
}
|
|
74
|
+
async getAiQueries(flowId) {
|
|
75
|
+
if (this.isLocal()) {
|
|
76
|
+
const activeHandle = this.flowRuntime.get(flowId);
|
|
77
|
+
if (activeHandle) {
|
|
78
|
+
return [...activeHandle.donobuFlow.aiQueries].sort((a, b) => a.startedAt - b.startedAt);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (const persistence of await this.flowsPersistenceRegistry.getAll()) {
|
|
82
|
+
try {
|
|
83
|
+
return await persistence.getAiQueries(flowId);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (!(error instanceof FlowNotFoundException_1.FlowNotFoundException)) {
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
throw FlowNotFoundException_1.FlowNotFoundException.forId(flowId);
|
|
92
|
+
}
|
|
74
93
|
async deleteFlow(flowId) {
|
|
75
94
|
for (const persistence of await this.flowsPersistenceRegistry.getAll()) {
|
|
76
95
|
await persistence.deleteFlow(flowId);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { InteractableElement } from './InteractableElement';
|
|
2
|
+
/**
|
|
3
|
+
* Represents a single AI decision cycle within a flow. Created at the start of
|
|
4
|
+
* a query cycle with just `id` and `queriedAt`, then progressively filled in as
|
|
5
|
+
* screenshots are captured and elements are discovered. The `error` field is
|
|
6
|
+
* populated only if the query fails.
|
|
7
|
+
*/
|
|
8
|
+
export type AiQuery = {
|
|
9
|
+
/**
|
|
10
|
+
* Unique identifier for this AI query.
|
|
11
|
+
*/
|
|
12
|
+
readonly id: string;
|
|
13
|
+
/**
|
|
14
|
+
* The ID of the clean (un-annotated) screenshot of the page at query time.
|
|
15
|
+
* Null until the screenshot is captured.
|
|
16
|
+
*/
|
|
17
|
+
readonly cleanScreenshotId: string | null;
|
|
18
|
+
/**
|
|
19
|
+
* The ID of the annotated screenshot (with numbered element badges) sent to the AI.
|
|
20
|
+
* Null until the screenshot is captured.
|
|
21
|
+
*/
|
|
22
|
+
readonly annotatedScreenshotId: string | null;
|
|
23
|
+
/**
|
|
24
|
+
* The interactable elements that were identified and sent to the AI.
|
|
25
|
+
* Null until element discovery completes.
|
|
26
|
+
*/
|
|
27
|
+
readonly interactableElements: InteractableElement[] | null;
|
|
28
|
+
/**
|
|
29
|
+
* If the AI query failed, the error message. Null on success.
|
|
30
|
+
*/
|
|
31
|
+
readonly error: string | null;
|
|
32
|
+
/**
|
|
33
|
+
* The Unix epoch millisecond timestamp of when the query was initiated.
|
|
34
|
+
*/
|
|
35
|
+
readonly startedAt: number;
|
|
36
|
+
/**
|
|
37
|
+
* The Unix epoch millisecond timestamp of when the query completed (success
|
|
38
|
+
* or failure). Null while the query is still in progress.
|
|
39
|
+
*/
|
|
40
|
+
readonly completedAt: number | null;
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=AiQuery.d.ts.map
|
|
@@ -395,6 +395,23 @@ CREATE INDEX IF NOT EXISTS idx_test_metadata_suite_id ON test_metadata(suite_id)
|
|
|
395
395
|
|
|
396
396
|
ALTER TABLE flow_metadata ADD COLUMN test_id TEXT NULL REFERENCES test_metadata(id) ON DELETE CASCADE;
|
|
397
397
|
CREATE INDEX IF NOT EXISTS idx_flow_metadata_test_id ON flow_metadata(test_id);
|
|
398
|
+
`);
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
// Create the ai_queries table to store AI decision-cycle records (clean
|
|
403
|
+
// and annotated screenshots, interactable elements, optional error).
|
|
404
|
+
version: 11,
|
|
405
|
+
up: (db) => {
|
|
406
|
+
db.exec(`
|
|
407
|
+
CREATE TABLE IF NOT EXISTS ai_queries (
|
|
408
|
+
id TEXT PRIMARY KEY,
|
|
409
|
+
flow_id TEXT NOT NULL,
|
|
410
|
+
started_at INTEGER NOT NULL,
|
|
411
|
+
ai_query TEXT NOT NULL,
|
|
412
|
+
FOREIGN KEY (flow_id) REFERENCES flow_metadata(id) ON DELETE CASCADE
|
|
413
|
+
);
|
|
414
|
+
CREATE INDEX IF NOT EXISTS idx_ai_queries_flow_id_started_at ON ai_queries(flow_id, started_at);
|
|
398
415
|
`);
|
|
399
416
|
},
|
|
400
417
|
},
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AiQuery } from '../../models/AiQuery';
|
|
1
2
|
import type { BrowserStorageState } from '../../models/BrowserStorageState';
|
|
2
3
|
import type { FlowMetadata, FlowsQuery } from '../../models/FlowMetadata';
|
|
3
4
|
import type { PaginatedResult } from '../../models/PaginatedResult';
|
|
@@ -50,6 +51,15 @@ export interface FlowsPersistence {
|
|
|
50
51
|
* Delete a persisted tool call from a specific flow.
|
|
51
52
|
*/
|
|
52
53
|
deleteToolCall(flowId: string, toolCallId: string): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Save an AI query record for a specific flow.
|
|
56
|
+
*/
|
|
57
|
+
setAiQuery(flowId: string, aiQuery: AiQuery): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Load all AI query records for a specific flow, ordered by queriedAt.
|
|
60
|
+
* @throws FlowNotFoundException if the flow is not found
|
|
61
|
+
*/
|
|
62
|
+
getAiQueries(flowId: string): Promise<AiQuery[]>;
|
|
53
63
|
/**
|
|
54
64
|
* Set video data for a specific flow.
|
|
55
65
|
*/
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AiQuery } from '../../models/AiQuery';
|
|
1
2
|
import { type BrowserStorageState } from '../../models/BrowserStorageState';
|
|
2
3
|
import type { FlowMetadata, FlowsQuery } from '../../models/FlowMetadata';
|
|
3
4
|
import type { PaginatedResult } from '../../models/PaginatedResult';
|
|
@@ -28,6 +29,8 @@ export declare class FlowsPersistenceDonobuApi implements FlowsPersistence {
|
|
|
28
29
|
setToolCall(flowId: string, toolCall: ToolCall): Promise<void>;
|
|
29
30
|
getToolCalls(flowId: string): Promise<ToolCall[]>;
|
|
30
31
|
deleteToolCall(flowId: string, toolCallId: string): Promise<void>;
|
|
32
|
+
setAiQuery(flowId: string, aiQuery: AiQuery): Promise<void>;
|
|
33
|
+
getAiQueries(flowId: string): Promise<AiQuery[]>;
|
|
31
34
|
saveScreenShot(flowId: string, bytes: Buffer): Promise<string>;
|
|
32
35
|
getScreenShot(flowId: string, screenShotId: string): Promise<Buffer | null>;
|
|
33
36
|
setVideo(_flowId: string, _bytes: Buffer): Promise<void>;
|
|
@@ -134,6 +134,24 @@ class FlowsPersistenceDonobuApi {
|
|
|
134
134
|
throw new Error(`Failed to delete tool call: ${response.status} ${response.statusText}`);
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
|
+
// -- AI Queries ----------------------------------------------------
|
|
138
|
+
async setAiQuery(flowId, aiQuery) {
|
|
139
|
+
const response = await this.jsonRequest(`/v1/flows/${encodeURIComponent(flowId)}/ai-queries/${encodeURIComponent(aiQuery.id)}`, 'PUT', { record: aiQuery });
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
throw new Error(`Failed to set AI query: ${response.status} ${response.statusText}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async getAiQueries(flowId) {
|
|
145
|
+
const response = await this.jsonRequest(`/v1/flows/${encodeURIComponent(flowId)}/ai-queries`, 'GET');
|
|
146
|
+
if (response.status === 404) {
|
|
147
|
+
throw FlowNotFoundException_1.FlowNotFoundException.forId(flowId);
|
|
148
|
+
}
|
|
149
|
+
if (!response.ok) {
|
|
150
|
+
throw new Error(`Failed to get AI queries: ${response.status} ${response.statusText}`);
|
|
151
|
+
}
|
|
152
|
+
const body = (await response.json());
|
|
153
|
+
return body.items;
|
|
154
|
+
}
|
|
137
155
|
// -- Screenshots ---------------------------------------------------
|
|
138
156
|
async saveScreenShot(flowId, bytes) {
|
|
139
157
|
const imageType = MiscUtils_1.MiscUtils.detectImageType(bytes);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type Database from 'better-sqlite3';
|
|
2
|
+
import type { AiQuery } from '../../models/AiQuery';
|
|
2
3
|
import { type BrowserStorageState } from '../../models/BrowserStorageState';
|
|
3
4
|
import type { FlowMetadata, FlowsQuery } from '../../models/FlowMetadata';
|
|
4
5
|
import type { PaginatedResult } from '../../models/PaginatedResult';
|
|
@@ -40,6 +41,8 @@ export declare class FlowsPersistenceSqlite implements FlowsPersistence {
|
|
|
40
41
|
setToolCall(flowId: string, toolCall: ToolCall): Promise<void>;
|
|
41
42
|
getToolCalls(flowId: string): Promise<ToolCall[]>;
|
|
42
43
|
deleteToolCall(flowId: string, toolCallId: string): Promise<void>;
|
|
44
|
+
setAiQuery(flowId: string, aiQuery: AiQuery): Promise<void>;
|
|
45
|
+
getAiQueries(flowId: string): Promise<AiQuery[]>;
|
|
43
46
|
setVideo(flowId: string, bytes: Buffer): Promise<void>;
|
|
44
47
|
getVideoSegment(flowId: string, startOffset: number, length: number): Promise<VideoSegment | null>;
|
|
45
48
|
getFlowFile(flowId: string, fileId: string): Promise<Buffer | null>;
|
|
@@ -212,6 +212,16 @@ class FlowsPersistenceSqlite {
|
|
|
212
212
|
const stmt = this.db.prepare('DELETE FROM tool_calls WHERE flow_id = ? AND id = ?');
|
|
213
213
|
stmt.run(flowId, toolCallId);
|
|
214
214
|
}
|
|
215
|
+
async setAiQuery(flowId, aiQuery) {
|
|
216
|
+
const stmt = this.db.prepare('INSERT OR REPLACE INTO ai_queries (id, flow_id, started_at, ai_query) VALUES (?, ?, ?, ?)');
|
|
217
|
+
stmt.run(aiQuery.id, flowId, aiQuery.startedAt, JSON.stringify(aiQuery));
|
|
218
|
+
}
|
|
219
|
+
async getAiQueries(flowId) {
|
|
220
|
+
await this.getFlowMetadataById(flowId);
|
|
221
|
+
const stmt = this.db.prepare('SELECT ai_query FROM ai_queries WHERE flow_id = ? ORDER BY started_at');
|
|
222
|
+
const rows = stmt.all(flowId);
|
|
223
|
+
return rows.map((row) => JSON.parse(row.ai_query));
|
|
224
|
+
}
|
|
215
225
|
async setVideo(flowId, bytes) {
|
|
216
226
|
// Ensure flow exists.
|
|
217
227
|
await this.getFlowMetadataById(flowId);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AiQuery } from '../../models/AiQuery';
|
|
1
2
|
import type { BrowserStorageState } from '../../models/BrowserStorageState';
|
|
2
3
|
import type { FlowMetadata, FlowsQuery } from '../../models/FlowMetadata';
|
|
3
4
|
import type { PaginatedResult } from '../../models/PaginatedResult';
|
|
@@ -14,7 +15,8 @@ export declare class FlowsPersistenceVolatile implements FlowsPersistence {
|
|
|
14
15
|
private readonly videos;
|
|
15
16
|
private readonly files;
|
|
16
17
|
private readonly browserStates;
|
|
17
|
-
|
|
18
|
+
private readonly aiQueries;
|
|
19
|
+
constructor(flows?: Map<string, FlowMetadata>, screenshots?: Map<string, Map<string, Buffer>>, toolCalls?: Map<string, ToolCall[]>, videos?: Map<string, Buffer>, files?: Map<string, Map<string, Buffer>>, browserStates?: Map<string, BrowserStorageState>, aiQueries?: Map<string, AiQuery[]>);
|
|
18
20
|
setFlowMetadata(flowMetadata: FlowMetadata): Promise<void>;
|
|
19
21
|
getFlowMetadataById(flowId: string): Promise<FlowMetadata>;
|
|
20
22
|
getFlowMetadataByName(flowName: string): Promise<FlowMetadata>;
|
|
@@ -27,6 +29,8 @@ export declare class FlowsPersistenceVolatile implements FlowsPersistence {
|
|
|
27
29
|
setToolCall(flowId: string, toolCall: ToolCall): Promise<void>;
|
|
28
30
|
getToolCalls(flowId: string): Promise<ToolCall[]>;
|
|
29
31
|
deleteToolCall(flowId: string, toolCallId: string): Promise<void>;
|
|
32
|
+
setAiQuery(flowId: string, aiQuery: AiQuery): Promise<void>;
|
|
33
|
+
getAiQueries(flowId: string): Promise<AiQuery[]>;
|
|
30
34
|
setVideo(flowId: string, bytes: Buffer): Promise<void>;
|
|
31
35
|
getVideoSegment(flowId: string, startOffset: number, length: number): Promise<VideoSegment | null>;
|
|
32
36
|
getFlowFile(flowId: string, fileId: string): Promise<Buffer | null>;
|
|
@@ -7,13 +7,14 @@ const MiscUtils_1 = require("../../utils/MiscUtils");
|
|
|
7
7
|
* A volatile (in-memory) implementation of FlowsPersistence.
|
|
8
8
|
*/
|
|
9
9
|
class FlowsPersistenceVolatile {
|
|
10
|
-
constructor(flows = new Map(), screenshots = new Map(), toolCalls = new Map(), videos = new Map(), files = new Map(), browserStates = new Map()) {
|
|
10
|
+
constructor(flows = new Map(), screenshots = new Map(), toolCalls = new Map(), videos = new Map(), files = new Map(), browserStates = new Map(), aiQueries = new Map()) {
|
|
11
11
|
this.flows = flows;
|
|
12
12
|
this.screenshots = screenshots;
|
|
13
13
|
this.toolCalls = toolCalls;
|
|
14
14
|
this.videos = videos;
|
|
15
15
|
this.files = files;
|
|
16
16
|
this.browserStates = browserStates;
|
|
17
|
+
this.aiQueries = aiQueries;
|
|
17
18
|
}
|
|
18
19
|
async setFlowMetadata(flowMetadata) {
|
|
19
20
|
this.flows.set(flowMetadata.id, { ...flowMetadata });
|
|
@@ -126,6 +127,26 @@ class FlowsPersistenceVolatile {
|
|
|
126
127
|
this.toolCalls.delete(flowId);
|
|
127
128
|
}
|
|
128
129
|
}
|
|
130
|
+
async setAiQuery(flowId, aiQuery) {
|
|
131
|
+
if (!this.aiQueries.has(flowId)) {
|
|
132
|
+
this.aiQueries.set(flowId, []);
|
|
133
|
+
}
|
|
134
|
+
const queries = this.aiQueries.get(flowId);
|
|
135
|
+
const existingIndex = queries.findIndex((q) => q.id === aiQuery.id);
|
|
136
|
+
if (existingIndex >= 0) {
|
|
137
|
+
queries[existingIndex] = { ...aiQuery };
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
queries.push({ ...aiQuery });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async getAiQueries(flowId) {
|
|
144
|
+
if (!this.flows.has(flowId)) {
|
|
145
|
+
throw FlowNotFoundException_1.FlowNotFoundException.forId(flowId);
|
|
146
|
+
}
|
|
147
|
+
const queries = this.aiQueries.get(flowId) || [];
|
|
148
|
+
return [...queries].sort((a, b) => a.startedAt - b.startedAt);
|
|
149
|
+
}
|
|
129
150
|
async setVideo(flowId, bytes) {
|
|
130
151
|
this.videos.set(flowId, Buffer.from(bytes));
|
|
131
152
|
}
|
|
@@ -167,6 +188,7 @@ class FlowsPersistenceVolatile {
|
|
|
167
188
|
this.flows.delete(flowId);
|
|
168
189
|
this.screenshots.delete(flowId);
|
|
169
190
|
this.toolCalls.delete(flowId);
|
|
191
|
+
this.aiQueries.delete(flowId);
|
|
170
192
|
this.videos.delete(flowId);
|
|
171
193
|
this.files.delete(flowId);
|
|
172
194
|
this.browserStates.delete(flowId);
|