{"id":19448185,"url":"https://github.com/synzen/prompt-anything","last_synced_at":"2025-04-14T21:21:32.389Z","repository":{"id":42922015,"uuid":"245537445","full_name":"synzen/prompt-anything","owner":"synzen","description":"Framework to build a a tree of modular and interactable prompts for anything","archived":false,"fork":false,"pushed_at":"2023-01-06T02:37:49.000Z","size":401,"stargazers_count":2,"open_issues_count":11,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2024-12-11T18:57:46.191Z","etag":null,"topics":["modular","prompt","prompts","tree"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/synzen.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-03-06T23:50:01.000Z","updated_at":"2022-08-05T02:51:49.000Z","dependencies_parsed_at":"2023-02-05T03:30:44.741Z","dependency_job_id":null,"html_url":"https://github.com/synzen/prompt-anything","commit_stats":null,"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/synzen%2Fprompt-anything","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/synzen%2Fprompt-anything/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/synzen%2Fprompt-anything/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/synzen%2Fprompt-anything/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/synzen","download_url":"https://codeload.github.com/synzen/prompt-anything/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":232950476,"owners_count":18601508,"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":["modular","prompt","prompts","tree"],"created_at":"2024-11-10T16:24:48.513Z","updated_at":"2025-01-07T23:20:06.042Z","avatar_url":"https://github.com/synzen.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\n# Prompt Anything\n[![Maintainability](https://badgen.net/codeclimate/maintainability/synzen/prompt-anything?style=flat)](https://codeclimate.com/github/synzen/prompt-anything/maintainability)\n[![Test Coverage](https://badgen.net/codeclimate/coverage/synzen/prompt-anything?style=flat)](https://codeclimate.com/github/synzen/prompt-anything/test_coverage)\n![Github license](https://badgen.net/github/license/synzen/prompt-anything?style=flat)\n\nA modular and customizable framework to build prompts of any kind (such as ones within the console)! Originally inspired by the need to create console-like prompts in other applications such as chatting with bots.\n\n```\nnpm install prompt-anything\n```\n\n### Table of Contents\n- [Implementation](#implementation)\n- [Usage](#usage)\n  - [Creating a Prompt](#creating-a-prompt)\n    - [Conditional Visuals](#conditional-visuals)\n    - [Rejecting Input](#rejecting-input)\n    - [Skipping Message Collection](#skipping-message-collection)\n    - [Time Limits](#time-limitstimeouts)\n  - [Connecting Prompts](#connecting-prompts)\n    - [Condition Nodes](#conditional-nodes)\n  - [Running Prompts](#running-prompts)\n    - [Error Handling](#error-handling)\n- [Testing](#testing)\n\n## Implementation\n\n1. The following interfaces should be implemented:\n```ts\ninterface VisualInterface = {\n  text: string;\n}\n\ninterface MessageInterface {\n  content: string;\n}\n\ninterface ChannelInterface\u003cMessageType extends MessageInterface\u003e {\n  send: (visual: VisualInterface) =\u003e Promise\u003cMessageType|MessageType[]\u003e;\n}\n```\n2. The `Prompt` class must be extended to implement the abstract methods:\n    - `createCollector` - Returns an event emitter that should also emit `message` whenever your collector gets a message\n    - `onReject` - Handles `Rejection` errors (see the rejecting input section)\n3. Your collector should stop when the emitter emits `stop`.\n4. You may optionally emit the `exit` event for the user to prematurely exit\n```ts\nclass MyPrompt\u003cDataType, MessageType\u003e extends Prompt\u003cDataType, MessageType\u003e {\n  createCollector(channel: ChannelInterface\u003cMessageType\u003e, data: DataType): PromptCollector\u003cDataType, MessageType\u003e {\n    const emitter: PromptCollector\u003cDataType, MessageType\u003e = new EventEmitter()\n    // Collect your messages via your listeners, and return an emitter that follows these rules\n    myCollector.on('myMessage', (message: MessageType) =\u003e {\n      // Emit the messages from your collector here\n      emitter.emit('message', message)\n      // Optionally allow exits\n      if (message === 'exit') {\n        emitter.emit('exit')\n      }\n    })\n    emitter.once('stop', () =\u003e {\n      // Stop your collector here\n      myCollector.stop()\n    })\n    return emitter\n  }\n  \n  // Implement abstract methods. These events are automatically called\n  abstract async onReject(error: Errors.Rejection, message: MessageType, channel: ChannelInterface\u003cMessageType\u003e): Promise\u003cvoid\u003e;\n}\n```\n\n## Usage\n\nSee the `examples/console.ts` for a functioning implementation that accepts input from the console.\n\n### Creating a Prompt\nA prompt is composed of two parts:\n\n1. `VisualInterface|VisualGenerator` - A object or function that determines how the prompt looks like to the user\n2. `PromptFunction` - An (ideally [pure](https://en.wikipedia.org/wiki/Pure_function)) function that runs on every input from your collector\n\n```ts\n// Data type that is passed to each prompt\ntype MyData = {\n  human?: boolean;\n  name?: string;\n  age?: number;\n}\n\nconst askNameVisual: VisualInterface = {\n  text: 'What is your name?'\n}\n\n// askNameFn is run on every message collected during this prompt. This should be a pure function. (see below for details)\nconst askNameFn: PromptFunction\u003cMyData, MessageType\u003e = async (m: MessageType, data: MyData) =\u003e {\n  // This data is returned to the next prompt\n  return {\n    ...data,\n    name: m.content\n  }\n}\n// Third argument is the optional PromptCondition\nconst askNamePrompt = new MyPrompt\u003cMyData, MessageType\u003e(askNameVisual, askNameFn)\n```\nThe `PromptFunction` should be [pure function](https://en.wikipedia.org/wiki/Pure_function) to \n\n1. Minimize side effects that can affect every other function that depends on the data. \n2. Simplify unit-testing\n\nAs a result, the function should always be referencing the original data variable passed from the previous prompt, regardless of how many times the function is run.\n\n#### Conditional Visuals\n\nIf you want a prompt's visual to be dependent on the given data, you can pass a function as the argument of a `Prompt` instead of an object.\n\n```ts\nconst askNameVisual = async (data: MyData): Promise\u003cVisualInterface\u003e =\u003e ({\n  text: `Hello ${data.human ? 'non-human' : 'human'}! What is your name?`\n})\nconst askNamePrompt = new MyPrompt\u003cMyData, MessageType\u003e(askNameVisual, askNameFn)\n```\n\n#### Rejecting Input\n\nTo reject input, you can check the the content of the message in `PromptFunction`, and throw a `Errors.Rejection`. Upon throwing it:\n\n1. The rejection's message will be sent via your channel implementation's `send` method\n2. The prompt will again wait for input\n3. Run the prompt function again\n\n```ts\nconst askAgeFn: PromptFunction\u003cMyData, MessageType\u003e = async (m: MessageType, data: MyData) =\u003e {\n  const age = Number(m.content)\n  if (isNaN(age)) {\n    throw new Errors.Rejection(`That's not a valid number! Try again.`)\n  }\n  return {\n    ...data,\n    age\n  }\n}\n```\n\n#### Skipping Message Collection\n\nTo skip message collecting and only send a prompt's visual (usually done at the end of prompts), simply leave the second argument of `Prompt` as `undefined`.\n\n```ts\nconst askNameVisual = {\n  text: 'The end is nigh'\n}\nconst askNamePrompt = new MyPrompt\u003cMyData, MessageType\u003e(askNameVisual)\n```\n\n#### Time Limits/Timeouts\n\nTo automatically end message collection after a set duration, pass your duration in milliseconds as the 3rd argument to `Prompt`.\n\n```ts\nconst duration = 90000\nconst askNamePrompt = new MyPrompt\u003cMyData, MessageType\u003e(askNameVisual, askNameFn, duration)\n```\n\nThis causes a `Errors.UserInactivityError` to be thrown when the timeout is reached. The default value is 90000.\n\n### Connecting Prompts\n\nTo connect prompts, you must put them into nodes and connect nodes together by setting their children. This allows prompts to be reused by attaching children to nodes instead of prompts.\n\n```ts\nconst askNameNode = new PromptNode\u003cMyData, MessageType\u003e(askNamePrompt)\nconst askAgeNode = new PromptNode\u003cMyData, MessageType\u003e(askAgePrompt)\nconst askLocationNode = new PromptNode\u003cMyData, MessageType\u003e(askLocationPrompt)\n\naskNameNode.addChild(askAgeNode)\naskAgeNode.addChild(askLocationNode)\n```\n\n#### Conditional Nodes\n\nIf you only want a node to run if it matches a condition (given the data from the previous prompt node), you can specify a condition function `PromptNodeCondition` as the second argument of a `PromptNode`.\n\n```ts\n// After we ask for the location, we'd like to send a prompt in a different language based on their input\nconst englishAskNodeCondition: PromptNodeCondition\u003cMyData\u003e = async (data) =\u003e !!data.location \u0026\u0026 data.location === 'loc1'\nconst englishAskNode = new PromptNode\u003cMyData, MessageType\u003e(englishAskPrompt, englishAskNodeCondition)\nconst spanishAskNodeCondition: PromptNodeCondition\u003cMyData\u003e = async (data) =\u003e !!data.location \u0026\u0026 data.location === 'loc2'\nconst spanishAskNode = new PromptNode\u003cMyData, MessageType\u003e(spanishAskPrompt, spanishAskNodeCondition)\n\naskNameNode.addChild(askAgeNode)\naskAgeNode.addChild(askLocationNode)\n// addChild can be daisy-chained\naskLocationNode\n  .addChild(englishAskNode)\n  .addChild(spanishAskNode)\n// setChildren also works\naskLocationNode.setChildren([englishAskNode, spanishAskNode])\n```\n\nThe order of the children matters. The first child that matches its condition based on the given data will run. In this example, if `englishAskPrompt`'s condition function returns `true`, then `spanishAskNode` will never run.\n\n### Running Prompts\n\nAfter your prompt nodes are created, create a `PromptRunner` that is initialized with the data you'll be passing to the first prompt, then call its run method with the first prompt node.\n\n```ts\n// The initial data that is given to the first prompt is passed to the PromptRunner's constructor\nconst runner = new PromptRunner\u003cMyData, MessageType\u003e({})\n\n// run resolves with the data returned from the last prompt\nconst channel: ChannelInterface = myImplementedChannel()\nconst lastPromptData: MyData = await runner.run(askNameNode, channel)\n// askName -\u003e askAge -\u003e askLocation -\u003e (englishAsk OR spanishAsk)\n// lastPromptData is the data returned from either englishAsk or spanishAsk\n```\n\nYou can also run an array of prompt nodes. The first node that either has no condition, or has a matching condition will be passd to the `run` method.\n\n```ts\nconst runner = new PromptRunner\u003cMyData\u003e({})\n\n// runArray resolves with the data returned from the last prompt\nconst channel: ChannelInterface\u003cMessageType\u003e = myImplementedChannel()\nconst lastPromptData: MyData = await runner.runArray([\n  askSurnameNode,\n  askNameNode\n], channel)\n// (askSurname OR askName) -\u003e askAge -\u003e askLocation -\u003e (englishAsk OR spanishAsk)\n```\n\n#### Error Handling\n\nAny error that throws within prompts will cause the `PromptRunner`'s `run` to reject. In addition to regular errors, it may throw\n\n1. `Errors.UserVoluntaryExitError` if you emit `exit` in `createCollector`\n2. `Errors.UserInactivityError` if timeout occurs (90000 ms by default)\n\nBoth are instances of `Errors.UserError`.\n\n```ts\ntry {\n  const lastPromptData: MyData = await runner.run(node, channel)\n} catch (err) {\n  if (err instanceof Errors.UserVoluntaryExitError) {\n    // show an exit message\n  } else if (err instanceof Errors.UserInactivityError) {\n    // show an expired message\n  } else {\n    // All other errors\n  }\n}\n```\n\n\n## Testing\n\nUnit testing is straightforward since the tree of responses is built up from individual prompts that can be exported for testing. The prompts can be further decomposed into their visual, functional and conditional parts for even more granular tests.\n\nIntegration testing can be asserted on the execution order of the phases. Unfortunately, a \"flush promises\" method must be used since we cannot normally `await` the promises while we are waiting for messages from `EventEmitter`, otherwise the promise would never resolve until the series of prompts has ended.\n\n```ts\nasync function flushPromises(): Promise\u003cvoid\u003e {\n  return new Promise(setImmediate);\n}\n\ntype MockMessage = {\n  content: string;\n}\n\nconst createMockMessage = (content = ''): MockMessage =\u003e ({\n  content\n})\n\nit('runs correctly for age \u003c= 20', () =\u003e {\n  type AgeData = {\n    name?: string;\n    age?: number;\n  }\n  // Set up spies and the global emitter we'll use\n  const emitter: PromptCollector\u003cAgeData, MessageType\u003e = new EventEmitter()\n  const spy = jest.spyOn(MyPrompt.prototype, 'createCollector')\n    .mockReturnValue(emitter)\n\n  // Ask name Prompt that collects messages\n  const askNameFn: PromptFunction\u003cAgeData, MessageType\u003e = async function (m, data) {\n    return {\n      ...data,\n      name: m.content\n    }\n  }\n  const askName = new MyPrompt\u003cAgeData\u003e(() =\u003e ({\n    text: `What's your name?`\n  }), askNameFn)\n\n  // Ask age Prompt that collects messages\n  const askAgeFn: PromptFunction\u003cAgeData, MessageType\u003e = async function (m, data) {\n    if (isNaN(Number(m.content))) {\n      throw new Errors.Rejection()\n    }\n    return {\n      ...data,\n      age: Number(m.content)\n    }\n  }\n  const askAge = new MyPrompt\u003cAgeData\u003e((data) =\u003e ({\n    text: `How old are you, ${data.name}?`\n  }), askAgeFn)\n\n  // Conditional Prompt with no collector (MyPrompt)\n  const tooOld = new MyPrompt\u003cAgeData\u003e((data) =\u003e ({\n    text: `Wow ${data.name}, you are pretty old at ${data.age} years old!`\n  }), undefined, async (data) =\u003e !!data.age \u0026\u0026 data.age \u003e 20)\n\n  // Conditional Prompt with no collector (MyPrompt)\n  const tooYoung = new MyPrompt\u003cAgeData\u003e((data) =\u003e ({\n    text: `Wow ${data.name}, you are pretty young at ${data.age} years old!`\n  }), undefined, async (data) =\u003e !!data.age \u0026\u0026 data.age \u003c= 20)\n\n  const askNameNode = new PromptNode(askName)\n  const askAgeNode = new PromptNode(askAge)\n  const tooYoungNode = new PromptNode(tooYoung)\n  const tooOldNode = new PromptNode(tooOld)\n  askNameNode.setChildren([askAgeNode])\n  // Nodes with more than 1 sibling must have conditions defined\n  askAgeNode.setChildren([tooOldNode, tooYoungNode])\n\n  const message = createMockMessage()\n  const name = 'George'\n  const age = '30'\n  const runner = new PromptRunner\u003cAgeData\u003e()\n  const promise = runner.run(askNameNode, message)\n  // Wait for all pending promise callbacks to be executed for the emitter to set up\n  await flushPromises()\n  // Accept the name\n  emitter.emit('message', createMockMessage(name))\n  await flushPromises()\n  // Assert askName ran first\n  expect(runner.indexOf(askName)).toEqual(0)\n  // Accept the age\n  emitter.emit('message', createMockMessage(age))\n  await flushPromises()\n  // Assert askAge ran second\n  expect(runner.indexOf(askAge)).toEqual(1)\n  await promise\n  // Assert tooOld ran third, and tooYoung never ran\n  expect(runner.indexesOf([tooOld, tooYoung]))\n    .toEqual([2, -1])\n\n  // Clean up\n  spy.mockRestore()\n})\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsynzen%2Fprompt-anything","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsynzen%2Fprompt-anything","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsynzen%2Fprompt-anything/lists"}