https://github.com/rubriclab/actions
https://github.com/rubriclab/actions
Last synced: about 1 year ago
JSON representation
- Host: GitHub
- URL: https://github.com/rubriclab/actions
- Owner: RubricLab
- Created: 2024-10-03T00:56:54.000Z (over 1 year ago)
- Default Branch: main
- Last Pushed: 2025-02-04T14:08:39.000Z (about 1 year ago)
- Last Synced: 2025-02-04T15:22:46.803Z (about 1 year ago)
- Language: TypeScript
- Size: 75.2 KB
- Stars: 0
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
Awesome Lists containing this project
README
# @rubriclab/actions
This package is part of a 3 package system that represents Rubric's framework for Generative UI. See also:
- @rubriclab/blocks
- @rubriclab/ui
The Actions package aims to provide a powerful and simple way to define actions (which are essentially API primitives) and chain them together in a typesafe way.
It is designed to be awesome for developers (providing really simple and powerful DX with excellent typesafety) and powerful for AI systems - allowing structured output models to export chains reliably.
## Get Started
### Installation
`bun add @rubriclab/actions`
> @rubriclab scope packages are not built, they are all raw typescript. If using in a next.js app, make sure to transpile.
```ts
// next.config.ts
import type { NextConfig } from 'next'
export default {
transpilePackages: ['@rubriclab/auth'],
reactStrictMode: true
} satisfies NextConfig
```
> If using inside the monorepo (@rubric), simply add `{"@rubriclab/actions": "*"}` to dependencies and then run `bun i`
### Define Actions
To get started, define a few actions.
```ts
import { createAction } from '@rubriclab/actions'
import { z } from 'zod'
const convertStringToNumber = createAction({
schema: {
input: z.object({
str: z.string()
}),
output: z.number()
},
execute: ({ str }) => Number(str)
})
const convertNumberToString = createAction({
schema: {
input: z.object({
num: z.number()
}),
output: z.string()
},
execute: ({ num }) => num.toString()
})
```
### Create an Executor
Pass all your actions into an executor to get an executor, zod schema, and a response_format (json schema for AI)
```ts
const { execute, schema, response_format } = createActionsExecutor({
convertStringToNumber,
convertNumberToString
})
```
### Execute a chain
Now that your actions are set up, you have typesafe chain execution.
```ts
const validSingle = execute({
action: 'convertStringToNumber',
params: {
str: "2"
}
})
const validChain = execute({
action: 'convertStringToNumber',
params: {
str: {
action: 'convertNumberToString',
params: {
num: 2
}
}
}
})
```
### Check if a chain is valid
#### At Build Time
The type `z.infer` validates chains
```ts
const invalidChain: z.infer = {
action: 'convertStringToNumber',
params: {
str: {
// you should see a TS issue here.
action: 'convertStringToNumber',
params: {
num: '2'
}
}
}
}
```
The input to execute() is also checked
```ts
const invalidChain = execute({
action: 'convertStringToNumber',
params: {
str: {
// you should see a TS issue here.
action: 'convertStringToNumber',
params: {
num: '2'
}
}
}
})
```
#### At Run Time
You can parse at run time using zod:
`schema.parse(invalidChain)`
`schema.safeParse(invalidChain)`
### Usage with AI
Use the response_format object for structured outputs.
```ts
const completion = await new openai().beta.chat.completions.parse({
model: 'gpt-4o-2024-08-06',
messages: [
{
role: 'system',
content: 'You are an actions executor. Your job is to create a single chain of actions that accomplishes the request.'
},
{
role: 'user',
content: 'parse 4 into a string and then back into a number 3 times.'
}
],
// the response_format works out of the box with structured outputs.
response_format
})
const { execution } = schema.parse(completion.choices[0]?.message.parsed)
console.dir(execution, { depth: null })
console.log(execute(execution))
```
### Advanced usage
#### Large amounts of actions
In theory, you can define lots and lots of actions and still get good outputs from AI. Log `response_format` to see that it is very flat and scalable!
#### Similar Objects
Out of the box, actions can be chained if they share IO primitives. For example, you can chain `convertStringToNumber` with `convertNumberToString` since the output of each is a primitive (`z.number()` and `z.string()` respectively) that corresponds to an input field of the other.
In more realistic scenarios, you will have more complex output types, for example, a contact.
```ts
const Contact = z.object({
id: z.string(),
name: z.string(),
email: z.string(),
image: z.string()
})
```
Notice that `id` could create problems, since it's seemingly compatible with any string. You wouldn't want AI or a developer to accidentally pass in a hallucinated string, an id from a different service, or the result of another action that returns a string that isn't actually a valid id.
In these cases, you can define a locked down type, such as a `GoogleContactID`:
```ts
const GoogleContactId = z.object({
type: z.literal('googleContactId'),
id: z.string()
})
```
Then you can enforce that this ID is specific to actions that use it:
```ts
const getFirstGoogleContactFromSearch = createAction({
schema: {
input: z.object({
search: z.string()
}),
output: GoogleContactId
},
execute: ({ search }) => ({
type: 'googleContactId' as const,
id: '...'
})
})
// a similar but not identical contact
const getFirstFacebookContactFromSearch = createAction({
schema: {
input: z.object({
search: z.string()
}),
output: z.object({
type: z.literal('facebookContactId'),
id: z.string()
})
},
execute: ({ search }) => ({
type: 'googleContactId' as const,
id: '...'
})
})
const sendEmail = createAction({
schema: {
input: z.object({
// only accept google contacts
to: GoogleContactId,
content: z.string()
}),
output: z.boolean()
},
execute: ({ to, content }) => {
console.log(`Sending email to ${to.id}: ${content}`)
return true
}
})
```
In this example, `sendEmail` will only be chainable with `getFirstGoogleContactFromSearch`. There will be a ts issue trying to send an email to a Facebook contact, and AI will not be able to erroneously chain.
Under the hood, we use a hashing mechanism to ensure that objects retain their exact uniqueness. Log `response_format` to see how that works!