{"id":19300470,"url":"https://github.com/rubriclab/actions","last_synced_at":"2025-02-24T01:26:21.620Z","repository":{"id":259497043,"uuid":"866840509","full_name":"RubricLab/actions","owner":"RubricLab","description":null,"archived":false,"fork":false,"pushed_at":"2025-02-04T14:08:39.000Z","size":77,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-02-04T15:22:46.803Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/RubricLab.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-10-03T00:56:54.000Z","updated_at":"2025-02-04T14:08:42.000Z","dependencies_parsed_at":"2024-10-26T01:47:21.155Z","dependency_job_id":"36ac1f8a-46f8-4fe0-8695-c54b89dcdae2","html_url":"https://github.com/RubricLab/actions","commit_stats":null,"previous_names":["rubriclab/actions"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RubricLab%2Factions","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RubricLab%2Factions/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RubricLab%2Factions/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RubricLab%2Factions/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/RubricLab","download_url":"https://codeload.github.com/RubricLab/actions/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":240402167,"owners_count":19795682,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-09T23:14:51.890Z","updated_at":"2025-02-24T01:26:21.598Z","avatar_url":"https://github.com/RubricLab.png","language":"TypeScript","readme":"# @rubriclab/actions\nThis package is part of a 3 package system that represents Rubric's framework for Generative UI. See also: \n- @rubriclab/blocks\n- @rubriclab/ui\n\nThe 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.\n\nIt 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.\n\n## Get Started\n### Installation\n`bun add @rubriclab/actions`\n\n\u003e @rubriclab scope packages are not built, they are all raw typescript. If using in a next.js app, make sure to transpile.\n\n```ts\n// next.config.ts\nimport type { NextConfig } from  'next' \nexport default {\n\ttranspilePackages: ['@rubriclab/auth'],\n\treactStrictMode: true\n} satisfies  NextConfig\n```\n\n\u003e If using inside the monorepo (@rubric), simply add `{\"@rubriclab/actions\": \"*\"}` to dependencies and then run `bun i`\n\n### Define Actions\nTo get started, define a few actions.\n\n```ts\nimport { createAction } from '@rubriclab/actions'\nimport { z } from 'zod'\n\nconst convertStringToNumber = createAction({\n\tschema: {\n\t\tinput: z.object({\n\t\t\tstr: z.string()\n\t\t}),\n\t\toutput: z.number()\n\t},\n\texecute: ({ str }) =\u003e Number(str)\n})\n\nconst convertNumberToString = createAction({\n\tschema: {\n\t\tinput: z.object({\n\t\t\tnum: z.number()\n\t\t}),\n\t\toutput: z.string()\n\t},\n\texecute: ({ num }) =\u003e num.toString()\n})\n```\n\n### Create an Executor\nPass all your actions into an executor to get an executor, zod schema, and a response_format (json schema for AI)\n\n```ts\nconst { execute, schema, response_format } = createActionsExecutor({\n\tconvertStringToNumber,\n\tconvertNumberToString\n})\n```\n\n### Execute a chain\nNow that your actions are set up, you have typesafe chain execution.\n\n```ts\nconst validSingle = execute({\n\taction: 'convertStringToNumber',\n\tparams: {\n\t\tstr: \"2\"\n\t}\n})\nconst validChain = execute({\n\taction: 'convertStringToNumber',\n\tparams: {\n\t\tstr: {\n\t\t\taction: 'convertNumberToString',\n\t\t\tparams: {\n\t\t\t\tnum: 2\n\t\t\t}\n\t\t}\n\t}\n})\n```\n\n### Check if a chain is valid\n#### At Build Time\nThe type `z.infer\u003ctypeof schema\u003e` validates chains\n\n```ts\nconst invalidChain: z.infer\u003ctypeof schema\u003e = {\n\taction: 'convertStringToNumber',\n\tparams: {\n\t\tstr: {\n\t\t\t// you should see a TS issue here.\n\t\t\taction: 'convertStringToNumber',\n\t\t\tparams: {\n\t\t\t\tnum: '2'\n\t\t\t}\n\t\t}\n\t}\n}\n```\n\nThe input to execute() is also checked\n\n```ts\n const invalidChain = execute({\n\taction: 'convertStringToNumber',\n\tparams: {\n\t\tstr: {\n\t\t\t// you should see a TS issue here.\n\t\t\taction: 'convertStringToNumber',\n\t\t\tparams: {\n\t\t\t\tnum: '2'\n\t\t\t}\n\t\t}\n\t}\n})\n```\n\n#### At Run Time\nYou can parse at run time using zod:\n`schema.parse(invalidChain)`\n`schema.safeParse(invalidChain)`\n\n\n### Usage with AI\nUse the response_format object for structured outputs.\n\n```ts\nconst  completion = await  new  openai().beta.chat.completions.parse({\n\tmodel: 'gpt-4o-2024-08-06',\n\tmessages: [\n\t\t{\n\t\t\trole: 'system',\n\t\t\tcontent: 'You are an actions executor. Your job is to create a single chain of actions that accomplishes the request.'\n\t\t},\n\t\t{\n\t\t\trole: 'user',\n\t\t\tcontent: 'parse 4 into a string and then back into a number 3 times.'\n\t\t}\n\t],\n\t// the response_format works out of the box with structured outputs.\n\tresponse_format\n})\nconst { execution } = schema.parse(completion.choices[0]?.message.parsed)\nconsole.dir(execution, { depth: null })\nconsole.log(execute(execution))\n```\n\n### Advanced usage\n\n#### Large amounts of actions\nIn 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!\n\n\n#### Similar Objects\nOut 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.\nIn more realistic scenarios, you will have more complex output types, for example, a contact.\n\n```ts\nconst Contact = z.object({\n\tid: z.string(),\n\tname: z.string(),\n\temail: z.string(),\n\timage: z.string()\n})\n```\n\nNotice 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.\n\nIn these cases, you can define a locked down type, such as a `GoogleContactID`:\n\n```ts\nconst GoogleContactId = z.object({\n\ttype: z.literal('googleContactId'),\n\tid: z.string()\n})\n```\n\nThen you can enforce that this ID is specific to actions that use it:\n\n```ts\nconst getFirstGoogleContactFromSearch = createAction({\n\tschema: {\n\t\tinput: z.object({\n\t\t\tsearch: z.string()\n\t\t}),\n\t\toutput: GoogleContactId\n\t},\n\texecute: ({ search }) =\u003e ({\n\t\ttype: 'googleContactId'  as  const,\n\t\tid: '...'\n\t})\n})\n\n// a similar but not identical contact\nconst getFirstFacebookContactFromSearch = createAction({\n\tschema: {\n\t\tinput: z.object({\n\t\t\tsearch: z.string()\n\t\t}),\n\t\toutput: z.object({\n\t\t\ttype: z.literal('facebookContactId'),\n\t\t\tid: z.string()\n\t\t})\n\t},\n\texecute: ({ search }) =\u003e ({\n\t\ttype: 'googleContactId'  as  const,\n\t\tid: '...'\n\t})\n})\n\nconst sendEmail = createAction({\n\tschema: {\n\t\tinput: z.object({\n\t\t\t// only accept google contacts\n\t\t\tto: GoogleContactId,\n\t\t\tcontent: z.string()\n\t\t}),\n\t\toutput: z.boolean()\n\t},\n\texecute: ({ to, content }) =\u003e {\n\t\tconsole.log(`Sending email to ${to.id}: ${content}`)\n\t\treturn true\n\t}\n})\n```\n\nIn 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.\nUnder the hood, we use a hashing mechanism to ensure that objects retain their exact uniqueness. Log `response_format` to see how that works!\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frubriclab%2Factions","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frubriclab%2Factions","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frubriclab%2Factions/lists"}