From ccc245fe58ba585ca917928ca783abd774f956f8 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Thu, 3 Apr 2025 16:19:03 -0700 Subject: [PATCH 01/19] AI generated README.md describing the framework --- recipes/README.md | 476 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 476 insertions(+) create mode 100644 recipes/README.md diff --git a/recipes/README.md b/recipes/README.md new file mode 100644 index 000000000..01be4752c --- /dev/null +++ b/recipes/README.md @@ -0,0 +1,476 @@ +# Recipe Framework Documentation + +## Overview + +The Recipe Framework appears to be a declarative, reactive system for building +integrations and data transformations. It uses a component-based architecture +where recipes are autonomous modules that can import, process, and export data. + +## Core Concepts + +### Recipe + +A recipe is the fundamental building block, defined using the `recipe()` +function. It takes three parameters: + +- Input Schema: Defines the input parameters and their types using JSON Schema +- Output Schema: Defines the output structure +- Implementation Function: A function that receives the inputs and returns + outputs + +### Schemas and Type Safety + +The framework provides two ways to define schemas for recipes, handlers, and lifted functions: + +1. **TypeScript Generics**: Directly pass TypeScript interfaces as type parameters + ```typescript + const myHandler = handler((input, state) => { /* ... */ }); + ``` + +2. **JSON Schema**: Define schemas in JSON format (recommended approach) + ```typescript + const myHandler = handler( + { type: "object", properties: { /* input schema */ } }, + { type: "object", properties: { /* state schema */ } }, + (input, state) => { /* ... */ } + ); + ``` + +The JSON Schema approach provides several benefits: +- Runtime validation +- Self-documentation +- Better serialization support +- Integration with framework tooling + +Importantly, the framework automatically derives TypeScript types from JSON Schemas, +giving you full type inference and type checking in your handler and lift functions. + +### Data Flow + +The framework uses a reactive programming model: + +- `cell`: Represents a reactive state container that can be updated and observed +- `derive`: Creates a derived value that updates when its dependencies change +- `lift`: Similar to derive, but lifts a regular function into the reactive + graph + - `derive(param, function)` is an alias to `lift(function)(param)` + +### Handlers vs Reactive Functions + +There are important differences between the types of functions in the framework: + +#### Handlers + +Handlers are functions that declare node types in the reactive graph that respond to events: + +- Created with `handler()` function +- Can be defined with JSON Schema or TypeScript generics (see Schemas section) +- The state schema can use `asCell: true` to receive cells directly: + ```typescript + // Using asCell: true + const updateCounter = handler( + { type: "object" }, // Input schema + { + type: "object", + properties: { + count: { + type: "number", + asCell: true // Will receive a Cell instance + } + } + }, + (input, { count }) => { + // Now count is a Cell instance + count.set(count.get() + 1); + } + ); + ``` +- Instantiated in recipes by passing parameters: + ```typescript + const stream = definedHandler({ cell1, cell2 }); + ``` +- Return a stream that can be: + - Passed to JSX components as event handlers (e.g., `onClick={stream}`) + - Returned by a recipe for external consumption + - Passed to another handler which can call `.send(...)` on it to generate events +- Can update cells and trigger side effects +- Support async operations for data processing +- React to outside events (user interactions, API responses) +- Cannot directly call built-in functions like `llm` + +#### Reactive Functions (lift/derive) + +- `lift`: Declares a reactive node type that transforms data in the reactive graph + - Can also use JSON Schema for type safety with input and output schemas: + ```typescript + const transformData = lift( + // Input schema + { + type: "object", + properties: { + value: { type: "number" }, + multiplier: { type: "number" } + }, + required: ["value", "multiplier"] + }, + // Output schema + { + type: "number" + }, + // Function with inferred types + ({ value, multiplier }) => value * multiplier + ); + ``` + - When instantiated, it inserts a reactive node in the graph: + ```typescript + const newCell = liftedFunction({ cell1, cell2 }); + ``` + - The result is a proxy cell that can be further referenced: + ```typescript + const compound = { data: newCell.field }; + ``` +- `derive`: A convenience wrapper around lift: + ```typescript + // These are equivalent: + const result1 = derive({ x, y }, ({ x, y }) => x + y); + const result2 = lift(({ x, y }) => x + y)({ x, y }); + ``` +- React to data changes within the reactive graph +- Cannot directly call built-in functions like `llm` + +### Data as Futures + +Within recipes, functions cannot directly read values - they can only pass references to other nodes. Think of the data passed to a recipe as "futures" - promises of values that will be available when the program runs. + +The system allows accessing fields using the dot notation (e.g., `cell.field`), but this doesn't actually read values - it's creating new references to future data. + +```tsx +// This doesn't read values, it creates references: +const data = { + firstName: user.firstName, + lastName: user.lastName +}; +``` + +### UI Components + +Recipes can include UI components using JSX syntax: + +- Common components like `common-input`, `common-hstack`, `common-vstack` +- Integration-specific components like `common-google-oauth` +- Custom components can be created as needed + +#### JSX and TypeScript + +The Recipe Framework uses custom JSX elements that may generate TypeScript +linter errors in the IDE. Common errors include: + +- `Property 'common-*' does not exist on type 'JSX.IntrinsicElements'` +- Style-related type errors when using string styles +- Event handler type mismatches + +These are expected in the development environment and don't affect runtime +functionality. The framework's processor handles these custom elements correctly +even though TypeScript doesn't recognize them. + +### Built-in Functions + +Several utility functions are available: + +- `llm`: Makes calls to language models with parameters for system prompt, user + prompt, etc. +- `fetchData`: Fetches data from URLs +- `streamData`: Streams data from URLs +- `ifElse`: Conditional logic for reactive flows + ```typescript + // Creates a reactive value that changes based on the condition + const message = ifElse( + user.isLoggedIn, + str`Welcome back, ${user.name}!`, + "Please log in to continue" + ); + ``` +- `str`: Template literal for string interpolation with reactive values, creating reactive strings + ```typescript + // Creates a reactive string that updates when cells change + const greeting = str`Hello, ${user.name}! You have ${notifications.count} new messages.`; + ``` + +**Important**: These built-in functions can only be called from within a recipe +function, not from handlers, lift, or derive functions. They create nodes in the +reactive graph and cannot be awaited directly. + +## JSX and Reactive Arrays + +The Recipe Framework has an interesting approach to handling arrays with JSX: + +```typescript +{ + items.map((item) => ); +} +``` + +While this looks like regular JSX mapping, in the Recipe framework, this +actually creates mini-recipes for each item in the array, constructing a +reactive graph. Each mapped item becomes a reactive node that updates when the +source data changes. + +In the todo-list example, this pattern is used to create draggable todo items, +where each item has its own encapsulated recipe: + +```typescript +{items.map((item: TodoItem) => ( + ({ + [UI]: ( + + ), + })), + )} + > + + +))} +``` + +This approach allows for efficient updates and encapsulation of item-specific +logic. + +## Best Practices + +1. **Use JSON Schema Over TypeScript Generics**: Prefer using JSON Schema for type definitions rather than TypeScript generics. This provides better runtime validation, self-documentation, and compatibility with framework tooling. + +2. **Use `asCell: true` for Handler State**: When defining handler state schema, use `asCell: true` for properties that need to be updated. This gives you direct access to the Cell methods like `.set()` and `.get()`. + +3. **Reference Data Instead of Copying**: When transforming data, reference the original objects rather than copying all their properties. This maintains reactivity and creates cleaner code: + + ```typescript + // DO THIS: Reference the original data + const processedItems = items.map(item => ({ + originalItem: item, // Direct reference + processed: processItem(item) + })); + + // NOT THIS: Spread/copy all properties + const processedItems = items.map(item => ({ + id: item.id, // Copying each field + name: item.name, // breaks the reactive + date: item.date, // connection to the + // ... more fields // original data + processed: processItem(item) + })); + ``` + +4. **Use Reactive String Templates**: Use the `str` template literal to create reactive strings that update when their inputs change: + + ```typescript + const message = str`Hello ${user.name}, you have ${notifications.count} notifications`; + ``` + +5. **Keep Logic Inside Recipes**: Place as much logic as possible inside recipe functions or the `map` function. This creates a cleaner reactive system where data flow is transparent. + +6. **Leverage Framework Reactivity**: Let the framework track changes and updates. Avoid manually tracking which items have been processed or creating complex state management patterns. + +7. **Composition**: Build complex flows by composing smaller recipes. + +8. **Minimize Side Effects**: Side effects should be managed through handlers rather than directly in recipes. + +9. **Schema Reuse**: Define schemas once and reuse them across recipes, handlers, and lifted functions to maintain consistency. + +10. **Follow Type Through Schema**: Leverage the framework's automatic type inference from JSON Schema rather than duplicating type definitions with TypeScript interfaces. + +## Advanced Type Concepts + +### Schema to TypeScript Inference + +The framework provides automatic type inference from JSON Schema to TypeScript types: + +```typescript +// Define a schema +const PersonSchema = { + type: "object", + properties: { + name: { type: "string" }, + age: { type: "number" } + }, + required: ["name"] +} as const; + +// Automatically infer TypeScript type +type Person = Schema; +// Equivalent to: type Person = { name: string; age?: number; }; +``` + +### Cell Type vs Value Type + +When working with handlers that use `asCell: true`, it's important to understand the distinction: + +```typescript +// Regular state property (value type) +{ count: number } // Handler receives the number directly + +// Cell-typed property +{ count: { type: "number", asCell: true } } // Handler receives Cell +``` + +With Cell-typed properties, the handler function receives actual Cell instances with methods: +- `cell.get()`: Get the current value +- `cell.set(newValue)`: Set a new value +- `cell.update(fn)`: Update using a function + +This allows for more control over state updates, including: +- Batching multiple updates +- Conditional updates based on current value +- Handling complex state transitions + +## Framework Goals + +The primary goal of this framework is to generate "low taint code" that enables effective data flow analysis. In this system: + +- Recipes are transparent to data flow analysis +- Functions passed to `handler` or `lift` aren't transparent (they're "tainted") +- JSON Schema provides a clean abstraction for data validation and type safety +- The `asCell: true` pattern maintains reactivity while allowing direct manipulation + +By following these principles, applications built with this framework can achieve predictable data flow, easier testing, and better security through data flow isolation. + +## Integration Process + +1. Define schemas for inputs and outputs +2. Create cells to hold state +3. Implement handlers for user interactions +4. Return an object with processed data and UI components + +## Example Pattern + +```typescript +export default recipe( + InputSchema, + OutputSchema, + ({ input1, input2 }) => { + const state = cell([]); + + // Define handlers and side effects + + return { + [NAME]: "Recipe Name", + [UI]: ( + // JSX component + ), + outputField1: state, + outputField2: derivedValue + }; + } +); +``` + +## Integration Between Recipes + +Recipes can be composed together, where the output of one recipe serves as the +input to another. This is done by: + +1. Defining a common data schema between recipes +2. Exporting data from the source recipe +3. Importing that data as input in the consuming recipe + +For example, our Email Summarizer recipe takes emails from the Gmail recipe as +input: + +- Gmail recipe exports an array of emails +- Email Summarizer recipe consumes these emails and processes them with LLM + +## LLM Integration + +The framework provides integration with language models through the `llm` +function: + +```typescript +const result = llm({ + system: "System prompt here", // Instructions for the LLM + prompt: "User prompt here", // Content to process + // Optional parameters + stop: "custom stop sequence", // Stop generation at this sequence + max_tokens: 1000, // Max tokens to generate +}); +``` + +**Important restrictions**: + +1. The `llm` function can only be called directly within a recipe function, not + in handlers, lift, or derive functions +2. You cannot `await` the result directly, as it's a node in the reactive graph +3. To use LLM results, you need to access them through the reactive graph (e.g., + via derive) + +The result object includes: + +- `result`: The generated text +- `partial`: Streaming partial results +- `pending`: Boolean indicating if the request is still processing +- `error`: Any error that occurred + +## Reactive Processing Pattern + +A common pattern in recipes is: + +1. Initialize state using `cell()` +2. Create derived values with `derive()` +3. Define handlers for UI events and data processing +4. Create async functions for complex operations +5. Return a recipe object with UI and exported values + +## Example Pattern for Data Transformation Recipes + +```typescript +export default recipe( + InputSchema, + OutputSchema, + ({ inputData, settings }) => { + // Initialize state + const processedData = cell([]); + + // Process data with LLM (directly in recipe) + // Notice we call map() directly on the cell - inputData is a cell + const processedItems = inputData.map(item => { + return { + originalItem: item, + llmResult: llm({ + system: "System prompt", + prompt: `Process this: ${item.content}`, + }) + }; + }); + + // Create derived value from LLM results + const summaries = derive(processedItems, items => + items.map(item => ({ + id: item.originalItem.id, + summary: item.llmResult.result || "Processing...", + })) + ); + + // Handler for user interactions + const refreshData = handler<{}, { processedData: Cell }>( + (_, state) => { + // Update state based on user action + // Note: Cannot call llm() directly here + } + ); + + return { + [NAME]: "Recipe Name", + [UI]: ( + // JSX UI component + ), + processedData, + }; + } +); +``` From bd65f0d8b885893ff6cc01d301dd15fb3370b980 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Thu, 3 Apr 2025 16:19:22 -0700 Subject: [PATCH 02/19] initial take on email summarizer --- recipes/email-summarizer.tsx | 392 +++++++++++++++++++++++++++++++++++ 1 file changed, 392 insertions(+) create mode 100644 recipes/email-summarizer.tsx diff --git a/recipes/email-summarizer.tsx b/recipes/email-summarizer.tsx new file mode 100644 index 000000000..2aff0f88e --- /dev/null +++ b/recipes/email-summarizer.tsx @@ -0,0 +1,392 @@ +import { h } from "@commontools/html"; +import { + cell, + derive, + handler, + ID, + JSONSchema, + lift, + llm, + NAME, + recipe, + Schema, + str, + UI, +} from "@commontools/builder"; +import { Cell } from "@commontools/runner"; + +// Email schema based on Gmail recipe +const EmailProperties = { + id: { + type: "string", + title: "Email ID", + description: "Unique identifier for the email", + }, + threadId: { + type: "string", + title: "Thread ID", + description: "Identifier for the email thread", + }, + labelIds: { + type: "array", + items: { type: "string" }, + title: "Labels", + description: "Gmail labels assigned to the email", + }, + snippet: { + type: "string", + title: "Snippet", + description: "Brief preview of the email content", + }, + subject: { + type: "string", + title: "Subject", + description: "Email subject line", + }, + from: { + type: "string", + title: "From", + description: "Sender's email address", + }, + date: { + type: "string", + title: "Date", + description: "Date and time when the email was sent", + }, + to: { + type: "string", + title: "To", + description: "Recipient's email address", + }, + plainText: { + type: "string", + title: "Plain Text Content", + description: "Email content in plain text format (often empty)", + }, + htmlContent: { + type: "string", + title: "HTML Content", + description: "Email content in HTML format", + }, + markdownContent: { + type: "string", + title: "Markdown Content", + description: "Email content converted to Markdown format", + }, +} as const; + +const EmailSchema = { + type: "object", + properties: EmailProperties, + required: Object.keys(EmailProperties), +} as const as JSONSchema; + +type Email = Schema; + +// Extend Email with summary property +interface SummarizedEmail extends Email { + summary: string; +} + +// Input Schema for Email Summarizer +const EmailSummarizerInputSchema = { + type: "object", + properties: { + emails: { + type: "array", + items: { + type: "object", + properties: EmailProperties, + }, + }, + settings: { + type: "object", + properties: { + summaryLength: { + type: "string", + enum: ["short", "medium", "long"], + default: "medium", + description: "Length of the summary", + }, + includeTags: { + type: "boolean", + default: true, + description: "Include tags in the summary", + }, + }, + required: ["summaryLength", "includeTags"], + }, + }, + required: ["emails", "settings"], + description: "Email Summarizer", +} as const satisfies JSONSchema; + +// Output schema - reference the original email rather than copying all properties +const ResultSchema = { + type: "object", + properties: { + summarizedEmails: { + type: "array", + items: { + type: "object", + properties: { + email: EmailSchema, // Reference the complete email + summary: { + type: "string", + title: "Summary", + description: "AI-generated summary of the email", + }, + }, + required: ["email", "summary"], + }, + }, + }, +} as const satisfies JSONSchema; + +// Declare a handler for updating summary length using JSON schema +const updateSummaryLength = handler( + // Input schema (what comes from the event) + { + type: "object", + properties: { + detail: { + type: "object", + properties: { + value: { type: "string" }, + }, + }, + }, + }, + // State schema (what's passed when instantiating the handler) + { + type: "object", + properties: { + summaryLength: { + type: "string", + asCell: true, // Mark as cell + }, + }, + required: ["summaryLength"], + }, + // Handler function with Cell-typed state + ({ detail }, { summaryLength }) => { + // Now summaryLength is a Cell instance + summaryLength.set(detail?.value ?? "medium"); + }, +); + +// Declare a handler for updating includeTags setting using JSON schema +const updateIncludeTags = handler( + // Input schema + { + type: "object", + properties: { + detail: { + type: "object", + properties: { + checked: { type: "boolean" }, + }, + }, + }, + }, + // State schema + { + type: "object", + properties: { + includeTags: { + type: "boolean", + asCell: true, // Mark as cell + }, + }, + required: ["includeTags"], + }, + // Handler function + ({ detail }, { includeTags }) => { + // Now includeTags is a Cell instance + includeTags.set(detail?.checked ?? true); + }, +); + +// Define a lifted function to process email content using JSON schema +const getEmailContent = lift( + // Input schema + { + type: "object", + properties: { + email: { + type: "object", + properties: EmailProperties, + required: Object.keys(EmailProperties), + }, + }, + required: ["email"], + }, + // Output schema + { + type: "object", + properties: { + email: { + type: "object", + properties: EmailProperties, + required: Object.keys(EmailProperties), + }, + content: { type: "string" }, + hasContent: { type: "boolean" }, + }, + required: ["email", "content", "hasContent"], + }, + // Function with inferred types from the schema + ({ email }) => { + const content = email.markdownContent || email.plainText || email.snippet || + ""; + + return { + email, + content: content.trim() ? content : "", + hasContent: content.trim().length > 0, + }; + }, +); + +// The main recipe +export default recipe( + EmailSummarizerInputSchema, + ResultSchema, + ({ emails, settings }) => { + // We'll use str template literals directly instead of helper functions + + // Directly map emails to summaries using proper reactive patterns + // The framework will track which emails need to be processed + const summarizedEmails = emails.map((email) => { + // Process the email content + const emailContent = getEmailContent({ email }); + + // Create prompts using the str template literal for proper reactivity + // This ensures the prompts update when settings change + const lengthInstructions = str`${ + settings.summaryLength === "short" + ? "in 1-2 sentences" + : settings.summaryLength === "long" + ? "in 5-7 sentences" + : "in 3-4 sentences" + }`; + + const tagInstructions = str`${ + settings.includeTags + ? "Include up to 3 relevant tags or keywords in the format #tag at the end of the summary." + : "" + }`; + + // Create system prompt with str to maintain reactivity + const systemPrompt = str` + You are an email assistant that creates concise, informative summaries. + Focus on the main point, action items, and key details. + Output should be ${lengthInstructions}. + ${tagInstructions} + `; + + // Create user prompt with str for reactivity + const userPrompt = str` + Subject: ${email.subject} + From: ${email.from} + Date: ${email.date} + + ${emailContent.content} + `; + + // Call LLM to generate summary + const summaryResult = llm({ + system: systemPrompt, + prompt: userPrompt, + }); + + // Return a simple object that references the original email + // This preserves reactivity and is cleaner + return { + email: email, // Direct reference to the original email + summary: summaryResult.result, // Reference to the LLM result + }; + }); + + // Simple counts derived from the arrays + const summarizedCount = derive( + summarizedEmails, + (emails) => emails.length, + ); + + const totalEmailCount = derive( + emails, + (emails) => emails.length, + ); + + // Instantiate handlers for the UI by passing cells + // Now the handler receives the actual cell instances rather than values + const summaryLengthHandler = updateSummaryLength({ + summaryLength: settings.summaryLength, // This is a cell reference + }); + + const includeTagsHandler = updateIncludeTags({ + includeTags: settings.includeTags, // This is a cell reference + }); + + // Recipe UI and exports + return { + [NAME]: str`Email Summarizer (${summarizedCount}/${totalEmailCount})`, + [UI]: ( + +

