///
import {
BuiltInLLMTool,
Cell,
compileAndRun,
computed,
fetchData,
fetchProgram,
handler,
ifElse,
navigateTo,
pattern,
wish,
} from "commontools";
///// COMMON TOOLS (get it?) ////
/**
* Calculate the result of a mathematical expression.
* Supports +, -, *, /, and parentheses.
*/
type CalculatorRequest = {
/** The mathematical expression to evaluate. */
expression: string;
/** The base to use for the calculation. */
base?: number;
};
export const calculator = pattern<
CalculatorRequest,
string | { error: string }
>(({ expression, base }) => {
return computed(() => {
const sanitized = expression.replace(/[^0-9+\-*/().\s]/g, "");
let sanitizedBase = Number(base);
if (
Number.isNaN(sanitizedBase) || sanitizedBase < 2 || sanitizedBase > 36
) {
sanitizedBase = 10;
}
let result;
try {
result = Function(
`"use strict"; return Number(${sanitized}).toString(${sanitizedBase})`,
)();
} catch (error) {
result = { error: (error as any)?.message || "" };
}
return result;
});
});
/** Add an item to the list. */
type AddListItemRequest = {
/** The item to add to the list. */
item: string;
result: Cell;
};
/** Read all items from the list. */
type ReadListItemsRequest = {
result: Cell;
};
export type ListItem = {
title: string;
};
export const addListItem = handler<
AddListItemRequest,
{ list: Cell }
>(
(args, state) => {
try {
state.list.push({ title: args.item });
args.result.set(`${state.list.get().length} items`);
} catch (error) {
args.result.set(`Error: ${(error as any)?.message || ""}`);
}
},
);
export const readListItems = handler<
ReadListItemsRequest,
{ list: ListItem[] }
>(
(args, state) => {
try {
const items = state.list;
if (items.length === 0) {
args.result.set("The list is empty");
} else {
const itemList = items.map((item, index) =>
`${index + 1}. ${item.title}`
).join("\n");
args.result.set(`List items (${items.length} total):\n${itemList}`);
}
} catch (error) {
args.result.set(`Error: ${(error as any)?.message || ""}`);
}
},
);
/** Search the web for information. */
type SearchQuery = {
/** The query to search the web for. */
query: string;
};
type SearchWebResult = {
results: {
title: string;
url: string;
description: string;
}[];
};
export const searchWeb = pattern<
SearchQuery,
SearchWebResult | { error: string }
>(({ query }) => {
const { result, error } = fetchData({
url: "/api/agent-tools/web-search",
mode: "json",
options: {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: {
query,
max_results: 5,
},
},
});
// TODO(seefeld): Should we instead return { result, error }? Or allocate a
// special [ERROR] for errors? Ideally this isn't specific to using recipes as
// tools but a general pattern.
return ifElse(error, { error }, result);
});
/** Read and extract content from a specific webpage URL. */
type ReadWebRequest = {
/** The URL of the webpage to read and extract content from. */
url: string;
};
type ReadWebResult = {
content: string;
metadata: {
title?: string;
author?: string;
date?: string;
word_count: number;
};
};
export const readWebpage = pattern<
ReadWebRequest,
ReadWebResult | { error: string }
>(({ url }) => {
const { result, error } = fetchData({
url: "/api/agent-tools/web-read",
mode: "json",
options: {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: {
url,
max_tokens: 4000,
include_code: true,
},
},
});
return ifElse(error, { error }, result);
});
type ToolsInput = {
list: ListItem[];
};
export default pattern(({ list }) => {
const tools: Record = {
search_web: {
pattern: searchWeb,
},
read_webpage: {
pattern: readWebpage,
},
calculator: {
pattern: calculator,
},
addListItem: {
handler: addListItem({ list }),
},
};
return { tools, list };
});
/**
* `fetchAndRunPattern({ url: "https://...", args: {} })`
*
* Instantiates patterns (e.g. from listPatternIndex) and returns the cell that
* contains the results. The instantiated pattern will keep running and updating
* the cell. Pass the resulting cell to `navigateTo` to show the pattern's UI.
*
* Pass in arguments to initialize the pattern. It's especially useful to pass
* in links to other cells as `{ "@link": "/of:bafe.../path/to/data" }`.
*/
type FetchAndRunPatternInput = {
url: string;
args: Cell;
};
export const fetchAndRunPattern = pattern(
({ url, args }) => {
const { pending: _fetchPending, result: program, error: _fetchError } =
fetchProgram({ url });
// Use computed to safely handle when program is undefined/pending
// Filter out undefined elements to handle race condition where array proxy
// pre-allocates with undefined before populating elements
const compileParams = computed(() => ({
// Note: Type predicate removed - doesn't work with OpaqueCell types after transformation
files: (program?.files ?? []).filter(
(f) => f !== undefined && f !== null && typeof f.name === "string",
),
main: program?.main ?? "",
input: args,
}));
const { pending, result, error } = compileAndRun(compileParams);
return ifElse(
computed(() => pending || (!result && !error)),
undefined,
{
cell: result,
error,
},
);
},
);
/**
* `navigateTo({ cell: { "@link": "/of:xyz" } })` - Navigates to that cell's UI
*
* Especially useful after instantiating a pattern with fetchAndRunPattern:
* Pass the "@link" you get at `cell` to navigate to the pattern's view.
*/
type NavigateToPatternInput = { cell: Cell }; // Hack to steer LLM
export const navigateToPattern = pattern(
({ cell }) => {
const success = navigateTo(cell);
return ifElse(success, { success }, undefined);
},
);
/**
* `listPatternIndex()` - Returns the index of patterns.
*
* Useful as input to fetchAndRun.
*/
type ListPatternIndexInput = Record;
export const listPatternIndex = pattern(
({ _ }) => {
const patternIndexUrl = wish<{ url: string }>({ query: "#pattern-index" });
const { pending, result } = fetchData({
url: computed(() =>
patternIndexUrl.result.url ?? "/api/patterns/index.md"
),
mode: "text",
});
return ifElse(computed(() => pending || !result), undefined, { result });
},
);