import { charmId, CharmManager, DEFAULT_MODEL } from "@commontools/charm"; import { nameSchema } from "@commontools/runner/schemas"; import { NAME } from "@commontools/runner"; import { extractTextFromLLMResponse, LLMClient } from "@commontools/llm"; import { Cell } from "@commontools/runner"; import { isObject } from "@commontools/utils/types"; export type CharmSearchResult = { charm: Cell; name: string; reason: string; }; export async function searchCharms( input: string, charmManager: CharmManager, ): Promise<{ charms: CharmSearchResult[]; thinking: string; }> { try { const charms = charmManager.getCharms(); await charmManager.sync(charms); const results = await Promise.all( charms.get().map(async (charm) => { try { const data = charm.asSchema(nameSchema).get(); const title = data?.[NAME] ?? "Untitled"; const recipe = await charmManager.syncRecipe(charm); return { title: title + ` (#${charmId(charm)!.slice(-4)})`, description: isObject(recipe.argumentSchema) ? recipe.argumentSchema.description : undefined, id: charmId(charm)!, value: charm.entityId!, }; } catch (error) { console.error(`Error processing charm:`, error); // Return a minimal viable object to keep the array intact return { title: "Error loading charm", description: "Failed to load charm details", id: charm.entityId ? charmId(charm)! : "unknown", value: charm.entityId || "unknown", }; } }), ); // Early return if no charms are found if (!results.length) { console.warn("No charms are available to search through."); return { thinking: "No charms are available to search through.", charms: [], }; } const response = await new LLMClient().sendRequest({ system: `Pick up to the 3 most appropriate (if any) charms from the list that match the user's request: ${ results.map((result) => ` ${result.title} ${result.description} ` ).join("\n ") } When responding, you may include a terse paragraph of your reasoning within a tag, then return a list of charms using Reason it's appropriate in the text.`, messages: [{ role: "user", content: input }], model: DEFAULT_MODEL, cache: false, metadata: { context: "workflow", workflow: "search-charms", generationId: crypto.randomUUID(), }, }); // Parse the thinking tag content const thinkingMatch = extractTextFromLLMResponse(response).match( /([\s\S]*?)<\/thinking>/, ); const thinking = thinkingMatch ? thinkingMatch[1].trim() : ""; // Parse all charm tags const charmMatches = extractTextFromLLMResponse(response).matchAll( /([\s\S]*?)<\/charm>/g, ); const selectedCharms: { charm: Cell; name: string; reason: string; }[] = []; if (charmMatches) { for (const match of charmMatches) { const charmId = match[1]; const charmName = match[2]; const reason = match[3].trim(); // Find the original charm data from results const originalCharm = await charmManager.get(charmId); if (originalCharm) { selectedCharms.push({ charm: originalCharm, name: charmName, reason, }); } } } return { thinking, charms: selectedCharms, }; } catch (error: unknown) { console.error( "Search charms error:", (isObject(error) && "message" in error) ? error.message : JSON.stringify(error), ); return { thinking: "An error occurred while searching for charms.", charms: [], }; } }