Email Summarizer

+ +
+ Emails: {totalEmailCount} + Summarized: {summarizedCount} +
+ + + +
+ + +
+ +
+ + +
+
+
+ +
+ + + + + + + + + + + {summarizedEmails.map((item) => ( + + + + + + + ))} + +
DATEFROMSUBJECTSUMMARY
{item.email.date}{item.email.from}{item.email.subject}{item.summary}
+
+
+ ), + summarizedEmails, + }; + }, +); From 6101568d408e42bb17e9ed688b3072a366fe6ef6 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Fri, 4 Apr 2025 09:29:50 -0700 Subject: [PATCH 03/19] lots of tips after rounds of debugging --- recipes/README.md | 211 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 210 insertions(+), 1 deletion(-) diff --git a/recipes/README.md b/recipes/README.md index 01be4752c..7604fc3ad 100644 --- a/recipes/README.md +++ b/recipes/README.md @@ -249,7 +249,65 @@ logic. 2. **Use `asCell: true` for Handler State**: When defining handler state schema, use `asCell: true` for properties that need to be updated. This gives you direct access to the Cell methods like `.set()` and `.get()`. -3. **Reference Data Instead of Copying**: When transforming data, reference the original objects rather than copying all their properties. This maintains reactivity and creates cleaner code: +3. **Avoid All Direct Conditionals in Recipes**: Never use direct if statements, ternary operators, or any other conditionals inside a recipe function - they won't work properly because they immediately evaluate data instead of creating reactive nodes: + + ```typescript + // DON'T DO THIS - if statements don't work in recipes + const result = emails.map(email => { + if (email.hasContent) { // This won't work! + return processEmail(email); + } else { + return { email, empty: true }; + } + }); + + // DON'T DO THIS EITHER - ternary operators also don't work + const tableHeader = + Name + {settings.showDetails ? Details : null} // This won't work! + ; + + // DON'T DO THIS - ternaries in string templates don't work + const prompt = str` + Process this data + ${settings.includeTimestamp ? "Include timestamps" : "No timestamps"} // This won't work! + `; + + // DO THIS INSTEAD - use ifElse function for conditionals in data flow + const result = emails.map(email => + ifElse( + email.hasContent, + () => processEmail(email), + () => ({ email, empty: true }) + ) + ); + + // USE ifElse IN JSX TOO + const tableHeader = + Name + {ifElse(settings.showDetails, Details, null)} + ; + + // USE ifElse IN STRING TEMPLATES + const includeTimestampText = ifElse( + settings.includeTimestamp, + "Include timestamps", + "No timestamps" + ); + const prompt = str` + Process this data + ${includeTimestampText} + `; + + // WHEN APPROPRIATE - skip conditionals entirely + // and let LLM handle edge cases: + const result = emails.map(email => { + const processed = processWithLLM(email); + return { email, result: processed }; + }); + ``` + +4. **Reference Data Instead of Copying**: When transforming data, reference the original objects rather than copying all their properties. This maintains reactivity and creates cleaner code: ```typescript // DO THIS: Reference the original data @@ -286,6 +344,157 @@ logic. 10. **Follow Type Through Schema**: Leverage the framework's automatic type inference from JSON Schema rather than duplicating type definitions with TypeScript interfaces. +## Schema Best Practices + +When defining schemas in the Recipe Framework, follow these guidelines for best results: + +1. **Define Schemas as Constants**: Declare schemas as constants for reuse and reference: + + ```typescript + const UserSchema = { + type: "object", + properties: { + id: { type: "string" }, + name: { type: "string" }, + email: { type: "string", format: "email" } + }, + required: ["id", "name"] + } as const satisfies JSONSchema; + ``` + +2. **Use `as const satisfies JSONSchema`**: Always use this pattern to ensure proper type inference and compile-time validation: + + ```typescript + // DO THIS + const schema = { /*...*/ } as const satisfies JSONSchema; + + // NOT THIS + const schema = { /*...*/ } as JSONSchema; // Doesn't provide proper type checking + ``` + +3. **Always Include Reasonable Defaults**: Where possible, provide sensible default values to improve usability and reduce errors: + + ```typescript + const SettingsSchema = { + type: "object", + properties: { + theme: { + type: "string", + enum: ["light", "dark", "system"], + default: "system" // Sensible default + }, + fontSize: { + type: "number", + minimum: 8, + maximum: 32, + default: 14 // Reasonable default + }, + notifications: { + type: "boolean", + default: true // Sensible default + } + } + } as const satisfies JSONSchema; + ``` + +4. **Extract Types from Schemas**: Use the `Schema` utility to derive TypeScript types from JSON Schemas: + + ```typescript + const UserSchema = { /*...*/ } as const satisfies JSONSchema; + type User = Schema; + + // Now User is a TypeScript type matching the schema + ``` + +5. **Reference Schemas Instead of Duplicating**: For nested objects, reference existing schemas: + + ```typescript + // Instead of duplicating user properties + const PostSchema = { + type: "object", + properties: { + author: UserSchema, // Reference the existing schema + content: { type: "string" }, + timestamp: { type: "string", format: "date-time" } + }, + required: ["author", "content"] + } as const satisfies JSONSchema; + ``` + +6. **Document Schemas with Descriptions**: Add descriptions to schemas and properties for better self-documentation: + + ```typescript + { + type: "string", + title: "Email", + description: "User's primary email address used for notifications", + format: "email" + } + ``` + +7. **Use `asCell: true` for Reactive State**: In handler state schemas, use `asCell: true` for properties that need direct access to Cell methods: + + ```typescript + const stateSchema = { + type: "object", + properties: { + counter: { + type: "number", + asCell: true, // Will be received as Cell + description: "Counter value that can be directly manipulated", + default: 0 // Provide a default value + } + } + }; + ``` + +8. **Define Required Properties Explicitly**: Always specify which properties are required: + + ```typescript + { + // Properties definition... + required: ["id", "name", "email"] + } + ``` + +9. **Use Schema Composition**: Break down complex schemas into smaller, reusable parts: + + ```typescript + const AddressSchema = { /*...*/ } as const satisfies JSONSchema; + const ContactSchema = { /*...*/ } as const satisfies JSONSchema; + + const UserSchema = { + type: "object", + properties: { + // Basic info + id: { type: "string" }, + name: { type: "string" }, + // Composed schemas + address: AddressSchema, + contact: ContactSchema + }, + required: ["id", "name"] + } as const satisfies JSONSchema; + ``` + +10. **Define Enums with Constant Arrays**: + + ```typescript + const StatusValues = ["pending", "active", "suspended", "deleted"] as const; + + const UserSchema = { + // ... + properties: { + // ... + status: { + type: "string", + enum: StatusValues, + default: "pending" // Provide a reasonable default + } + } + } as const satisfies JSONSchema; + ``` + ## Advanced Type Concepts ### Schema to TypeScript Inference From 0e3d6ee38f04cede668f2a50f8f4db3d72bfa385 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Fri, 4 Apr 2025 09:29:59 -0700 Subject: [PATCH 04/19] date extractor example --- recipes/email-date-extractor.tsx | 595 +++++++++++++++++++++++++++++++ 1 file changed, 595 insertions(+) create mode 100644 recipes/email-date-extractor.tsx diff --git a/recipes/email-date-extractor.tsx b/recipes/email-date-extractor.tsx new file mode 100644 index 000000000..58b2a53f3 --- /dev/null +++ b/recipes/email-date-extractor.tsx @@ -0,0 +1,595 @@ +import { h } from "@commontools/html"; +import { + derive, + handler, + ifElse, + JSONSchema, + lift, + llm, + NAME, + recipe, + Schema, + str, + UI, +} from "@commontools/builder"; + +// Reuse email schema from email-summarizer.tsx +const EmailProperties = { + id: { + type: "string", + title: "Email ID", + description: "Unique identifier for the email", + }, + threadId: { + type: "string", + title: "Thread ID", + description: "Identifier for the email thread", + }, + labelIds: { + type: "array", + items: { type: "string" }, + title: "Labels", + description: "Gmail labels assigned to the email", + }, + snippet: { + type: "string", + title: "Snippet", + description: "Brief preview of the email content", + }, + subject: { + type: "string", + title: "Subject", + description: "Email subject line", + }, + from: { + type: "string", + title: "From", + description: "Sender's email address", + }, + date: { + type: "string", + title: "Date", + description: "Date and time when the email was sent", + }, + to: { + type: "string", + title: "To", + description: "Recipient's email address", + }, + plainText: { + type: "string", + title: "Plain Text Content", + description: "Email content in plain text format (often empty)", + }, + htmlContent: { + type: "string", + title: "HTML Content", + description: "Email content in HTML format", + }, + markdownContent: { + type: "string", + title: "Markdown Content", + description: "Email content converted to Markdown format", + }, +} as const; + +const EmailSchema = { + type: "object", + properties: EmailProperties, + required: Object.keys(EmailProperties), +} as const satisfies JSONSchema; + +// Define the date item schema +const DateItemSchema = { + type: "object", + properties: { + dateText: { + type: "string", + title: "Date Text", + description: "The raw date text found in the email", + }, + normalizedDate: { + type: "string", + title: "Normalized Date", + description: "The date in ISO format (YYYY-MM-DD)", + }, + normalizedTime: { + type: "string", + title: "Normalized Time", + description: "The time in 24-hour format (HH:MM) if available", + }, + context: { + type: "string", + title: "Context", + description: "Brief context around the date mention", + }, + confidence: { + type: "number", + title: "Confidence", + description: "Confidence score (0-1) that this is a relevant date", + }, + }, + required: ["dateText", "normalizedDate", "context", "confidence"], +} as const satisfies JSONSchema; + +// Input Schema for Email Date Extractor +const EmailDateExtractorInputSchema = { + type: "object", + properties: { + emails: { + type: "array", + items: EmailSchema, + }, + settings: { + type: "object", + properties: { + includeEmailDate: { + type: "boolean", + default: false, + description: "Whether to include the email's sent date in results", + }, + extractTimes: { + type: "boolean", + default: true, + description: "Whether to extract time information along with dates", + }, + contextLength: { + type: "number", + default: 100, + description: "Length of context to include around each date mention", + }, + minConfidence: { + type: "number", + default: 0.7, + description: "Minimum confidence threshold for included dates (0-1)", + }, + }, + defailt: {}, + required: [ + "includeEmailDate", + "extractTimes", + "contextLength", + "minConfidence", + ], + }, + }, + required: ["emails", "settings"], + description: "Email Date Extractor", +} as const as JSONSchema; + +// Output Schema +const ResultSchema = { + type: "object", + properties: { + emailsWithDates: { + type: "array", + items: { + type: "object", + properties: { + email: EmailSchema, + dates: { + type: "array", + items: DateItemSchema, + }, + }, + required: ["email", "dates"], + }, + }, + allDates: { + type: "array", + items: DateItemSchema, + }, + }, + required: ["emailsWithDates", "allDates"], +} as const satisfies JSONSchema; + +// Define a handler for updating the includeEmailDate setting +const updateIncludeEmailDate = handler( + { + type: "object", + properties: { + detail: { + type: "object", + properties: { + checked: { type: "boolean" }, + }, + }, + }, + }, + { + type: "object", + properties: { + includeEmailDate: { + type: "boolean", + asCell: true, + }, + }, + required: ["includeEmailDate"], + }, + ({ detail }, { includeEmailDate }) => { + includeEmailDate.set(detail?.checked ?? false); + }, +); + +// Define a handler for updating the extractTimes setting +const updateExtractTimes = handler( + { + type: "object", + properties: { + detail: { + type: "object", + properties: { + checked: { type: "boolean" }, + }, + }, + }, + }, + { + type: "object", + properties: { + extractTimes: { + type: "boolean", + asCell: true, + }, + }, + required: ["extractTimes"], + }, + ({ detail }, { extractTimes }) => { + extractTimes.set(detail?.checked ?? true); + }, +); + +// Handler for updating context length +const updateContextLength = handler( + { + type: "object", + properties: { + detail: { + type: "object", + properties: { + value: { type: "string" }, + }, + }, + }, + }, + { + type: "object", + properties: { + contextLength: { + type: "number", + asCell: true, + }, + }, + required: ["contextLength"], + }, + ({ detail }, { contextLength }) => { + const value = parseInt(detail?.value ?? "100", 10); + contextLength.set(isNaN(value) ? 100 : value); + }, +); + +// Handler for updating confidence threshold +const updateMinConfidence = handler( + { + type: "object", + properties: { + detail: { + type: "object", + properties: { + value: { type: "string" }, + }, + }, + }, + }, + { + type: "object", + properties: { + minConfidence: { + type: "number", + asCell: true, + }, + }, + required: ["minConfidence"], + }, + ({ detail }, { minConfidence }) => { + const value = parseFloat(detail?.value ?? "0.7"); + minConfidence.set(isNaN(value) ? 0.7 : Math.max(0, Math.min(1, value))); + }, +); + +// Define a lifted function to extract content from email +const getEmailContent = lift( + // Input schema + { + type: "object", + properties: { + email: EmailSchema, + }, + required: ["email"], + }, + // Output schema + { + type: "object", + properties: { + email: EmailSchema, + content: { type: "string" }, + hasContent: { type: "boolean" }, + }, + required: ["email", "content", "hasContent"], + }, + // Implementation + ({ email }) => { + const content = email.markdownContent || email.plainText || email.snippet || + ""; + return { + email, + content: content.trim() ? content : "", + hasContent: content.trim().length > 0, + }; + }, +); + +// The main recipe +export default recipe( + EmailDateExtractorInputSchema, + ResultSchema, + ({ emails, settings }) => { + // Process each email to extract dates + const emailsWithDates = emails.map((email) => { + // First get the email content + const emailContent = getEmailContent({ email }); + + // Create LLM prompt for date extraction using ifElse instead of ternary operators + const timeInstruction = ifElse( + settings.extractTimes, + "If time is mentioned, include normalized time in 24-hour format (HH:MM)", + "Ignore time information", + ); + + const timeField = ifElse( + settings.extractTimes, + `"normalizedTime": "14:30",`, + "", + ); + + const dateInclusionInstruction = ifElse( + settings.includeEmailDate, + "Include the email's sent date if it's mentioned in the content.", + "Do not include the email's sent date.", + ); + + const systemPrompt = str` + You are a specialized date extraction assistant. Extract all dates mentioned in the email. + For each date found: + 1. Extract the raw text of the date as mentioned + 2. Normalize to ISO format (YYYY-MM-DD) + 3. ${timeInstruction} + 4. Include a brief context snippet (${settings.contextLength} characters) around the date mention + 5. Assign a confidence score (0-1) that this is a relevant future date/deadline/appointment + + Return only JSON in this exact format: + { + "dates": [ + { + "dateText": "next Monday", + "normalizedDate": "2025-04-07", + ${timeField} + "context": "Let's meet next Monday to discuss the project timeline.", + "confidence": 0.95 + }, + ...more dates + ] + } + + ${dateInclusionInstruction} + Only include dates with confidence score >= ${settings.minConfidence}. + `; + + const userPrompt = str` + Subject: ${email.subject} + Date: ${email.date} + From: ${email.from} + + ${emailContent.content} + `; + + // Call LLM to get structured data - no conditional check needed + // The framework will handle empty content cases reactively + const extractionResult = llm({ + system: systemPrompt, + prompt: userPrompt, + }); + + // Return email with extracted dates + // The framework will handle the async nature of the LLM result + return { + email: email, + dates: derive(extractionResult, (result) => { + try { + // Handle possible null result during processing + if (!result?.result) return []; + + // Parse the result as JSON + const parsed = typeof result.result === "string" + ? JSON.parse(result.result) + : result.result; + + return parsed?.dates || []; + } catch (e) { + // Return empty array if parsing fails + return []; + } + }), + }; + }); + + // Derive a flattened list of all dates across all emails + const allDates = derive( + emailsWithDates, + (items) => { + // Flatten all dates from all emails into a single array + return items.flatMap((item) => item.dates || []); + }, + ); + + // Count of emails and dates + const emailCount = derive(emails, (emails) => emails.length); + const dateCount = derive(allDates, (dates) => dates.length); + + // Instantiate handlers + const includeEmailDateHandler = updateIncludeEmailDate({ + includeEmailDate: settings.includeEmailDate, + }); + + const extractTimesHandler = updateExtractTimes({ + extractTimes: settings.extractTimes, + }); + + const contextLengthHandler = updateContextLength({ + contextLength: settings.contextLength, + }); + + const minConfidenceHandler = updateMinConfidence({ + minConfidence: settings.minConfidence, + }); + + // Return recipe results + return { + [NAME]: + str`Email Date Extractor (${dateCount} dates from ${emailCount} emails)`, + [UI]: ( + +

