Skip to content

Commit 33b013b

Browse files
authored
feat(js/core): DAP expansion: Dynamic Actions are now expanded in the… (#3927)
1 parent b5a28b7 commit 33b013b

File tree

4 files changed

+136
-9
lines changed

4 files changed

+136
-9
lines changed

‎js/core/src/dynamic-action-provider.ts‎

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616

1717
import type * as z from 'zod';
1818
import { Action, ActionMetadata, defineAction } from './action.js';
19-
import { ActionType, Registry } from './registry.js';
19+
import { GenkitError } from './error.js';
20+
import { ActionMetadataRecord, ActionType, Registry } from './registry.js';
2021

2122
type DapValue = {
2223
[K in ActionType]?: Action<z.ZodTypeAny, z.ZodTypeAny, z.ZodTypeAny>[];
@@ -42,7 +43,12 @@ class SimpleCache {
4243
: config.cacheConfig?.ttlMillis;
4344
}
4445

45-
async getOrFetch(): Promise<DapValue> {
46+
/**
47+
* Gets or fetches the DAP data.
48+
* @param skipTrace Don't run the action. i.e. don't create a trace log.
49+
* @returns The DAP data
50+
*/
51+
async getOrFetch(params?: { skipTrace?: boolean }): Promise<DapValue> {
4652
const isStale =
4753
!this.value ||
4854
!this.expiresAt ||
@@ -59,9 +65,13 @@ class SimpleCache {
5965
this.value = await this.dapFn(); // this returns the actual actions
6066
this.expiresAt = Date.now() + this.ttlMillis;
6167

62-
// Also run the action
63-
this.dap.run(this.value); // This returns metadata and shows up in dev UI
64-
68+
if (!params?.skipTrace) {
69+
// Also run the action
70+
// This returns metadata and shows up in dev UI
71+
// It does not change what we return, it just makes
72+
// the content of the DAP visible in the trace.
73+
await this.dap.run(this.value);
74+
}
6575
return this.value;
6676
} catch (error) {
6777
console.error('Error fetching Dynamic Action Provider value:', error);
@@ -92,6 +102,7 @@ export interface DynamicRegistry {
92102
actionType: string,
93103
actionName: string
94104
): Promise<ActionMetadata[]>;
105+
getActionMetadataRecord(dapPrefix: string): Promise<ActionMetadataRecord>;
95106
}
96107

97108
export type DynamicActionProviderAction = Action<
@@ -210,4 +221,29 @@ function implementDap(
210221
// Single match or empty array
211222
return metadata.filter((m) => m.name == actionName);
212223
};
224+
225+
// This is called by listResolvableActions which is used by the
226+
// reflection API.
227+
dap.getActionMetadataRecord = async (dapPrefix: string) => {
228+
const dapActions = {} as ActionMetadataRecord;
229+
// We want to skip traces so we don't get a new action trace
230+
// every time the DevUI requests the list of actions.
231+
// This is ok, because the DevUI will show the actions, so
232+
// not having them in the trace is fine.
233+
const result = await dap.__cache.getOrFetch({ skipTrace: true });
234+
for (const [actionType, actions] of Object.entries(result)) {
235+
const metadataList = actions.map((a) => a.__action);
236+
for (const metadata of metadataList) {
237+
if (!metadata.name) {
238+
throw new GenkitError({
239+
status: 'INVALID_ARGUMENT',
240+
message: `Invalid metadata when listing dynamic actions from ${dapPrefix} - name required`,
241+
});
242+
}
243+
const key = `${dapPrefix}:${actionType}/${metadata.name}`;
244+
dapActions[key] = metadata;
245+
}
246+
}
247+
return dapActions;
248+
};
213249
}

‎js/core/src/registry.ts‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ export class Registry {
353353
* @returns All resolvable action metadata as a map of <key, action metadata>.
354354
*/
355355
async listResolvableActions(): Promise<ActionMetadataRecord> {
356-
const resolvableActions = {} as ActionMetadataRecord;
356+
let resolvableActions = {} as ActionMetadataRecord;
357357
// We listActions for all plugins in parallel.
358358
await Promise.all(
359359
Object.entries(this.pluginsByName).map(async ([pluginName, plugin]) => {
@@ -380,9 +380,22 @@ export class Registry {
380380
}
381381
})
382382
);
383-
// Also add actions that are already registered.
383+
// Also add actions that are already registered, and expand DAP actions
384384
for (const [key, action] of Object.entries(await this.listActions())) {
385385
resolvableActions[key] = action.__action;
386+
if (isDynamicActionProvider(action)) {
387+
try {
388+
// Include the dynamic actions
389+
const dapPrefix = `${action.__action.actionType}/${action.__action.name}`;
390+
const dapMetadataRecord =
391+
await action.getActionMetadataRecord(dapPrefix);
392+
resolvableActions = { ...resolvableActions, ...dapMetadataRecord };
393+
} catch (e) {
394+
logger.error(
395+
`Error listing actions for Dynamic Action Provider ${action.__action.name}`
396+
);
397+
}
398+
}
386399
}
387400
return {
388401
...(await this.parent?.listResolvableActions()),

‎js/core/tests/dynamic-action-provider_test.ts‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,25 @@ describe('dynamic action provider', () => {
171171
assert.strictEqual(callCount, 1);
172172
});
173173

174+
it('gets action metadata record', async () => {
175+
let callCount = 0;
176+
const dap = defineDynamicActionProvider(registry, 'my-dap', async () => {
177+
callCount++;
178+
return {
179+
tool: [tool1, tool2],
180+
flow: [tool1],
181+
};
182+
});
183+
184+
const record = await dap.getActionMetadataRecord('dap/my-dap');
185+
assert.deepStrictEqual(record, {
186+
'dap/my-dap:tool/tool1': tool1.__action,
187+
'dap/my-dap:tool/tool2': tool2.__action,
188+
'dap/my-dap:flow/tool1': tool1.__action,
189+
});
190+
assert.strictEqual(callCount, 1);
191+
});
192+
174193
it('handles concurrent requests', async () => {
175194
let callCount = 0;
176195
const dap = defineDynamicActionProvider(registry, 'my-dap', async () => {
@@ -224,6 +243,33 @@ describe('dynamic action provider', () => {
224243
});
225244
});
226245

246+
it('skips trace when requested', async () => {
247+
let callCount = 0;
248+
const dap = defineDynamicActionProvider(registry, 'my-dap', async () => {
249+
callCount++;
250+
return {
251+
tool: [tool1, tool2],
252+
};
253+
});
254+
255+
const originalRun = dap.run.bind(dap);
256+
let runCalled = false;
257+
dap.run = async (input, options) => {
258+
runCalled = true;
259+
return originalRun(input, options);
260+
};
261+
262+
await dap.__cache.getOrFetch({ skipTrace: true });
263+
assert.strictEqual(runCalled, false);
264+
assert.strictEqual(callCount, 1);
265+
266+
dap.invalidateCache();
267+
268+
await dap.__cache.getOrFetch();
269+
assert.strictEqual(runCalled, true);
270+
assert.strictEqual(callCount, 2);
271+
});
272+
227273
it('identifies dynamic action providers', async () => {
228274
const dap = defineDynamicActionProvider(registry, 'my-dap', async () => {
229275
return {};

‎js/core/tests/registry_test.ts‎

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ import {
2121
defineAction,
2222
runInActionRuntimeContext,
2323
} from '../src/action.js';
24-
import { initNodeAsyncContext } from '../src/node-async-context.js';
24+
import { defineDynamicActionProvider } from '../src/dynamic-action-provider.js';
25+
import { initNodeFeatures } from '../src/node.js';
2526
import { Registry } from '../src/registry.js';
2627

27-
initNodeAsyncContext();
28+
initNodeFeatures();
2829

2930
describe('registry class', () => {
3031
var registry: Registry;
@@ -344,6 +345,37 @@ describe('registry class', () => {
344345
},
345346
});
346347
});
348+
it('expands actions from dynamic action providers', async () => {
349+
const tool1 = action(
350+
{ name: 'fs/tool1', actionType: 'tool' },
351+
async () => {}
352+
);
353+
const tool2 = action(
354+
{ name: 'fs/tool2', actionType: 'tool' },
355+
async () => {}
356+
);
357+
const resource1 = action(
358+
{ name: 'abc/res1', actionType: 'resource' },
359+
async () => {}
360+
);
361+
const resource2 = action(
362+
{ name: 'abc/res2', actionType: 'resource' },
363+
async () => {}
364+
);
365+
const dap = defineDynamicActionProvider(registry, 'my-dap', async () => ({
366+
tool: [tool1, tool2],
367+
resource: [resource1, resource2],
368+
}));
369+
370+
const resolvableActions = await registry.listResolvableActions();
371+
assert.deepStrictEqual(resolvableActions, {
372+
'/dynamic-action-provider/my-dap': dap.__action,
373+
'dynamic-action-provider/my-dap:tool/fs/tool1': tool1.__action,
374+
'dynamic-action-provider/my-dap:tool/fs/tool2': tool2.__action,
375+
'dynamic-action-provider/my-dap:resource/abc/res1': resource1.__action,
376+
'dynamic-action-provider/my-dap:resource/abc/res2': resource2.__action,
377+
});
378+
});
347379
});
348380

349381
describe('lookupAction', () => {

0 commit comments

Comments
 (0)