Skip to content

Commit 8fe3719

Browse files
authored
feat(js): added dynamicTool factory function that does not depend on genkit instance (#3026)
1 parent 1749893 commit 8fe3719

File tree

13 files changed

+442
-197
lines changed

13 files changed

+442
-197
lines changed

js/ai/src/generate.ts

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
import {
1818
GenkitError,
19+
isAction,
20+
isDetachedAction,
1921
runWithContext,
2022
runWithStreamingCallback,
2123
sentinelNoopStreamingCallback,
@@ -52,8 +54,13 @@ import {
5254
type ToolRequestPart,
5355
type ToolResponsePart,
5456
} from './model.js';
55-
import type { ExecutablePrompt } from './prompt.js';
56-
import { resolveTools, toToolDefinition, type ToolArgument } from './tool.js';
57+
import { isExecutablePrompt } from './prompt.js';
58+
import {
59+
isDynamicTool,
60+
resolveTools,
61+
toToolDefinition,
62+
type ToolArgument,
63+
} from './tool.js';
5764
export { GenerateResponse, GenerateResponseChunk };
5865

5966
/** Specifies how tools should be called by the model. */
@@ -264,15 +271,11 @@ async function toolsToActionRefs(
264271
for (const t of toolOpt) {
265272
if (typeof t === 'string') {
266273
tools.push(await resolveFullToolName(registry, t));
267-
} else if ((t as Action).__action) {
268-
tools.push(
269-
`/${(t as Action).__action.metadata?.type}/${(t as Action).__action.name}`
270-
);
271-
} else if (typeof (t as ExecutablePrompt).asTool === 'function') {
272-
const promptToolAction = await (t as ExecutablePrompt).asTool();
274+
} else if (isAction(t) || isDynamicTool(t)) {
275+
tools.push(`/${t.__action.metadata?.type}/${t.__action.name}`);
276+
} else if (isExecutablePrompt(t)) {
277+
const promptToolAction = await t.asTool();
273278
tools.push(`/prompt/${promptToolAction.__action.name}`);
274-
} else if (t.name) {
275-
tools.push(await resolveFullToolName(registry, t.name));
276279
} else {
277280
throw new Error(`Unable to determine type of tool: ${JSON.stringify(t)}`);
278281
}
@@ -370,11 +373,10 @@ function maybeRegisterDynamicTools<
370373
>(registry: Registry, options: GenerateOptions<O, CustomOptions>): Registry {
371374
let hasDynamicTools = false;
372375
options?.tools?.forEach((t) => {
373-
if (
374-
(t as Action).__action &&
375-
(t as Action).__action.metadata?.type === 'tool' &&
376-
(t as Action).__action.metadata?.dynamic
377-
) {
376+
if (isDynamicTool(t)) {
377+
if (isDetachedAction(t)) {
378+
t = t.attach(registry);
379+
}
378380
if (!hasDynamicTools) {
379381
hasDynamicTools = true;
380382
// Create a temporary registry with dynamic tools for the duration of this

js/ai/src/prompt.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -694,7 +694,7 @@ async function renderDotpromptToParts<
694694
/**
695695
* Checks whether the provided object is an executable prompt.
696696
*/
697-
export function isExecutablePrompt(obj: any): boolean {
697+
export function isExecutablePrompt(obj: any): obj is ExecutablePrompt {
698698
return (
699699
!!(obj as ExecutablePrompt)?.render &&
700700
!!(obj as ExecutablePrompt)?.asTool &&

js/ai/src/tool.ts

Lines changed: 93 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,20 @@
1515
*/
1616

1717
import {
18-
action,
1918
assertUnstable,
2019
defineAction,
20+
detachedAction,
21+
isAction,
22+
isDetachedAction,
2123
stripUndefinedProps,
2224
z,
2325
type Action,
2426
type ActionContext,
2527
type ActionRunOptions,
28+
type DetachedAction,
2629
type JSONSchema7,
2730
} from '@genkit-ai/core';
28-
import type { HasRegistry, Registry } from '@genkit-ai/core/registry';
31+
import type { Registry } from '@genkit-ai/core/registry';
2932
import { parseSchema, toJsonSchema } from '@genkit-ai/core/schema';
3033
import { setCustomMetadataAttributes } from '@genkit-ai/core/tracing';
3134
import type {
@@ -34,20 +37,12 @@ import type {
3437
ToolRequestPart,
3538
ToolResponsePart,
3639
} from './model.js';
37-
import type { ExecutablePrompt } from './prompt.js';
40+
import { isExecutablePrompt, type ExecutablePrompt } from './prompt.js';
3841

39-
/**
40-
* An action with a `tool` type.
41-
*/
42-
export type ToolAction<
42+
export interface Resumable<
4343
I extends z.ZodTypeAny = z.ZodTypeAny,
4444
O extends z.ZodTypeAny = z.ZodTypeAny,
45-
> = Action<I, O, z.ZodTypeAny, ToolRunOptions> & {
46-
__action: {
47-
metadata: {
48-
type: 'tool';
49-
};
50-
};
45+
> {
5146
/**
5247
* respond constructs a tool response corresponding to the provided interrupt tool request
5348
* using the provided reply data, validating it against the output schema of the tool if
@@ -90,7 +85,37 @@ export type ToolAction<
9085
replaceInput?: z.infer<I>;
9186
}
9287
): ToolRequestPart;
93-
};
88+
}
89+
90+
/**
91+
* An action with a `tool` type.
92+
*/
93+
export type ToolAction<
94+
I extends z.ZodTypeAny = z.ZodTypeAny,
95+
O extends z.ZodTypeAny = z.ZodTypeAny,
96+
> = Action<I, O, z.ZodTypeAny, ToolRunOptions> &
97+
Resumable<I, O> & {
98+
__action: {
99+
metadata: {
100+
type: 'tool';
101+
};
102+
};
103+
};
104+
105+
/**
106+
* A dynamic action with a `tool` type. Dynamic tools are detached actions -- not associated with any registry.
107+
*/
108+
export type DynamicToolAction<
109+
I extends z.ZodTypeAny = z.ZodTypeAny,
110+
O extends z.ZodTypeAny = z.ZodTypeAny,
111+
> = DetachedAction<I, O, z.ZodTypeAny, ToolRunOptions> &
112+
Resumable<I, O> & {
113+
__action: {
114+
metadata: {
115+
type: 'tool';
116+
};
117+
};
118+
};
94119

95120
export interface ToolRunOptions extends ActionRunOptions<z.ZodTypeAny> {
96121
/**
@@ -128,7 +153,12 @@ export interface ToolConfig<I extends z.ZodTypeAny, O extends z.ZodTypeAny> {
128153
export type ToolArgument<
129154
I extends z.ZodTypeAny = z.ZodTypeAny,
130155
O extends z.ZodTypeAny = z.ZodTypeAny,
131-
> = string | ToolAction<I, O> | Action<I, O> | ExecutablePrompt<any, any, any>;
156+
> =
157+
| string
158+
| ToolAction<I, O>
159+
| DynamicToolAction<I, O>
160+
| Action<I, O>
161+
| ExecutablePrompt<any, any, any>;
132162

133163
/**
134164
* Converts an action to a tool action by setting the appropriate metadata.
@@ -170,14 +200,15 @@ export async function resolveTools<
170200
tools.map(async (ref): Promise<ToolAction> => {
171201
if (typeof ref === 'string') {
172202
return await lookupToolByName(registry, ref);
173-
} else if ((ref as Action).__action) {
174-
return asTool(registry, ref as Action);
175-
} else if (typeof (ref as ExecutablePrompt).asTool === 'function') {
176-
return await (ref as ExecutablePrompt).asTool();
177-
} else if (ref.name) {
203+
} else if (isAction(ref)) {
204+
return asTool(registry, ref);
205+
} else if (isExecutablePrompt(ref)) {
206+
return await ref.asTool();
207+
} else if ((ref as ToolDefinition).name) {
178208
return await lookupToolByName(
179209
registry,
180-
(ref as ToolDefinition).metadata?.originalName || ref.name
210+
(ref as ToolDefinition).metadata?.originalName ||
211+
(ref as ToolDefinition).name
181212
);
182213
}
183214
throw new Error('Tools must be strings, tool definitions, or actions.');
@@ -278,14 +309,16 @@ export function defineTool<I extends z.ZodTypeAny, O extends z.ZodTypeAny>(
278309
function implementTool<I extends z.ZodTypeAny, O extends z.ZodTypeAny>(
279310
a: ToolAction<I, O>,
280311
config: ToolConfig<I, O>,
281-
registry: Registry
312+
registry?: Registry
282313
) {
283314
(a as ToolAction<I, O>).respond = (interrupt, responseData, options) => {
284-
assertUnstable(
285-
registry,
286-
'beta',
287-
"The 'tool.reply' method is part of the 'interrupts' beta feature."
288-
);
315+
if (registry) {
316+
assertUnstable(
317+
registry,
318+
'beta',
319+
"The 'tool.reply' method is part of the 'interrupts' beta feature."
320+
);
321+
}
289322
parseSchema(responseData, {
290323
jsonSchema: config.outputJsonSchema,
291324
schema: config.outputSchema,
@@ -303,11 +336,13 @@ function implementTool<I extends z.ZodTypeAny, O extends z.ZodTypeAny>(
303336
};
304337

305338
(a as ToolAction<I, O>).restart = (interrupt, resumedMetadata, options) => {
306-
assertUnstable(
307-
registry,
308-
'beta',
309-
"The 'tool.restart' method is part of the 'interrupts' beta feature."
310-
);
339+
if (registry) {
340+
assertUnstable(
341+
registry,
342+
'beta',
343+
"The 'tool.restart' method is part of the 'interrupts' beta feature."
344+
);
345+
}
311346
let replaceInput = options?.replaceInput;
312347
if (replaceInput) {
313348
replaceInput = parseSchema(replaceInput, {
@@ -352,6 +387,14 @@ export function isToolResponse(part: Part): part is ToolResponsePart {
352387
return !!part.toolResponse;
353388
}
354389

390+
export function isDynamicTool(t: unknown): t is DynamicToolAction {
391+
return (
392+
(isDetachedAction(t) || isAction(t)) &&
393+
t.__action.metadata?.type === 'tool' &&
394+
t.__action.metadata?.dynamic
395+
);
396+
}
397+
355398
export function defineInterrupt<I extends z.ZodTypeAny, O extends z.ZodTypeAny>(
356399
registry: Registry,
357400
config: InterruptConfig<I, O>
@@ -396,19 +439,17 @@ function interruptTool(registry: Registry) {
396439
* Genkit registry and can be defined dynamically at runtime.
397440
*/
398441
export function dynamicTool<I extends z.ZodTypeAny, O extends z.ZodTypeAny>(
399-
ai: HasRegistry,
400442
config: ToolConfig<I, O>,
401443
fn?: ToolFn<I, O>
402-
): ToolAction<I, O> {
403-
const a = action(
404-
ai.registry,
444+
): DynamicToolAction<I, O> {
445+
const a = detachedAction(
405446
{
406447
...config,
407448
actionType: 'tool',
408449
metadata: { ...(config.metadata || {}), type: 'tool', dynamic: true },
409450
},
410451
(i, runOptions) => {
411-
const interrupt = interruptTool(ai.registry);
452+
const interrupt = interruptTool(runOptions.registry);
412453
if (fn) {
413454
return fn(i, {
414455
...runOptions,
@@ -419,6 +460,19 @@ export function dynamicTool<I extends z.ZodTypeAny, O extends z.ZodTypeAny>(
419460
return interrupt();
420461
}
421462
);
422-
implementTool(a as ToolAction<I, O>, config, ai.registry);
423-
return a as ToolAction<I, O>;
463+
implementTool(a as any, config);
464+
return {
465+
__action: {
466+
...a.__action,
467+
metadata: {
468+
...a.__action.metadata,
469+
type: 'tool',
470+
},
471+
},
472+
attach(registry) {
473+
const bound = a.attach(registry);
474+
implementTool(bound as ToolAction<I, O>, config);
475+
return bound;
476+
},
477+
} as DynamicToolAction<I, O>;
424478
}

0 commit comments

Comments
 (0)