Email Date Extractor

+ +
+ Emails processed: {emailCount} + Dates found: {dateCount} +
+ + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ +
+

All Extracted Dates

+ + + + + + {ifElse(settings.extractTimes, , null)} + + + + + + + {allDates.map((date) => ( + + + + {ifElse( + settings.extractTimes, + , + null, + )} + + + + + ))} + +
DATE TEXTNORMALIZEDTIMECONTEXTCONFIDENCEEMAIL
{date.dateText}{date.normalizedDate} + {ifElse(date.normalizedTime, date.normalizedTime, "-")} + {date.context}{(date.confidence * 100).toFixed(0)}% + {emailsWithDates.find((e) => e.dates.includes(date)) + ?.email.subject || ""} +
+
+ +
+

Dates by Email

+ {emailsWithDates + .filter((item) => item.dates && item.dates.length > 0) + .map((item) => ( +
+

{item.email.subject}

+ + + + + + {ifElse(settings.extractTimes, , null)} + + + + + + {item.dates.map((date) => ( + + + + {ifElse( + settings.extractTimes, + , + null, + )} + + + + ))} + +
DATE TEXTNORMALIZEDTIMECONTEXTCONFIDENCE
{date.dateText}{date.normalizedDate} + {ifElse( + date.normalizedTime, + date.normalizedTime, + "-", + )} + {date.context}{(date.confidence * 100).toFixed(0)}%
+
+ ))} +
+
+ ), + emailsWithDates, + allDates, + }; + }, +); From 0988311035f2161dbf50c2bcda0b0bbcdb8619f9 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Fri, 4 Apr 2025 09:30:12 -0700 Subject: [PATCH 05/19] remove extraneous imports --- recipes/email-summarizer.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/recipes/email-summarizer.tsx b/recipes/email-summarizer.tsx index 2aff0f88e..cdbdffd36 100644 --- a/recipes/email-summarizer.tsx +++ b/recipes/email-summarizer.tsx @@ -1,9 +1,7 @@ import { h } from "@commontools/html"; import { - cell, derive, handler, - ID, JSONSchema, lift, llm, @@ -13,7 +11,6 @@ import { str, UI, } from "@commontools/builder"; -import { Cell } from "@commontools/runner"; // Email schema based on Gmail recipe const EmailProperties = { From 07f53e06da29646d201db74cbcedc8d723e2c292 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Fri, 4 Apr 2025 15:59:08 -0700 Subject: [PATCH 06/19] fix linter errors in README.md --- recipes/README.md | 327 ++++++++++++++++++++++++++++------------------ 1 file changed, 202 insertions(+), 125 deletions(-) diff --git a/recipes/README.md b/recipes/README.md index 7604fc3ad..bd292062e 100644 --- a/recipes/README.md +++ b/recipes/README.md @@ -20,30 +20,38 @@ function. It takes three parameters: ### Schemas and Type Safety -The framework provides two ways to define schemas for recipes, handlers, and lifted functions: +The framework provides two ways to define schemas for recipes, handlers, and +lifted functions: + +1. **TypeScript Generics**: Directly pass TypeScript interfaces as type + parameters -1. **TypeScript Generics**: Directly pass TypeScript interfaces as type parameters ```typescript - const myHandler = handler((input, state) => { /* ... */ }); + const myHandler = handler( + (input, state) => {/* ... */}, + ); ``` 2. **JSON Schema**: Define schemas in JSON format (recommended approach) + ```typescript const myHandler = handler( - { type: "object", properties: { /* input schema */ } }, - { type: "object", properties: { /* state schema */ } }, - (input, state) => { /* ... */ } + { type: "object", properties: {/* input schema */} }, + { type: "object", properties: {/* state schema */} }, + (input, state) => {/* ... */}, ); ``` The JSON Schema approach provides several benefits: + - Runtime validation - Self-documentation - Better serialization support - Integration with framework tooling -Importantly, the framework automatically derives TypeScript types from JSON Schemas, -giving you full type inference and type checking in your handler and lift functions. +Importantly, the framework automatically derives TypeScript types from JSON +Schemas, giving you full type inference and type checking in your handler and +lift functions. ### Data Flow @@ -61,38 +69,44 @@ There are important differences between the types of functions in the framework: #### Handlers -Handlers are functions that declare node types in the reactive graph that respond to events: +Handlers are functions that declare node types in the reactive graph that +respond to events: - Created with `handler()` function - Can be defined with JSON Schema or TypeScript generics (see Schemas section) - The state schema can use `asCell: true` to receive cells directly: + ```typescript // Using asCell: true const updateCounter = handler( { type: "object" }, // Input schema - { + { type: "object", properties: { - count: { - type: "number", - asCell: true // Will receive a Cell instance - } - } + count: { + type: "number", + asCell: true, // Will receive a Cell instance + }, + }, }, (input, { count }) => { // Now count is a Cell instance count.set(count.get() + 1); - } + }, ); ``` + - Instantiated in recipes by passing parameters: + ```typescript const stream = definedHandler({ cell1, cell2 }); ``` + - Return a stream that can be: - Passed to JSX components as event handlers (e.g., `onClick={stream}`) - Returned by a recipe for external consumption - - Passed to another handler which can call `.send(...)` on it to generate events + - Passed to another handler which can call `.send(...)` on it to generate + events - Can update cells and trigger side effects - Support async operations for data processing - React to outside events (user interactions, API responses) @@ -100,8 +114,10 @@ Handlers are functions that declare node types in the reactive graph that respon #### Reactive Functions (lift/derive) -- `lift`: Declares a reactive node type that transforms data in the reactive graph +- `lift`: Declares a reactive node type that transforms data in the reactive + graph - Can also use JSON Schema for type safety with input and output schemas: + ```typescript const transformData = lift( // Input schema @@ -109,46 +125,57 @@ Handlers are functions that declare node types in the reactive graph that respon type: "object", properties: { value: { type: "number" }, - multiplier: { type: "number" } + multiplier: { type: "number" }, }, - required: ["value", "multiplier"] + required: ["value", "multiplier"], }, // Output schema { - type: "number" + type: "number", }, // Function with inferred types - ({ value, multiplier }) => value * multiplier + ({ value, multiplier }) => value * multiplier, ); ``` + - When instantiated, it inserts a reactive node in the graph: + ```typescript const newCell = liftedFunction({ cell1, cell2 }); ``` + - The result is a proxy cell that can be further referenced: + ```typescript const compound = { data: newCell.field }; ``` + - `derive`: A convenience wrapper around lift: + ```typescript // These are equivalent: const result1 = derive({ x, y }, ({ x, y }) => x + y); const result2 = lift(({ x, y }) => x + y)({ x, y }); ``` + - React to data changes within the reactive graph - Cannot directly call built-in functions like `llm` ### Data as Futures -Within recipes, functions cannot directly read values - they can only pass references to other nodes. Think of the data passed to a recipe as "futures" - promises of values that will be available when the program runs. +Within recipes, functions cannot directly read values - they can only pass +references to other nodes. Think of the data passed to a recipe as "futures" - +promises of values that will be available when the program runs. -The system allows accessing fields using the dot notation (e.g., `cell.field`), but this doesn't actually read values - it's creating new references to future data. +The system allows accessing fields using the dot notation (e.g., `cell.field`), +but this doesn't actually read values - it's creating new references to future +data. ```tsx // This doesn't read values, it creates references: -const data = { - firstName: user.firstName, - lastName: user.lastName +const data = { + firstName: user.firstName, + lastName: user.lastName, }; ``` @@ -182,18 +209,23 @@ Several utility functions are available: - `fetchData`: Fetches data from URLs - `streamData`: Streams data from URLs - `ifElse`: Conditional logic for reactive flows + ```typescript // Creates a reactive value that changes based on the condition const message = ifElse( - user.isLoggedIn, - str`Welcome back, ${user.name}!`, - "Please log in to continue" + user.isLoggedIn, + str`Welcome back, ${user.name}!`, + "Please log in to continue", ); ``` -- `str`: Template literal for string interpolation with reactive values, creating reactive strings + +- `str`: Template literal for string interpolation with reactive values, + creating reactive strings + ```typescript // Creates a reactive string that updates when cells change - const greeting = str`Hello, ${user.name}! You have ${notifications.count} new messages.`; + const greeting = + str`Hello, ${user.name}! You have ${notifications.count} new messages.`; ``` **Important**: These built-in functions can only be called from within a recipe @@ -245,110 +277,138 @@ logic. ## Best Practices -1. **Use JSON Schema Over TypeScript Generics**: Prefer using JSON Schema for type definitions rather than TypeScript generics. This provides better runtime validation, self-documentation, and compatibility with framework tooling. +1. **Use JSON Schema Over TypeScript Generics**: Prefer using JSON Schema for + type definitions rather than TypeScript generics. This provides better + runtime validation, self-documentation, and compatibility with framework + tooling. -2. **Use `asCell: true` for Handler State**: When defining handler state schema, use `asCell: true` for properties that need to be updated. This gives you direct access to the Cell methods like `.set()` and `.get()`. +2. **Use `asCell: true` for Handler State**: When defining handler state schema, + use `asCell: true` for properties that need to be updated. This gives you + direct access to the Cell methods like `.set()` and `.get()`. -3. **Avoid All Direct Conditionals in Recipes**: Never use direct if statements, ternary operators, or any other conditionals inside a recipe function - they won't work properly because they immediately evaluate data instead of creating reactive nodes: +3. **Avoid All Direct Conditionals in Recipes**: Never use direct if statements, + ternary operators, or any other conditionals inside a recipe function - they + won't work properly because they immediately evaluate data instead of + creating reactive nodes: ```typescript // DON'T DO THIS - if statements don't work in recipes - const result = emails.map(email => { - if (email.hasContent) { // This won't work! + const result = emails.map((email) => { + if (email.hasContent) { // This won't work! return processEmail(email); } else { return { email, empty: true }; } }); - + // DON'T DO THIS EITHER - ternary operators also don't work - const tableHeader = - Name - {settings.showDetails ? Details : null} // This won't work! - ; - + const tableHeader = ( + + Name + {settings.showDetails ? Details : null} // This won't work! + + ); + // DON'T DO THIS - ternaries in string templates don't work const prompt = str` Process this data - ${settings.includeTimestamp ? "Include timestamps" : "No timestamps"} // This won't work! + ${ + settings.includeTimestamp ? "Include timestamps" : "No timestamps" + } // This won't work! `; - + // DO THIS INSTEAD - use ifElse function for conditionals in data flow - const result = emails.map(email => + const result = emails.map((email) => ifElse( email.hasContent, () => processEmail(email), - () => ({ email, empty: true }) + () => ({ email, empty: true }), ) ); - + // USE ifElse IN JSX TOO - const tableHeader = - Name - {ifElse(settings.showDetails, Details, null)} - ; - + const tableHeader = ( + + Name + {ifElse(settings.showDetails, Details, null)} + + ); + // USE ifElse IN STRING TEMPLATES const includeTimestampText = ifElse( settings.includeTimestamp, "Include timestamps", - "No timestamps" + "No timestamps", ); const prompt = str` Process this data ${includeTimestampText} `; - + // WHEN APPROPRIATE - skip conditionals entirely // and let LLM handle edge cases: - const result = emails.map(email => { + const result = emails.map((email) => { const processed = processWithLLM(email); return { email, result: processed }; }); ``` -4. **Reference Data Instead of Copying**: When transforming data, reference the original objects rather than copying all their properties. This maintains reactivity and creates cleaner code: +4. **Reference Data Instead of Copying**: When transforming data, reference the + original objects rather than copying all their properties. This maintains + reactivity and creates cleaner code: ```typescript // DO THIS: Reference the original data - const processedItems = items.map(item => ({ + const processedItems = items.map((item) => ({ originalItem: item, // Direct reference - processed: processItem(item) + processed: processItem(item), })); - + // NOT THIS: Spread/copy all properties - const processedItems = items.map(item => ({ - id: item.id, // Copying each field - name: item.name, // breaks the reactive - date: item.date, // connection to the + const processedItems = items.map((item) => ({ + id: item.id, // Copying each field + name: item.name, // breaks the reactive + date: item.date, // connection to the // ... more fields // original data - processed: processItem(item) + processed: processItem(item), })); ``` -4. **Use Reactive String Templates**: Use the `str` template literal to create reactive strings that update when their inputs change: +5. **Use Reactive String Templates**: Use the `str` template literal to create + reactive strings that update when their inputs change: ```typescript - const message = str`Hello ${user.name}, you have ${notifications.count} notifications`; + const message = + str`Hello ${user.name}, you have ${notifications.count} notifications`; ``` -5. **Keep Logic Inside Recipes**: Place as much logic as possible inside recipe functions or the `map` function. This creates a cleaner reactive system where data flow is transparent. +6. **Keep Logic Inside Recipes**: Place as much logic as possible inside recipe + functions or the `map` function. This creates a cleaner reactive system where + data flow is transparent. -6. **Leverage Framework Reactivity**: Let the framework track changes and updates. Avoid manually tracking which items have been processed or creating complex state management patterns. +7. **Leverage Framework Reactivity**: Let the framework track changes and + updates. Avoid manually tracking which items have been processed or creating + complex state management patterns. -7. **Composition**: Build complex flows by composing smaller recipes. +8. **Composition**: Build complex flows by composing smaller recipes. -8. **Minimize Side Effects**: Side effects should be managed through handlers rather than directly in recipes. +9. **Minimize Side Effects**: Side effects should be managed through handlers + rather than directly in recipes. -9. **Schema Reuse**: Define schemas once and reuse them across recipes, handlers, and lifted functions to maintain consistency. +10. **Schema Reuse**: Define schemas once and reuse them across recipes, + handlers, and lifted functions to maintain consistency. -10. **Follow Type Through Schema**: Leverage the framework's automatic type inference from JSON Schema rather than duplicating type definitions with TypeScript interfaces. +11. **Follow Type Through Schema**: Leverage the framework's automatic type + inference from JSON Schema rather than duplicating type definitions with + TypeScript interfaces. ## Schema Best Practices -When defining schemas in the Recipe Framework, follow these guidelines for best results: +When defining schemas in the Recipe Framework, follow these guidelines for best +results: -1. **Define Schemas as Constants**: Declare schemas as constants for reuse and reference: +1. **Define Schemas as Constants**: Declare schemas as constants for reuse and + reference: ```typescript const UserSchema = { @@ -356,57 +416,61 @@ When defining schemas in the Recipe Framework, follow these guidelines for best properties: { id: { type: "string" }, name: { type: "string" }, - email: { type: "string", format: "email" } + email: { type: "string", format: "email" }, }, - required: ["id", "name"] + required: ["id", "name"], } as const satisfies JSONSchema; ``` -2. **Use `as const satisfies JSONSchema`**: Always use this pattern to ensure proper type inference and compile-time validation: +2. **Use `as const satisfies JSONSchema`**: Always use this pattern to ensure + proper type inference and compile-time validation: ```typescript // DO THIS - const schema = { /*...*/ } as const satisfies JSONSchema; - + const schema = {/*...*/} as const satisfies JSONSchema; + // NOT THIS - const schema = { /*...*/ } as JSONSchema; // Doesn't provide proper type checking + const schema = {/*...*/} as JSONSchema; // Doesn't provide proper type checking ``` -3. **Always Include Reasonable Defaults**: Where possible, provide sensible default values to improve usability and reduce errors: +3. **Always Include Reasonable Defaults**: Where possible, provide sensible + default values to improve usability and reduce errors: ```typescript const SettingsSchema = { type: "object", properties: { - theme: { - type: "string", + theme: { + type: "string", enum: ["light", "dark", "system"], - default: "system" // Sensible default + default: "system", // Sensible default }, - fontSize: { + fontSize: { type: "number", minimum: 8, maximum: 32, - default: 14 // Reasonable default + default: 14, // Reasonable default }, notifications: { type: "boolean", - default: true // Sensible default - } - } + default: true, // Sensible default + }, + }, } as const satisfies JSONSchema; ``` -4. **Extract Types from Schemas**: Use the `Schema` utility to derive TypeScript types from JSON Schemas: +4. **Extract Types from Schemas**: Use the `Schema` utility to derive TypeScript + types from JSON Schemas: ```typescript - const UserSchema = { /*...*/ } as const satisfies JSONSchema; + const UserSchema = {/*...*/} as const satisfies JSONSchema; type User = Schema; - + // Now User is a TypeScript type matching the schema ``` -5. **Reference Schemas Instead of Duplicating**: For nested objects, reference existing schemas: +5. **Reference Schemas Instead of Duplicating**: For nested objects, reference + existing schemas: ```typescript // Instead of duplicating user properties @@ -415,13 +479,14 @@ When defining schemas in the Recipe Framework, follow these guidelines for best properties: { author: UserSchema, // Reference the existing schema content: { type: "string" }, - timestamp: { type: "string", format: "date-time" } + timestamp: { type: "string", format: "date-time" }, }, - required: ["author", "content"] + required: ["author", "content"], } as const satisfies JSONSchema; ``` -6. **Document Schemas with Descriptions**: Add descriptions to schemas and properties for better self-documentation: +6. **Document Schemas with Descriptions**: Add descriptions to schemas and + properties for better self-documentation: ```typescript { @@ -432,37 +497,40 @@ When defining schemas in the Recipe Framework, follow these guidelines for best } ``` -7. **Use `asCell: true` for Reactive State**: In handler state schemas, use `asCell: true` for properties that need direct access to Cell methods: +7. **Use `asCell: true` for Reactive State**: In handler state schemas, use + `asCell: true` for properties that need direct access to Cell methods: ```typescript const stateSchema = { type: "object", properties: { - counter: { - type: "number", - asCell: true, // Will be received as Cell + counter: { + type: "number", + asCell: true, // Will be received as Cell description: "Counter value that can be directly manipulated", - default: 0 // Provide a default value - } - } + default: 0, // Provide a default value + }, + }, }; ``` -8. **Define Required Properties Explicitly**: Always specify which properties are required: +8. **Define Required Properties Explicitly**: Always specify which properties + are required: ```typescript { // Properties definition... - required: ["id", "name", "email"] + required: ["id", "name", "email"]; } ``` -9. **Use Schema Composition**: Break down complex schemas into smaller, reusable parts: +9. **Use Schema Composition**: Break down complex schemas into smaller, reusable + parts: ```typescript - const AddressSchema = { /*...*/ } as const satisfies JSONSchema; - const ContactSchema = { /*...*/ } as const satisfies JSONSchema; - + const AddressSchema = {/*...*/} as const satisfies JSONSchema; + const ContactSchema = {/*...*/} as const satisfies JSONSchema; + const UserSchema = { type: "object", properties: { @@ -471,9 +539,9 @@ When defining schemas in the Recipe Framework, follow these guidelines for best name: { type: "string" }, // Composed schemas address: AddressSchema, - contact: ContactSchema + contact: ContactSchema, }, - required: ["id", "name"] + required: ["id", "name"], } as const satisfies JSONSchema; ``` @@ -481,7 +549,7 @@ When defining schemas in the Recipe Framework, follow these guidelines for best ```typescript const StatusValues = ["pending", "active", "suspended", "deleted"] as const; - + const UserSchema = { // ... properties: { @@ -489,9 +557,9 @@ When defining schemas in the Recipe Framework, follow these guidelines for best status: { type: "string", enum: StatusValues, - default: "pending" // Provide a reasonable default - } - } + default: "pending", // Provide a reasonable default + }, + }, } as const satisfies JSONSchema; ``` @@ -499,7 +567,8 @@ When defining schemas in the Recipe Framework, follow these guidelines for best ### Schema to TypeScript Inference -The framework provides automatic type inference from JSON Schema to TypeScript types: +The framework provides automatic type inference from JSON Schema to TypeScript +types: ```typescript // Define a schema @@ -507,19 +576,20 @@ const PersonSchema = { type: "object", properties: { name: { type: "string" }, - age: { type: "number" } + age: { type: "number" }, }, - required: ["name"] + required: ["name"], } as const; // Automatically infer TypeScript type -type Person = Schema; +type Person = Schema; // Equivalent to: type Person = { name: string; age?: number; }; ``` ### Cell Type vs Value Type -When working with handlers that use `asCell: true`, it's important to understand the distinction: +When working with handlers that use `asCell: true`, it's important to understand +the distinction: ```typescript // Regular state property (value type) @@ -529,26 +599,33 @@ When working with handlers that use `asCell: true`, it's important to understand { count: { type: "number", asCell: true } } // Handler receives Cell ``` -With Cell-typed properties, the handler function receives actual Cell instances with methods: +With Cell-typed properties, the handler function receives actual Cell instances +with methods: + - `cell.get()`: Get the current value - `cell.set(newValue)`: Set a new value - `cell.update(fn)`: Update using a function This allows for more control over state updates, including: + - Batching multiple updates - Conditional updates based on current value - Handling complex state transitions ## Framework Goals -The primary goal of this framework is to generate "low taint code" that enables effective data flow analysis. In this system: +The primary goal of this framework is to generate "low taint code" that enables +effective data flow analysis. In this system: - Recipes are transparent to data flow analysis - Functions passed to `handler` or `lift` aren't transparent (they're "tainted") - JSON Schema provides a clean abstraction for data validation and type safety -- The `asCell: true` pattern maintains reactivity while allowing direct manipulation +- The `asCell: true` pattern maintains reactivity while allowing direct + manipulation -By following these principles, applications built with this framework can achieve predictable data flow, easier testing, and better security through data flow isolation. +By following these principles, applications built with this framework can +achieve predictable data flow, easier testing, and better security through data +flow isolation. ## Integration Process From 1b33e8c0057d2eb107870f5d2f7e6ce1a112d86f Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Mon, 14 Apr 2025 12:28:44 -0700 Subject: [PATCH 07/19] parse inputs properly --- cli/main.ts | 55 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/cli/main.ts b/cli/main.ts index fb00f1bea..50ff6bc6a 100644 --- a/cli/main.ts +++ b/cli/main.ts @@ -2,6 +2,8 @@ import { parseArgs } from "@std/cli/parse-args"; import { CharmManager, compileRecipe } from "@commontools/charm"; import { + getCellFromLink, + getDocByEntityId, getEntityId, idle, isStream, @@ -14,7 +16,7 @@ import { Identity, type Session, } from "@commontools/identity"; -import { assert } from "@commontools/memory/fact"; +import { isObj } from "@commontools/utils"; const { spaceName, @@ -129,15 +131,32 @@ async function main() { // and replace them with the corresponding JSON object. // // Example: "@#bafed0de/path/to/value" and "{ foo: @#bafed0de/a/path }" - const regex = /(? - JSON.stringify({ - cell: { "/": hash, path: path.split("/").map(decodeURIComponent) }, - }), + (match, fullRef) => { + // Extract hash and path from the full reference + // fullRef format is @#hash/path + const hashMatch = fullRef.match( + /@#([a-zA-Z0-9]+)((?:\/[^\/\s"',}]+)*)/, + ); + if (!hashMatch) return match; + + const [_, hash, path] = hashMatch; + + // Create the cell JSON object + const linkJson = JSON.stringify({ + cell: { "/": hash }, + path: path.split("/").filter(Boolean).map(decodeURIComponent), + }); + + // If the match starts with @, it means the reference is at the beginning of the string + // or the entire string is a reference - don't prepend any character + return match.charAt(0) === "@" ? linkJson : match.charAt(0) + linkJson; + }, ); try { + console.log("inputTransformed:", inputTransformed); inputValue = JSON.parse(inputTransformed); } catch (error) { console.error("Error parsing input:", error); @@ -145,6 +164,30 @@ async function main() { } } + function mapToCell(value: unknown): unknown { + if ( + isObj(value) && isObj(value.cell) && + typeof value.cell["/"] === "string" && + Array.isArray(value.path) + ) { + const space: string = (value.space as string) ?? spaceDID!; + return getCellFromLink({ + space, + cell: getDocByEntityId(space, value.cell as { "/": string }, true)!, + path: value.path, + }); + } else if (Array.isArray(value)) { + return value.map(mapToCell); + } else if (isObj(value)) { + return Object.fromEntries( + Object.entries(value).map(([key, value]) => [key, mapToCell(value)]), + ); + } + return value; + } + + inputValue = mapToCell(inputValue); + if (recipeFile) { try { const recipeSrc = await Deno.readTextFile(recipeFile); From 39e7207c4c89221543b3ba056aa344aaae856225 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Mon, 14 Apr 2025 12:29:02 -0700 Subject: [PATCH 08/19] various not-doing-recipes right fixes --- recipes/email-date-extractor.tsx | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/recipes/email-date-extractor.tsx b/recipes/email-date-extractor.tsx index 58b2a53f3..c02cd419b 100644 --- a/recipes/email-date-extractor.tsx +++ b/recipes/email-date-extractor.tsx @@ -488,7 +488,7 @@ export default recipe( @@ -500,7 +500,7 @@ export default recipe( step="0.1" min="0" max="1" - value={settings.minConfidence.toString()} + value={settings.minConfidence} oncommon-input={minConfidenceHandler} /> @@ -533,10 +533,14 @@ export default recipe( null, )} {date.context} - {(date.confidence * 100).toFixed(0)}% - {emailsWithDates.find((e) => e.dates.includes(date)) - ?.email.subject || ""} + {derive(date, (d) => (d.confidence * 100).toFixed(0))}% + + + {derive(emailsWithDates, (items) => + items.find((e) => e.dates.includes(date))?.email + .subject || + "")} ))} @@ -546,8 +550,10 @@ export default recipe(

Dates by Email

- {emailsWithDates - .filter((item) => item.dates && item.dates.length > 0) + {derive(emailsWithDates, (items) => + items.filter((item) => + item.dates && item.dates.length > 0 + )) .map((item) => (

{item.email.subject}

@@ -578,7 +584,12 @@ export default recipe( null, )} {date.context} - {(date.confidence * 100).toFixed(0)}% + + {derive( + date, + (d) => (d.confidence * 100).toFixed(0), + )}% + ))} From b10fa8b31778125be9747c327dbe03731003b431 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Mon, 14 Apr 2025 14:22:26 -0700 Subject: [PATCH 09/19] allow static values as ifElse return values --- runner/src/builtins/if-else.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/runner/src/builtins/if-else.ts b/runner/src/builtins/if-else.ts index f5be468f8..1a52ddd29 100644 --- a/runner/src/builtins/if-else.ts +++ b/runner/src/builtins/if-else.ts @@ -1,7 +1,9 @@ import { type DocImpl, getDoc } from "../doc.ts"; +import { isCellLink } from "../cell.ts"; import { type Action } from "../scheduler.ts"; import { type ReactivityLog } from "../scheduler.ts"; -import { getCellLinkOrThrow } from "../query-result-proxy.ts"; +import { getCellLinkOrValue } from "../query-result-proxy.ts"; + export function ifElse( inputsCell: DocImpl<[any, any, any]>, sendResult: (result: any) => void, @@ -15,9 +17,12 @@ export function ifElse( return (log: ReactivityLog) => { const condition = inputsCell.getAsQueryResult([0], log); - const ref = getCellLinkOrThrow( + const current = getCellLinkOrValue( inputsCell.getAsQueryResult([condition ? 1 : 2], log), ); - result.send(ref.cell.getAtPath(ref.path), log); + const value = isCellLink(current) + ? current.cell.getAtPath(current.path) + : current; + result.send(value, log); }; } From fca1a031d6b7b02402efe413fc7505d8560799a5 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Mon, 21 Apr 2025 12:51:51 -0700 Subject: [PATCH 10/19] don't use common-checkbox, it doesn't exist --- recipes/email-date-extractor.tsx | 36 +++++++++++++++++++------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/recipes/email-date-extractor.tsx b/recipes/email-date-extractor.tsx index c02cd419b..f3396e580 100644 --- a/recipes/email-date-extractor.tsx +++ b/recipes/email-date-extractor.tsx @@ -8,7 +8,6 @@ import { llm, NAME, recipe, - Schema, str, UI, } from "@commontools/builder"; @@ -398,6 +397,8 @@ export default recipe( const extractionResult = llm({ system: systemPrompt, prompt: userPrompt, + model: "google:gemini-2.0-flash", + mode: "json", }); // Return email with extracted dates @@ -469,39 +470,41 @@ export default recipe(
-
-
-
-
@@ -534,11 +537,14 @@ export default recipe( )} {date.context} - {derive(date, (d) => (d.confidence * 100).toFixed(0))}% + {derive(date, (d) => + (d?.confidence ?? 0 * 100).toFixed(0))}% {derive(emailsWithDates, (items) => - items.find((e) => e.dates.includes(date))?.email + items.find((e) => + e.dates.includes(date) + )?.email .subject || "")} @@ -562,7 +568,7 @@ export default recipe( DATE TEXT NORMALIZED - {ifElse(settings.extractTimes, TIME, null)} + {ifElse(settings.extractTimes, (TIME), null)} CONTEXT CONFIDENCE @@ -574,20 +580,20 @@ export default recipe( {date.normalizedDate} {ifElse( settings.extractTimes, - + ( {ifElse( date.normalizedTime, date.normalizedTime, "-", )} - , + ), null, )} {date.context} {derive( date, - (d) => (d.confidence * 100).toFixed(0), + (d) => (d?.confidence ?? 0 * 100).toFixed(0), )}% From 8efff54a7d642d1b1690216684a013542f6c4090 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Mon, 21 Apr 2025 12:54:42 -0700 Subject: [PATCH 11/19] allow llm calls from Deno --- llm/src/client.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/llm/src/client.ts b/llm/src/client.ts index e628c64da..4f9141925 100644 --- a/llm/src/client.ts +++ b/llm/src/client.ts @@ -5,6 +5,8 @@ type PartialCallback = (text: string) => void; let llmApiUrl = typeof globalThis.location !== "undefined" ? globalThis.location.protocol + "//" + globalThis.location.host + "/api/ai/llm" + : Deno?.env.get("TOOLSHED_API_URL") + ? new URL("/api/ai/llm", Deno.env.get("TOOLSHED_API_URL")!).toString() : "//api/ai/llm"; export const setLLMUrl = (toolshedUrl: string) => { From d879b44aa795fda6d5118746d99367f7562c15e7 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Mon, 21 Apr 2025 12:57:12 -0700 Subject: [PATCH 12/19] ifElse: Update alias to correct branch, now supports constants on these --- runner/src/builtins/if-else.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/runner/src/builtins/if-else.ts b/runner/src/builtins/if-else.ts index 1a52ddd29..581cf1a9e 100644 --- a/runner/src/builtins/if-else.ts +++ b/runner/src/builtins/if-else.ts @@ -5,7 +5,7 @@ import { type ReactivityLog } from "../scheduler.ts"; import { getCellLinkOrValue } from "../query-result-proxy.ts"; export function ifElse( - inputsCell: DocImpl<[any, any, any]>, + inputsDoc: DocImpl<[any, any, any]>, sendResult: (result: any) => void, _addCancel: (cancel: () => void) => void, cause: DocImpl[], @@ -15,14 +15,11 @@ export function ifElse( sendResult(result); return (log: ReactivityLog) => { - const condition = inputsCell.getAsQueryResult([0], log); + const condition = inputsDoc.getAsQueryResult([0], log); - const current = getCellLinkOrValue( - inputsCell.getAsQueryResult([condition ? 1 : 2], log), + result.send( + { $alias: { cell: inputsDoc, path: [condition ? 1 : 2] } }, + log, ); - const value = isCellLink(current) - ? current.cell.getAtPath(current.path) - : current; - result.send(value, log); }; } From 5cf4c210a6509f8ecb1d5077e9bf860d5731e5cd Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Fri, 2 May 2025 10:40:25 -0700 Subject: [PATCH 13/19] accept undefined for emails, etc. --- recipes/email-date-extractor.tsx | 8 ++++---- recipes/email-summarizer.tsx | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/recipes/email-date-extractor.tsx b/recipes/email-date-extractor.tsx index f3396e580..955c75874 100644 --- a/recipes/email-date-extractor.tsx +++ b/recipes/email-date-extractor.tsx @@ -143,7 +143,7 @@ const EmailDateExtractorInputSchema = { description: "Minimum confidence threshold for included dates (0-1)", }, }, - defailt: {}, + default: {}, required: [ "includeEmailDate", "extractTimes", @@ -154,7 +154,7 @@ const EmailDateExtractorInputSchema = { }, required: ["emails", "settings"], description: "Email Date Extractor", -} as const as JSONSchema; +} as const satisfies JSONSchema; // Output Schema const ResultSchema = { @@ -434,8 +434,8 @@ export default recipe( ); // Count of emails and dates - const emailCount = derive(emails, (emails) => emails.length); - const dateCount = derive(allDates, (dates) => dates.length); + const emailCount = derive(emails, (emails) => emails?.length); + const dateCount = derive(allDates, (dates) => dates?.length); // Instantiate handlers const includeEmailDateHandler = updateIncludeEmailDate({ diff --git a/recipes/email-summarizer.tsx b/recipes/email-summarizer.tsx index cdbdffd36..96bffae11 100644 --- a/recipes/email-summarizer.tsx +++ b/recipes/email-summarizer.tsx @@ -70,13 +70,13 @@ const EmailProperties = { title: "Markdown Content", description: "Email content converted to Markdown format", }, -} as const; +} as const satisfies JSONSchema; const EmailSchema = { type: "object", properties: EmailProperties, required: Object.keys(EmailProperties), -} as const as JSONSchema; +} as const satisfies JSONSchema; type Email = Schema; From 146535a00b28c5961a1d2a4d58dc8d9a0cdf9bf6 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Fri, 2 May 2025 10:46:31 -0700 Subject: [PATCH 14/19] fix `llm` call --- builder/src/built-in.ts | 1 + recipes/email-date-extractor.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/builder/src/built-in.ts b/builder/src/built-in.ts index c83cbb9f9..946af23cb 100644 --- a/builder/src/built-in.ts +++ b/builder/src/built-in.ts @@ -7,6 +7,7 @@ export interface BuiltInLLMParams { system?: string; stop?: string; maxTokens?: number; + mode?: "json"; } export interface BuiltInLLMState { diff --git a/recipes/email-date-extractor.tsx b/recipes/email-date-extractor.tsx index 955c75874..7c05c9b4d 100644 --- a/recipes/email-date-extractor.tsx +++ b/recipes/email-date-extractor.tsx @@ -396,7 +396,7 @@ export default recipe( // The framework will handle empty content cases reactively const extractionResult = llm({ system: systemPrompt, - prompt: userPrompt, + messages: [userPrompt], model: "google:gemini-2.0-flash", mode: "json", }); From fc4b2ab18abbcaec9ba4355299fac4ae7a39213a Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Mon, 5 May 2025 14:45:42 -0700 Subject: [PATCH 15/19] Update builder/src/built-in.ts LLM documentation Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- builder/src/built-in.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/builder/src/built-in.ts b/builder/src/built-in.ts index 946af23cb..0f0851d28 100644 --- a/builder/src/built-in.ts +++ b/builder/src/built-in.ts @@ -7,6 +7,11 @@ export interface BuiltInLLMParams { system?: string; stop?: string; maxTokens?: number; + /** + * Specifies the mode of operation for the LLM. + * - `"json"`: Indicates that the LLM should process and return data in JSON format. + * This parameter is optional and defaults to undefined, which may result in standard behavior. + */ mode?: "json"; } From 42e63615418e0af432d330ef00d31f125afbd0ce Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Mon, 5 May 2025 14:48:06 -0700 Subject: [PATCH 16/19] remove extraneous ! --- llm/src/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llm/src/client.ts b/llm/src/client.ts index 4f9141925..5e73b9bce 100644 --- a/llm/src/client.ts +++ b/llm/src/client.ts @@ -6,7 +6,7 @@ let llmApiUrl = typeof globalThis.location !== "undefined" ? globalThis.location.protocol + "//" + globalThis.location.host + "/api/ai/llm" : Deno?.env.get("TOOLSHED_API_URL") - ? new URL("/api/ai/llm", Deno.env.get("TOOLSHED_API_URL")!).toString() + ? new URL("/api/ai/llm", Deno.env.get("TOOLSHED_API_URL")).toString() : "//api/ai/llm"; export const setLLMUrl = (toolshedUrl: string) => { From c1030ee82711434b8f21bcb48f9738998fb6e632 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Tue, 6 May 2025 09:30:44 -0700 Subject: [PATCH 17/19] fix wrong brackets around ?? term! --- recipes/email-date-extractor.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/recipes/email-date-extractor.tsx b/recipes/email-date-extractor.tsx index 7c05c9b4d..a2d5fba30 100644 --- a/recipes/email-date-extractor.tsx +++ b/recipes/email-date-extractor.tsx @@ -537,14 +537,14 @@ export default recipe( )} {date.context} - {derive(date, (d) => - (d?.confidence ?? 0 * 100).toFixed(0))}% + {derive( + date, + (d) => ((d?.confidence ?? 0) * 100).toFixed(0), + )}% {derive(emailsWithDates, (items) => - items.find((e) => - e.dates.includes(date) - )?.email + items.find((e) => e.dates.includes(date))?.email .subject || "")} From 5e33494b8b9f44334f2b689282c7fcc1949dcddf Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Tue, 6 May 2025 09:30:57 -0700 Subject: [PATCH 18/19] switch to new isRecord --- cli/main.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/main.ts b/cli/main.ts index 50ff6bc6a..9156ffc53 100644 --- a/cli/main.ts +++ b/cli/main.ts @@ -16,7 +16,7 @@ import { Identity, type Session, } from "@commontools/identity"; -import { isObj } from "@commontools/utils"; +import { isRecord } from "@commontools/utils/types"; const { spaceName, @@ -166,7 +166,7 @@ async function main() { function mapToCell(value: unknown): unknown { if ( - isObj(value) && isObj(value.cell) && + isRecord(value) && isRecord(value.cell) && typeof value.cell["/"] === "string" && Array.isArray(value.path) ) { @@ -178,7 +178,7 @@ async function main() { }); } else if (Array.isArray(value)) { return value.map(mapToCell); - } else if (isObj(value)) { + } else if (isRecord(value)) { return Object.fromEntries( Object.entries(value).map(([key, value]) => [key, mapToCell(value)]), ); From 1c27e99dd83c50ce1106b62be7bd559dfe1b5e29 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Tue, 6 May 2025 09:38:39 -0700 Subject: [PATCH 19/19] add --allow-env to test runs --- builder/deno.json | 2 +- charm/deno.json | 2 +- runner/deno.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/builder/deno.json b/builder/deno.json index c04292618..17db75dfc 100644 --- a/builder/deno.json +++ b/builder/deno.json @@ -2,6 +2,6 @@ "name": "@commontools/builder", "exports": "./src/index.ts", "tasks": { - "test": "deno test" + "test": "deno test --allow-env" } } diff --git a/charm/deno.json b/charm/deno.json index b3994ebe5..13af0f354 100644 --- a/charm/deno.json +++ b/charm/deno.json @@ -1,7 +1,7 @@ { "name": "@commontools/charm", "tasks": { - "test": "deno test" + "test": "deno test --allow-env" }, "exports": "./src/index.ts" } diff --git a/runner/deno.json b/runner/deno.json index fd5678fdd..b99781d60 100644 --- a/runner/deno.json +++ b/runner/deno.json @@ -1,7 +1,7 @@ { "name": "@commontools/runner", "tasks": { - "test": "deno test" + "test": "deno test --allow-env" }, "exports": { ".": "./src/index.ts",