Skip to content

Commit e5dc106

Browse files
committed
fix: add legacy prompt support with tool function resolution
1 parent db56836 commit e5dc106

2 files changed

Lines changed: 215 additions & 1 deletion

File tree

scripts/functions-runner.ts

Lines changed: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,22 @@ type CodeRegistryItem = {
3030
type EventRegistryItem = {
3131
project?: ProjectRef;
3232
toFunctionDefinition?: (resolver: Resolver) => Promise<JsonObject>;
33+
name?: string;
34+
slug?: string;
35+
description?: string;
36+
ifExists?: string;
37+
metadata?: JsonValue;
38+
prompt?: JsonValue;
39+
toolFunctions?: LegacyToolFunction[];
40+
};
41+
42+
type LegacyToolFunction = {
43+
type?: string;
44+
id?: string;
45+
name?: string;
46+
slug?: string;
47+
project?: ProjectRef;
48+
project_id?: string;
3349
};
3450

3551
type CodeEntry = {
@@ -101,6 +117,7 @@ function currentRegistry(fallback: EvalRegistry): EvalRegistry {
101117

102118
async function collectFunctionEvents(
103119
items: EventRegistryItem[],
120+
includeLegacyPrompts: boolean,
104121
): Promise<FunctionEventEntry[]> {
105122
const entries: FunctionEventEntry[] = [];
106123

@@ -113,6 +130,12 @@ async function collectFunctionEvents(
113130

114131
for (const item of items) {
115132
if (!item.toFunctionDefinition) {
133+
if (includeLegacyPrompts) {
134+
const entry = await collectLegacyPromptEvent(item, resolver);
135+
if (entry) {
136+
entries.push(entry);
137+
}
138+
}
116139
continue;
117140
}
118141

@@ -141,6 +164,107 @@ async function collectFunctionEvents(
141164
return entries;
142165
}
143166

167+
async function collectLegacyPromptEvent(
168+
item: EventRegistryItem,
169+
resolver: Resolver,
170+
): Promise<FunctionEventEntry | null> {
171+
if (typeof item.name !== "string" || typeof item.slug !== "string") {
172+
return null;
173+
}
174+
175+
const normalizedPrompt = toJsonValue(item.prompt ?? {});
176+
if (!isJsonObject(normalizedPrompt)) {
177+
return null;
178+
}
179+
180+
const promptData: JsonObject = { ...normalizedPrompt };
181+
const toolFunctions = Array.isArray(item.toolFunctions)
182+
? item.toolFunctions
183+
: [];
184+
if (toolFunctions.length > 0) {
185+
const resolvedTools: JsonValue[] = [];
186+
for (const tool of toolFunctions) {
187+
const resolved = await resolveLegacyToolFunction(tool, resolver);
188+
if (resolved) {
189+
resolvedTools.push(resolved);
190+
}
191+
}
192+
if (resolvedTools.length > 0) {
193+
promptData.tool_functions = resolvedTools;
194+
}
195+
}
196+
197+
const selector = asProjectSelector(item.project);
198+
const projectId =
199+
typeof selector.project_id === "string" ? selector.project_id : undefined;
200+
const projectName =
201+
typeof selector.project_name === "string"
202+
? selector.project_name
203+
: undefined;
204+
205+
const event: JsonObject = {
206+
name: item.name,
207+
slug: item.slug,
208+
description: typeof item.description === "string" ? item.description : "",
209+
function_data: {
210+
type: "prompt",
211+
},
212+
prompt_data: promptData,
213+
};
214+
if (typeof item.ifExists === "string") {
215+
event.if_exists = item.ifExists;
216+
}
217+
if (item.metadata !== undefined) {
218+
event.metadata = item.metadata;
219+
}
220+
221+
return {
222+
kind: "function_event",
223+
project_id: projectId,
224+
project_name: projectName,
225+
event,
226+
};
227+
}
228+
229+
async function resolveLegacyToolFunction(
230+
tool: LegacyToolFunction,
231+
resolver: Resolver,
232+
): Promise<JsonObject | null> {
233+
if (
234+
typeof tool.slug === "string" &&
235+
tool.slug.length > 0 &&
236+
tool.project !== undefined
237+
) {
238+
const projectId = await resolver.resolve(tool.project);
239+
if (projectId.length > 0) {
240+
return {
241+
type: "slug",
242+
project_id: projectId,
243+
slug: tool.slug,
244+
};
245+
}
246+
}
247+
248+
const direct: JsonObject = {};
249+
if (typeof tool.type === "string") {
250+
direct.type = tool.type;
251+
}
252+
if (typeof tool.id === "string") {
253+
direct.id = tool.id;
254+
}
255+
if (typeof tool.name === "string") {
256+
direct.name = tool.name;
257+
}
258+
if (typeof tool.project_id === "string") {
259+
direct.project_id = tool.project_id;
260+
}
261+
if (typeof tool.slug === "string") {
262+
direct.slug = tool.slug;
263+
}
264+
265+
return Object.keys(direct).length > 0 ? direct : null;
266+
}
267+
144268
function collectCodeEntries(items: CodeRegistryItem[]): CodeEntry[] {
145269
const entries: CodeEntry[] = [];
146270

@@ -197,9 +321,13 @@ async function processFile(filePath: string): Promise<ManifestFile> {
197321

198322
const entries: Array<CodeEntry | FunctionEventEntry> = [
199323
...collectCodeEntries(registry.functions as CodeRegistryItem[]),
200-
...(await collectFunctionEvents(registry.prompts as EventRegistryItem[])),
324+
...(await collectFunctionEvents(
325+
registry.prompts as EventRegistryItem[],
326+
true,
327+
)),
201328
...(await collectFunctionEvents(
202329
registry.parameters as EventRegistryItem[],
330+
false,
203331
)),
204332
];
205333

src/functions/push.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,7 @@ async fn push_file(
848848
Value::String(args.if_exists.as_str().to_string()),
849849
);
850850
}
851+
default_prompt_function_type(object);
851852
}
852853

853854
function_events.push(event);
@@ -962,6 +963,36 @@ fn calculate_upload_counts(total_entries: usize, ignored_entries: Option<usize>)
962963
(uploaded_entries, ignored_entries)
963964
}
964965

966+
fn default_prompt_function_type(event: &mut Map<String, Value>) {
967+
if !is_prompt_function_event(event) {
968+
return;
969+
}
970+
971+
if function_type_missing_or_empty(event.get("function_type")) {
972+
event.insert(
973+
"function_type".to_string(),
974+
Value::String("prompt".to_string()),
975+
);
976+
}
977+
}
978+
979+
fn is_prompt_function_event(event: &Map<String, Value>) -> bool {
980+
event
981+
.get("function_data")
982+
.and_then(Value::as_object)
983+
.and_then(|function_data| function_data.get("type"))
984+
.and_then(Value::as_str)
985+
== Some("prompt")
986+
}
987+
988+
fn function_type_missing_or_empty(value: Option<&Value>) -> bool {
989+
match value {
990+
None | Some(Value::Null) => true,
991+
Some(Value::String(s)) => s.trim().is_empty(),
992+
Some(_) => false,
993+
}
994+
}
995+
965996
fn run_functions_runner(
966997
args: &PushArgs,
967998
files: &[PathBuf],
@@ -2848,6 +2879,61 @@ mod tests {
28482879
assert_eq!(calculate_upload_counts(3, None), (3, 0));
28492880
}
28502881

2882+
#[test]
2883+
fn prompt_event_defaults_function_type_when_missing() {
2884+
let mut event = Map::new();
2885+
event.insert(
2886+
"function_data".to_string(),
2887+
serde_json::json!({
2888+
"type": "prompt"
2889+
}),
2890+
);
2891+
2892+
default_prompt_function_type(&mut event);
2893+
2894+
assert_eq!(
2895+
event.get("function_type"),
2896+
Some(&Value::String("prompt".to_string()))
2897+
);
2898+
}
2899+
2900+
#[test]
2901+
fn prompt_event_preserves_existing_function_type() {
2902+
let mut event = Map::new();
2903+
event.insert(
2904+
"function_data".to_string(),
2905+
serde_json::json!({
2906+
"type": "prompt"
2907+
}),
2908+
);
2909+
event.insert(
2910+
"function_type".to_string(),
2911+
Value::String("scorer".to_string()),
2912+
);
2913+
2914+
default_prompt_function_type(&mut event);
2915+
2916+
assert_eq!(
2917+
event.get("function_type"),
2918+
Some(&Value::String("scorer".to_string()))
2919+
);
2920+
}
2921+
2922+
#[test]
2923+
fn non_prompt_event_does_not_default_function_type() {
2924+
let mut event = Map::new();
2925+
event.insert(
2926+
"function_data".to_string(),
2927+
serde_json::json!({
2928+
"type": "code"
2929+
}),
2930+
);
2931+
2932+
default_prompt_function_type(&mut event);
2933+
2934+
assert!(!event.contains_key("function_type"));
2935+
}
2936+
28512937
#[test]
28522938
fn requirements_reference_escape_is_rejected() {
28532939
let dir = tempfile::tempdir().expect("tempdir");

0 commit comments

Comments
 (0)