├── .eslintrc.base.json
├── .github
└── workflows
│ └── publish.yml
├── .gitignore
├── .npmrc
├── .nvmrc
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── examples
├── .env.example
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── README.md
├── package.json
├── priompt
│ ├── ArvidStory
│ │ └── example01.yaml
│ ├── SimpleFunction
│ │ └── example01.yaml
│ ├── SimplePrompt
│ │ └── example01.yaml
│ ├── examplePrompt
│ │ └── example01.yaml
│ └── functionCallingPrompt
│ │ └── example01.yaml
├── src
│ ├── function-calling-prompt.tsx
│ ├── index.ts
│ ├── priompt-preview-handlers.ts
│ └── prompt.tsx
├── tsconfig.json
└── vitest.config.ts
├── init.sh
├── priompt-preview
├── .eslintrc.cjs
├── .gitignore
├── .npmignore
├── README.md
├── components.json
├── index.html
├── package.json
├── postcss.config.js
├── scripts
│ └── serve.cjs
├── src
│ ├── App.tsx
│ ├── components
│ │ └── ui
│ │ │ ├── button.tsx
│ │ │ ├── command.tsx
│ │ │ ├── dialog.tsx
│ │ │ └── textarea.tsx
│ ├── index.css
│ ├── lib
│ │ └── utils.ts
│ ├── main.tsx
│ ├── openai.ts
│ ├── openai_interfaces.ts
│ └── vite-env.d.ts
├── tailwind.config.js
├── tsconfig.json
├── tsconfig.node.json
├── vite.config.d.ts
├── vite.config.js
└── vite.config.ts
├── priompt
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── .npmignore
├── README.md
├── package.json
├── src
│ ├── __snapshots__
│ │ └── components.test.tsx.snap
│ ├── base.test.tsx
│ ├── components.test.tsx
│ ├── components.tsx
│ ├── index.ts
│ ├── lib.ts
│ ├── openai.ts
│ ├── outputCatcher.ai.impl.ts
│ ├── outputCatcher.ai.test.ts
│ ├── outputCatcher.ai.ts
│ ├── preview.ts
│ ├── sourcemap.test.tsx
│ ├── statsd.ts
│ ├── tokenizer.ts
│ └── types.d.ts
├── tsconfig.json
└── vitest.config.ts
├── publish.sh
├── pull-from-open-source.sh
├── push-to-open-source.sh
├── rustfmt.toml
└── tiktoken-node
├── .gitignore
├── .npmignore
├── Cargo.toml
├── README.md
├── build.rs
├── index.d.ts
├── index.js
├── npm
├── darwin-arm64
│ ├── README.md
│ └── package.json
├── darwin-x64
│ ├── README.md
│ └── package.json
├── linux-arm64-gnu
│ ├── README.md
│ └── package.json
├── linux-x64-gnu
│ ├── README.md
│ └── package.json
├── win32-arm64-msvc
│ ├── README.md
│ └── package.json
└── win32-x64-msvc
│ ├── README.md
│ └── package.json
├── package.json
└── src
└── lib.rs
/.eslintrc.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "es2021": true,
4 | "node": true
5 | },
6 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
7 | "rules": {
8 | "@typescript-eslint/require-array-sort-compare": "error",
9 | "@typescript-eslint/strict-boolean-expressions": ["error"],
10 | "@typescript-eslint/no-floating-promises": [
11 | "error",
12 | { "ignoreVoid": false }
13 | ],
14 | "@typescript-eslint/await-thenable": "error",
15 | "@typescript-eslint/no-misused-promises": "error",
16 | "constructor-super": "error",
17 | "eqeqeq": "error",
18 | "@typescript-eslint/switch-exhaustiveness-check": "error",
19 | "@typescript-eslint/no-inferrable-types": "off",
20 | "no-buffer-constructor": "error",
21 | "no-caller": "error",
22 | "no-case-declarations": "error",
23 | "no-debugger": "error",
24 | "no-duplicate-case": "error",
25 | "no-eval": "error",
26 | "no-async-promise-executor": "error",
27 | "no-extra-semi": "error",
28 | "sonarjs/no-ignored-return": "error",
29 | "no-new-wrappers": "error",
30 | "no-redeclare": "off",
31 | "no-sparse-arrays": "error",
32 | "@typescript-eslint/no-unused-vars": "off",
33 | "@typescript-eslint/no-unused-expressions": "error",
34 | "@typescript-eslint/no-empty-function": "off",
35 | "no-throw-literal": "error",
36 | "no-constant-condition": "off",
37 | "no-unsafe-finally": "error",
38 | "no-unused-labels": "error",
39 | "no-restricted-globals": [
40 | "warn",
41 | "name",
42 | "length",
43 | "event",
44 | "closed",
45 | "external",
46 | "status",
47 | "origin",
48 | "orientation",
49 | "context"
50 | ],
51 | "no-var": "off",
52 | "semi": "off",
53 | "@typescript-eslint/naming-convention": [
54 | "error",
55 | {
56 | "selector": "class",
57 | "format": ["PascalCase"]
58 | }
59 | ]
60 | },
61 | "overrides": [],
62 | "parser": "@typescript-eslint/parser",
63 | "parserOptions": {
64 | "ecmaVersion": "latest",
65 | "sourceType": "module",
66 | "project": "./tsconfig.json"
67 | },
68 | "ignorePatterns": ["node_modules/"],
69 | "plugins": ["@typescript-eslint", "sonarjs"]
70 | }
71 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches: [main, publish]
7 | pull_request:
8 |
9 | env:
10 | DEBUG: "napi:*"
11 | MACOSX_DEPLOYMENT_TARGET: "10.13"
12 |
13 | jobs:
14 | build:
15 | runs-on: ubuntu-latest
16 | defaults:
17 | run:
18 | working-directory: tiktoken-node
19 | strategy:
20 | matrix:
21 | target:
22 | - x86_64-pc-windows-msvc
23 | - x86_64-unknown-linux-gnu
24 | - aarch64-unknown-linux-gnu
25 | - x86_64-apple-darwin
26 | - aarch64-apple-darwin
27 | - aarch64-pc-windows-msvc
28 |
29 | steps:
30 | - uses: actions/checkout@v3
31 |
32 | - name: Setup Node
33 | uses: actions/setup-node@v3
34 | with:
35 | node-version: 20.12.2
36 |
37 | - uses: anysphere/action-setup@0eb0e970826653e8af98de91bec007fbd58a23e0
38 | name: Install pnpm
39 | id: pnpm-install
40 | with:
41 | version: "=8.6.0"
42 |
43 | - name: Setup Rust
44 | uses: dtolnay/rust-toolchain@stable
45 | with:
46 | toolchain: stable
47 | targets: ${{ matrix.target }}
48 |
49 | - name: Run init.sh
50 | working-directory: .
51 | run: ./init.sh
52 |
53 | - uses: Swatinem/rust-cache@v2
54 |
55 | - name: Install ziglang
56 | uses: goto-bus-stop/setup-zig@v1
57 | with:
58 | version: 0.10.0
59 |
60 | - run: cargo install cargo-xwin
61 | if: matrix.target == 'x86_64-pc-windows-msvc' || matrix.target == 'aarch64-pc-windows-msvc'
62 |
63 | - name: Check formatting
64 | run: cargo fmt --all --check
65 |
66 | - name: Node install
67 | run: pnpm i
68 |
69 | - name: Build Mac and Linux
70 | if: matrix.target != 'x86_64-pc-windows-msvc' && matrix.target != 'x86_64-unknown-linux-gnu' && matrix.target != 'aarch64-pc-windows-msvc'
71 | run: pnpm run build -- --zig --target ${{ matrix.target }}
72 |
73 | - name: Build Windows
74 | if: matrix.target == 'x86_64-pc-windows-msvc' || matrix.target == 'x86_64-unknown-linux-gnu' || matrix.target == 'aarch64-pc-windows-msvc'
75 | run: pnpm run build -- --target ${{ matrix.target }}
76 |
77 | - name: Upload artifact
78 | uses: actions/upload-artifact@v3
79 | with:
80 | name: bindings-${{ matrix.target }}
81 | path: tiktoken-node/tiktoken-node.*.node
82 | if-no-files-found: error
83 |
84 | publish:
85 | if: ${{ github.repository == 'anysphere/priompt' && github.event_name == 'push' && github.ref == 'refs/heads/publish' }}
86 | runs-on: ubuntu-20.04
87 | needs: build
88 |
89 | steps:
90 | - uses: actions/checkout@v3
91 |
92 | - name: Setup Node
93 | uses: actions/setup-node@v3
94 | with:
95 | node-version: 20.12.2
96 |
97 | - uses: anysphere/action-setup@0eb0e970826653e8af98de91bec007fbd58a23e0
98 | name: Install pnpm
99 | id: pnpm-install
100 | with:
101 | version: "=8.6.0"
102 |
103 | - name: Run init.sh
104 | working-directory: .
105 | run: ./init.sh
106 |
107 | - name: Download build
108 | uses: actions/download-artifact@v3
109 | with:
110 | path: tiktoken-node/artifacts
111 |
112 | - name: LS artifacts
113 | run: ls -R tiktoken-node/artifacts
114 | shell: bash
115 |
116 | - name: Move artifacts
117 | working-directory: tiktoken-node
118 | run: pnpm artifacts
119 |
120 | - name: LS post-move
121 | run: ls -R tiktoken-node/npm
122 | shell: bash
123 |
124 | - name: npm version
125 | run: npm --version
126 | shell: bash
127 |
128 | - name: Build priompt
129 | working-directory: priompt
130 | run: pnpm build
131 |
132 | - name: Build priompt-preview
133 | working-directory: priompt-preview
134 | run: pnpm build
135 |
136 | - name: globally install napi-rs
137 | run: npm install -g @napi-rs/cli
138 |
139 | - name: Set publishing config
140 | run: pnpm config set '//registry.npmjs.org/:_authToken' "${NODE_AUTH_TOKEN}"
141 | env:
142 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
143 |
144 | - name: Publish to npm
145 | run: pnpm publish --recursive --access=public --no-git-checks
146 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 | priompt-opensource
4 | commit.patch
5 | commit.template
6 | .wireit
7 | target/
8 | *.tsbuildinfo
9 |
10 | # todo: we should figure out a good way to sync these with the internal repo instead of having to generate them with the init script
11 | Cargo.lock
12 | Cargo.toml
13 | pnpm-workspace.yaml
14 | pnpm-lock.yaml
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | publish-branch=publish
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 20.12.2
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "eslint.workingDirectories": [
3 | {
4 | "directory": "examples",
5 | "changeProcessCWD": true
6 | },
7 | {
8 | "directory": "priompt",
9 | "changeProcessCWD": true
10 | },
11 | {
12 | "directory": "priompt-preview",
13 | "changeProcessCWD": true
14 | }
15 | ],
16 | "[javascript]": {
17 | "editor.defaultFormatter": "vscode.typescript-language-features",
18 | "editor.insertSpaces": false,
19 | "editor.formatOnSave": true
20 | },
21 | "[typescript]": {
22 | "editor.defaultFormatter": "vscode.typescript-language-features",
23 | "editor.insertSpaces": false,
24 | "editor.formatOnSave": true
25 | },
26 | "[jsonc]": {
27 | "editor.defaultFormatter": "esbenp.prettier-vscode",
28 | "editor.formatOnSave": true
29 | },
30 | "files.associations": {
31 | "*.env*": "properties"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright 2023 Anysphere, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Priompt
2 |
3 | Priompt (_priority + prompt_) is a JSX-based prompting library. It uses priorities to decide what to include in the context window.
4 |
5 | Priompt is an attempt at a _prompt design_ library, inspired by web design libraries like React. Read more about the motivation [here](https://arvid.xyz/prompt-design).
6 |
7 | ## Installation
8 |
9 | Install from npm:
10 |
11 | ```bash
12 | npm install @anysphere/priompt && npm install -D @anysphere/priompt-preview
13 | ```
14 |
15 | or
16 |
17 | ```bash
18 | yarn add @anysphere/priompt && yarn add --dev @anysphere/priompt-preview
19 | ```
20 |
21 | or
22 |
23 | ```bash
24 | pnpm add @anysphere/priompt && pnpm add -D @anysphere/priompt-preview
25 | ```
26 |
27 | ## Examples
28 |
29 | Read [examples/README.md](examples/README.md) to run the examples.
30 |
31 | ## Principles
32 |
33 | Prompts are rendered from a JSX component, which can look something like this:
34 |
35 | ```jsx
36 | function ExamplePrompt(
37 | props: PromptProps<{
38 | name: string,
39 | message: string,
40 | history: { case: "user" | "assistant", message: string }[],
41 | }>
42 | ): PromptElement {
43 | const capitalizedName = props.name[0].toUpperCase() + props.name.slice(1);
44 | return (
45 | <>
46 |
47 | The user's name is {capitalizedName}. Please respond to them kindly.
48 |
49 | {props.history.map((m, i) => (
50 |
51 | {m.case === "user" ? (
52 | {m.message}
53 | ) : (
54 | {m.message}
55 | )}
56 |
57 | ))}
58 | {props.message}
59 |
60 | >
61 | );
62 | }
63 | ```
64 |
65 | A component is rendered only once. Each child has a priority, where a higher priority means that the child is more important to include in the prompt. If no priority is specified, the child is included if and only if its parent is included. Absolute priorities are specified with `p` and relative ones are specified with `prel`.
66 |
67 | In the example above, we always include the system message and the latest user message, and are including as many messages from the history as possible, where later messages are prioritized over earlier messages.
68 |
69 | The key promise of the priompt renderer is:
70 |
71 | > Let $T$ be the token limit and $\text{Prompt}(p_\text{cutoff})$ be the function that creates a prompt by including all scopes with priority $p_\text{scope} \geq p_\text{cutoff}$, and no other. Then, the rendered prompt is $\text{\textbf{P}} = \text{Prompt}(p_\text{opt-cutoff})$ where $p_\text{opt-cutoff}$ is the minimum value such that $|\text{Prompt}(p_\text{opt-cutoff})| \leq T$.
72 |
73 | The building blocks of a priompt prompt are:
74 |
75 | 1. ``: this allows you to set priorities `p` for absolute or `prel` for relative.
76 | 2. ``: the first child with a sufficiently high priority will be included, and all children below it will not. This is useful for fallbacks for implementing something like "when the result is too long we want to say `(result omitted)`".
77 | 3. ``: for specifying empty space, useful for reserving tokens for generation.
78 | 4. ``: capture the output and parse it right within the prompt.
79 | 5. ``: isolate a section of the prompt with its own token limit. This is useful for guaranteeing that the start of the prompt will be the same for caching purposes. it would be nice to extend this to allow token limits like `100% - 100`.
80 | 6. `
`: force a token break at a particular location, which is useful for ensuring exact tokenization matches between two parts of a prompt (e.g. when implementing something like speculative edits).
81 | 7. ``: specify a few common configuration properties, such as `stop` token and `maxResponseTokens`, which can make the priompt dump more self-contained and help with evals.
82 |
83 | You can create components all you want, just like in React. The builtin components are:
84 |
85 | 1. ``, `` and ``: for building message-based prompts.
86 | 2. ``: for adding images into the prompt.
87 | 3. ``, ``: for specifying tools that the AI can call, either using a JSON schema or a Zod type.
88 |
89 | ## Advanced features
90 |
91 | 1. `onEject` and `onInclude`: callbacks that can be passed into any scope, which are called when the scope is either excluded or included in the final prompt. This allows you to change your logic depending on if something is too large for the prompt.
92 | 2. Sourcemaps: when setting `shouldBuildSourceMap` to `true`, the renderer computes a map between the actual characters in the prompt and the part of the JSX tree that they came from. This can be useful to figure out where cache misses are coming from in the prompt.
93 | 3. Prepend `DO_NOT_DUMP` to your priompt props key to prevent it from being dumped, which is useful for really big objects.
94 |
95 |
96 | ## Future
97 |
98 | A few things that would be cool to add:
99 |
100 | 1. A `` block: specify a `limit` on the number of tokens within a scope, but unlike ``, include the inner scopes in the global priority calculation.
101 | 2. Performance-optimized rendering of big trees: minimizing time spent tokenizing is part of it, but part of it is also working around JavaScript object allocation, and it is possible that writing the entire rendering engine in Rust, for example, would make it a lot faster.
102 |
103 | ## Caveats
104 |
105 | 1. We've discovered that adding priorities to everything is sort of an anti-pattern. It is possible that priorities are the wrong abstraction. We have found them useful though for including long files in the prompt in a line-by-line way.
106 | 2. The Priompt renderer has no builtin support for creating cacheable prompts. If you overuse priorities, it is easy to make hard-to-cache prompts, which may increase your cost or latency for LLM inference. We are interested in good solutions here, but for now it is up to the prompt designer to think about caching.
107 | 1. *Update: Priompt sourcemaps help with caching debugging!*
108 | 3. The current version of priompt only supports around 10K scopes reasonably fast (this is enough for most use cases). If you want to include a file in the prompt that is really long (>10K lines), and you split it line-by-line, you probably want to implement something like "for lines farther than 1000 lines away from the cursor position we have coarser scopes of 10 lines at a time".
109 | 4. For latency-critical prompts you want to monitor the time usage in the priompt preview dashboard. If there are too many scopes you may want to optimize for performance.
110 | 5. The Priompt renderer is not always guaranteed to produce the perfect $p_\text{opt-cutoff}$. For example, if a higher-priority child of a `` has more tokens than a lower-priority child, the currently implemented binary search renderer may return a (very slightly) incorrect result.
111 |
112 | ## Contributions
113 |
114 | Contributions are very welcome! This entire repo is MIT-licensed.
115 |
--------------------------------------------------------------------------------
/examples/.env.example:
--------------------------------------------------------------------------------
1 | SERVER_PORT=8008
2 | NODE_ENV=development
3 | OPENAI_API_KEY=sk-your-openai-secret-key
4 |
5 | PRIOMPT_PREVIEW_PORT=6284
6 | PRIOMPT_PREVIEW_SERVER_PORT=$SERVER_PORT
7 | PRIOMPT_PREVIEW_OPENAI_KEY=$OPENAI_API_KEY
--------------------------------------------------------------------------------
/examples/.eslintignore:
--------------------------------------------------------------------------------
1 | vitest.config.ts
--------------------------------------------------------------------------------
/examples/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["../.eslintrc.base"],
3 | "rules": {
4 | // additional rules specific to this config
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/examples/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 | .env
4 | priompt/*/dumps/**/*
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | # Priompt examples
2 |
3 | An example showing how to use `priompt` and `priompt-preview`; somewhat useful for testing random prompts.
4 |
5 | This example uses `fastify` for the server, but any server library or framework should work
6 |
7 | ## Running
8 |
9 | First run:
10 |
11 | ```bash
12 | cd .. && ./init.sh
13 | ```
14 |
15 | Then configure your OpenAI key in `.env`.
16 |
17 | In one terminal:
18 |
19 | ```bash
20 | pnpm priompt
21 | ```
22 |
23 | In another:
24 |
25 | ```bash
26 | pnpm watch
27 | ```
28 |
29 | In a third:
30 |
31 | ```bash
32 | curl 'localhost:8008/message?message=what%20is%20the%20advantage%20of%20rust%20over%20c&name=a%20curious%20explorer'
33 | ```
34 |
35 | You should get a response within a few seconds.
36 |
37 | Go to [localhost:6284](http://localhost:6284) to see the prompt in the priompt preview.
38 |
--------------------------------------------------------------------------------
/examples/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "priompt-examples",
3 | "license": "MIT",
4 | "engines": {
5 | "node": ">= 18.15.0"
6 | },
7 | "scripts": {
8 | "watch": "npm-run-all -p watch-src watch-priompt watch-priompt-preview",
9 | "watch-src": "sleep 3 && dotenv -e .env tsx watch --clear-screen=false src",
10 | "watch-priompt": "cd ../priompt && pnpm build-watch",
11 | "watch-priompt-preview": "cd ../priompt-preview && pnpm build-watch",
12 | "lint": "tsc --noEmit && eslint .",
13 | "test": "vitest",
14 | "coverage": "vitest run --coverage",
15 | "priompt": "dotenv -e .env pnpm npx @anysphere/priompt-preview serve"
16 | },
17 | "devDependencies": {
18 | "@anysphere/priompt-preview": "workspace:*",
19 | "@types/node": "^20.12.0",
20 | "@typescript-eslint/eslint-plugin": "^5.59.0",
21 | "@typescript-eslint/parser": "^5.59.0",
22 | "@vitest/coverage-v8": "^1.2.2",
23 | "eslint": "^8.38.0",
24 | "npm-run-all": "^4.1.5",
25 | "tsx": "^3.12.6",
26 | "typescript": "^5.2.0",
27 | "vitest": "^1.2.2"
28 | },
29 | "dependencies": {
30 | "@anysphere/priompt": "workspace:*",
31 | "@fastify/cors": "^8.3.0",
32 | "dotenv": "^16.1.4",
33 | "dotenv-cli": "^7.2.1",
34 | "fastify": "^4.17.0",
35 | "openai-v4": "npm:openai@4.0.0-beta.6",
36 | "openai": "^3.3.0",
37 | "zod": "^3.21.4"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/examples/priompt/ArvidStory/example01.yaml:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/examples/priompt/SimpleFunction/example01.yaml:
--------------------------------------------------------------------------------
1 | code: |-
2 | function x() {
3 | return z.object({
4 | a: z.string(),
5 | b: z.number(),
6 | });
7 | }
8 | error: '''z'' is not defined'
9 |
--------------------------------------------------------------------------------
/examples/priompt/SimplePrompt/example01.yaml:
--------------------------------------------------------------------------------
1 | text: Cursor är den bästa plattformen för att skriva kod.
2 | language: swahili
3 |
--------------------------------------------------------------------------------
/examples/priompt/examplePrompt/example01.yaml:
--------------------------------------------------------------------------------
1 | message: what is the advantage of rust over c
2 | name: arvid
3 |
--------------------------------------------------------------------------------
/examples/priompt/functionCallingPrompt/example01.yaml:
--------------------------------------------------------------------------------
1 | message: bad the prompt buton not work
2 | includeFunctions:
3 | - insert_sql_row
4 |
--------------------------------------------------------------------------------
/examples/src/function-calling-prompt.tsx:
--------------------------------------------------------------------------------
1 | import * as Priompt from "@anysphere/priompt";
2 | import {
3 | PreviewConfig,
4 | PreviewManager,
5 | PromptElement,
6 | PromptProps,
7 | SystemMessage,
8 | UserMessage,
9 | Function,
10 | FunctionMessage,
11 | AssistantMessage,
12 | ZFunction,
13 | } from "@anysphere/priompt";
14 | import { z } from "zod";
15 |
16 | const FunctionCallingPromptConfig: PreviewConfig = {
17 | id: "functionCallingPrompt",
18 | prompt: FunctionCallingPrompt,
19 | };
20 |
21 | export type FunctionCallingPromptProps = PromptProps<{
22 | message: string;
23 | includeFunctions: string[];
24 | causeConfusion: boolean;
25 | }>;
26 |
27 | PreviewManager.registerConfig(FunctionCallingPromptConfig);
28 |
29 | // array of 10000 integers
30 | const arr = Array.from(Array(800).keys());
31 |
32 | export function FunctionCallingPrompt(
33 | props: FunctionCallingPromptProps,
34 | args?: { dump?: boolean }
35 | ): PromptElement {
36 | if (args?.dump === true) {
37 | PreviewManager.dump(FunctionCallingPromptConfig, props);
38 | }
39 | return (
40 | <>
41 | {props.includeFunctions.includes("insert_sql_row") && (
42 |
61 | )}
62 | {props.includeFunctions.includes("update_sql_row") && (
63 |
86 | )}
87 |
88 | You are a database manager, responsible for taking the user's message
89 | and inserting it into our database.
90 |
91 | {props.causeConfusion && (
92 | <>
93 | i love the color theme
94 |
103 |
104 | Inserted 1 row.
105 |
106 | >
107 | )}
108 |
109 | {props.message}
110 | {/* {arr.map((i) => (
111 | {props.message}
112 | ))} */}
113 |
114 |
115 | >
116 | );
117 | }
118 |
119 | // returns the new code
120 | PreviewManager.register(SimpleFunction);
121 | export function SimpleFunction(
122 | props: PromptProps<
123 | {
124 | code: string;
125 | error: string;
126 | },
127 | | {
128 | type: "newImport";
129 | newImport: string;
130 | }
131 | | {
132 | type: "newCode";
133 | newCode: string;
134 | }
135 | >
136 | ) {
137 | return (
138 | <>
139 | {
148 | return await props.onReturn({
149 | type: "newImport",
150 | newImport: args.import,
151 | });
152 | }}
153 | />
154 |
155 | You are a coding assistant. The user will give you a function that has
156 | linter errors. Your job is to fix the errors. You have two options:
157 | either, you can call the `add_import` function, which adds an import
158 | statement at the top of the file, or you can rewrite the entire
159 | function. If you rewrite the function, start your message with ```.
160 |
161 |
162 | Function:
163 |
164 | ```
165 |
166 | {props.code}
167 |
168 | ```
169 |
170 |
171 | Errors:
172 |
173 | ```
174 |
175 | {props.error}
176 |
177 | ```
178 |
179 |
180 | {
182 | if (msg.content !== undefined) {
183 | return await props.onReturn({
184 | type: "newCode",
185 | newCode: msg.content,
186 | });
187 | }
188 | }}
189 | />
190 | >
191 | );
192 | }
193 |
--------------------------------------------------------------------------------
/examples/src/index.ts:
--------------------------------------------------------------------------------
1 | import { promptToOpenAIChatMessages, promptToOpenAIChatRequest, render, renderun } from '@anysphere/priompt';
2 | import { handlePriomptPreview } from './priompt-preview-handlers';
3 | import { ArvidStory, ExamplePrompt, SimplePrompt } from './prompt';
4 | import fastifyCors from "@fastify/cors";
5 | import Fastify, { FastifyError, FastifyLoggerOptions, FastifyReply, FastifyRequest, RawServerDefault, RouteGenericInterface } from "fastify";
6 | import { OpenAI as OpenAIV4 } from 'openai-v4';
7 | import { FunctionCallingPrompt, SimpleFunction } from './function-calling-prompt';
8 | import { ChatCompletionResponseMessage, Configuration, CreateChatCompletionRequest, OpenAIApi } from 'openai';
9 |
10 | const portString = process.env.SERVER_PORT;
11 | if (portString === undefined || Number.isNaN(parseInt(portString))) {
12 | throw new Error("SERVER_PORT is undefined. Please run the ./init.sh script to create a .env file.");
13 | }
14 | const port = parseInt(portString);
15 |
16 | const S = Fastify();
17 |
18 | if (process.env.OPENAI_API_KEY === undefined || process.env.OPENAI_API_KEY === "" || process.env.OPENAI_API_KEY === "sk-your-openai-secret-key") {
19 | throw new Error("OPENAI_API_KEY is undefined. Please run the ./init.sh script to create a .env file, and then insert your API key in the .env file.");
20 | }
21 |
22 | const openaiV4 = new OpenAIV4({
23 | apiKey: process.env.OPENAI_API_KEY,
24 | });
25 | const configuration = new Configuration({
26 | apiKey: process.env.OPENAI_API_KEY,
27 | });
28 | const openai = new OpenAIApi(configuration);
29 |
30 | function messageAdapter(old: ChatCompletionResponseMessage[]): OpenAIV4.Chat.CompletionCreateParams.CreateChatCompletionRequestNonStreaming.Message[] {
31 | return old as OpenAIV4.Chat.CompletionCreateParams.CreateChatCompletionRequestNonStreaming.Message[];
32 | }
33 | function messageAdapterReverse(n: OpenAIV4.Chat.CompletionCreateParams.CreateChatCompletionRequestNonStreaming.Message): ChatCompletionResponseMessage {
34 | return n as ChatCompletionResponseMessage;
35 | }
36 | function requestAdapter(old: CreateChatCompletionRequest): OpenAIV4.Chat.CompletionCreateParams.CreateChatCompletionRequestNonStreaming {
37 | return old as OpenAIV4.Chat.CompletionCreateParams.CreateChatCompletionRequestNonStreaming;
38 | }
39 |
40 | async function main() {
41 |
42 | if (process.env.NODE_ENV === "development") {
43 | await handlePriomptPreview(S);
44 | }
45 |
46 | await S.register(fastifyCors, {
47 | origin: [
48 | `http://localhost:${process.env.PRIOMPT_PREVIEW_PORT}`
49 | ],
50 | });
51 |
52 | // here we can add any other routes we want! this can be good for testing stuff
53 | S.get("/", (_, reply) => {
54 | return reply.type("text/plain").send(`Welcome to Priompt examples.`);
55 | });
56 | S.get("/message", async (request, reply) => {
57 | const query = request.query as { message: string; name: string };
58 | if (query.message === undefined || query.name === undefined) {
59 | return reply.status(400).send("Bad request; message and name are required.");
60 | }
61 | const message = query.message as string;
62 | const name = query.name as string;
63 | const prompt = ExamplePrompt({ message, name }, { dump: process.env.NODE_ENV === "development" });
64 | const output = await render(prompt, {
65 | model: "gpt-3.5-turbo"
66 | });
67 |
68 | const requestConfig: CreateChatCompletionRequest = {
69 | model: "gpt-3.5-turbo",
70 | messages: promptToOpenAIChatMessages(output.prompt),
71 | };
72 |
73 | try {
74 | const openaiResult = await openai.createChatCompletion(requestConfig);
75 |
76 | const openaiOutput = openaiResult.data.choices[0].message;
77 |
78 | return reply.type("text/plain").send(openaiOutput?.content);
79 | } catch (error) {
80 | console.error(error);
81 | return reply.status(500).send("Internal server error.");
82 | }
83 | });
84 | S.get("/database", async (request, reply) => {
85 | const query = request.query as { message: string; confuse: string | undefined; };
86 | if (query.message === undefined) {
87 | return reply.status(400).send("Bad request; message is required.");
88 | }
89 | const message = query.message as string;
90 | const prompt = FunctionCallingPrompt({ message, includeFunctions: ["insert_sql_row", "update_sql_row"], causeConfusion: query.confuse === "true" }, { dump: process.env.NODE_ENV === "development" });
91 | const output = await render(prompt, {
92 | model: "gpt-3.5-turbo"
93 | });
94 |
95 | console.log(JSON.stringify(output.prompt, null, 2));
96 |
97 | const requestConfig: CreateChatCompletionRequest = {
98 | ...promptToOpenAIChatRequest(output.prompt),
99 | model: "gpt-3.5-turbo-0613",
100 | };
101 |
102 | // make this print all nested values in node
103 | console.log(JSON.stringify(requestConfig, null, 2));
104 |
105 | try {
106 | const openaiResult = await openai.createChatCompletion(requestConfig);
107 |
108 | const openaiOutput = openaiResult.data.choices[0];
109 |
110 | return reply.type("text/json").send(JSON.stringify(openaiOutput));
111 | } catch (error) {
112 | console.error(error);
113 | return reply.status(500).send("Internal server error.");
114 | }
115 | });
116 |
117 | S.get("/simple", async (request, reply) => {
118 | const query = request.query as { language: string; };
119 | if (query.language === undefined) {
120 | return reply.status(400).send("Bad request; language is required.");
121 | }
122 | const language = query.language as string;
123 | const text = "Cursor är den bästa plattformen för att skriva kod.";
124 | try {
125 | const answer = await renderun({
126 | prompt: SimplePrompt,
127 | props: { text, language },
128 | renderOptions: {
129 | model: "gpt-3.5-turbo",
130 | },
131 | modelCall: async (x) => { return { type: "output", value: (await openai.createChatCompletion({ ...x, model: "gpt-3.5-turbo" })).data } }
132 | });
133 | return reply.type("text/plain").send(JSON.stringify({ answer }));
134 | } catch (error) {
135 | console.error(error);
136 | return reply.status(500).send("Internal server error.");
137 | }
138 | });
139 |
140 | S.get("/arvidstory", async (request, reply) => {
141 | try {
142 | const answer = await renderun({
143 | prompt: ArvidStory,
144 | props: {},
145 | renderOptions: {
146 | model: "gpt-3.5-turbo",
147 | },
148 | modelCall: async (x) => {
149 | const y = await openaiV4.chat.completions.create({ ...requestAdapter({ ...x, model: "gpt-3.5-turbo" }), stream: true });
150 | return {
151 | type: "stream",
152 | value: (async function* () {
153 | for await (const message of y) {
154 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
155 | yield messageAdapterReverse(message.choices[0].delta as any);
156 | }
157 | })()
158 | }
159 | }
160 | });
161 | let s = "";
162 | for await (const part of answer) {
163 | s += part;
164 | console.log(part);
165 | }
166 | return reply.type("text/plain").send(JSON.stringify({ answer: s }));
167 | } catch (error) {
168 | console.error(error);
169 | return reply.status(500).send("Internal server error.");
170 | }
171 | });
172 |
173 | S.get("/fixcode", async (request, reply) => {
174 | const query = request.query as { type: string; };
175 | let code, error: string;
176 | if (query.type === undefined || query.type !== 'code') {
177 | code = "function x() {\n\treturn z.object({\n\t\ta: z.string(),\n\t\tb: z.number(),\n\t});\n}";
178 | error = "'z' is not defined";
179 | } else {
180 | code = "function x() {\n\treturn z.object({\n\t\ta: z.string(),\n\t\tb: z.umber(),\n\t});\n}";
181 | error = "'umber' is not defined";
182 | }
183 | try {
184 | const action = await renderun({
185 | prompt: SimpleFunction,
186 | props: { code, error },
187 | renderOptions: {
188 | model: "gpt-4",
189 | },
190 | modelCall: async (x) => { return { type: 'output', value: (await openai.createChatCompletion({ ...x, model: "gpt-4" })).data } }
191 | });
192 | return reply.type("text/plain").send(JSON.stringify(action));
193 | } catch (error) {
194 | console.error(error);
195 | return reply.status(500).send("Internal server error.");
196 | }
197 | });
198 |
199 | await S.listen({ host: "0.0.0.0", port });
200 |
201 | console.log(`Server listening on port ${port}.`);
202 | }
203 |
204 | void main();
--------------------------------------------------------------------------------
/examples/src/priompt-preview-handlers.ts:
--------------------------------------------------------------------------------
1 | import { FastifyInstance } from 'fastify';
2 | import { PreviewManager, PreviewManagerGetPromptQuery, PreviewManagerLiveModeQuery, PreviewManagerLiveModeResultQuery } from '@anysphere/priompt';
3 | import { PreviewManagerGetPromptOutputQuery } from '@anysphere/priompt/dist/preview';
4 |
5 |
6 | export async function handlePriomptPreview(S: FastifyInstance) {
7 | S.get("/priompt/getPreviews", async (_, reply) => {
8 | return reply.type("text/json").send(JSON.stringify(PreviewManager.getPreviews()));
9 | });
10 |
11 | S.get('/priompt/getPrompt', async (request, reply) => {
12 | const query = request.query as PreviewManagerGetPromptQuery;
13 | return reply.type("text/json").send(JSON.stringify(await PreviewManager.getPrompt(query)));
14 | });
15 |
16 | S.get('/priompt/getPromptOutput', async (request, reply) => {
17 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
18 | const newQ = request.query as any;
19 | const query = JSON.parse(newQ.v) as PreviewManagerGetPromptOutputQuery;
20 | const stringified = JSON.stringify(await PreviewManager.getPromptOutput(query));
21 | return reply.type("text/json").send(stringified);
22 | });
23 |
24 | S.get('/priompt/liveMode', async (request, reply) => {
25 | const query = request.query as PreviewManagerLiveModeQuery;
26 |
27 | try {
28 | const output = await PreviewManager.liveMode(query)
29 | await reply.type("text/json").send(JSON.stringify(output));
30 | } catch (error) {
31 | if (error.name === 'AbortError') {
32 | return reply.status(500).send({ error: 'Request aborted' });
33 | } else {
34 | throw error;
35 | }
36 | }
37 | });
38 | S.get("/priompt/liveModeResult", (request, reply) => {
39 | const query = request.query as PreviewManagerLiveModeResultQuery;
40 | PreviewManager.liveModeResult(query);
41 | return reply.type("text/json").send(JSON.stringify({}));
42 | });
43 |
44 | }
--------------------------------------------------------------------------------
/examples/src/prompt.tsx:
--------------------------------------------------------------------------------
1 | import * as Priompt from "@anysphere/priompt";
2 | import {
3 | PreviewConfig,
4 | PreviewManager,
5 | PromptElement,
6 | PromptProps,
7 | SystemMessage,
8 | UserMessage,
9 | } from "@anysphere/priompt";
10 |
11 | const ExamplePromptConfig: PreviewConfig = {
12 | id: "examplePrompt",
13 | prompt: ExamplePrompt,
14 | };
15 | PreviewManager.registerConfig(ExamplePromptConfig);
16 |
17 | export type ExamplePromptProps = PromptProps<{
18 | name: string;
19 | message: string;
20 | }>;
21 |
22 | export function ExamplePrompt(
23 | props: ExamplePromptProps,
24 | args?: { dump?: boolean }
25 | ): PromptElement {
26 | if (args?.dump === true) {
27 | PreviewManager.dump(ExamplePromptConfig, props);
28 | }
29 | return (
30 | <>
31 |
32 | The user's name is {props.name}. Please always greet them in an
33 | extremely formal, medieval style, with lots of fanfare. Then seamlessly
34 | proceed to reply to their message in the most casual, 2010s, cool dude
35 | texting style. Please be over-the-top in both respects, and make the
36 | transition seem like it never happened.
37 |
38 | {props.message}
39 |
40 | >
41 | );
42 | }
43 |
44 | PreviewManager.register(SimplePrompt);
45 | export function SimplePrompt(
46 | props: PromptProps<
47 | {
48 | language: string;
49 | text: string;
50 | },
51 | boolean
52 | >
53 | ): PromptElement {
54 | return (
55 | <>
56 |
57 | Please determine if the following text is in {props.language}. If it is,
58 | please reply with "yes". If it is not, please reply with "no". Do not
59 | output anything else.
60 |
61 | {props.text}
62 |
63 | {
65 | if (output.content?.toLowerCase().includes("yes") === true) {
66 | return await props.onReturn(true);
67 | } else if (output.content?.toLowerCase().includes("no") === true) {
68 | return await props.onReturn(false);
69 | }
70 | // bad
71 | throw new Error(`Invalid output: ${output.content}`);
72 | }}
73 | />
74 | >
75 | );
76 | }
77 |
78 | PreviewManager.register(ArvidStory);
79 | export function ArvidStory(
80 | props: PromptProps>
81 | ): PromptElement {
82 | return (
83 | <>
84 |
85 | Please write a short story about a young boy named Arvid. Only a
86 | paragraph please.
87 |
88 |
89 | {
91 | // we want to replace every R with a J
92 | await props.onReturn(
93 | (async function* () {
94 | for await (const chunk of stream) {
95 | if (chunk.content === undefined) {
96 | continue;
97 | }
98 | yield chunk.content.replace(/r/g, "j");
99 | }
100 | })()
101 | );
102 | }}
103 | />
104 | >
105 | );
106 | }
107 |
--------------------------------------------------------------------------------
/examples/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": true,
4 | "outDir": "./dist",
5 | "strictNullChecks": true,
6 | "noImplicitAny": true,
7 | "declaration": true,
8 | "isolatedModules": true,
9 | "target": "es2022",
10 | "moduleResolution": "node",
11 | "jsx": "react",
12 | "jsxFactory": "Priompt.createElement",
13 | "jsxFragmentFactory": "Priompt.Fragment",
14 | "strictPropertyInitialization": true
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/examples/vitest.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | test: {
3 | include: [
4 | 'src/**/*.{test,spec}.{js,ts,jsx,tsx}',
5 | // Also include top level files
6 | 'src/*.{test,spec}.{js,ts,jsx,tsx}'
7 | ],
8 | exclude: ['build/**/*'],
9 | },
10 | };
--------------------------------------------------------------------------------
/init.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
4 |
5 | echo '{
6 | "packages": ["priompt", "priompt-preview", "examples", "tiktoken-node"]
7 | }' > "$SCRIPT_DIR"/pnpm-workspace.yaml
8 |
9 | echo '[workspace]
10 | members = ["tiktoken-node"]
11 | resolver = "2"
12 | ' > "$SCRIPT_DIR"/Cargo.toml
13 |
14 | # copy over the examples/.env.example to examples/.env
15 | cp -f "$SCRIPT_DIR"/examples/.env.example "$SCRIPT_DIR"/examples/.env
16 |
17 | pnpm i -r
18 |
--------------------------------------------------------------------------------
/priompt-preview/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: { browser: true, es2020: true, node: true },
3 | extends: [
4 | 'eslint:recommended',
5 | 'plugin:@typescript-eslint/recommended',
6 | 'plugin:react-hooks/recommended',
7 | ],
8 | parser: '@typescript-eslint/parser',
9 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
10 | plugins: ['react-refresh'],
11 | rules: {
12 | 'react-refresh/only-export-components': 'warn',
13 | // disable unused vars check
14 | '@typescript-eslint/no-unused-vars': 'off',
15 | '@typescript-eslint/no-empty-interface': 'off',
16 | '@typescript-eslint/no-inferrable-types': 'off',
17 | },
18 | }
19 |
--------------------------------------------------------------------------------
/priompt-preview/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/priompt-preview/.npmignore:
--------------------------------------------------------------------------------
1 | .wireit
2 | !dist/**
--------------------------------------------------------------------------------
/priompt-preview/README.md:
--------------------------------------------------------------------------------
1 | Run `pnpm build` or `pnpm build-watch` to build the library.
2 |
--------------------------------------------------------------------------------
/priompt-preview/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "src/index.css",
9 | "baseColor": "stone",
10 | "cssVariables": false
11 | },
12 | "aliases": {
13 | "components": "@/components",
14 | "utils": "@/lib/utils"
15 | }
16 | }
--------------------------------------------------------------------------------
/priompt-preview/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Priompt Preview
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/priompt-preview/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@anysphere/priompt-preview",
3 | "license": "MIT",
4 | "version": "0.2.1",
5 | "description": "An interactive preview of priompt prompts.",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/anysphere/priompt"
9 | },
10 | "homepage": "https://github.com/anysphere/priompt",
11 | "author": "Arvid Lunnemark",
12 | "type": "module",
13 | "scripts": {
14 | "dev": "vite",
15 | "build": "wireit",
16 | "build-watch": "nodemon --watch 'src/**/*' --ext '*' --exec 'pnpm build'",
17 | "lint": "wireit",
18 | "tsc-build": "wireit",
19 | "priompt:build": "wireit",
20 | "preview": "vite preview"
21 | },
22 | "wireit": {
23 | "build": {
24 | "command": "vite build",
25 | "files": [
26 | "src/**/*.ts",
27 | "src/**/*.tsx",
28 | "tsconfig.json"
29 | ],
30 | "output": [
31 | "dist/**/*"
32 | ],
33 | "clean": "if-file-deleted",
34 | "dependencies": [
35 | "priompt:build"
36 | ]
37 | },
38 | "tsc-build": {
39 | "command": "tsc --build --pretty",
40 | "clean": "if-file-deleted",
41 | "files": [
42 | "src/**/*.ts",
43 | "src/**/*.tsx",
44 | "tsconfig.json"
45 | ],
46 | "output": [
47 | "dist/**/*"
48 | ],
49 | "dependencies": [
50 | "priompt:build"
51 | ]
52 | },
53 | "priompt:build": {
54 | "command": "pnpm i",
55 | "dependencies": [
56 | "../priompt:build"
57 | ],
58 | "files": [],
59 | "output": []
60 | },
61 | "lint": {
62 | "command": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
63 | "files": [
64 | "src/**/*",
65 | "tsconfig.json"
66 | ],
67 | "clean": "if-file-deleted",
68 | "dependencies": [
69 | "priompt:build"
70 | ]
71 | }
72 | },
73 | "dependencies": {
74 | "@radix-ui/react-dialog": "^1.0.5",
75 | "@radix-ui/react-icons": "^1.3.0",
76 | "@radix-ui/react-slot": "^1.0.2",
77 | "class-variance-authority": "^0.7.0",
78 | "clsx": "^2.0.0",
79 | "cmdk": "^0.2.0",
80 | "js-tiktoken": "^1.0.7",
81 | "lucide-react": "^0.263.1",
82 | "tailwind-merge": "^1.14.0",
83 | "tailwindcss-animate": "^1.0.6",
84 | "uuid": "^9.0.0",
85 | "@anysphere/priompt": "workspace:*"
86 | },
87 | "devDependencies": {
88 | "@types/react": "^18.0.28",
89 | "@types/react-dom": "^18.0.11",
90 | "@types/uuid": "^9.0.1",
91 | "@typescript-eslint/eslint-plugin": "^5.57.1",
92 | "@typescript-eslint/parser": "^5.57.1",
93 | "@vitejs/plugin-react": "^4.0.0",
94 | "autoprefixer": "^10.4.14",
95 | "axios": "^0.26.1",
96 | "eslint": "^8.38.0",
97 | "eslint-plugin-react-hooks": "^4.6.0",
98 | "eslint-plugin-react-refresh": "^0.3.4",
99 | "nodemon": "^2.0.22",
100 | "postcss": "^8.4.27",
101 | "react": "^18.2.0",
102 | "react-dom": "^18.2.0",
103 | "tailwindcss": "^3.3.3",
104 | "typescript": "^5.2.0",
105 | "use-debounce": "^9.0.4",
106 | "vite": "^4.3.2",
107 | "wireit": "^0.14.0"
108 | },
109 | "bin": {
110 | "serve": "./scripts/serve.cjs"
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/priompt-preview/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/priompt-preview/scripts/serve.cjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | /* eslint-disable no-undef */
3 | /* eslint-disable @typescript-eslint/no-var-requires */
4 |
5 | const http = require('http');
6 | const fs = require('fs');
7 | const path = require('path');
8 | const url = require('url');
9 |
10 | // Add these new imports
11 | const yargs = require('yargs/yargs');
12 | const { hideBin } = require('yargs/helpers');
13 |
14 | // Parse command line arguments
15 | const argv = yargs(hideBin(process.argv))
16 | .option('config', {
17 | describe: 'Path to config JSON file',
18 | type: 'string'
19 | })
20 | .argv;
21 |
22 | let config = {};
23 | if (argv.config) {
24 | try {
25 | const configFile = fs.readFileSync(argv.config, 'utf8');
26 | config = JSON.parse(configFile);
27 | } catch (error) {
28 | console.error(`Error reading config file: ${error.message}`);
29 | process.exit(1);
30 | }
31 | }
32 |
33 | const port = process.env.PRIOMPT_PREVIEW_PORT || 6283;
34 | const server_port = process.env.PRIOMPT_PREVIEW_SERVER_PORT;
35 | if (!server_port) {
36 | console.error('PRIOMPT_PREVIEW_SERVER_PORT is not set. it needs to be set of the port where the priompt server is run');
37 | process.exit(1);
38 | }
39 | const distPath = path.join(path.dirname(__dirname), 'dist');
40 |
41 | const requestListener = (req, res) => {
42 | const parsedUrl = url.parse(req.url);
43 | const filePath = path.join(distPath, parsedUrl.pathname === '/' ? 'index.html' : parsedUrl.pathname);
44 | const extname = String(path.extname(filePath)).toLowerCase();
45 | const mimeTypes = {
46 | '.html': 'text/html',
47 | '.js': 'application/javascript',
48 | '.css': 'text/css',
49 | '.jpg': 'image/jpeg',
50 | '.jpeg': 'image/jpeg',
51 | '.png': 'image/png',
52 | '.gif': 'image/gif',
53 | '.svg': 'image/svg+xml',
54 | // Add more MIME types if needed
55 | };
56 |
57 | const contentType = mimeTypes[extname] || 'application/octet-stream';
58 |
59 | fs.readFile(filePath, (err, data) => {
60 | if (err) {
61 | if (err.code === 'ENOENT') {
62 | res.writeHead(404);
63 | res.end('Not found');
64 | } else {
65 | res.writeHead(500);
66 | res.end('Error loading file');
67 | }
68 | } else {
69 | res.writeHead(200, { 'Content-Type': contentType });
70 | if (data.toString().includes('localhost:3000')) {
71 | data = data.toString().replace(/localhost:3000/g, `localhost:${server_port}`);
72 | }
73 |
74 | // Use config.chatModels if available, otherwise fall back to PRIOMPT_PREVIEW_MODELS
75 | if (config.chatModels) {
76 | data = data.toString().replace(/\["gpt-3.5-turbo","gpt-4"\]/, JSON.stringify(config.chatModels));
77 | } else if (process.env.PRIOMPT_PREVIEW_MODELS) {
78 | data = data.toString().replace(/\["gpt-3.5-turbo","gpt-4"\]/, process.env.PRIOMPT_PREVIEW_MODELS);
79 | }
80 |
81 | // Use config.completionModels if available, otherwise fall back to PRIOMPT_PREVIEW_COMPLETION_MODELS
82 | if (config.completionModels) {
83 | data = data.toString().replace(/text-davinci-003,code-davinci-002/, config.completionModels.join(','));
84 | } else if (process.env.PRIOMPT_PREVIEW_COMPLETION_MODELS) {
85 | const completionModels = process.env.PRIOMPT_PREVIEW_COMPLETION_MODELS.split(',');
86 | data = data.toString().replace(/text-davinci-003,code-davinci-002/, completionModels);
87 | }
88 |
89 | if ((extname === '.html' || extname === '.js') && data.toString().includes('PRIOMPT_PREVIEW_OPENAI_KEY')) {
90 | data = data.toString().replace(/PRIOMPT_PREVIEW_OPENAI_KEY/g, `${process.env.PRIOMPT_PREVIEW_OPENAI_KEY}`);
91 | }
92 | if ((extname === '.html' || extname === '.js') && data.toString().includes('PRIOMPT_PREVIEW_OSS_ENDPOINTS_JSON_STRING')) {
93 | data = data.toString().replace(/PRIOMPT_PREVIEW_OSS_ENDPOINTS_JSON_STRING/g, `${process.env.PRIOMPT_PREVIEW_OSS_ENDPOINTS_JSON_STRING ?? "PRIOMPT_PREVIEW_OSS_ENDPOINTS_JSON_STRING"}`);
94 | }
95 | res.end(data);
96 | }
97 | });
98 | };
99 |
100 | const server = http.createServer(requestListener);
101 | server.listen(port, () => {
102 | console.log(JSON.stringify({
103 | "level": "info",
104 | "time": new Date().getTime(),
105 | "pid": process.pid,
106 | "msg": `Server is running on http://localhost:${port}`
107 | }));
108 | });
109 |
--------------------------------------------------------------------------------
/priompt-preview/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-stone-400 disabled:pointer-events-none disabled:opacity-50 dark:focus-visible:ring-stone-800",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-stone-900 text-stone-50 shadow hover:bg-stone-900/90 dark:bg-stone-50 dark:text-stone-900 dark:hover:bg-stone-50/90",
14 | destructive:
15 | "bg-red-500 text-stone-50 shadow-sm hover:bg-red-500/90 dark:bg-red-900 dark:text-red-50 dark:hover:bg-red-900/90",
16 | outline:
17 | "border border-stone-200 bg-white shadow-sm hover:bg-stone-100 hover:text-stone-900 dark:border-stone-800 dark:bg-stone-950 dark:hover:bg-stone-800 dark:hover:text-stone-50",
18 | secondary:
19 | "bg-stone-100 text-stone-900 shadow-sm hover:bg-stone-100/80 dark:bg-stone-800 dark:text-stone-50 dark:hover:bg-stone-800/80",
20 | ghost:
21 | "hover:bg-stone-100 hover:text-stone-900 dark:hover:bg-stone-800 dark:hover:text-stone-50",
22 | link: "text-stone-900 underline-offset-4 hover:underline dark:text-stone-50",
23 | },
24 | size: {
25 | default: "h-9 px-1 py-1",
26 | sm: "h-8 rounded-md px-1 text-xs",
27 | lg: "h-10 rounded-md px-1",
28 | icon: "h-9 w-9",
29 | },
30 | },
31 | defaultVariants: {
32 | variant: "default",
33 | size: "default",
34 | },
35 | }
36 | );
37 |
38 | export interface ButtonProps
39 | extends React.ButtonHTMLAttributes,
40 | VariantProps {
41 | asChild?: boolean;
42 | }
43 |
44 | const Button = React.forwardRef(
45 | ({ className, variant, size, asChild = false, ...props }, ref) => {
46 | const Comp = asChild ? Slot : "button";
47 | return (
48 |
53 | );
54 | }
55 | );
56 | Button.displayName = "Button";
57 |
58 | // eslint-disable-next-line react-refresh/only-export-components
59 | export { Button, buttonVariants };
60 |
--------------------------------------------------------------------------------
/priompt-preview/src/components/ui/command.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { DialogProps } from "@radix-ui/react-dialog"
3 | import { MagnifyingGlassIcon } from "@radix-ui/react-icons"
4 | import { Command as CommandPrimitive } from "cmdk"
5 |
6 | import { cn } from "@/lib/utils"
7 | import { Dialog, DialogContent } from "@/components/ui/dialog"
8 |
9 | const Command = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 | ))
22 | Command.displayName = CommandPrimitive.displayName
23 |
24 | interface CommandDialogProps extends DialogProps {}
25 |
26 | const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
27 | return (
28 |
35 | )
36 | }
37 |
38 | const CommandInput = React.forwardRef<
39 | React.ElementRef,
40 | React.ComponentPropsWithoutRef
41 | >(({ className, ...props }, ref) => (
42 |
43 |
44 |
52 |
53 | ))
54 |
55 | CommandInput.displayName = CommandPrimitive.Input.displayName
56 |
57 | const CommandList = React.forwardRef<
58 | React.ElementRef,
59 | React.ComponentPropsWithoutRef
60 | >(({ className, ...props }, ref) => (
61 |
66 | ))
67 |
68 | CommandList.displayName = CommandPrimitive.List.displayName
69 |
70 | const CommandEmpty = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >((props, ref) => (
74 |
79 | ))
80 |
81 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName
82 |
83 | const CommandGroup = React.forwardRef<
84 | React.ElementRef,
85 | React.ComponentPropsWithoutRef
86 | >(({ className, ...props }, ref) => (
87 |
95 | ))
96 |
97 | CommandGroup.displayName = CommandPrimitive.Group.displayName
98 |
99 | const CommandSeparator = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName
110 |
111 | const CommandItem = React.forwardRef<
112 | React.ElementRef,
113 | React.ComponentPropsWithoutRef
114 | >(({ className, ...props }, ref) => (
115 |
123 | ))
124 |
125 | CommandItem.displayName = CommandPrimitive.Item.displayName
126 |
127 | const CommandShortcut = ({
128 | className,
129 | ...props
130 | }: React.HTMLAttributes) => {
131 | return (
132 |
139 | )
140 | }
141 | CommandShortcut.displayName = "CommandShortcut"
142 |
143 | export {
144 | Command,
145 | CommandDialog,
146 | CommandInput,
147 | CommandList,
148 | CommandEmpty,
149 | CommandGroup,
150 | CommandItem,
151 | CommandShortcut,
152 | CommandSeparator,
153 | }
154 |
--------------------------------------------------------------------------------
/priompt-preview/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as DialogPrimitive from "@radix-ui/react-dialog"
3 | import { Cross2Icon } from "@radix-ui/react-icons"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Dialog = DialogPrimitive.Root
8 |
9 | const DialogTrigger = DialogPrimitive.Trigger
10 |
11 | const DialogPortal = DialogPrimitive.Portal
12 |
13 | const DialogClose = DialogPrimitive.Close
14 |
15 | const DialogOverlay = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
29 |
30 | const DialogContent = React.forwardRef<
31 | React.ElementRef,
32 | React.ComponentPropsWithoutRef
33 | >(({ className, children, ...props }, ref) => (
34 |
35 |
36 |
44 | {children}
45 |
46 |
47 | Close
48 |
49 |
50 |
51 | ))
52 | DialogContent.displayName = DialogPrimitive.Content.displayName
53 |
54 | const DialogHeader = ({
55 | className,
56 | ...props
57 | }: React.HTMLAttributes) => (
58 |
65 | )
66 | DialogHeader.displayName = "DialogHeader"
67 |
68 | const DialogFooter = ({
69 | className,
70 | ...props
71 | }: React.HTMLAttributes) => (
72 |
79 | )
80 | DialogFooter.displayName = "DialogFooter"
81 |
82 | const DialogTitle = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef
85 | >(({ className, ...props }, ref) => (
86 |
94 | ))
95 | DialogTitle.displayName = DialogPrimitive.Title.displayName
96 |
97 | const DialogDescription = React.forwardRef<
98 | React.ElementRef,
99 | React.ComponentPropsWithoutRef
100 | >(({ className, ...props }, ref) => (
101 |
106 | ))
107 | DialogDescription.displayName = DialogPrimitive.Description.displayName
108 |
109 | export {
110 | Dialog,
111 | DialogPortal,
112 | DialogOverlay,
113 | DialogTrigger,
114 | DialogClose,
115 | DialogContent,
116 | DialogHeader,
117 | DialogFooter,
118 | DialogTitle,
119 | DialogDescription,
120 | }
121 |
--------------------------------------------------------------------------------
/priompt-preview/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | );
20 | }
21 | );
22 | Textarea.displayName = "Textarea";
23 |
24 | export { Textarea };
25 |
--------------------------------------------------------------------------------
/priompt-preview/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
--------------------------------------------------------------------------------
/priompt-preview/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/priompt-preview/src/main.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as ReactDOM from "react-dom/client";
3 | import './index.css';
4 | import App from "./App";
5 |
6 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
7 |
8 |
9 |
10 | );
11 |
--------------------------------------------------------------------------------
/priompt-preview/src/openai.ts:
--------------------------------------------------------------------------------
1 | import { ChatCompletionRequestMessage, CreateChatCompletionRequest, CreateCompletionRequest, CreateCompletionResponse, StreamChatCompletionResponse } from '@anysphere/priompt/dist/openai';
2 | import { encodingForModel } from "js-tiktoken";
3 |
4 |
5 |
6 | export async function* streamChatLocalhost(createChatCompletionRequest: CreateChatCompletionRequest, options?: RequestInit, abortSignal?: AbortSignal) {
7 | let streamer: AsyncGenerator | undefined = undefined;
8 |
9 | const newAbortSignal = new AbortController();
10 | abortSignal?.addEventListener('abort', () => {
11 | newAbortSignal.abort();
12 | });
13 |
14 | let timeout = setTimeout(() => {
15 | console.error("OpenAI request timed out after 200 seconds..... Not good.")
16 | // Next, we abort the signal
17 | newAbortSignal.abort();
18 | }, 200_000);
19 |
20 | try {
21 | const prompt = joinMessages(createChatCompletionRequest.messages, true);
22 | let tokens = enc.encode(prompt).length;
23 | if (createChatCompletionRequest.model.includes('00')) {
24 | tokens = enc_old.encode(prompt).length;
25 | }
26 | const maxTokens = Math.min((getTokenLimit(createChatCompletionRequest.model)) - tokens, getOutputTokenLimit(createChatCompletionRequest.model))
27 | const requestOptions: RequestInit = {
28 | ...options,
29 | method: 'POST',
30 | headers: {
31 | 'Content-Type': 'application/json',
32 | },
33 | signal: newAbortSignal.signal,
34 | body: JSON.stringify({
35 | ...createChatCompletionRequest,
36 | ...(
37 | openaiNonStreamableModels.includes(createChatCompletionRequest.model) ? {
38 | stream: undefined,
39 | max_tokens: undefined,
40 | } : {
41 | stream: true,
42 | max_tokens: maxTokens
43 | }
44 | )
45 | }),
46 | };
47 |
48 | // const url = getBaseUrl(createChatCompletionRequest.model) + '/chat/completions';
49 | const response = await fetch('http://localhost:8000/priompt/chat/completions', requestOptions);
50 | if (!response.ok) {
51 | throw new Error(`HTTP error! status: ${response.status}. message: ${await response.text()}`);
52 | }
53 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
54 | streamer = streamSource(response.body!);
55 | for await (const data of streamer) {
56 | clearTimeout(timeout);
57 |
58 | timeout = setTimeout(() => {
59 | console.error("OpenAI request timed out after 10 seconds..... Not good.")
60 | newAbortSignal.abort();
61 | }, 10_000);
62 |
63 | yield data;
64 |
65 | clearTimeout(timeout);
66 | }
67 | } finally {
68 | clearTimeout(timeout);
69 | if (streamer !== undefined) {
70 | await streamer.return(undefined);
71 | }
72 | newAbortSignal.abort();
73 | }
74 | }
75 |
76 | const getOutputTokenLimit = (model: string) => {
77 | if (model.includes('22b')) {
78 | return 32_768;
79 | } else if (model.includes("claude-3-5-sonnet-20240620")) {
80 | return 8_192;
81 | } else {
82 | return 4_096;
83 | }
84 | }
85 |
86 | const getTokenLimit = (model: string) => {
87 | const maybeTokenLimit = TOKEN_LIMIT[model];
88 | if (maybeTokenLimit !== undefined) {
89 | return maybeTokenLimit;
90 | }
91 |
92 | if (model.includes('22b')) {
93 | return 32_768;
94 | }
95 |
96 | return 1_000_000;
97 | }
98 |
99 | const TOKEN_LIMIT: Record = {
100 | "gpt-3.5-turbo": 4096,
101 | "azure-3.5-turbo": 4096,
102 | "gpt-4": 8192,
103 | "gpt-4-cursor-completions": 128_000,
104 | "gpt-4-cursor-vinod": 128_000,
105 | "gpt-4-0314": 8192,
106 | "gpt-4-32k": 32000,
107 | "gpt-4-1106-preview": 128000,
108 | "gpt-4-0125-preview": 128000,
109 | "gpt-3.5-turbo-1106": 16000,
110 | "text-davinci-003": 4096,
111 | "code-davinci-002": 4096,
112 | };
113 | const enc = encodingForModel("gpt-4");
114 | const enc_old = encodingForModel("text-davinci-003");
115 |
116 | const openaiNonStreamableModels = ["o1-mini-2024-09-12", "o1-preview-2024-09-12"];
117 |
118 | // TODO (Aman): Make this work for non-oai models (or error if it doesn't work for them)!
119 | export async function* streamChatCompletionLocalhost(createChatCompletionRequest: CreateChatCompletionRequest, options?: RequestInit, abortSignal?: AbortSignal): AsyncGenerator {
120 | // If this is an anthropic or fireworks model, we can streamchat having partially filled in the last assistant message
121 | if (createChatCompletionRequest.model.includes('claude-3') || createChatCompletionRequest.model.includes('deepseek')) {
122 | // The last message must be an assistant message
123 | if (createChatCompletionRequest.messages.length === 0 || createChatCompletionRequest.messages[createChatCompletionRequest.messages.length - 1].role !== 'assistant') {
124 | throw new Error('Last message must not be an assistant message');
125 | }
126 | yield* streamChatLocalhost(createChatCompletionRequest, options, abortSignal);
127 | return;
128 | }
129 | const prompt = joinMessages(createChatCompletionRequest.messages, true);
130 | let tokens = enc.encode(prompt).length;
131 | if (createChatCompletionRequest.model.includes('00')) {
132 | tokens = enc_old.encode(prompt).length;
133 | }
134 | const createCompletionRequest = {
135 | max_tokens: Math.min((getTokenLimit(createChatCompletionRequest.model)) - tokens, getOutputTokenLimit(createChatCompletionRequest.model)), // hacky but most models only support 4k output tokens
136 | ...createChatCompletionRequest,
137 | messages: undefined,
138 | prompt,
139 | stop: ['<|im_end|>', '<|diff_marker|>']
140 | } as CreateCompletionRequest;
141 |
142 |
143 | let streamer: AsyncGenerator | undefined = undefined;
144 |
145 | const newAbortSignal = new AbortController();
146 | abortSignal?.addEventListener('abort', () => {
147 | newAbortSignal.abort();
148 | });
149 |
150 | let timeout = setTimeout(() => {
151 | console.error("OpenAI request timed out after 200 seconds..... Not good.")
152 | newAbortSignal.abort();
153 | }, 200_000);
154 |
155 | try {
156 | const requestOptions: RequestInit = {
157 | ...options,
158 | method: 'POST',
159 | headers: {
160 | 'Content-Type': 'application/json',
161 | },
162 | signal: newAbortSignal.signal,
163 | body: JSON.stringify({
164 | ...createCompletionRequest,
165 | stream: true
166 | }),
167 | };
168 |
169 | const response = await fetch('http://localhost:8000/priompt/completions', requestOptions);
170 | if (!response.ok) {
171 | throw new Error(`HTTP error! status: ${response.status}. message: ${await response.text()}`);
172 | }
173 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
174 | streamer = streamSource(response.body!);
175 | for await (const data of streamer) {
176 | clearTimeout(timeout);
177 |
178 | timeout = setTimeout(() => {
179 | console.error("OpenAI request timed out after 10 seconds..... Not good.")
180 | newAbortSignal.abort();
181 | }, 10_000);
182 |
183 | yield {
184 | ...data,
185 | choices: data.choices.map((choice) => {
186 | return {
187 | delta: {
188 | role: 'assistant',
189 | content: choice.text?.replace(prompt, ''),
190 | }
191 | }
192 | })
193 | }
194 |
195 | clearTimeout(timeout);
196 | }
197 | } finally {
198 | clearTimeout(timeout);
199 | if (streamer !== undefined) {
200 | await streamer.return(undefined);
201 | }
202 | newAbortSignal.abort();
203 | }
204 | }
205 |
206 |
207 | async function* streamSource(stream: ReadableStream): AsyncGenerator {
208 | // Buffer exists for overflow when event stream doesn't end on a newline
209 | let buffer = '';
210 |
211 | // Create a reader to read the response body as a stream
212 | const reader = stream.getReader();
213 |
214 | // Loop until the stream is done
215 | while (true) {
216 | const { done, value } = await reader.read();
217 | if (done) {
218 | break;
219 | }
220 |
221 | buffer += new TextDecoder().decode(value);
222 | const lines = buffer.split('\n');
223 | for (const line of lines.slice(0, -1)) {
224 | if (line.startsWith('data: ')) {
225 | const jsonString = line.slice(6);
226 | if (jsonString === '[DONE]') {
227 | return;
228 | }
229 | try {
230 | const ans = JSON.parse(jsonString) as T;
231 | yield ans;
232 | } catch (e) {
233 | console.log(jsonString);
234 | throw e;
235 | }
236 | }
237 | }
238 | buffer = lines[lines.length - 1];
239 | }
240 |
241 | if (buffer.startsWith('data: ')) {
242 | const jsonString = buffer.slice(6);
243 | if (jsonString === '[DONE]') {
244 | return;
245 | }
246 | try {
247 | const ans = JSON.parse(jsonString) as T;
248 | yield ans;
249 | } catch (e) {
250 | console.log(jsonString);
251 | throw e;
252 | }
253 | }
254 | }
255 |
256 | export function joinMessages(messages: ChatCompletionRequestMessage[], lastIsIncomplete: boolean = false, isLlama: boolean = false) {
257 | if (!isLlama) {
258 | return messages.map((message, index) => {
259 | let ret = `<|im_start|>${message.role}<|im_sep|>${message.content}`;
260 | if (!lastIsIncomplete || index !== messages.length - 1) {
261 | ret += `<|im_end|>`;
262 | }
263 | return ret;
264 | }).join('');
265 | } else {
266 | let ret = '<|begin_of_text|>'
267 | for (const [index, message] of messages.entries()) {
268 | ret += `<|start_header_id|>${message.role}<|end_header_id|>${message.content}`;
269 | if (!lastIsIncomplete || index !== messages.length - 1) {
270 | ret += `<|eot_id|>`;
271 | }
272 | }
273 | return ret;
274 | }
275 | }
276 |
--------------------------------------------------------------------------------
/priompt-preview/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/priompt-preview/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | corePlugins: {
4 | preflight: false,
5 | },
6 | darkMode: ["class"],
7 | content: [
8 | './pages/**/*.{ts,tsx}',
9 | './components/**/*.{ts,tsx}',
10 | './app/**/*.{ts,tsx}',
11 | './src/**/*.{ts,tsx}',
12 | ],
13 | theme: {
14 | container: {
15 | center: true,
16 | padding: "2rem",
17 | screens: {
18 | "2xl": "1400px",
19 | },
20 | },
21 | extend: {
22 | keyframes: {
23 | "accordion-down": {
24 | from: { height: 0 },
25 | to: { height: "var(--radix-accordion-content-height)" },
26 | },
27 | "accordion-up": {
28 | from: { height: "var(--radix-accordion-content-height)" },
29 | to: { height: 0 },
30 | },
31 | },
32 | animation: {
33 | "accordion-down": "accordion-down 0.2s ease-out",
34 | "accordion-up": "accordion-up 0.2s ease-out",
35 | },
36 | },
37 | },
38 | plugins: [require("tailwindcss-animate")],
39 | }
--------------------------------------------------------------------------------
/priompt-preview/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
5 | "module": "ESNext",
6 | "skipLibCheck": true,
7 |
8 | /* Bundler mode */
9 | "moduleResolution": "node",
10 | "resolveJsonModule": true,
11 | "isolatedModules": true,
12 | "noEmit": true,
13 | "jsx": "react-jsx",
14 | "incremental": true,
15 |
16 | /* Linting */
17 | "strict": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "noFallthroughCasesInSwitch": true,
21 | "baseUrl": ".",
22 | "paths": {
23 | "@/*": ["./src/*"]
24 | }
25 | },
26 | "include": ["src"],
27 | "references": [{ "path": "./tsconfig.node.json" }]
28 | }
29 |
--------------------------------------------------------------------------------
/priompt-preview/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "node",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/priompt-preview/vite.config.d.ts:
--------------------------------------------------------------------------------
1 | declare const _default: import("vite").UserConfig;
2 | export default _default;
3 |
--------------------------------------------------------------------------------
/priompt-preview/vite.config.js:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import react from "@vitejs/plugin-react";
3 | import { defineConfig } from "vite";
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | resolve: {
8 | alias: {
9 | "@": path.resolve(__dirname, "./src"),
10 | },
11 | },
12 | });
13 |
--------------------------------------------------------------------------------
/priompt-preview/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from "path"
2 | import react from "@vitejs/plugin-react"
3 | import { defineConfig } from "vite"
4 |
5 | // https://vitejs.dev/config/
6 | export default defineConfig({
7 | plugins: [react()],
8 | resolve: {
9 | alias: {
10 | "@": path.resolve(__dirname, "./src"),
11 | },
12 | },
13 | })
14 |
--------------------------------------------------------------------------------
/priompt/.eslintignore:
--------------------------------------------------------------------------------
1 | dist
2 | esbuild.ts
3 | vitest.config.ts
--------------------------------------------------------------------------------
/priompt/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["../.eslintrc.base"],
3 | "rules": {
4 | // additional rules specific to this config
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/priompt/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
--------------------------------------------------------------------------------
/priompt/.npmignore:
--------------------------------------------------------------------------------
1 | # dist is important to include!
2 | !dist/**
3 | .wireit
--------------------------------------------------------------------------------
/priompt/README.md:
--------------------------------------------------------------------------------
1 | Run `pnpm build` or `pnpm build-watch` to build the library.
2 |
--------------------------------------------------------------------------------
/priompt/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@anysphere/priompt",
3 | "license": "MIT",
4 | "version": "0.2.1",
5 | "description": "A JSX-based prompt design library.",
6 | "keywords": [
7 | "prompting",
8 | "prompt design",
9 | "prompt engineering"
10 | ],
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/anysphere/priompt"
14 | },
15 | "homepage": "https://github.com/anysphere/priompt",
16 | "author": "Arvid Lunnemark",
17 | "engines": {
18 | "node": ">= 18.15.0"
19 | },
20 | "main": "./dist/index.js",
21 | "types": "./dist/index.d.ts",
22 | "files": [
23 | "dist/**/*",
24 | "package.json"
25 | ],
26 | "scripts": {
27 | "watch": "tsx watch --clear-screen=false src",
28 | "build": "wireit",
29 | "build-watch": "nodemon --watch 'src/**/*' --ext '*' --exec 'pnpm build'",
30 | "lint": "tsc --noEmit && eslint .",
31 | "test": "wireit",
32 | "test:nowatch": "vitest run",
33 | "coverage": "vitest run --coverage"
34 | },
35 | "wireit": {
36 | "build": {
37 | "command": "tsc --build --pretty && cp src/types.d.ts dist/types.d.ts",
38 | "files": [
39 | "src/**/*",
40 | "tsconfig.json"
41 | ],
42 | "output": [
43 | "dist/**/*"
44 | ],
45 | "dependencies": [
46 | "../tiktoken-node:build"
47 | ],
48 | "clean": "if-file-deleted"
49 | },
50 | "test": {
51 | "command": "vitest",
52 | "dependencies": [
53 | "build"
54 | ]
55 | }
56 | },
57 | "devDependencies": {
58 | "@types/js-yaml": "^4.0.5",
59 | "@types/json-schema": "^7.0.12",
60 | "@types/node": "^20.12.0",
61 | "@typescript-eslint/eslint-plugin": "^5.59.0",
62 | "@typescript-eslint/parser": "^5.59.0",
63 | "@vitest/coverage-v8": "^1.2.2",
64 | "esbuild": "^0.18.20",
65 | "eslint": "^8.38.0",
66 | "nodemon": "^2.0.22",
67 | "npm-run-all": "^4.1.5",
68 | "rimraf": "^5.0.0",
69 | "tiny-glob": "^0.2.9",
70 | "tsx": "^3.12.6",
71 | "typescript": "^5.2.0",
72 | "vite": "^5.0.12",
73 | "vitest": "^1.2.2",
74 | "wireit": "^0.14.0"
75 | },
76 | "dependencies": {
77 | "@anysphere/tiktoken-node": "workspace:*",
78 | "hot-shots": "^10.0.0",
79 | "js-yaml": "https://github.com/anysphere/js-yaml.git#4761daebc257cf86e64bb775ba00696f30d7ff22",
80 | "openai": "^3.3.0",
81 | "zod": "^3.21.4",
82 | "zod-to-json-schema": "^3.21.3"
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/priompt/src/base.test.tsx:
--------------------------------------------------------------------------------
1 | import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2 | import * as Priompt from "./index";
3 | import {
4 | isChatPrompt,
5 | isPlainPrompt,
6 | promptHasFunctions,
7 | promptToTokens,
8 | render,
9 | } from "./lib";
10 | import { PromptElement, PromptProps } from "./types";
11 | import { AssistantMessage, SystemMessage, UserMessage } from "./components";
12 | import { getTokenizerByName_ONLY_FOR_OPENAI_TOKENIZERS } from "./tokenizer";
13 |
14 | describe("isolate", () => {
15 | function Isolate(
16 | props: PromptProps<{ isolate: boolean; tokenLimit: number }>
17 | ): PromptElement {
18 | if (props.isolate) {
19 | return (
20 | <>
21 |
22 | {props.children}
23 |
24 | >
25 | );
26 | } else {
27 | return (
28 | <>
29 |
30 | {props.children}
31 |
32 | >
33 | );
34 | }
35 | }
36 |
37 | function Test(props: PromptProps<{ isolate: boolean }>): PromptElement {
38 | return (
39 | <>
40 | This is the start of the prompt.
41 |
42 | {Array.from({ length: 1000 }, (_, i) => (
43 | <>
44 |
45 | This is an SHOULDBEINCLUDEDONLYIFISOLATED user message number{" "}
46 | {i}
47 |
48 | >
49 | ))}
50 |
51 | {Array.from({ length: 1000 }, (_, i) => (
52 | <>
53 | This is user message number {i}
54 | >
55 | ))}
56 |
57 | {Array.from({ length: 1000 }, (_, i) => (
58 | <>
59 |
60 | {i},xl,x,,
61 | {i > 100 ? "SHOULDBEINCLUDEDONLYIFNOTISOLATED" : ""}
62 |
63 | >
64 | ))}
65 |
66 | >
67 | );
68 | }
69 |
70 | it("should have isolate work", async () => {
71 | const renderedIsolated = await render(Test({ isolate: true }), {
72 | tokenLimit: 1000,
73 | tokenizer: getTokenizerByName_ONLY_FOR_OPENAI_TOKENIZERS("cl100k_base"),
74 | });
75 | expect(renderedIsolated.tokenCount).toBeLessThanOrEqual(1000);
76 | expect(isPlainPrompt(renderedIsolated.prompt)).toBe(true);
77 | if (!isPlainPrompt(renderedIsolated.prompt)) return;
78 | expect(
79 | renderedIsolated.prompt.includes("SHOULDBEINCLUDEDONLYIFISOLATED")
80 | ).toBe(true);
81 | expect(
82 | renderedIsolated.prompt.includes("SHOULDBEINCLUDEDONLYIFNOTISOLATED")
83 | ).toBe(false);
84 |
85 | const renderedUnIsolated = await render(Test({ isolate: false }), {
86 | tokenLimit: 1000,
87 | tokenizer: getTokenizerByName_ONLY_FOR_OPENAI_TOKENIZERS("cl100k_base"),
88 | });
89 | expect(renderedUnIsolated.tokenCount).toBeLessThanOrEqual(1000);
90 | expect(isPlainPrompt(renderedUnIsolated.prompt)).toBe(true);
91 | if (!isPlainPrompt(renderedUnIsolated.prompt)) return;
92 | expect(
93 | renderedUnIsolated.prompt.includes("SHOULDBEINCLUDEDONLYIFISOLATED")
94 | ).toBe(false);
95 | expect(
96 | renderedUnIsolated.prompt.includes("SHOULDBEINCLUDEDONLYIFNOTISOLATED")
97 | ).toBe(true);
98 | });
99 |
100 | function SimplePrompt(
101 | props: PromptProps<{ breaktoken: boolean }>
102 | ): PromptElement {
103 | return (
104 | <>
105 | This is the start of the p{props.breaktoken ? : <>>}
106 | rompt. This is the second part of the prompt.
107 | >
108 | );
109 | }
110 |
111 | it("promptToTokens should work", async () => {
112 | const donotbreak = await render(SimplePrompt({ breaktoken: false }), {
113 | tokenLimit: 1000,
114 | tokenizer: getTokenizerByName_ONLY_FOR_OPENAI_TOKENIZERS("cl100k_base"),
115 | });
116 | const toTokens = await promptToTokens(
117 | donotbreak.prompt,
118 | getTokenizerByName_ONLY_FOR_OPENAI_TOKENIZERS("cl100k_base")
119 | );
120 | expect(donotbreak.tokenCount).toBe(toTokens.length);
121 | expect(toTokens).toStrictEqual([
122 | 2028, 374, 279, 1212, 315, 279, 10137, 13, 1115, 374, 279, 2132, 961, 315,
123 | 279, 10137, 13,
124 | ]);
125 |
126 | const dobreak = await render(SimplePrompt({ breaktoken: true }), {
127 | tokenLimit: 1000,
128 | tokenizer: getTokenizerByName_ONLY_FOR_OPENAI_TOKENIZERS("cl100k_base"),
129 | });
130 | expect(dobreak.tokenCount).toBe(donotbreak.tokenCount + 1);
131 | const toTokens2 = await promptToTokens(
132 | dobreak.prompt,
133 | getTokenizerByName_ONLY_FOR_OPENAI_TOKENIZERS("cl100k_base")
134 | );
135 | expect(dobreak.tokenCount).toBe(toTokens2.length);
136 | expect(toTokens2).toStrictEqual([
137 | 2028, 374, 279, 1212, 315, 279, 281, 15091, 13, 1115, 374, 279, 2132, 961,
138 | 315, 279, 10137, 13,
139 | ]);
140 | });
141 |
142 | function SimpleMessagePrompt(
143 | props: PromptProps<{ breaktoken: boolean }>
144 | ): PromptElement {
145 | return (
146 | <>
147 |
148 | This is the start of the prompt.
149 |
150 | {props.breaktoken ? : <>>}
151 |
152 | This is the second part of the prompt.
153 |
154 | hi!
155 | >
156 | );
157 | }
158 |
159 | it("promptToTokens should work", async () => {
160 | const donotbreak = await render(
161 | SimpleMessagePrompt({ breaktoken: false }),
162 | {
163 | tokenLimit: 1000,
164 | tokenizer: getTokenizerByName_ONLY_FOR_OPENAI_TOKENIZERS("cl100k_base"),
165 | lastMessageIsIncomplete: true,
166 | }
167 | );
168 | const toTokens = await promptToTokens(
169 | donotbreak.prompt,
170 | getTokenizerByName_ONLY_FOR_OPENAI_TOKENIZERS("cl100k_base")
171 | );
172 | expect(donotbreak.tokenCount).toBe(toTokens.length);
173 | expect(toTokens).toStrictEqual([
174 | 100264, 9125, 100266, 2028, 374, 279, 1212, 315, 279, 10137, 382, 2028,
175 | 374, 279, 2132, 961, 315, 279, 10137, 13, 100265, 100264, 882, 100266,
176 | 6151, 0,
177 | ]);
178 |
179 | const dobreak = await render(SimpleMessagePrompt({ breaktoken: true }), {
180 | tokenLimit: 1000,
181 | tokenizer: getTokenizerByName_ONLY_FOR_OPENAI_TOKENIZERS("cl100k_base"),
182 | lastMessageIsIncomplete: true,
183 | });
184 | expect(dobreak.tokenCount).toBe(donotbreak.tokenCount + 1);
185 | const toTokens2 = await promptToTokens(
186 | dobreak.prompt,
187 | getTokenizerByName_ONLY_FOR_OPENAI_TOKENIZERS("cl100k_base")
188 | );
189 | expect(dobreak.tokenCount).toBe(toTokens2.length);
190 | expect(toTokens2).toStrictEqual([
191 | 100264, 9125, 100266, 2028, 374, 279, 1212, 315, 279, 10137, 627, 198,
192 | 2028, 374, 279, 2132, 961, 315, 279, 10137, 13, 100265, 100264, 882,
193 | 100266, 6151, 0,
194 | ]);
195 | });
196 |
197 | function SpecialTokensPrompt(): PromptElement {
198 | return (
199 | <>
200 | {"<|im_start|>"}
201 | {"<|diff_marker|>"}
202 | {"<|endoftext|>"}
203 | >
204 | );
205 | }
206 |
207 | it("handle special tokens", async () => {
208 | const specialTokens = await render(SpecialTokensPrompt(), {
209 | tokenLimit: 1000,
210 | tokenizer: getTokenizerByName_ONLY_FOR_OPENAI_TOKENIZERS("cl100k_base"),
211 | lastMessageIsIncomplete: true,
212 | });
213 |
214 | expect(specialTokens.tokenCount).toBeGreaterThanOrEqual(24);
215 | const toTokens = await promptToTokens(
216 | specialTokens.prompt,
217 | getTokenizerByName_ONLY_FOR_OPENAI_TOKENIZERS("cl100k_base")
218 | );
219 | expect(specialTokens.tokenCount).toBe(toTokens.length);
220 | expect(toTokens).toStrictEqual([
221 | 100264, 9125, 100266, 27, 91, 318, 5011, 91, 29, 100265, 100264, 882,
222 | 100266, 27, 91, 13798, 27363, 91, 29, 100265, 100264, 78191, 100266, 27,
223 | 91, 8862, 728, 428, 91, 29,
224 | ]);
225 | });
226 | it("handle all special tokens encoded", async () => {
227 | const specialTokens = await render(SpecialTokensPrompt(), {
228 | tokenLimit: 1000,
229 | tokenizer: getTokenizerByName_ONLY_FOR_OPENAI_TOKENIZERS(
230 | "cl100k_base_special_tokens"
231 | ),
232 | lastMessageIsIncomplete: true,
233 | });
234 |
235 | expect(specialTokens.tokenCount).toBeGreaterThanOrEqual(24);
236 | const toTokens = await promptToTokens(
237 | specialTokens.prompt,
238 | getTokenizerByName_ONLY_FOR_OPENAI_TOKENIZERS(
239 | "cl100k_base_special_tokens"
240 | )
241 | );
242 | expect(specialTokens.tokenCount).toBe(toTokens.length);
243 | expect(toTokens).toStrictEqual([
244 | 100264, 9125, 100266, 100264, 100265, 100264, 882, 100266, 27, 91, 13798,
245 | 27363, 91, 29, 100265, 100264, 78191, 100266, 27, 91, 8862, 728, 428, 91,
246 | 29,
247 | ]);
248 | });
249 | });
250 |
251 | describe("config", () => {
252 | function TestConfig(
253 | props: PromptProps<{ numConfigs: number }>
254 | ): PromptElement {
255 | return (
256 | <>
257 | This is the start of the prompt.
258 |
259 |
260 | >
261 | );
262 | }
263 |
264 | it("should have config work", async () => {
265 | const rendered = await render(TestConfig({ numConfigs: 1 }), {
266 | tokenLimit: 1000,
267 | tokenizer: getTokenizerByName_ONLY_FOR_OPENAI_TOKENIZERS("cl100k_base"),
268 | });
269 | expect(rendered.tokenCount).toBeLessThanOrEqual(1000);
270 | expect(isPlainPrompt(rendered.prompt)).toBe(true);
271 | expect(rendered.config.stop).toBe("\n");
272 | expect(rendered.config.maxResponseTokens).toBe("tokensReserved");
273 | });
274 | });
275 |
--------------------------------------------------------------------------------
/priompt/src/components.tsx:
--------------------------------------------------------------------------------
1 | import * as Priompt from "./lib";
2 | import {
3 | BasePromptProps,
4 | Capture,
5 | ChatAssistantFunctionToolCall,
6 | ImageProps,
7 | OutputHandler,
8 | PromptElement,
9 | PromptProps,
10 | } from "./types";
11 | import { JSONSchema7 } from "json-schema";
12 | import { ChatCompletionResponseMessage } from "openai";
13 | import { z } from "zod";
14 | import zodToJsonSchemaImpl from "zod-to-json-schema";
15 | import { StreamChatCompletionResponse } from "./openai";
16 |
17 | export function SystemMessage(
18 | props: PromptProps<{
19 | name?: string;
20 | to?: string;
21 | }>
22 | ): PromptElement {
23 | return {
24 | type: "chat",
25 | role: "system",
26 | name: props.name,
27 | to: props.to,
28 | children:
29 | props.children !== undefined
30 | ? Array.isArray(props.children)
31 | ? props.children.flat()
32 | : [props.children]
33 | : [],
34 | };
35 | }
36 |
37 | export function UserMessage(
38 | props: PromptProps<{
39 | name?: string;
40 | to?: string;
41 | }>
42 | ): PromptElement {
43 | return {
44 | type: "chat",
45 | role: "user",
46 | name: props.name,
47 | to: props.to,
48 | children:
49 | props.children !== undefined
50 | ? Array.isArray(props.children)
51 | ? props.children.flat()
52 | : [props.children]
53 | : [],
54 | };
55 | }
56 |
57 | export function AssistantMessage(
58 | props: PromptProps<{
59 | functionCall?: {
60 | name: string;
61 | arguments: string; // json string
62 | };
63 | toolCalls?: {
64 | id: string;
65 | index: number;
66 | tool: ChatAssistantFunctionToolCall;
67 | }[];
68 | to?: string;
69 | }>
70 | ): PromptElement {
71 | return {
72 | type: "chat",
73 | role: "assistant",
74 | functionCall: props.functionCall,
75 | toolCalls: props.toolCalls,
76 | to: props.to,
77 | children:
78 | props.children !== undefined
79 | ? Array.isArray(props.children)
80 | ? props.children.flat()
81 | : [props.children]
82 | : [],
83 | };
84 | }
85 |
86 | export function ImageComponent(props: PromptProps): PromptElement {
87 | return {
88 | type: "image",
89 | bytes: props.bytes,
90 | dimensions: props.dimensions,
91 | detail: props.detail,
92 | };
93 | }
94 |
95 | export function FunctionMessage(
96 | props: PromptProps<{
97 | name: string;
98 | to?: string;
99 | }>
100 | ): PromptElement {
101 | return {
102 | type: "chat",
103 | role: "function",
104 | name: props.name,
105 | to: props.to,
106 | children:
107 | props.children !== undefined
108 | ? Array.isArray(props.children)
109 | ? props.children.flat()
110 | : [props.children]
111 | : [],
112 | };
113 | }
114 |
115 | export function ToolResultMessage(
116 | props: PromptProps<{
117 | name: string;
118 | to?: string;
119 | }>
120 | ): PromptElement {
121 | return {
122 | type: "chat",
123 | role: "tool",
124 | name: props.name,
125 | to: props.to,
126 | children:
127 | props.children !== undefined
128 | ? Array.isArray(props.children)
129 | ? props.children.flat()
130 | : [props.children]
131 | : [],
132 | };
133 | }
134 |
135 | const populateOnStreamResponseObjectFromOnStream = (
136 | captureProps: Capture & {
137 | onStream: (
138 | stream: AsyncIterable
139 | ) => Promise;
140 | }
141 | ) => ({
142 | ...captureProps,
143 | onStreamResponseObject: async (
144 | stream: AsyncIterable
145 | ) => {
146 | const messageStream = (async function* () {
147 | for await (const s of stream) {
148 | if (s.choices.length === 0) continue;
149 | if (s.choices[0].delta === undefined) continue;
150 | const message = s.choices[0].delta;
151 | yield message;
152 | }
153 | })();
154 | await captureProps.onStream(messageStream);
155 | },
156 | });
157 |
158 | // design choice: can only have 1 tools component per prompt, and cannot have any other onStream
159 | export function Tools(
160 | props: PromptProps<{
161 | tools: {
162 | name: string;
163 | description: string;
164 | parameters: JSONSchema7;
165 | onCall?: (
166 | args: string,
167 | toolCallId: string,
168 | toolName: string,
169 | toolIndex: number
170 | ) => Promise;
171 | onFormatAndYield?: (
172 | args: string,
173 | toolCallId: string,
174 | toolName: string,
175 | toolIndex: number
176 | ) => Promise;
177 | }[];
178 | onReturn: OutputHandler>;
179 | }>
180 | ): PromptElement {
181 | return (
182 | <>
183 | {props.tools.map((tool) => (
184 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
185 | // @ts-ignore
186 |
191 | ))}
192 | {populateOnStreamResponseObjectFromOnStream({
193 | type: "capture",
194 | onStream: async (
195 | stream: AsyncIterable
196 | ) => {
197 | await props.onReturn(
198 | (async function* () {
199 | // index -> {name, args}
200 | const toolCallsMap = new Map<
201 | number,
202 | { name: string; args: string; toolCallId: string }
203 | >();
204 | for await (const message of stream) {
205 | if (message.content !== undefined) {
206 | yield message.content;
207 | }
208 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
209 | // @ts-ignore
210 | const toolCalls = message.tool_calls as any[];
211 | if (!Array.isArray(toolCalls)) {
212 | continue;
213 | }
214 | for (const toolCall of toolCalls) {
215 | const { index, id } = toolCall;
216 | let toolInfo = toolCallsMap.get(index);
217 | if (toolInfo === undefined) {
218 | toolInfo = { name: "", args: "", toolCallId: id };
219 | }
220 | if (toolCall.function.name !== undefined) {
221 | toolInfo.name = toolCall.function.name;
222 | }
223 | if (toolCall.function.arguments !== undefined) {
224 | toolInfo.args += toolCall.function.arguments;
225 |
226 | const tool = props.tools.find(
227 | (tool) => tool.name === toolInfo?.name
228 | );
229 | if (
230 | tool !== undefined &&
231 | tool.onFormatAndYield !== undefined
232 | ) {
233 | // try parsing as JSON, if successful, yield the parsed JSON
234 | try {
235 | const parsedArgs = JSON.parse(toolInfo.args);
236 | yield tool.onFormatAndYield(
237 | toolInfo.args,
238 | toolInfo.toolCallId,
239 | toolInfo.name,
240 | index
241 | );
242 | } catch (error) {
243 | // do nothing
244 | }
245 | }
246 | }
247 | toolCallsMap.set(index, toolInfo);
248 | }
249 | }
250 | for (const [toolIndex, toolInfo] of toolCallsMap.entries()) {
251 | if (
252 | toolInfo.name !== undefined &&
253 | toolInfo.args !== undefined
254 | ) {
255 | const tool = props.tools.find(
256 | (tool) => tool.name === toolInfo.name
257 | );
258 | if (tool !== undefined && tool.onCall !== undefined) {
259 | await tool.onCall(
260 | toolInfo.args,
261 | toolInfo.toolCallId,
262 | toolInfo.name,
263 | toolIndex
264 | );
265 | }
266 | }
267 | }
268 | })()
269 | );
270 | },
271 | })}
272 | >
273 | );
274 | }
275 |
276 | export function ZTools(
277 | props: PromptProps<{
278 | tools: {
279 | name: string;
280 | description: string;
281 | parameters: z.ZodType;
282 | onCall?: (
283 | args: ParamT,
284 | toolCallId: string,
285 | toolName: string,
286 | toolIndex: number
287 | ) => Promise;
288 | onParseError?: (error: z.ZodError, rawArgs: string) => Promise;
289 | onFormatAndYield?: (
290 | args: ParamT,
291 | toolCallId: string,
292 | toolName: string,
293 | toolIndex: number
294 | ) => Promise;
295 | }[];
296 | onReturn: OutputHandler>;
297 | useAnthropic?: boolean;
298 | }>
299 | ): PromptElement {
300 | return (
301 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
302 | // @ts-ignore
303 | ({
305 | name: tool.name,
306 | description: tool.description,
307 | parameters: zodToJsonSchema(tool.parameters),
308 | onCall: async (
309 | args: string,
310 | toolCallId: string,
311 | toolName: string,
312 | toolIndex: number
313 | ) => {
314 | try {
315 | const parsedArgs = tool.parameters.parse(JSON.parse(args));
316 | await tool.onCall?.(parsedArgs, toolCallId, toolName, toolIndex);
317 | } catch (error) {
318 | console.error(
319 | `Error parsing arguments for tool ${tool.name}:`,
320 | error
321 | );
322 | if (tool.onParseError !== undefined) {
323 | await tool.onParseError(error, args);
324 | } else {
325 | throw error;
326 | }
327 | }
328 | },
329 | onFormatAndYield: tool.onFormatAndYield
330 | ? async (
331 | args: string,
332 | toolCallId: string,
333 | toolName: string,
334 | toolIndex: number
335 | ) => {
336 | try {
337 | const parsedArgs = tool.parameters.parse(JSON.parse(args));
338 | return (
339 | tool.onFormatAndYield?.(
340 | parsedArgs,
341 | toolCallId,
342 | toolName,
343 | toolIndex
344 | ) ?? args
345 | );
346 | } catch (error) {
347 | console.error(
348 | `Error formatting arguments for tool ${tool.name}:`,
349 | error
350 | );
351 | return args;
352 | }
353 | }
354 | : undefined,
355 | }))}
356 | onReturn={props.onReturn}
357 | />
358 | );
359 | }
360 |
361 | function Tool(
362 | props: PromptProps<{
363 | name: string;
364 | description: string;
365 | parameters: JSONSchema7;
366 | }>
367 | ): PromptElement {
368 | return (
369 | <>
370 | {{
371 | type: "toolDefinition",
372 | tool: {
373 | type: "function",
374 | function: {
375 | name: props.name,
376 | description: props.description,
377 | parameters: props.parameters,
378 | },
379 | },
380 | }}
381 | >
382 | );
383 | }
384 |
385 | export function Function(
386 | props: PromptProps<{
387 | name: string;
388 | description: string;
389 | parameters: JSONSchema7;
390 | onCall?: (args: string) => Promise;
391 | }>
392 | ): PromptElement {
393 | if (!validFunctionName(props.name)) {
394 | throw new Error(
395 | `Invalid function name: ${props.name}. Function names must be between 1 and 64 characters long and may only contain a-z, A-Z, 0-9, and underscores.`
396 | );
397 | }
398 |
399 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
400 | // @ts-ignore
401 | return (
402 | <>
403 | {{
404 | type: "functionDefinition",
405 | name: props.name,
406 | description: props.description,
407 | parameters: props.parameters,
408 | }}
409 | {{
410 | type: "capture",
411 | onOutput: async (output: ChatCompletionResponseMessage) => {
412 | if (
413 | props.onCall !== undefined &&
414 | output.function_call !== undefined &&
415 | output.function_call.name === props.name &&
416 | output.function_call.arguments !== undefined
417 | ) {
418 | await props.onCall(output.function_call.arguments);
419 | }
420 | },
421 | }}
422 | >
423 | );
424 | }
425 |
426 | // May contain a-z, A-Z, 0-9, and underscores, with a maximum length of 64 characters.
427 | function validFunctionName(name: string): boolean {
428 | return /^[a-zA-Z0-9_]{1,64}$/.test(name);
429 | }
430 |
431 | export function ZFunction(
432 | props: PromptProps<{
433 | name: string;
434 | description: string;
435 | parameters: z.ZodType;
436 | // if the args fail to parse, we throw here
437 | onCall?: (args: ParamT) => Promise;
438 | // if onParseError is provided, then we don't throw
439 | // this can be useful in case a failed parse can still be useful for us
440 | // in cases when we really want the output, we can also call a model here to parse the output
441 | onParseError?: (error: z.ZodError, rawArgs: string) => Promise;
442 | // TODO: add an autoheal here
443 | }>
444 | ) {
445 | return (
446 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
447 | // @ts-ignore
448 | {
453 | if (props.onCall === undefined) {
454 | // do nothing
455 | return;
456 | }
457 | try {
458 | const args = props.parameters.parse(JSON.parse(rawArgs));
459 | await props.onCall(args);
460 | } catch (error) {
461 | if (props.onParseError !== undefined) {
462 | await props.onParseError(error, rawArgs);
463 | } else {
464 | throw error;
465 | }
466 | }
467 | }}
468 | />
469 | );
470 | }
471 |
472 | function zodToJsonSchema(schema: z.ZodType): JSONSchema7 {
473 | const fullSchema = zodToJsonSchemaImpl(schema, { $refStrategy: "none" });
474 | const {
475 | $schema,
476 | default: defaultVal,
477 | definitions,
478 | description,
479 | markdownDescription,
480 | ...rest
481 | } = fullSchema;
482 | // delete additionalProperties
483 | if ("additionalProperties" in rest) {
484 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
485 | // @ts-ignore
486 | delete rest.additionalProperties;
487 | }
488 | return rest as JSONSchema7;
489 | }
490 |
--------------------------------------------------------------------------------
/priompt/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './lib';
2 |
3 | export * from './components';
4 |
5 | export { PreviewManager, dumpProps, register } from './preview';
6 | export type { PreviewManagerGetPromptQuery, PreviewManagerLiveModeQuery, PreviewManagerLiveModeResultQuery } from './preview';
7 |
8 | export type { RenderOptions, RenderunOptions, RenderOutput, JSX, RenderedPrompt, Prompt, PromptElement, BaseProps, PromptProps, ChatAndFunctionPromptFunction, ChatPrompt, PreviewConfig, SynchronousPreviewConfig, SourceMap } from './types';
9 |
--------------------------------------------------------------------------------
/priompt/src/openai.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ChatCompletionFunctions,
3 | ChatCompletionRequestMessageFunctionCall,
4 | ChatCompletionRequestMessageRoleEnum,
5 | ChatCompletionResponseMessageRoleEnum,
6 | CreateChatCompletionRequestFunctionCall,
7 | CreateChatCompletionRequestStop,
8 | CreateChatCompletionResponse,
9 | CreateChatCompletionResponseChoicesInner,
10 | CreateCompletionRequestPrompt,
11 | CreateCompletionRequestStop,
12 | } from 'openai';
13 |
14 | export {
15 | CreateChatCompletionResponse,
16 | ChatCompletionFunctions,
17 | // Setup
18 | OpenAIApi,
19 | Configuration,
20 | // Embeddings
21 | CreateEmbeddingRequest,
22 | CreateEmbeddingResponse,
23 | CreateEmbeddingResponseDataInner,
24 | // Completions
25 | CreateCompletionResponse,
26 | CreateCompletionRequestPrompt,
27 | ChatCompletionRequestMessageRoleEnum,
28 | CreateCompletionResponseUsage,
29 | // Function
30 | CreateChatCompletionRequestFunctionCall,
31 | ChatCompletionRequestMessageFunctionCall,
32 | // Misc
33 | CreateCompletionResponseChoicesInnerLogprobs
34 | } from 'openai';
35 |
36 | import { UsableTokenizer } from './tokenizer';
37 |
38 | // tokenizers
39 | const encoder = new TextEncoder();
40 | export function approximateTokensUsingBytecount(text: string, tokenizer: UsableTokenizer): number {
41 | const byteLength = encoder.encode(text).length;
42 | switch (tokenizer) {
43 | case 'cl100k_base':
44 | case 'o200k_base':
45 | return byteLength / 4;
46 | default:
47 | return byteLength / 3;
48 | }
49 | }
50 |
51 | // docs here: https://platform.openai.com/docs/guides/chat/introduction (out of date!)
52 | // linear factor is <|im_start|>system<|im_sep|> and <|im_end|>
53 | export const CHATML_PROMPT_EXTRA_TOKEN_COUNT_LINEAR_FACTOR = 4;
54 | // this is <|im_start|>assistant<|im_sep|>
55 | export const CHATML_PROMPT_EXTRA_TOKEN_COUNT_CONSTANT = 3;
56 |
57 | export type Content = {
58 | type: 'text';
59 | text: string;
60 | } | {
61 | type: 'image_url';
62 | image_url: {
63 | url: string,
64 | detail?: 'low' | 'high' | 'auto'
65 | // Temporary addition by Aman needed for token calculation
66 | dimensions: {
67 | width: number;
68 | height: number;
69 | }
70 | }
71 | }
72 |
73 | export function hasImages(message: ChatCompletionRequestMessage) {
74 | return typeof message.content !== 'string';
75 | }
76 |
77 | export function hasNoImages(message: ChatCompletionRequestMessage): message is ChatCompletionRequestMessageWithoutImages {
78 | return typeof message.content === 'string';
79 | }
80 |
81 | export interface ChatCompletionRequestMessage {
82 | /**
83 | * The role of the messages author. One of `system`, `user`, `assistant`, or `function`.
84 | * @type {string}
85 | * @memberof ChatCompletionRequestMessage
86 | */
87 | 'role': ChatCompletionRequestMessageRoleEnum | 'tool';
88 | /**
89 | * The contents of the message. `content` is required for all messages except assistant messages with function calls.
90 | * @type {string}
91 | * @memberof ChatCompletionRequestMessage
92 | */
93 | 'content'?: string | Content[];
94 | /**
95 | * The name of the author of this message. `name` is required if role is `function`, and it should be the name of the function whose response is in the `content`. May contain a-z, A-Z, 0-9, and underscores, with a maximum length of 64 characters.
96 | * @type {string}
97 | * @memberof ChatCompletionRequestMessage
98 | */
99 | 'name'?: string;
100 | /**
101 | *
102 | * @type {ChatCompletionRequestMessageFunctionCall}
103 | * @memberof ChatCompletionRequestMessage
104 | */
105 | 'function_call'?: ChatCompletionRequestMessageFunctionCall;
106 | }
107 |
108 | export interface ChatCompletionRequestMessageWithoutImages {
109 | /**
110 | * The role of the messages author. One of `system`, `user`, `assistant`, or `function`.
111 | * @type {string}
112 | * @memberof ChatCompletionRequestMessage
113 | */
114 | 'role': ChatCompletionRequestMessageRoleEnum | 'tool';
115 | /**
116 | * The contents of the message. `content` is required for all messages except assistant messages with function calls.
117 | * @type {string}
118 | * @memberof ChatCompletionRequestMessage
119 | */
120 | 'content'?: string | Content[];
121 | /**
122 | * The name of the author of this message. `name` is required if role is `function`, and it should be the name of the function whose response is in the `content`. May contain a-z, A-Z, 0-9, and underscores, with a maximum length of 64 characters.
123 | * @type {string}
124 | * @memberof ChatCompletionRequestMessage
125 | */
126 | 'name'?: string;
127 | /**
128 | *
129 | * @type {ChatCompletionRequestMessageFunctionCall}
130 | * @memberof ChatCompletionRequestMessage
131 | */
132 | 'function_call'?: ChatCompletionRequestMessageFunctionCall;
133 | }
134 |
135 | export interface CreateChatCompletionRequest {
136 | /**
137 | * ID of the model to use. See the [model endpoint compatibility](/docs/models/model-endpoint-compatibility) table for details on which models work with the Chat API.
138 | * @type {string}
139 | * @memberof CreateChatCompletionRequest
140 | */
141 | 'model': string;
142 | /**
143 | * A list of messages comprising the conversation so far. [Example Python code](https://github.com/openai/openai-cookbook/blob/main/examples/How_to_format_inputs_to_ChatGPT_models.ipynb).
144 | * @type {Array}
145 | * @memberof CreateChatCompletionRequest
146 | */
147 | 'messages': Array;
148 | /**
149 | * A list of functions the model may generate JSON inputs for.
150 | * @type {Array}
151 | * @memberof CreateChatCompletionRequest
152 | */
153 | 'functions'?: Array;
154 | /**
155 | *
156 | * @type {CreateChatCompletionRequestFunctionCall}
157 | * @memberof CreateChatCompletionRequest
158 | */
159 | 'function_call'?: CreateChatCompletionRequestFunctionCall;
160 | /**
161 | * What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. We generally recommend altering this or `top_p` but not both.
162 | * @type {number}
163 | * @memberof CreateChatCompletionRequest
164 | */
165 | 'temperature'?: number | null;
166 | /**
167 | * An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We generally recommend altering this or `temperature` but not both.
168 | * @type {number}
169 | * @memberof CreateChatCompletionRequest
170 | */
171 | 'top_p'?: number | null;
172 | /**
173 | * How many chat completion choices to generate for each input message.
174 | * @type {number}
175 | * @memberof CreateChatCompletionRequest
176 | */
177 | 'n'?: number | null;
178 | /**
179 | * If set, partial message deltas will be sent, like in ChatGPT. Tokens will be sent as data-only [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format) as they become available, with the stream terminated by a `data: [DONE]` message. [Example Python code](https://github.com/openai/openai-cookbook/blob/main/examples/How_to_stream_completions.ipynb).
180 | * @type {boolean}
181 | * @memberof CreateChatCompletionRequest
182 | */
183 | 'stream'?: boolean | null;
184 | /**
185 | *
186 | * @type {CreateChatCompletionRequestStop}
187 | * @memberof CreateChatCompletionRequest
188 | */
189 | 'stop'?: CreateChatCompletionRequestStop;
190 | /**
191 | * The maximum number of [tokens](/tokenizer) to generate in the chat completion. The total length of input tokens and generated tokens is limited by the model\'s context length. [Example Python code](https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb) for counting tokens.
192 | * @type {number}
193 | * @memberof CreateChatCompletionRequest
194 | */
195 | 'max_tokens'?: number;
196 | /**
197 | * Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model\'s likelihood to talk about new topics. [See more information about frequency and presence penalties.](/docs/api-reference/parameter-details)
198 | * @type {number}
199 | * @memberof CreateChatCompletionRequest
200 | */
201 | 'presence_penalty'?: number | null;
202 | /**
203 | * Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model\'s likelihood to repeat the same line verbatim. [See more information about frequency and presence penalties.](/docs/api-reference/parameter-details)
204 | * @type {number}
205 | * @memberof CreateChatCompletionRequest
206 | */
207 | 'frequency_penalty'?: number | null;
208 | /**
209 | * Modify the likelihood of specified tokens appearing in the completion. Accepts a json object that maps tokens (specified by their token ID in the tokenizer) to an associated bias value from -100 to 100. Mathematically, the bias is added to the logits generated by the model prior to sampling. The exact effect will vary per model, but values between -1 and 1 should decrease or increase likelihood of selection; values like -100 or 100 should result in a ban or exclusive selection of the relevant token.
210 | * @type {object}
211 | * @memberof CreateChatCompletionRequest
212 | */
213 | 'logit_bias'?: object | null;
214 | /**
215 | * A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. [Learn more](/docs/guides/safety-best-practices/end-user-ids).
216 | * @type {string}
217 | * @memberof CreateChatCompletionRequest
218 | */
219 | 'user'?: string;
220 | /**
221 | * A speculation string to use for the completion, which is used to perform server side speculative edits. This is only supported by Fireworks.
222 | * @type {string}
223 | * @memberof CreateChatCompletionRequest
224 | */
225 | 'speculation'?: string | number[];
226 | }
227 |
228 | export interface CreateCompletionRequest {
229 | /**
230 | * ID of the model to use. You can use the [List models](/docs/api-reference/models/list) API to see all of your available models, or see our [Model overview](/docs/models/overview) for descriptions of them.
231 | * @type {string}
232 | * @memberof CreateCompletionRequest
233 | */
234 | 'model': string;
235 | /**
236 | *
237 | * @type {CreateCompletionRequestPrompt}
238 | * @memberof CreateCompletionRequest
239 | */
240 | 'prompt'?: CreateCompletionRequestPrompt | null;
241 | /**
242 | * The suffix that comes after a completion of inserted text.
243 | * @type {string}
244 | * @memberof CreateCompletionRequest
245 | */
246 | 'suffix'?: string | null;
247 | /**
248 | * The maximum number of [tokens](/tokenizer) to generate in the completion. The token count of your prompt plus `max_tokens` cannot exceed the model\'s context length. [Example Python code](https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb) for counting tokens.
249 | * @type {number}
250 | * @memberof CreateCompletionRequest
251 | */
252 | 'max_tokens'?: number | null;
253 | /**
254 | * What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. We generally recommend altering this or `top_p` but not both.
255 | * @type {number}
256 | * @memberof CreateCompletionRequest
257 | */
258 | 'temperature'?: number | null;
259 | /**
260 | * An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We generally recommend altering this or `temperature` but not both.
261 | * @type {number}
262 | * @memberof CreateCompletionRequest
263 | */
264 | 'top_p'?: number | null;
265 | /**
266 | * How many completions to generate for each prompt. **Note:** Because this parameter generates many completions, it can quickly consume your token quota. Use carefully and ensure that you have reasonable settings for `max_tokens` and `stop`.
267 | * @type {number}
268 | * @memberof CreateCompletionRequest
269 | */
270 | 'n'?: number | null;
271 | /**
272 | * Whether to stream back partial progress. If set, tokens will be sent as data-only [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format) as they become available, with the stream terminated by a `data: [DONE]` message. [Example Python code](https://github.com/openai/openai-cookbook/blob/main/examples/How_to_stream_completions.ipynb).
273 | * @type {boolean}
274 | * @memberof CreateCompletionRequest
275 | */
276 | 'stream'?: boolean | null;
277 | /**
278 | * Include the log probabilities on the `logprobs` most likely tokens, as well the chosen tokens. For example, if `logprobs` is 5, the API will return a list of the 5 most likely tokens. The API will always return the `logprob` of the sampled token, so there may be up to `logprobs+1` elements in the response. The maximum value for `logprobs` is 5.
279 | * @type {number}
280 | * @memberof CreateCompletionRequest
281 | */
282 | 'logprobs'?: number | null;
283 | /**
284 | * Echo back the prompt in addition to the completion
285 | * @type {boolean}
286 | * @memberof CreateCompletionRequest
287 | */
288 | 'echo'?: boolean | null;
289 | /**
290 | *
291 | * @type {CreateCompletionRequestStop}
292 | * @memberof CreateCompletionRequest
293 | */
294 | 'stop'?: CreateCompletionRequestStop | null;
295 | /**
296 | * Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model\'s likelihood to talk about new topics. [See more information about frequency and presence penalties.](/docs/api-reference/parameter-details)
297 | * @type {number}
298 | * @memberof CreateCompletionRequest
299 | */
300 | 'presence_penalty'?: number | null;
301 | /**
302 | * Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model\'s likelihood to repeat the same line verbatim. [See more information about frequency and presence penalties.](/docs/api-reference/parameter-details)
303 | * @type {number}
304 | * @memberof CreateCompletionRequest
305 | */
306 | 'frequency_penalty'?: number | null;
307 | /**
308 | * Generates `best_of` completions server-side and returns the \"best\" (the one with the highest log probability per token). Results cannot be streamed. When used with `n`, `best_of` controls the number of candidate completions and `n` specifies how many to return – `best_of` must be greater than `n`. **Note:** Because this parameter generates many completions, it can quickly consume your token quota. Use carefully and ensure that you have reasonable settings for `max_tokens` and `stop`.
309 | * @type {number}
310 | * @memberof CreateCompletionRequest
311 | */
312 | 'best_of'?: number | null;
313 | /**
314 | * Modify the likelihood of specified tokens appearing in the completion. Accepts a json object that maps tokens (specified by their token ID in the GPT tokenizer) to an associated bias value from -100 to 100. You can use this [tokenizer tool](/tokenizer?view=bpe) (which works for both GPT-2 and GPT-3) to convert text to token IDs. Mathematically, the bias is added to the logits generated by the model prior to sampling. The exact effect will vary per model, but values between -1 and 1 should decrease or increase likelihood of selection; values like -100 or 100 should result in a ban or exclusive selection of the relevant token. As an example, you can pass `{\"50256\": -100}` to prevent the <|endoftext|> token from being generated.
315 | * @type {object}
316 | * @memberof CreateCompletionRequest
317 | */
318 | 'logit_bias'?: object | null;
319 | /**
320 | * A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. [Learn more](/docs/guides/safety-best-practices/end-user-ids).
321 | * @type {string}
322 | * @memberof CreateCompletionRequest
323 | */
324 | 'user'?: string;
325 | /**
326 | * A speculation string to use for the completion, which is used to perform server side speculative edits. This is only supported by Fireworks.
327 | * @type {string}
328 | * @memberof CreateCompletionRequest
329 | */
330 | speculation?: string | number[];
331 | }
332 |
333 | export interface StreamChatCompletionResponse extends CreateChatCompletionResponse {
334 | /**
335 | *
336 | * @type {Array}
337 | * @memberof StreamChatCompletionResponse
338 | */
339 | 'choices': Array;
340 | }
341 |
342 | export interface ChatCompletionRequestMessageFunctionToolCall {
343 | type: 'function';
344 | function: {
345 | name?: string;
346 | arguments: string;
347 | }
348 | }
349 |
350 | export type ChatCompletionRequestMessageToolCall = {
351 | id?: string;
352 | index?: number;
353 | } & (ChatCompletionRequestMessageFunctionToolCall);
354 |
355 |
356 | /**
357 | *
358 | * @export
359 | * @interface ChatCompletionResponseMessage
360 | */
361 | export interface ChatCompletionResponseMessage {
362 | /**
363 | * The role of the author of this message.
364 | * @type {string}
365 | * @memberof ChatCompletionResponseMessage
366 | */
367 | 'role': ChatCompletionResponseMessageRoleEnum | 'tool';
368 | /**
369 | * The contents of the message.
370 | * @type {string}
371 | * @memberof ChatCompletionResponseMessage
372 | */
373 | 'content'?: string;
374 | /**
375 | *
376 | * @type {ChatCompletionRequestMessageFunctionCall}
377 | * @memberof ChatCompletionResponseMessage
378 | */
379 | 'function_call'?: ChatCompletionRequestMessageFunctionCall;
380 | /**
381 | *
382 | */
383 | 'tool_calls'?: Array;
384 | }
385 |
386 | interface StreamChatCompletionResponseChoicesInner extends CreateChatCompletionResponseChoicesInner {
387 | delta?: ChatCompletionResponseMessage;
388 | }
389 |
--------------------------------------------------------------------------------
/priompt/src/outputCatcher.ai.impl.ts:
--------------------------------------------------------------------------------
1 | import { OutputCatcher } from './outputCatcher.ai';
2 |
3 | export class OutputCatcherImpl implements OutputCatcher {
4 | private outputs: { output: T, priority: number | null }[] = [];
5 | private noPriorityOutputs: { output: T, priority: null }[] = [];
6 |
7 | async onOutput(output: T, options?: { p?: number }): Promise {
8 | if (options?.p !== undefined) {
9 | this.outputs.push({ output, priority: options.p });
10 | this.outputs.sort((a, b) => (b.priority as number) - (a.priority as number));
11 | } else {
12 | this.noPriorityOutputs.push({ output, priority: null });
13 | }
14 | }
15 |
16 | getOutputs(): T[] {
17 | return [...this.outputs, ...this.noPriorityOutputs].map(o => o.output);
18 | }
19 |
20 | getOutput(): T | undefined {
21 | return this.outputs.length > 0 ? this.outputs[0].output : this.noPriorityOutputs.length > 0 ? this.noPriorityOutputs[0].output : undefined;
22 | }
23 | }
24 | // @cursor-agent {"dependsOn": "interface", "hash": "7034e4452cc668449b0b967116683a95303c4509d263ed535851b081164751bb"}
25 |
--------------------------------------------------------------------------------
/priompt/src/outputCatcher.ai.test.ts:
--------------------------------------------------------------------------------
1 |
2 | import { OutputCatcher, NewOutputCatcher } from "./outputCatcher.ai";
3 |
4 | // @cursor-agent {"dependsOn": "interface", "hash": "7034e4452cc668449b0b967116683a95303c4509d263ed535851b081164751bb"}
5 | // @cursor-agent:begin-test-plan
6 | // T01: Test that `onOutput` correctly adds an output with no priority to the list of outputs.
7 | // T02: Test that `onOutput` correctly adds an output with a priority to the list of outputs.
8 | // T03: Test that `getOutputs` returns a list of outputs in the correct order, with the highest priority first and then all the ones with no priority assigned, in the order they were added.
9 | // T04: Test that `getOutput` returns the first output in the list.
10 | // T05: Test that `getOutput` returns undefined if there are no outputs in the list.
11 | // T06: Test that `onOutput` correctly handles multiple outputs with the same priority.
12 | // T07: Test that `onOutput` correctly handles multiple outputs with different priorities.
13 | // T08: Test that `onOutput` correctly handles multiple outputs with no priority.
14 | // T09: Test that `getOutputs` correctly handles an empty list of outputs.
15 | // T10: Test that `getOutputs` correctly handles a list of outputs with only one output.
16 | // T11: Test that `getOutputs` correctly handles a list of outputs with multiple outputs with the same priority.
17 | // T12: Test that `getOutputs` correctly handles a list of outputs with multiple outputs with different priorities.
18 | // T13: Test that `getOutputs` correctly handles a list of outputs with multiple outputs with no priority.
19 | // T14: Test that `getOutput` correctly handles a list of outputs with only one output.
20 | // T15: Test that `getOutput` correctly handles a list of outputs with multiple outputs with the same priority.
21 | // T16: Test that `getOutput` correctly handles a list of outputs with multiple outputs with different priorities.
22 | // T17: Test that `getOutput` correctly handles a list of outputs with multiple outputs with no priority.
23 | // @cursor-agent:end-test-plan
24 |
25 | import { describe, it, expect } from "vitest";
26 |
27 |
28 | describe("OutputCatcher", () => {
29 |
30 | // @cursor-agent:add-tests-here
31 |
32 |
33 | // @cursor-agent:test-begin:T17
34 | // @cursor-agent {"dependsOn": "interface", "hash": "7034e4452cc668449b0b967116683a95303c4509d263ed535851b081164751bb"}
35 | // @cursor-agent {"dependsOn": "testPlan", "hash": "2fd5729af0825f1ab89a030c1f9b035ecaafc20906f0a7579240fc4a2ca69eda"}
36 | // @cursor-agent {"id": "T17"}
37 | it('should correctly handle a list of outputs with multiple outputs with no priority', async () => {
38 | const outputCatcher = NewOutputCatcher();
39 |
40 | // Add outputs with no priority
41 | await outputCatcher.onOutput('output1');
42 | await outputCatcher.onOutput('output2');
43 | await outputCatcher.onOutput('output3');
44 |
45 | // Check if the first output is correct
46 | const firstOutput = outputCatcher.getOutput();
47 | expect(firstOutput).toBe('output1');
48 |
49 | // Check if the outputs are in the correct order
50 | const outputs = outputCatcher.getOutputs();
51 | expect(outputs).toEqual(['output1', 'output2', 'output3']);
52 | });
53 | // @cursor-agent:test-end:T17
54 |
55 |
56 |
57 | // @cursor-agent:test-begin:T16
58 | // @cursor-agent {"dependsOn": "interface", "hash": "7034e4452cc668449b0b967116683a95303c4509d263ed535851b081164751bb"}
59 | // @cursor-agent {"dependsOn": "testPlan", "hash": "705d3c64a82e431d967a6202dd705d4d73d542576a464fa50114834100012adb"}
60 | // @cursor-agent {"id": "T16"}
61 | it('should correctly handle a list of outputs with multiple outputs with different priorities', async () => {
62 | const outputCatcher = NewOutputCatcher();
63 |
64 | await outputCatcher.onOutput('output1', { p: 2 });
65 | await outputCatcher.onOutput('output2', { p: 1 });
66 | await outputCatcher.onOutput('output3', { p: 3 });
67 |
68 | const firstOutput = outputCatcher.getOutput();
69 |
70 | expect(firstOutput).toBe('output3');
71 | });
72 | // @cursor-agent:test-end:T16
73 |
74 |
75 |
76 | // @cursor-agent:test-begin:T15
77 | // @cursor-agent {"dependsOn": "interface", "hash": "7034e4452cc668449b0b967116683a95303c4509d263ed535851b081164751bb"}
78 | // @cursor-agent {"dependsOn": "testPlan", "hash": "52e1cabe3ba083495b0f3585522dcaeef206f46efd571b9eb81455bf949d3551"}
79 | // @cursor-agent {"id": "T15"}
80 | it('should correctly handle a list of outputs with multiple outputs with the same priority', async () => {
81 | const outputCatcher = NewOutputCatcher();
82 |
83 | // Add outputs with the same priority
84 | await outputCatcher.onOutput('output1', { p: 1 });
85 | await outputCatcher.onOutput('output2', { p: 1 });
86 | await outputCatcher.onOutput('output3', { p: 1 });
87 |
88 | // Check if the first output is correct
89 | const firstOutput = outputCatcher.getOutput();
90 | expect(firstOutput).toBe('output1');
91 |
92 | // Check if the outputs are sorted correctly
93 | const outputs = outputCatcher.getOutputs();
94 | expect(outputs).toEqual(['output1', 'output2', 'output3']);
95 | });
96 | // @cursor-agent:test-end:T15
97 |
98 |
99 |
100 | // @cursor-agent:test-begin:T14
101 | // @cursor-agent {"dependsOn": "interface", "hash": "7034e4452cc668449b0b967116683a95303c4509d263ed535851b081164751bb"}
102 | // @cursor-agent {"dependsOn": "testPlan", "hash": "2090cb75b222cb54f3319ec6bd2455703427a3a478979e3b3258aed0a969280f"}
103 | // @cursor-agent {"id": "T14"}
104 | it('should correctly handle a list of outputs with only one output', async () => {
105 | const outputCatcher = NewOutputCatcher();
106 | const output = 'Test Output';
107 |
108 | await outputCatcher.onOutput(output);
109 |
110 | const firstOutput = outputCatcher.getOutput();
111 |
112 | expect(firstOutput).toBe(output);
113 | });
114 | // @cursor-agent:test-end:T14
115 |
116 |
117 |
118 | // @cursor-agent:test-begin:T13
119 | // @cursor-agent {"dependsOn": "interface", "hash": "7034e4452cc668449b0b967116683a95303c4509d263ed535851b081164751bb"}
120 | // @cursor-agent {"dependsOn": "testPlan", "hash": "30937a9d8dcb8df28feecd5c7e3b204b04e93c4f26e3b3a61c51578bbc201231"}
121 | // @cursor-agent {"id": "T13"}
122 | it('should handle a list of outputs with multiple outputs with no priority correctly', async () => {
123 | const outputCatcher = NewOutputCatcher();
124 |
125 | // Add outputs with no priority
126 | await outputCatcher.onOutput('output1');
127 | await outputCatcher.onOutput('output2');
128 | await outputCatcher.onOutput('output3');
129 |
130 | // Add outputs with priority
131 | await outputCatcher.onOutput('output4', { p: 1 });
132 | await outputCatcher.onOutput('output5', { p: 2 });
133 |
134 | const outputs = outputCatcher.getOutputs();
135 |
136 | // Check that the outputs with priority are at the beginning of the list
137 | expect(outputs[0]).toBe('output5');
138 | expect(outputs[1]).toBe('output4');
139 |
140 | // Check that the outputs with no priority are in the order they were added
141 | expect(outputs[2]).toBe('output1');
142 | expect(outputs[3]).toBe('output2');
143 | expect(outputs[4]).toBe('output3');
144 | });
145 | // @cursor-agent:test-end:T13
146 |
147 |
148 |
149 |
150 |
151 |
152 | // @cursor-agent:test-begin:T12
153 | // @cursor-agent {"dependsOn": "interface", "hash": "7034e4452cc668449b0b967116683a95303c4509d263ed535851b081164751bb"}
154 | // @cursor-agent {"dependsOn": "testPlan", "hash": "2da0899bc14155cb0b63a83c0a8648d540232eb11e1d253f97739432a152fbdd"}
155 | // @cursor-agent {"id": "T12"}
156 | it('should correctly handle a list of outputs with multiple outputs with different priorities', async () => {
157 | const outputCatcher = NewOutputCatcher();
158 |
159 | await outputCatcher.onOutput('output1', { p: 2 });
160 | await outputCatcher.onOutput('output2', { p: 1 });
161 | await outputCatcher.onOutput('output3', { p: 3 });
162 | await outputCatcher.onOutput('output4');
163 |
164 | const outputs = outputCatcher.getOutputs();
165 |
166 | expect(outputs).toEqual(['output3', 'output1', 'output2', 'output4']);
167 | });
168 | // @cursor-agent:test-end:T12
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 | // @cursor-agent:test-begin:T11
179 | // @cursor-agent {"dependsOn": "interface", "hash": "7034e4452cc668449b0b967116683a95303c4509d263ed535851b081164751bb"}
180 | // @cursor-agent {"dependsOn": "testPlan", "hash": "2ba7dc9f2ce28f32e9ba4a145c6cd71a54b533561b7b422883a75ea0d212b106"}
181 | // @cursor-agent {"id": "T11"}
182 | it('should correctly handle a list of outputs with multiple outputs with the same priority', async () => {
183 | const outputCatcher = NewOutputCatcher();
184 |
185 | await outputCatcher.onOutput('output1', { p: 1 });
186 | await outputCatcher.onOutput('output2', { p: 2 });
187 | await outputCatcher.onOutput('output3', { p: 2 });
188 | await outputCatcher.onOutput('output4', { p: 1 });
189 |
190 | const outputs = outputCatcher.getOutputs();
191 |
192 | expect(outputs).toEqual(['output2', 'output3', 'output1', 'output4']);
193 | });
194 | // @cursor-agent:test-end:T11
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 | // @cursor-agent:test-begin:T10
205 | // @cursor-agent {"dependsOn": "interface", "hash": "7034e4452cc668449b0b967116683a95303c4509d263ed535851b081164751bb"}
206 | // @cursor-agent {"dependsOn": "testPlan", "hash": "9647f55c33afc9626a08bb5bc9fc3ad3bff2a12769f2440bda746c0636cbea43"}
207 | // @cursor-agent {"id": "T10"}
208 | it('should correctly handle a list of outputs with only one output', async () => {
209 | const outputCatcher = NewOutputCatcher();
210 | const output = 'Test Output';
211 | await outputCatcher.onOutput(output);
212 |
213 | const outputs = outputCatcher.getOutputs();
214 | expect(outputs).toHaveLength(1);
215 | expect(outputs[0]).toBe(output);
216 | });
217 | // @cursor-agent:test-end:T10
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 | // @cursor-agent:test-begin:T09
228 | // @cursor-agent {"dependsOn": "interface", "hash": "7034e4452cc668449b0b967116683a95303c4509d263ed535851b081164751bb"}
229 | // @cursor-agent {"dependsOn": "testPlan", "hash": "8c28351285c444e37a7251e9722b474d8e3f86290a1bc53c762f8504603c0438"}
230 | // @cursor-agent {"id": "T09"}
231 | it('should handle an empty list of outputs correctly', async () => {
232 | const outputCatcher = NewOutputCatcher();
233 | const outputs = outputCatcher.getOutputs();
234 | expect(outputs).toEqual([]);
235 | });
236 | // @cursor-agent:test-end:T09
237 |
238 |
239 |
240 |
241 |
242 |
243 | // @cursor-agent:test-begin:T08
244 | // @cursor-agent {"dependsOn": "interface", "hash": "7034e4452cc668449b0b967116683a95303c4509d263ed535851b081164751bb"}
245 | // @cursor-agent {"dependsOn": "testPlan", "hash": "8ed95684475af78a10deba0840d25230f1242f8745c7eb53ed1476ce2c8a1f44"}
246 | // @cursor-agent {"id": "T08"}
247 | it('should correctly handle multiple outputs with no priority', async () => {
248 | const outputCatcher = NewOutputCatcher();
249 |
250 | await outputCatcher.onOutput('output1');
251 | await outputCatcher.onOutput('output2');
252 | await outputCatcher.onOutput('output3');
253 |
254 | const outputs = outputCatcher.getOutputs();
255 |
256 | expect(outputs).toEqual(['output1', 'output2', 'output3']);
257 | });
258 | // @cursor-agent:test-end:T08
259 |
260 |
261 |
262 |
263 |
264 |
265 | // @cursor-agent:test-begin:T07
266 | // @cursor-agent {"dependsOn": "interface", "hash": "7034e4452cc668449b0b967116683a95303c4509d263ed535851b081164751bb"}
267 | // @cursor-agent {"dependsOn": "testPlan", "hash": "ced51a023776d7532a37bf3d5ea669fe80508a21326c932163dde56fa6ddb5ef"}
268 | // @cursor-agent {"id": "T07"}
269 | it('should correctly handle multiple outputs with different priorities', async () => {
270 | const outputCatcher = NewOutputCatcher();
271 |
272 | await outputCatcher.onOutput('output1', { p: 2 });
273 | await outputCatcher.onOutput('output2', { p: 1 });
274 | await outputCatcher.onOutput('output3', { p: 3 });
275 |
276 | const outputs = outputCatcher.getOutputs();
277 |
278 | expect(outputs).toEqual(['output3', 'output1', 'output2']);
279 | });
280 | // @cursor-agent:test-end:T07
281 |
282 |
283 |
284 |
285 |
286 |
287 | // @cursor-agent:test-begin:T06
288 | // @cursor-agent {"dependsOn": "interface", "hash": "7034e4452cc668449b0b967116683a95303c4509d263ed535851b081164751bb"}
289 | // @cursor-agent {"dependsOn": "testPlan", "hash": "743746ca04d5b14b5e90f25c21d1adc87b7ea4d7d205c6899ca6143455869567"}
290 | // @cursor-agent {"id": "T06"}
291 | it('should correctly handle multiple outputs with the same priority', async () => {
292 | const outputCatcher = NewOutputCatcher();
293 |
294 | await outputCatcher.onOutput('output1', { p: 1 });
295 | await outputCatcher.onOutput('output2', { p: 1 });
296 | await outputCatcher.onOutput('output3', { p: 1 });
297 |
298 | const outputs = outputCatcher.getOutputs();
299 |
300 | expect(outputs).toEqual(['output1', 'output2', 'output3']);
301 | });
302 | // @cursor-agent:test-end:T06
303 |
304 |
305 |
306 |
307 |
308 |
309 | // @cursor-agent:test-begin:T05
310 | // @cursor-agent {"dependsOn": "interface", "hash": "7034e4452cc668449b0b967116683a95303c4509d263ed535851b081164751bb"}
311 | // @cursor-agent {"dependsOn": "testPlan", "hash": "1cadfdeb3fd039e28d5ba302889d5f6e82f5504439e9fa7dcfa7bc81d1e42538"}
312 | // @cursor-agent {"id": "T05"}
313 | it('should return undefined if there are no outputs in the list', async () => {
314 | const outputCatcher = NewOutputCatcher();
315 | const output = outputCatcher.getOutput();
316 | expect(output).toBeUndefined();
317 | });
318 | // @cursor-agent:test-end:T05
319 |
320 |
321 |
322 |
323 |
324 |
325 | // @cursor-agent:test-begin:T04
326 | // @cursor-agent {"dependsOn": "interface", "hash": "7034e4452cc668449b0b967116683a95303c4509d263ed535851b081164751bb"}
327 | // @cursor-agent {"dependsOn": "testPlan", "hash": "a8944d34a7042b9cc995a1f9b4ffe600dd75484c5a5eff540737872a25994be3"}
328 | // @cursor-agent {"id": "T04"}
329 | it('should return the first output in the list when getOutput is called', async () => {
330 | const outputCatcher = NewOutputCatcher();
331 | await outputCatcher.onOutput('Test1', { p: 1 });
332 | await outputCatcher.onOutput('Test2', { p: 2 });
333 | await outputCatcher.onOutput('Test3', { p: 3 });
334 |
335 | const firstOutput = outputCatcher.getOutput();
336 | const outputs = outputCatcher.getOutputs();
337 | expect(firstOutput).toBe(outputs[0]);
338 | });
339 | // @cursor-agent:test-end:T04
340 |
341 |
342 |
343 |
344 |
345 |
346 | // @cursor-agent:test-begin:T03
347 | // @cursor-agent {"dependsOn": "interface", "hash": "7034e4452cc668449b0b967116683a95303c4509d263ed535851b081164751bb"}
348 | // @cursor-agent {"dependsOn": "testPlan", "hash": "d7a293c66b50c2f6ba6f251d1e49b9ac66d734199f4ea918e1e7305dc3140c54"}
349 | // @cursor-agent {"id": "T03"}
350 | it('should return a list of outputs in the correct order', async () => {
351 | const outputCatcher = NewOutputCatcher();
352 |
353 | await outputCatcher.onOutput('output1', { p: 2 });
354 | await outputCatcher.onOutput('output2');
355 | await outputCatcher.onOutput('output3', { p: 3 });
356 | await outputCatcher.onOutput('output4');
357 |
358 | const outputs = outputCatcher.getOutputs();
359 |
360 | expect(outputs).toEqual(['output3', 'output1', 'output2', 'output4']);
361 | });
362 | // @cursor-agent:test-end:T03
363 |
364 |
365 |
366 |
367 |
368 |
369 | // @cursor-agent:test-begin:T02
370 | // @cursor-agent {"dependsOn": "interface", "hash": "7034e4452cc668449b0b967116683a95303c4509d263ed535851b081164751bb"}
371 | // @cursor-agent {"dependsOn": "testPlan", "hash": "73c6b3f085b811b0f52b5651fe76b6e10a8cb3f92000cb916f2873d1932c26b7"}
372 | // @cursor-agent {"id": "T02"}
373 | it('should correctly add an output with a priority to the list of outputs', async () => {
374 | const outputCatcher = NewOutputCatcher();
375 | const output = 'Test Output';
376 | const priority = 5;
377 |
378 | await outputCatcher.onOutput(output, { p: priority });
379 |
380 | const outputs = outputCatcher.getOutputs();
381 | expect(outputs).toContain(output);
382 |
383 | const firstOutput = outputCatcher.getOutput();
384 | expect(firstOutput).toBe(output);
385 | });
386 | // @cursor-agent:test-end:T02
387 |
388 |
389 |
390 |
391 |
392 |
393 | // @cursor-agent:test-begin:T01
394 | // @cursor-agent {"dependsOn": "interface", "hash": "7034e4452cc668449b0b967116683a95303c4509d263ed535851b081164751bb"}
395 | // @cursor-agent {"dependsOn": "testPlan", "hash": "6c19bf21fe639eaa6f3404b1a3087f9e94d29bbd8abe44c027087b0bf88d9ad0"}
396 | // @cursor-agent {"id": "T01"}
397 | it('should correctly add an output with no priority to the list of outputs', async () => {
398 | const outputCatcher = NewOutputCatcher();
399 | const output = 'Test Output';
400 |
401 | await outputCatcher.onOutput(output);
402 |
403 | const outputs = outputCatcher.getOutputs();
404 | expect(outputs).toContain(output);
405 | });
406 | // @cursor-agent:test-end:T01
407 |
408 |
409 |
410 |
411 | })
412 |
413 |
--------------------------------------------------------------------------------
/priompt/src/outputCatcher.ai.ts:
--------------------------------------------------------------------------------
1 | import { OutputCatcherImpl } from './outputCatcher.ai.impl';
2 | export interface OutputCatcher {
3 | // p is a priority
4 | onOutput(output: T, options?: { p?: number }): Promise;
5 |
6 | // get a sorted list of the outputs, with the highest priority first
7 | // then come all the ones with no priority assigned, in the order they were added
8 | getOutputs(): T[];
9 |
10 | // get the first output
11 | getOutput(): T | undefined;
12 | }
13 |
14 | // @cursor-agent {"dependsOn": "interface", "hash": "7034e4452cc668449b0b967116683a95303c4509d263ed535851b081164751bb"}
15 | // @cursor-agent {"dependsOn": "implementation", "hash": "083f9244af4f56b541391df75e2a6bfe7e352f5ee6ed3ffe2eabd36dc06cdcf8"}
16 | export function NewOutputCatcher(): OutputCatcher {
17 | return new OutputCatcherImpl();
18 | }
19 |
20 | // @cursor-agent {"dependsOn": "interface", "hash": "7034e4452cc668449b0b967116683a95303c4509d263ed535851b081164751bb"}
21 | // @cursor-agent {"passedInitialVerification": true}
22 |
23 |
24 | // @cursor-agent {"dependsOn": "allFiles", "hash": "bd574f28bd5ebd5a493044d7bdaf54e43529f4b849ae540d9a5df45b9ad44ad1"}
25 | // @cursor-agent {"passedAllTests": true}
26 |
--------------------------------------------------------------------------------
/priompt/src/sourcemap.test.tsx:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from "vitest";
2 | import * as Priompt from "./index";
3 | import { render } from "./lib";
4 | import { PromptElement, PromptProps, SourceMap } from "./types";
5 | import { SystemMessage, UserMessage } from "./components";
6 | import { getTokenizerByName_ONLY_FOR_OPENAI_TOKENIZERS } from "./tokenizer";
7 |
8 | const validateSourceMap = (
9 | absoluteSourceMap: SourceMap,
10 | text: string
11 | ): boolean => {
12 | // Leaves which are raw strings are annotated with the string field
13 | // If we have their absolute range they should be an exact match
14 | const getLeaves = (sourceMap: SourceMap): SourceMap[] => {
15 | const children = sourceMap.children || [];
16 | if (children.length > 0) {
17 | return children.flatMap((child) => getLeaves(child));
18 | }
19 | return [sourceMap];
20 | };
21 |
22 | const leaves: SourceMap[] = getLeaves(absoluteSourceMap);
23 | const leavesWithString: SourceMap[] = leaves.filter(
24 | (leaf) => leaf.string !== undefined
25 | );
26 |
27 | const incorrectLeaves: SourceMap[] = leavesWithString.filter((leaf) => {
28 | const expectedString = leaf.string;
29 | const actualString = text.substring(leaf.start, leaf.end);
30 | return expectedString !== actualString;
31 | });
32 |
33 | if (incorrectLeaves.length > 0) {
34 | console.log(
35 | "Failed to validate source map, incorrect leaves are",
36 | incorrectLeaves.sort((a, b) => a.start - b.start)
37 | );
38 | return false;
39 | }
40 |
41 | return true;
42 | };
43 |
44 | const absolutifySourceMap = (
45 | sourceMap: SourceMap,
46 | offset: number = 0
47 | ): SourceMap => {
48 | const newOffset = sourceMap.start + offset;
49 | const children =
50 | sourceMap.children?.map((child) => absolutifySourceMap(child, newOffset)) ||
51 | [];
52 | return {
53 | ...sourceMap,
54 | start: newOffset,
55 | end: sourceMap.end + offset,
56 | children,
57 | };
58 | };
59 |
60 | describe("sourcemap", () => {
61 | function TestPromptTrivial(
62 | props: PromptProps<{ message: string }>
63 | ): PromptElement {
64 | return (
65 | <>
66 | {props.message}
67 |
68 | Testing sourcemap!
69 |
70 | abcdef
71 |
72 | >
73 | );
74 | }
75 | function TestPromptEmojiAndJapanese(): PromptElement {
76 | return (
77 | <>
78 | 🫨
79 |
80 | 🍋
81 | これはレモン
82 |
83 | >
84 | );
85 | }
86 |
87 | function TestSourceMapMoreComplex(
88 | props: PromptProps<{ message: string }>
89 | ): PromptElement {
90 | const lines = props.message.split("\n");
91 | return (
92 | <>
93 | The System Message
94 |
95 |
96 | This is the first line
97 |
98 | {lines[0]}
99 |
100 |
101 |
102 | {lines.map((line, i) => (
103 | {line}
104 | ))}
105 |
106 |
107 |
108 |
109 | const rendered = await render(TestPromptSplit(props.message))
110 | tokenLimit: 100, tokenizer: getTokenizerByName("cl100k_base"),
111 | buildSourceMap: true,
112 |
113 |
114 | const promptString =
115 | Priompt.promptToString_VULNERABLE_TO_PROMPT_INJECTION(rendered.prompt,
116 | getTokenizerByName("cl100k_base")) const sourceMap =
117 | rendered.sourceMap
118 |
119 |
120 | expect(sourceMap).toBeDefined() expect(sourceMap?.start).toBe(0)
121 |
122 | {props.message.split("\n").map((line, i) => (
123 | {line}
124 | ))}
125 |
126 | expect(sourceMap?.end).toBe(promp
127 | tString.length)
128 | expect(
129 |
130 | validateSourceMap(
131 |
132 | absolutifySourceMap(sourceMap!, 0)
133 |
134 | ,
135 |
136 | promptString
137 |
138 | )
139 |
140 | ).toBe(true)
141 |
142 |
143 | >
144 | );
145 | }
146 |
147 | it("should generate simple sourcemap correctly", async () => {
148 | const rendered = await render(
149 | TestPromptTrivial({ message: "System message for sourcemap test." }),
150 | {
151 | tokenLimit: 1000,
152 | tokenizer: getTokenizerByName_ONLY_FOR_OPENAI_TOKENIZERS("cl100k_base"),
153 | shouldBuildSourceMap: true,
154 | }
155 | );
156 | expect(rendered.sourceMap).toBeDefined();
157 | const promptString = Priompt.promptToString_VULNERABLE_TO_PROMPT_INJECTION(
158 | rendered.prompt,
159 | getTokenizerByName_ONLY_FOR_OPENAI_TOKENIZERS("cl100k_base")
160 | );
161 | expect(rendered.sourceMap?.start).toBe(0);
162 | expect(rendered.sourceMap?.end).toBe(promptString.length);
163 | if (rendered.sourceMap === undefined) {
164 | throw new Error("bad");
165 | }
166 | expect(
167 | validateSourceMap(
168 | absolutifySourceMap(rendered.sourceMap, 0),
169 | promptString
170 | )
171 | ).toBe(true);
172 | });
173 | it("should work with emoji and japanese", async () => {
174 | const rendered = await render(TestPromptEmojiAndJapanese(), {
175 | tokenLimit: 1000,
176 | tokenizer: getTokenizerByName_ONLY_FOR_OPENAI_TOKENIZERS("cl100k_base"),
177 | shouldBuildSourceMap: true,
178 | });
179 | expect(rendered.sourceMap).toBeDefined();
180 | const promptString = Priompt.promptToString_VULNERABLE_TO_PROMPT_INJECTION(
181 | rendered.prompt,
182 | getTokenizerByName_ONLY_FOR_OPENAI_TOKENIZERS("cl100k_base")
183 | );
184 | expect(rendered.sourceMap?.start).toBe(0);
185 | expect(rendered.sourceMap?.end).toBe(promptString.length);
186 | if (rendered.sourceMap === undefined) {
187 | throw new Error("bad");
188 | }
189 | expect(
190 | validateSourceMap(
191 | absolutifySourceMap(rendered.sourceMap, 0),
192 | promptString
193 | )
194 | ).toBe(true);
195 | });
196 | it("should generate sourcemap correctly", async () => {
197 | const message = `one lever that we haven’t really touched is
198 | creating features that depend substantially
199 | on the user investing time
200 | in configuring them to work well
201 | eg if a user could spend a day configuring an agent
202 | that would consistently make them 50% more productive, it’d be worth it
203 | or a big company spending a month to
204 | structure their codebase in such a way that many more things than before can be ai automated.
205 | The polish we’re missing on cmd+k is making it rly fast and snappy for the 1-2 line use case
206 | but it should be able to handle 100s of lines of code in a reasonable timeframe
207 | `;
208 | const rendered = await render(TestSourceMapMoreComplex({ message }), {
209 | tokenLimit: 300,
210 | tokenizer: getTokenizerByName_ONLY_FOR_OPENAI_TOKENIZERS("cl100k_base"),
211 | shouldBuildSourceMap: true,
212 | });
213 | const promptString = Priompt.promptToString_VULNERABLE_TO_PROMPT_INJECTION(
214 | rendered.prompt,
215 | getTokenizerByName_ONLY_FOR_OPENAI_TOKENIZERS("cl100k_base")
216 | );
217 | const sourceMap = rendered.sourceMap;
218 | expect(sourceMap).toBeDefined();
219 | expect(sourceMap?.start).toBe(0);
220 | expect(sourceMap?.end).toBe(promptString.length);
221 | if (sourceMap === undefined) {
222 | throw new Error("bad");
223 | }
224 | expect(
225 | validateSourceMap(absolutifySourceMap(sourceMap, 0), promptString)
226 | ).toBe(true);
227 | });
228 | });
229 |
--------------------------------------------------------------------------------
/priompt/src/statsd.ts:
--------------------------------------------------------------------------------
1 | import { StatsD } from 'hot-shots';
2 | import { isIP } from 'net';
3 | import dns from 'dns';
4 |
5 | const statsdHost = (
6 | process.env.DD_AGENT_HOST !== undefined
7 | ? process.env.DD_AGENT_HOST
8 | : '127.0.0.1'
9 | );
10 |
11 |
12 | export const statsd = new StatsD({
13 | host: statsdHost,
14 | port: 8125,
15 | prefix: 'cursor.',
16 | globalTags: { env: 'prod' },
17 | udpSocketOptions: { // workaround for https://github.com/brightcove/hot-shots/issues/198
18 | type: 'udp4',
19 | lookup: (host, opts, callback) => {
20 | if (isIP(host)) {
21 | callback(null, host, 4);
22 | return;
23 | }
24 | dns.lookup(host, opts, callback);
25 | }
26 | }
27 | });
--------------------------------------------------------------------------------
/priompt/src/types.d.ts:
--------------------------------------------------------------------------------
1 |
2 | // First picks out the first child (in order) that is prioritized enough
3 |
4 | import { JSONSchema7 } from 'json-schema';
5 | import { PriomptTokenizer, UsableTokenizer } from './tokenizer';
6 | import { ChatCompletionResponseMessage, StreamChatCompletionResponse } from './openai';
7 |
8 | export type FunctionBody = {
9 | name: string;
10 | description: string;
11 | parameters: JSONSchema7;
12 | }
13 |
14 | // It is a REQUIREMENT that the children have decreasing token counts
15 | export type First = {
16 | type: 'first';
17 | children: Scope[];
18 | onEject?: () => void;
19 | onInclude?: () => void;
20 | };
21 |
22 | export type Empty = {
23 | type: 'empty';
24 | tokenCount: number | undefined;
25 | tokenFunction: ((tokenizer: (s: string) => Promise) => Promise) | undefined;
26 | };
27 |
28 | export type BreakToken = {
29 | type: 'breaktoken';
30 | };
31 |
32 | export type Capture = {
33 | type: 'capture';
34 | } & CaptureProps;
35 |
36 | export type Config = {
37 | type: 'config';
38 | } & ConfigProps;
39 |
40 | export type ConfigProps = {
41 | maxResponseTokens: number | "tokensReserved" | "tokensRemaining" | undefined;
42 | // at most 4 of these
43 | stop: string | string[] | undefined;
44 | }
45 |
46 | export type Isolate = {
47 | type: 'isolate';
48 | children: Node[];
49 | cachedRenderOutput?: RenderOutput;
50 | } & IsolateProps;
51 |
52 | export type ChatImage = {
53 | type: 'image';
54 | } & ImageProps;
55 |
56 | // TODO: make the Capture work for other kinds of completions that aren't chat and aren't openai
57 | export type CaptureProps = {
58 | onOutput?: OutputHandler;
59 | onStream?: OutputHandler>;
60 | onStreamResponseObject?: OutputHandler>;
61 | }
62 |
63 | export type IsolateProps = {
64 | tokenLimit: number;
65 | }
66 |
67 | export type ImageProps = {
68 | bytes: Uint8Array;
69 | detail: 'low' | 'high' | 'auto';
70 | dimensions: {
71 | width: number;
72 | height: number;
73 | };
74 |
75 | }
76 |
77 | // the scope will exist iff the final priority is lower than the priority here
78 | // it shouldn't be the case that both the relative priority and the absolute priority is set
79 | export type Scope = {
80 | type: 'scope';
81 | children: Node[];
82 | // absolute priority takes precedence over relative priority
83 | absolutePriority: number | undefined;
84 | // relativePriority is relative to the parent of this scope
85 | // it should always be negative (or else it will not be displayed)
86 | relativePriority: number | undefined;
87 | name?: string;
88 | onEject?: () => void;
89 | onInclude?: () => void;
90 | };
91 |
92 | export type ChatUserSystemMessage = {
93 | type: 'chat';
94 | role: 'user' | 'system';
95 | name?: string;
96 | to?: string;
97 | children: Node[];
98 | }
99 |
100 | export type ChatAssistantFunctionToolCall = {
101 | type: 'function';
102 | function: {
103 | name: string;
104 | arguments: string; // json string
105 | }
106 | }
107 |
108 | export type ChatAssistantMessage = {
109 | type: 'chat';
110 | role: 'assistant';
111 | to?: string;
112 | children: Node[]; // can be empty!
113 |
114 | // the functionCall is provided by the assistant
115 | functionCall?: {
116 | name: string;
117 | arguments: string; // json string
118 | };
119 |
120 | // the toolCalls are provided by the assistant
121 | toolCalls?: {
122 | index: number;
123 | id: string;
124 | tool: ChatAssistantFunctionToolCall;
125 | }[]
126 | }
127 |
128 | export type ChatFunctionResultMessage = {
129 | type: 'chat';
130 | role: 'function';
131 | name: string;
132 | to?: string;
133 | children: Node[];
134 | }
135 |
136 | export type ChatToolResultMessage = {
137 | type: 'chat';
138 | role: 'tool';
139 | name: string;
140 | to?: string;
141 | children: Node[];
142 | }
143 |
144 | export type ChatMessage = ChatUserSystemMessage | ChatFunctionResultMessage | ChatToolResultMessage | ChatAssistantMessage;
145 |
146 | export type FunctionDefinition = {
147 | type: 'functionDefinition';
148 | name: string;
149 | description: string;
150 | parameters: JSONSchema7;
151 | }
152 |
153 | export type ToolDefinition = {
154 | type: 'toolDefinition';
155 | tool: FunctionToolDefinition;
156 | }
157 |
158 | export type FunctionToolDefinition = {
159 | type: 'function';
160 | function: {
161 | name: string;
162 | description: string;
163 | parameters: JSONSchema7;
164 | strict?: boolean;
165 | }
166 | }
167 |
168 | export type Node = FunctionDefinition | ToolDefinition | BreakToken | First | Isolate | Capture | Config | Scope | Empty | ChatMessage | ChatImage | string | null | undefined | number | false;
169 |
170 | export type PromptElement = Node[] | Node;
171 |
172 | export type BaseProps = {
173 | // absolute priority takes precedence over relative priority
174 | // maximum supported priority level is 1e6
175 | p?: number;
176 | prel?: number;
177 | name?: string, // a label for debugging purposes
178 | // TODO: add a max (token count) here. the max functions as follows:
179 | // first we optimize over the outest token count scope. if any max exceeds its token count, it is capped to the token count. once we have a global solution we seek the local solution
180 | // this works, but leads to something that may be a little bit weird: something of priority 1000 in a maxed out scope is not included while something with a priority of 0 outside the maxed out scope is included. but that's fine. i guess the whole point of the max is to break the global opptimization
181 | children?: PromptElement[] | PromptElement;
182 | onEject?: () => void;
183 | onInclude?: () => void;
184 | };
185 |
186 | export type ReturnProps = {
187 | onReturn: OutputHandler;
188 | }
189 |
190 | type BasePromptProps> = (keyof T extends never ? BaseProps : BaseProps & T);
191 | export type PromptProps, ReturnT = never> = ([ReturnT] extends [never] ? BasePromptProps : BasePromptProps & ReturnProps);
192 |
193 | export namespace JSX {
194 | interface IntrinsicElements {
195 | scope: BaseProps;
196 | br: Omit;
197 | hr: Omit;
198 | breaktoken: Omit;
199 | // automatically use a certain number of tokens (useful for leaving space for the model to give its answer)
200 | empty: BaseProps & { tokens: number | ((tokenizer: (s: string) => Promise) => Promise); };
201 | first: Omit, 'prel'>;
202 | capture: Omit & CaptureProps;
203 | isolate: BaseProps & IsolateProps;
204 | config: Omit & Partial;
205 | }
206 | type Element = PromptElement;
207 | interface ElementAttributesProperty {
208 | props: BaseProps; // specify the property name to use
209 | }
210 | }
211 |
212 | // if prompt string is a list of strings, then those strings should be tokenized independently
213 | // this prevents tokens from crossing the boundary between strings, which is useful for things when you
214 | // need exact copying
215 | export type PromptString = string | string[];
216 |
217 | export type PromptContentWrapper = {
218 | type: 'prompt_content',
219 | content: PromptString;
220 | images?: ImagePromptContent[];
221 | }
222 |
223 | export type TextPromptContent = {
224 | type: 'text',
225 | text: string
226 | }
227 | export type ImagePromptContent = {
228 | type: 'image_url',
229 | image_url: {
230 | url: string;
231 | detail: 'low' | 'high' | 'auto';
232 | dimensions: {
233 | width: number;
234 | height: number;
235 | }
236 | }
237 | }
238 | export type PromptContent = TextPromptContent | ImagePromptContent;
239 |
240 | export type ChatPromptSystemMessage = {
241 | role: 'system';
242 | name?: string;
243 | to?: string | undefined;
244 | content: PromptString;
245 | }
246 |
247 | export type ChatPromptUserMessage = {
248 | role: 'user';
249 | name?: string;
250 | to?: string | undefined;
251 | content: PromptString;
252 | images?: ImagePromptContent[];
253 | }
254 |
255 | export type ChatPromptAssistantMessage = {
256 | role: 'assistant';
257 | to?: string | undefined;
258 | content?: PromptString;
259 | functionCall?: {
260 | name: string;
261 | arguments: string; // json string
262 | }
263 | toolCalls?: {
264 | index: number;
265 | id: string;
266 | tool: ChatAssistantFunctionToolCall;
267 | }[]
268 | }
269 |
270 | export type ChatPromptFunctionResultMessage = {
271 | role: 'function';
272 | name: string;
273 | to?: string | undefined;
274 | content: PromptString;
275 | };
276 |
277 | export type ChatPromptToolResultMessage = {
278 | role: 'tool';
279 | name?: string;
280 | to: string | undefined;
281 | content: PromptString;
282 | };
283 |
284 | export type ChatPromptMessage = ChatPromptSystemMessage | ChatPromptUserMessage | ChatPromptAssistantMessage | ChatPromptFunctionResultMessage | ChatPromptToolResultMessage;
285 |
286 | export type ChatPrompt = {
287 | type: 'chat';
288 | messages: ChatPromptMessage[];
289 | }
290 |
291 | export type TextPrompt = {
292 | type: 'text';
293 | text: PromptString;
294 | }
295 |
296 | export type ChatAndFunctionPromptFunction = {
297 | name: string;
298 | description: string;
299 | parameters: JSONSchema7;
300 | }
301 |
302 | export type FunctionPrompt = {
303 | functions: ChatAndFunctionPromptFunction[];
304 | }
305 |
306 | // https://platform.openai.com/docs/api-reference/chat/create
307 | export type ChatAndToolPromptToolFunction = {
308 | type: 'function';
309 | function: {
310 | name: string;
311 | description?: string;
312 | parameters?: JSONSchema7;
313 | }
314 | }
315 |
316 | export type ToolPrompt = {
317 | tools: ChatAndToolPromptToolFunction[];
318 | }
319 |
320 | // the p is used to specify the priority of the handler
321 | // higher priority handler will be called first in case there are multiple
322 | export type OutputHandler = (output: T, options?: { p?: number }) => Promise;
323 |
324 | export type RenderedPrompt = PromptString | ChatPrompt | (ChatPrompt & FunctionPrompt) | (ChatPrompt & ToolPrompt) | (TextPrompt & FunctionPrompt) | (TextPrompt & ToolPrompt) | PromptContentWrapper;
325 |
326 | export type Prompt = ((props: PromptProps) => (PromptElement | Promise)) & {
327 | config?: PreviewConfig;
328 | };
329 | export type SynchronousPrompt = ((props: PromptProps) => (PromptElement)) & {
330 | config?: SynchronousPreviewConfig;
331 | };
332 |
333 | export type PreviewConfig = {
334 | id: string;
335 | prompt: Prompt;
336 | // defaults to yaml but can be overridden
337 | dump?: (props: Omit) => string;
338 | hydrate?: (dump: string) => PropsT;
339 | }
340 |
341 | export type SynchronousPreviewConfig = {
342 | id: string;
343 | prompt: SynchronousPrompt;
344 | // defaults to yaml but can be overridden
345 | dump?: (props: Omit) => string;
346 | hydrate?: (dump: string) => PropsT;
347 | dumpExtension?: string;
348 | }
349 |
350 | // TODO: should the components have access to the token limit?
351 | // argument against: no, it should all be responsive to the token limit and we shouldn't need this
352 | // argument for: CSS has media queries because it is very hard to have something that's fully responsive without changing any of the layout
353 | // decision: wait for now, see if it is needed
354 | export type RenderOptions = {
355 | tokenLimit: number;
356 | tokenizer: PriomptTokenizer;
357 | countTokensFast_UNSAFE?: boolean;
358 | shouldBuildSourceMap?: boolean;
359 |
360 | // if it is, then we need to count tokens differently
361 | lastMessageIsIncomplete?: boolean;
362 | };
363 |
364 | export type RenderunCountTokensFast_UNSAFE = "try_retry" | "yes" | "no";
365 |
366 | export type RenderunOptions = Omit & {
367 | countTokensFast_UNSAFE?: RenderunCountTokensFast_UNSAFE;
368 | };
369 |
370 | // A sourcemap is an optional piece of data priompt can produce to map
371 | // from prompt elements, e.g. the jsx tree, to the actual characters
372 | // in the final prompt. This can be used to "blame" where cache misses
373 | // which are character / token-wise correspond to in the prompt tree.
374 | // Each sourcemap represents a node in the prompt tree and has a range
375 | // as to the characters it represents (start and end) which are *relative*
376 | // to the range of its parent. A leaf has undefined children, and leaves which
377 | // are strings have `string` filled in for validation.
378 | export type SourceMap = {
379 | name: string;
380 | children?: SourceMap[];
381 | string?: string,
382 | start: number;
383 | end: number;
384 | }
385 |
386 | export type AbsoluteSourceMap = Omit & {
387 | children?: AbsoluteSourceMap[];
388 | __brand: 'absolute';
389 | }
390 |
391 | export type RenderOutput = {
392 | prompt: RenderedPrompt;
393 | tokenCount: number;
394 | tokenLimit: number;
395 | tokenizer: PriomptTokenizer;
396 | tokensReserved: number;
397 | priorityCutoff: number;
398 | outputHandlers: OutputHandler[];
399 | streamHandlers: OutputHandler>[];
400 | streamResponseObjectHandlers: OutputHandler>[];
401 | config: ConfigProps;
402 | durationMs?: number;
403 | sourceMap?: SourceMap
404 | };
405 |
--------------------------------------------------------------------------------
/priompt/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": true,
4 | "outDir": "./dist",
5 | "strictNullChecks": true,
6 | "noImplicitAny": true,
7 | "declaration": true,
8 | "target": "ES2019",
9 | "module": "NodeNext",
10 | // we need this because vitest 1 requires nodenext, and vitest 0.33 and vitest 1 cannot coexist
11 | "moduleResolution": "nodenext",
12 | "jsx": "react",
13 | "jsxFactory": "Priompt.createElement",
14 | "jsxFragmentFactory": "Priompt.Fragment",
15 | "sourceMap": true,
16 | "inlineSources": true,
17 | // we need this to fix this weird vitest problem: https://github.com/vitejs/vite/issues/11552
18 | "skipLibCheck": true,
19 | "strictPropertyInitialization": true,
20 | "declarationMap": true
21 | },
22 | "include": ["./src/**/*.ts", "./src/**/*.tsx"]
23 | }
24 |
--------------------------------------------------------------------------------
/priompt/vitest.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | test: {
3 | include: [
4 | 'src/**/*.{test,spec}.{js,ts,jsx,tsx}',
5 | // Also include top level files
6 | 'src/*.{test,spec}.{js,ts,jsx,tsx}'
7 | ],
8 | exclude: ['build/**/*'],
9 | // setupFiles: ['dotenv/config']
10 | },
11 | };
--------------------------------------------------------------------------------
/publish.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
6 | cd "$SCRIPT_DIR"
7 |
8 | if [[ -n $(git status --porcelain) ]]; then
9 | echo -e "${RED}Your git state is not empty. Aborting the script...${NC}"
10 | exit 1
11 | fi
12 |
13 | # Check if a version bumping flag is provided
14 | if [ $# -ne 1 ]; then
15 | echo "Error: Version bumping flag (patch, minor, or major) is required."
16 | exit 1
17 | fi
18 |
19 | # Validate the version bumping flag
20 | case $1 in
21 | patch|minor|major)
22 | ;;
23 | *)
24 | echo "Error: Invalid version bumping flag. Use patch, minor, or major."
25 | exit 1
26 | ;;
27 | esac
28 | # Change to the priompt directory, increment the version, and publish the package
29 | cd $SCRIPT_DIR/priompt
30 | npm version $1
31 | cd $SCRIPT_DIR/priompt-preview
32 | npm version $1
33 | cd $SCRIPT_DIR/tiktoken-node
34 | npm version $1
35 |
36 | git commit -am "update version"
37 |
38 | git push
39 |
40 | CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
41 | DEPLOY_BRANCH=publish
42 | git branch -D $DEPLOY_BRANCH || true
43 | git checkout -b $DEPLOY_BRANCH
44 | git push origin $DEPLOY_BRANCH -f
45 | git checkout $CURRENT_BRANCH
--------------------------------------------------------------------------------
/pull-from-open-source.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -e
4 |
5 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
6 |
7 | if [[ -n $(git status --porcelain) ]]; then
8 | echo -e "${RED}Your git state is not empty. Aborting the script...${NC}"
9 | exit 1
10 | fi
11 |
12 | # make sure we are on main, otherwise print warning
13 | if [[ $(git branch --show-current) != "main" ]]; then
14 | echo "WARNING: You are not on main branch, please switch to main branch before running this script."
15 | exit 1
16 | fi
17 |
18 | if [[ ! -d "$SCRIPT_DIR/priompt-opensource" ]]; then
19 | git clone git@github.com:anysphere/priompt "$SCRIPT_DIR/priompt-opensource"
20 | fi
21 |
22 | cd "$SCRIPT_DIR/priompt-opensource"
23 | git checkout main
24 | git checkout -- . || true
25 | git restore --staged . || true
26 | git checkout -- . || true
27 | git clean -fd . || true
28 | git pull
29 | if [[ -n $(git status --porcelain) ]]; then
30 | echo -e "${RED}Your git state inside priompt-opensource is not empty. Aborting the script...${NC}"
31 | exit 1
32 | fi
33 |
34 | LAST_SYNCED_COMMIT=$(cat "$SCRIPT_DIR/../priompt-last-open-source-synced-commit.txt")
35 | echo "LAST_SYNCED_COMMIT: $LAST_SYNCED_COMMIT"
36 | COMMIT_IDS=$(git rev-list --reverse HEAD...$LAST_SYNCED_COMMIT)
37 |
38 | echo "Commit IDs:"
39 | echo $COMMIT_IDS
40 |
41 | for COMMIT_ID in $COMMIT_IDS
42 | do
43 | cd "$SCRIPT_DIR/priompt-opensource"
44 | git show $COMMIT_ID > "$SCRIPT_DIR/commit.patch"
45 | sd 'a/' 'a/' "$SCRIPT_DIR/commit.patch"
46 | sd 'b/' 'b/' "$SCRIPT_DIR/commit.patch"
47 | cd "$SCRIPT_DIR/../../.."
48 | git apply "$SCRIPT_DIR/commit.patch"
49 | git add .
50 | COMMIT_MSG=$(cd $SCRIPT_DIR/priompt-opensource && git log -1 --pretty=%B $COMMIT_ID | tr -d '\r')
51 | echo "$COMMIT_MSG" > "$SCRIPT_DIR/commit.template"
52 | echo -e "\n\n" >> "$SCRIPT_DIR/commit.template"
53 | COMMIT_AUTHOR=$(cd $SCRIPT_DIR/priompt-opensource && git log -1 --pretty=%an $COMMIT_ID)
54 | COMMIT_EMAIL=$(cd $SCRIPT_DIR/priompt-opensource && git log -1 --pretty=%ae $COMMIT_ID)
55 | echo "Co-authored-by: $COMMIT_AUTHOR <$COMMIT_EMAIL>" >> "$SCRIPT_DIR/commit.template"
56 | echo -e "\n\n" >> "$SCRIPT_DIR/commit.template"
57 | FULL_COMMIT=$(cd $SCRIPT_DIR && cat "$SCRIPT_DIR/commit.patch")
58 | echo "$FULL_COMMIT" | while IFS= read -r line
59 | do
60 | echo -e "# $line" >> "$SCRIPT_DIR/commit.template"
61 | done
62 | git commit --template="$SCRIPT_DIR/commit.template"
63 | COMMIT_ID_MAIN=$(git rev-parse HEAD)
64 | echo "$COMMIT_ID_MAIN" > "$SCRIPT_DIR/../priompt-last-internal-synced-commit.txt"
65 | echo "$COMMIT_ID" > "$SCRIPT_DIR/../priompt-last-open-source-synced-commit.txt"
66 | done
67 |
68 | echo "DONE! Now please push inside the main repo."
69 |
70 |
--------------------------------------------------------------------------------
/push-to-open-source.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -e
4 |
5 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
6 |
7 | if [[ -n $(git status --porcelain) ]]; then
8 | echo -e "${RED}Your git state is not empty. Aborting the script...${NC}"
9 | exit 1
10 | fi
11 |
12 | # copy over the eslintrc.base.json
13 | cp -f "$SCRIPT_DIR"/../../.eslintrc.base.json "$SCRIPT_DIR"/.eslintrc.base.json
14 | if [[ -n $(git status --porcelain) ]]; then
15 | git add .
16 | git commit -m "update the eslintrc.base.json"
17 | fi
18 |
19 | if [[ ! -d "$SCRIPT_DIR/priompt-opensource" ]]; then
20 | git clone git@github.com:anysphere/priompt "$SCRIPT_DIR/priompt-opensource"
21 | fi
22 |
23 | cd "$SCRIPT_DIR/priompt-opensource"
24 | git checkout main
25 | git checkout -- . || true
26 | git restore --staged . || true
27 | git checkout -- . || true
28 | git clean -fd . || true
29 | # git pull
30 | if [[ -n $(git status --porcelain) ]]; then
31 | echo -e "${RED}Your git state inside priompt-opensource is not empty. Aborting the script...${NC}"
32 | exit 1
33 | fi
34 | cd "$SCRIPT_DIR"
35 |
36 | cd "$SCRIPT_DIR/../../.."
37 |
38 | LAST_SYNCED_COMMIT=$(cat "$SCRIPT_DIR/../priompt-last-internal-synced-commit.txt")
39 | echo "LAST_SYNCED_COMMIT: $LAST_SYNCED_COMMIT"
40 | COMMIT_IDS=$(git rev-list --reverse HEAD...$LAST_SYNCED_COMMIT -- "backend/packages/priompt")
41 |
42 |
43 | echo "Commit IDs:"
44 | echo $COMMIT_IDS
45 |
46 | for COMMIT_ID in $COMMIT_IDS
47 | do
48 | git show $COMMIT_ID -- "backend/packages/priompt" > "$SCRIPT_DIR/commit.patch"
49 | sd '' '' "$SCRIPT_DIR/commit.patch"
50 | cd "$SCRIPT_DIR/priompt-opensource"
51 | git apply "$SCRIPT_DIR/commit.patch"
52 | git add .
53 | COMMIT_MSG=$(cd $SCRIPT_DIR && git log -1 --pretty=%B $COMMIT_ID | tr -d '\r')
54 | echo "$COMMIT_MSG" > "$SCRIPT_DIR/commit.template"
55 | echo -e "\n\n" >> "$SCRIPT_DIR/commit.template"
56 | COMMIT_AUTHOR=$(cd $SCRIPT_DIR && git log -1 --pretty=%an $COMMIT_ID)
57 | COMMIT_EMAIL=$(cd $SCRIPT_DIR && git log -1 --pretty=%ae $COMMIT_ID)
58 | echo "Co-authored-by: $COMMIT_AUTHOR <$COMMIT_EMAIL>" >> "$SCRIPT_DIR/commit.template"
59 | echo -e "\n\n" >> "$SCRIPT_DIR/commit.template"
60 | FULL_COMMIT=$(cd $SCRIPT_DIR && cat "$SCRIPT_DIR/commit.patch")
61 | echo "$FULL_COMMIT" | while IFS= read -r line
62 | do
63 | echo -e "# $line" >> "$SCRIPT_DIR/commit.template"
64 | done
65 | git commit --template="$SCRIPT_DIR/commit.template"
66 | COMMIT_ID_OPENSOURCE=$(git rev-parse HEAD)
67 | cd -
68 | echo "$COMMIT_ID_OPENSOURCE" > "$SCRIPT_DIR/../priompt-last-open-source-synced-commit.txt"
69 | echo "$COMMIT_ID" > "$SCRIPT_DIR/../priompt-last-internal-synced-commit.txt"
70 | done
71 |
72 | echo "DONE! Now please push inside the open source folder."
73 |
--------------------------------------------------------------------------------
/rustfmt.toml:
--------------------------------------------------------------------------------
1 | use_small_heuristics = "Max"
2 | newline_style = "Unix"
3 | edition = "2021"
4 | tab_spaces = 2
5 |
--------------------------------------------------------------------------------
/tiktoken-node/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/node
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=node
3 |
4 | ### Node ###
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | yarn-debug.log*
10 | yarn-error.log*
11 | lerna-debug.log*
12 |
13 | # Diagnostic reports (https://nodejs.org/api/report.html)
14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
15 |
16 | # Runtime data
17 | pids
18 | *.pid
19 | *.seed
20 | *.pid.lock
21 |
22 | # Directory for instrumented libs generated by jscoverage/JSCover
23 | lib-cov
24 |
25 | # Coverage directory used by tools like istanbul
26 | coverage
27 | *.lcov
28 |
29 | # nyc test coverage
30 | .nyc_output
31 |
32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
33 | .grunt
34 |
35 | # Bower dependency directory (https://bower.io/)
36 | bower_components
37 |
38 | # node-waf configuration
39 | .lock-wscript
40 |
41 | # Compiled binary addons (https://nodejs.org/api/addons.html)
42 | build/Release
43 |
44 | # Dependency directories
45 | node_modules/
46 | jspm_packages/
47 |
48 | # TypeScript v1 declaration files
49 | typings/
50 |
51 | # TypeScript cache
52 | *.tsbuildinfo
53 |
54 | # Optional npm cache directory
55 | .npm
56 |
57 | # Optional eslint cache
58 | .eslintcache
59 |
60 | # Microbundle cache
61 | .rpt2_cache/
62 | .rts2_cache_cjs/
63 | .rts2_cache_es/
64 | .rts2_cache_umd/
65 |
66 | # Optional REPL history
67 | .node_repl_history
68 |
69 | # Output of 'npm pack'
70 | *.tgz
71 |
72 | # Yarn Integrity file
73 | .yarn-integrity
74 |
75 | # dotenv environment variables file
76 | .env
77 | .env.test
78 |
79 | # parcel-bundler cache (https://parceljs.org/)
80 | .cache
81 |
82 | # Next.js build output
83 | .next
84 |
85 | # Nuxt.js build / generate output
86 | .nuxt
87 | dist
88 |
89 | # Gatsby files
90 | .cache/
91 | # Comment in the public line in if your project uses Gatsby and not Next.js
92 | # https://nextjs.org/blog/next-9-1#public-directory-support
93 | # public
94 |
95 | # vuepress build output
96 | .vuepress/dist
97 |
98 | # Serverless directories
99 | .serverless/
100 |
101 | # FuseBox cache
102 | .fusebox/
103 |
104 | # DynamoDB Local files
105 | .dynamodb/
106 |
107 | # TernJS port file
108 | .tern-port
109 |
110 | # Stores VSCode versions used for testing VSCode extensions
111 | .vscode-test
112 |
113 | # End of https://www.toptal.com/developers/gitignore/api/node
114 |
115 | # Created by https://www.toptal.com/developers/gitignore/api/macos
116 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos
117 |
118 | ### macOS ###
119 | # General
120 | .DS_Store
121 | .AppleDouble
122 | .LSOverride
123 |
124 | # Icon must end with two
125 | Icon
126 |
127 |
128 | # Thumbnails
129 | ._*
130 |
131 | # Files that might appear in the root of a volume
132 | .DocumentRevisions-V100
133 | .fseventsd
134 | .Spotlight-V100
135 | .TemporaryItems
136 | .Trashes
137 | .VolumeIcon.icns
138 | .com.apple.timemachine.donotpresent
139 |
140 | # Directories potentially created on remote AFP share
141 | .AppleDB
142 | .AppleDesktop
143 | Network Trash Folder
144 | Temporary Items
145 | .apdisk
146 |
147 | ### macOS Patch ###
148 | # iCloud generated files
149 | *.icloud
150 |
151 | # End of https://www.toptal.com/developers/gitignore/api/macos
152 |
153 | # Created by https://www.toptal.com/developers/gitignore/api/windows
154 | # Edit at https://www.toptal.com/developers/gitignore?templates=windows
155 |
156 | ### Windows ###
157 | # Windows thumbnail cache files
158 | Thumbs.db
159 | Thumbs.db:encryptable
160 | ehthumbs.db
161 | ehthumbs_vista.db
162 |
163 | # Dump file
164 | *.stackdump
165 |
166 | # Folder config file
167 | [Dd]esktop.ini
168 |
169 | # Recycle Bin used on file shares
170 | $RECYCLE.BIN/
171 |
172 | # Windows Installer files
173 | *.cab
174 | *.msi
175 | *.msix
176 | *.msm
177 | *.msp
178 |
179 | # Windows shortcuts
180 | *.lnk
181 |
182 | # End of https://www.toptal.com/developers/gitignore/api/windows
183 |
184 | #Added by cargo
185 |
186 | /target
187 | Cargo.lock
188 |
189 | .pnp.*
190 | .yarn/*
191 | !.yarn/patches
192 | !.yarn/plugins
193 | !.yarn/releases
194 | !.yarn/sdks
195 | !.yarn/versions
196 |
197 | *.node
198 |
--------------------------------------------------------------------------------
/tiktoken-node/.npmignore:
--------------------------------------------------------------------------------
1 | target
2 | Cargo.lock
3 | .cargo
4 | .github
5 | npm
6 | .eslintrc
7 | .prettierignore
8 | rustfmt.toml
9 | yarn.lock
10 | *.node
11 | .yarn
12 | __test__
13 | renovate.json
14 | .wireit
--------------------------------------------------------------------------------
/tiktoken-node/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | edition = "2021"
3 | name = "anysphere_tiktoken-node"
4 | version = "0.0.1"
5 |
6 | [lib]
7 | crate-type = ["cdylib"]
8 |
9 | [dependencies]
10 | # Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix
11 | napi = { version = "2.16.11", default-features = false, features = [
12 | "napi4",
13 | "async",
14 | ] }
15 | napi-derive = "2.16.11"
16 | rustc-hash = "1.1.0"
17 | base64 = "0.21.0"
18 | pathdiff = "0.2"
19 | log = { version = "0.4.21", features = ["kv"] }
20 | tiktoken = { git = "https://github.com/anysphere/tiktoken-rs", rev = "4b43ef814eba03cf062b0e777eebaad9b27451e8" }
21 | rayon = "1.7.0"
22 | anyhow = "1.0.69"
23 | tokio = { version = "1.13.0", features = [
24 | "rt-multi-thread",
25 | "sync",
26 | "rt",
27 | "macros",
28 | ] }
29 | once_cell = "1.18.0"
30 | async-channel = "2.3.1"
31 |
32 | [build-dependencies]
33 | napi-build = "2.0.1"
34 |
--------------------------------------------------------------------------------
/tiktoken-node/README.md:
--------------------------------------------------------------------------------
1 | # @anysphere/tiktoken-node
2 |
3 | We use our own fork for now because we are making changes that may not be useful to everyone. For example, we add support for special tokens, and we also add support for running tokenization asynchronously with the computation happening on a different thread.
4 |
--------------------------------------------------------------------------------
/tiktoken-node/build.rs:
--------------------------------------------------------------------------------
1 | extern crate napi_build;
2 |
3 | fn main() {
4 | napi_build::setup();
5 | }
6 |
--------------------------------------------------------------------------------
/tiktoken-node/index.d.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 |
4 | /* auto-generated by NAPI-RS */
5 |
6 | export const enum SupportedEncoding {
7 | Cl100k = 0,
8 | Llama3 = 1,
9 | O200k = 2,
10 | Codestral = 3
11 | }
12 | export const enum SpecialTokenAction {
13 | /** The special token is forbidden. If it is included in the string, an error will be returned. */
14 | Forbidden = 0,
15 | /** The special token is tokenized as normal text. */
16 | NormalText = 1,
17 | /** The special token is treated as the special token it is. If this is applied to a specific text and the text is NOT a special token then an error will be returned. If it is the default action no error will be returned, don't worry. */
18 | Special = 2
19 | }
20 | export declare function getTokenizer(): Tokenizer
21 | export class Tokenizer {
22 | exactNumTokensNoSpecialTokens(text: string, encoding: SupportedEncoding): Promise
23 | exactNumTokens(text: string, encoding: SupportedEncoding, specialTokenDefaultAction: SpecialTokenAction, specialTokenOverrides: Record): Promise
24 | encodeCl100KNoSpecialTokens(text: string): Promise>
25 | approxNumTokens(text: string, encoding: SupportedEncoding, replaceSpacesWithLowerOneEighthBlock: boolean): Promise
26 | encode(text: string, encoding: SupportedEncoding, specialTokenDefaultAction: SpecialTokenAction, specialTokenOverrides: Record): Promise>
27 | encodeSingleToken(bytes: Uint8Array, encoding: SupportedEncoding): Promise
28 | decodeByte(token: number, encoding: SupportedEncoding): Promise
29 | decode(encodedTokens: Array, encoding: SupportedEncoding): Promise
30 | }
31 | export class SyncTokenizer {
32 | constructor()
33 | approxNumTokens(text: string, encoding: SupportedEncoding): number
34 | }
35 |
--------------------------------------------------------------------------------
/tiktoken-node/index.js:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | /* prettier-ignore */
4 |
5 | /* auto-generated by NAPI-RS */
6 |
7 | const { existsSync, readFileSync } = require('fs')
8 | const { join } = require('path')
9 |
10 | const { platform, arch } = process
11 |
12 | let nativeBinding = null
13 | let localFileExisted = false
14 | let loadError = null
15 |
16 | function isMusl() {
17 | // For Node 10
18 | if (!process.report || typeof process.report.getReport !== 'function') {
19 | try {
20 | const lddPath = require('child_process').execSync('which ldd').toString().trim()
21 | return readFileSync(lddPath, 'utf8').includes('musl')
22 | } catch (e) {
23 | return true
24 | }
25 | } else {
26 | const { glibcVersionRuntime } = process.report.getReport().header
27 | return !glibcVersionRuntime
28 | }
29 | }
30 |
31 | switch (platform) {
32 | case 'android':
33 | switch (arch) {
34 | case 'arm64':
35 | localFileExisted = existsSync(join(__dirname, 'tiktoken-node.android-arm64.node'))
36 | try {
37 | if (localFileExisted) {
38 | nativeBinding = require('./tiktoken-node.android-arm64.node')
39 | } else {
40 | nativeBinding = require('@anysphere/tiktoken-node-android-arm64')
41 | }
42 | } catch (e) {
43 | loadError = e
44 | }
45 | break
46 | case 'arm':
47 | localFileExisted = existsSync(join(__dirname, 'tiktoken-node.android-arm-eabi.node'))
48 | try {
49 | if (localFileExisted) {
50 | nativeBinding = require('./tiktoken-node.android-arm-eabi.node')
51 | } else {
52 | nativeBinding = require('@anysphere/tiktoken-node-android-arm-eabi')
53 | }
54 | } catch (e) {
55 | loadError = e
56 | }
57 | break
58 | default:
59 | throw new Error(`Unsupported architecture on Android ${arch}`)
60 | }
61 | break
62 | case 'win32':
63 | switch (arch) {
64 | case 'x64':
65 | localFileExisted = existsSync(
66 | join(__dirname, 'tiktoken-node.win32-x64-msvc.node')
67 | )
68 | try {
69 | if (localFileExisted) {
70 | nativeBinding = require('./tiktoken-node.win32-x64-msvc.node')
71 | } else {
72 | nativeBinding = require('@anysphere/tiktoken-node-win32-x64-msvc')
73 | }
74 | } catch (e) {
75 | loadError = e
76 | }
77 | break
78 | case 'ia32':
79 | localFileExisted = existsSync(
80 | join(__dirname, 'tiktoken-node.win32-ia32-msvc.node')
81 | )
82 | try {
83 | if (localFileExisted) {
84 | nativeBinding = require('./tiktoken-node.win32-ia32-msvc.node')
85 | } else {
86 | nativeBinding = require('@anysphere/tiktoken-node-win32-ia32-msvc')
87 | }
88 | } catch (e) {
89 | loadError = e
90 | }
91 | break
92 | case 'arm64':
93 | localFileExisted = existsSync(
94 | join(__dirname, 'tiktoken-node.win32-arm64-msvc.node')
95 | )
96 | try {
97 | if (localFileExisted) {
98 | nativeBinding = require('./tiktoken-node.win32-arm64-msvc.node')
99 | } else {
100 | nativeBinding = require('@anysphere/tiktoken-node-win32-arm64-msvc')
101 | }
102 | } catch (e) {
103 | loadError = e
104 | }
105 | break
106 | default:
107 | throw new Error(`Unsupported architecture on Windows: ${arch}`)
108 | }
109 | break
110 | case 'darwin':
111 | localFileExisted = existsSync(join(__dirname, 'tiktoken-node.darwin-universal.node'))
112 | try {
113 | if (localFileExisted) {
114 | nativeBinding = require('./tiktoken-node.darwin-universal.node')
115 | } else {
116 | nativeBinding = require('@anysphere/tiktoken-node-darwin-universal')
117 | }
118 | break
119 | } catch {}
120 | switch (arch) {
121 | case 'x64':
122 | localFileExisted = existsSync(join(__dirname, 'tiktoken-node.darwin-x64.node'))
123 | try {
124 | if (localFileExisted) {
125 | nativeBinding = require('./tiktoken-node.darwin-x64.node')
126 | } else {
127 | nativeBinding = require('@anysphere/tiktoken-node-darwin-x64')
128 | }
129 | } catch (e) {
130 | loadError = e
131 | }
132 | break
133 | case 'arm64':
134 | localFileExisted = existsSync(
135 | join(__dirname, 'tiktoken-node.darwin-arm64.node')
136 | )
137 | try {
138 | if (localFileExisted) {
139 | nativeBinding = require('./tiktoken-node.darwin-arm64.node')
140 | } else {
141 | nativeBinding = require('@anysphere/tiktoken-node-darwin-arm64')
142 | }
143 | } catch (e) {
144 | loadError = e
145 | }
146 | break
147 | default:
148 | throw new Error(`Unsupported architecture on macOS: ${arch}`)
149 | }
150 | break
151 | case 'freebsd':
152 | if (arch !== 'x64') {
153 | throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
154 | }
155 | localFileExisted = existsSync(join(__dirname, 'tiktoken-node.freebsd-x64.node'))
156 | try {
157 | if (localFileExisted) {
158 | nativeBinding = require('./tiktoken-node.freebsd-x64.node')
159 | } else {
160 | nativeBinding = require('@anysphere/tiktoken-node-freebsd-x64')
161 | }
162 | } catch (e) {
163 | loadError = e
164 | }
165 | break
166 | case 'linux':
167 | switch (arch) {
168 | case 'x64':
169 | if (isMusl()) {
170 | localFileExisted = existsSync(
171 | join(__dirname, 'tiktoken-node.linux-x64-musl.node')
172 | )
173 | try {
174 | if (localFileExisted) {
175 | nativeBinding = require('./tiktoken-node.linux-x64-musl.node')
176 | } else {
177 | nativeBinding = require('@anysphere/tiktoken-node-linux-x64-musl')
178 | }
179 | } catch (e) {
180 | loadError = e
181 | }
182 | } else {
183 | localFileExisted = existsSync(
184 | join(__dirname, 'tiktoken-node.linux-x64-gnu.node')
185 | )
186 | try {
187 | if (localFileExisted) {
188 | nativeBinding = require('./tiktoken-node.linux-x64-gnu.node')
189 | } else {
190 | nativeBinding = require('@anysphere/tiktoken-node-linux-x64-gnu')
191 | }
192 | } catch (e) {
193 | loadError = e
194 | }
195 | }
196 | break
197 | case 'arm64':
198 | if (isMusl()) {
199 | localFileExisted = existsSync(
200 | join(__dirname, 'tiktoken-node.linux-arm64-musl.node')
201 | )
202 | try {
203 | if (localFileExisted) {
204 | nativeBinding = require('./tiktoken-node.linux-arm64-musl.node')
205 | } else {
206 | nativeBinding = require('@anysphere/tiktoken-node-linux-arm64-musl')
207 | }
208 | } catch (e) {
209 | loadError = e
210 | }
211 | } else {
212 | localFileExisted = existsSync(
213 | join(__dirname, 'tiktoken-node.linux-arm64-gnu.node')
214 | )
215 | try {
216 | if (localFileExisted) {
217 | nativeBinding = require('./tiktoken-node.linux-arm64-gnu.node')
218 | } else {
219 | nativeBinding = require('@anysphere/tiktoken-node-linux-arm64-gnu')
220 | }
221 | } catch (e) {
222 | loadError = e
223 | }
224 | }
225 | break
226 | case 'arm':
227 | localFileExisted = existsSync(
228 | join(__dirname, 'tiktoken-node.linux-arm-gnueabihf.node')
229 | )
230 | try {
231 | if (localFileExisted) {
232 | nativeBinding = require('./tiktoken-node.linux-arm-gnueabihf.node')
233 | } else {
234 | nativeBinding = require('@anysphere/tiktoken-node-linux-arm-gnueabihf')
235 | }
236 | } catch (e) {
237 | loadError = e
238 | }
239 | break
240 | default:
241 | throw new Error(`Unsupported architecture on Linux: ${arch}`)
242 | }
243 | break
244 | default:
245 | throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
246 | }
247 |
248 | if (!nativeBinding) {
249 | if (loadError) {
250 | throw loadError
251 | }
252 | throw new Error(`Failed to load native binding`)
253 | }
254 |
255 | const { SupportedEncoding, Tokenizer, SpecialTokenAction, SyncTokenizer, getTokenizer } = nativeBinding
256 |
257 | module.exports.SupportedEncoding = SupportedEncoding
258 | module.exports.Tokenizer = Tokenizer
259 | module.exports.SpecialTokenAction = SpecialTokenAction
260 | module.exports.SyncTokenizer = SyncTokenizer
261 | module.exports.getTokenizer = getTokenizer
262 |
--------------------------------------------------------------------------------
/tiktoken-node/npm/darwin-arm64/README.md:
--------------------------------------------------------------------------------
1 | # `@anysphere/tiktoken-node-darwin-arm64`
2 |
3 | This is the **aarch64-apple-darwin** binary for `@anysphere/tiktoken-node`
4 |
--------------------------------------------------------------------------------
/tiktoken-node/npm/darwin-arm64/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@anysphere/tiktoken-node-darwin-arm64",
3 | "version": "0.2.1",
4 | "os": [
5 | "darwin"
6 | ],
7 | "cpu": [
8 | "arm64"
9 | ],
10 | "main": "tiktoken-node.darwin-arm64.node",
11 | "files": [
12 | "tiktoken-node.darwin-arm64.node"
13 | ],
14 | "license": "MIT",
15 | "engines": {
16 | "node": ">= 10"
17 | }
18 | }
--------------------------------------------------------------------------------
/tiktoken-node/npm/darwin-x64/README.md:
--------------------------------------------------------------------------------
1 | # `@anysphere/tiktoken-node-darwin-x64`
2 |
3 | This is the **x86_64-apple-darwin** binary for `@anysphere/tiktoken-node`
4 |
--------------------------------------------------------------------------------
/tiktoken-node/npm/darwin-x64/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@anysphere/tiktoken-node-darwin-x64",
3 | "version": "0.2.1",
4 | "os": [
5 | "darwin"
6 | ],
7 | "cpu": [
8 | "x64"
9 | ],
10 | "main": "tiktoken-node.darwin-x64.node",
11 | "files": [
12 | "tiktoken-node.darwin-x64.node"
13 | ],
14 | "license": "MIT",
15 | "engines": {
16 | "node": ">= 10"
17 | }
18 | }
--------------------------------------------------------------------------------
/tiktoken-node/npm/linux-arm64-gnu/README.md:
--------------------------------------------------------------------------------
1 | # `@anysphere/tiktoken-node-linux-arm64-gnu`
2 |
3 | This is the **aarch64-unknown-linux-gnu** binary for `@anysphere/tiktoken-node`
4 |
--------------------------------------------------------------------------------
/tiktoken-node/npm/linux-arm64-gnu/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@anysphere/tiktoken-node-linux-arm64-gnu",
3 | "version": "0.2.1",
4 | "os": [
5 | "linux"
6 | ],
7 | "cpu": [
8 | "arm64"
9 | ],
10 | "main": "tiktoken-node.linux-arm64-gnu.node",
11 | "files": [
12 | "tiktoken-node.linux-arm64-gnu.node"
13 | ],
14 | "license": "MIT",
15 | "engines": {
16 | "node": ">= 10"
17 | },
18 | "libc": [
19 | "glibc"
20 | ]
21 | }
--------------------------------------------------------------------------------
/tiktoken-node/npm/linux-x64-gnu/README.md:
--------------------------------------------------------------------------------
1 | # `@anysphere/tiktoken-node-linux-x64-gnu`
2 |
3 | This is the **x86_64-unknown-linux-gnu** binary for `@anysphere/tiktoken-node`
4 |
--------------------------------------------------------------------------------
/tiktoken-node/npm/linux-x64-gnu/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@anysphere/tiktoken-node-linux-x64-gnu",
3 | "version": "0.2.1",
4 | "os": [
5 | "linux"
6 | ],
7 | "cpu": [
8 | "x64"
9 | ],
10 | "main": "tiktoken-node.linux-x64-gnu.node",
11 | "files": [
12 | "tiktoken-node.linux-x64-gnu.node"
13 | ],
14 | "license": "MIT",
15 | "engines": {
16 | "node": ">= 10"
17 | },
18 | "libc": [
19 | "glibc"
20 | ]
21 | }
--------------------------------------------------------------------------------
/tiktoken-node/npm/win32-arm64-msvc/README.md:
--------------------------------------------------------------------------------
1 | # `@anysphere/tiktoken-node-win32-arm64-msvc`
2 |
3 | This is the **aarch64-pc-windows-msvc** binary for `@anysphere/tiktoken-node`
4 |
--------------------------------------------------------------------------------
/tiktoken-node/npm/win32-arm64-msvc/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@anysphere/tiktoken-node-win32-arm64-msvc",
3 | "version": "0.2.1",
4 | "os": [
5 | "win32"
6 | ],
7 | "cpu": [
8 | "arm64"
9 | ],
10 | "main": "tiktoken-node.win32-arm64-msvc.node",
11 | "files": [
12 | "tiktoken-node.win32-arm64-msvc.node"
13 | ],
14 | "license": "MIT",
15 | "engines": {
16 | "node": ">= 10"
17 | }
18 | }
--------------------------------------------------------------------------------
/tiktoken-node/npm/win32-x64-msvc/README.md:
--------------------------------------------------------------------------------
1 | # `@anysphere/tiktoken-node-win32-x64-msvc`
2 |
3 | This is the **x86_64-pc-windows-msvc** binary for `@anysphere/tiktoken-node`
4 |
--------------------------------------------------------------------------------
/tiktoken-node/npm/win32-x64-msvc/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@anysphere/tiktoken-node-win32-x64-msvc",
3 | "version": "0.2.1",
4 | "os": [
5 | "win32"
6 | ],
7 | "cpu": [
8 | "x64"
9 | ],
10 | "main": "tiktoken-node.win32-x64-msvc.node",
11 | "files": [
12 | "tiktoken-node.win32-x64-msvc.node"
13 | ],
14 | "license": "MIT",
15 | "engines": {
16 | "node": ">= 10"
17 | }
18 | }
--------------------------------------------------------------------------------
/tiktoken-node/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@anysphere/tiktoken-node",
3 | "version": "0.2.1",
4 | "main": "index.js",
5 | "types": "index.d.ts",
6 | "files": [
7 | "index.js",
8 | "tiktoken-node.node",
9 | "tiktoken-node.*.node",
10 | "package.json"
11 | ],
12 | "napi": {
13 | "name": "tiktoken-node",
14 | "triples": {
15 | "additional": [
16 | "aarch64-apple-darwin",
17 | "aarch64-pc-windows-msvc",
18 | "aarch64-unknown-linux-gnu"
19 | ]
20 | }
21 | },
22 | "devDependencies": {
23 | "@napi-rs/cli": "^2.16.2",
24 | "ava": "^5.1.1",
25 | "wireit": "^0.14.0"
26 | },
27 | "engines": {
28 | "node": ">= 18.15.0"
29 | },
30 | "scripts": {
31 | "artifacts": "napi artifacts",
32 | "build": "wireit",
33 | "build:debug": "napi build --platform",
34 | "prepublishOnly": "napi prepublish -t npm --skip-gh-release",
35 | "universal": "napi universal",
36 | "version": "napi version"
37 | },
38 | "wireit": {
39 | "build": {
40 | "clean": "if-file-deleted",
41 | "command": "napi build --platform --release",
42 | "files": [
43 | "src/**",
44 | "Cargo.toml",
45 | "Cargo.lock",
46 | "build.rs"
47 | ],
48 | "output": [
49 | "tiktoken-node.*.node"
50 | ]
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/tiktoken-node/src/lib.rs:
--------------------------------------------------------------------------------
1 | use anyhow::Context;
2 | use async_channel::{bounded, Receiver, Sender};
3 | use napi::bindgen_prelude::create_custom_tokio_runtime;
4 | use napi::bindgen_prelude::Error;
5 | use napi_derive::napi;
6 | use once_cell::sync::Lazy;
7 | use tiktoken::EncodingFactoryError;
8 | use tokio::runtime::Builder;
9 |
10 | use std::collections::HashMap;
11 | use std::sync::Arc;
12 |
13 | // we use the actor pattern to have good cache locality
14 | // this means that no tokenization requests will ever run in parallel, but i think that's almost certainly fine
15 | use napi::tokio::sync::oneshot;
16 |
17 | static TOKENIZER: Lazy> =
18 | Lazy::new(|| Tokenizer::new().map_err(|e| Error::from_reason(e.to_string())));
19 |
20 | static ENCODINGS: Lazy, EncodingFactoryError>> = Lazy::new(|| {
21 | Ok(Arc::new(Encodings {
22 | cl100k_encoding: tiktoken::EncodingFactory::cl100k_im()?,
23 | llama3_encoding: tiktoken::EncodingFactory::llama3()?,
24 | o200k_encoding: tiktoken::EncodingFactory::o200k_im()?,
25 | codestral_encoding: tiktoken::EncodingFactory::codestral()?,
26 | }))
27 | });
28 |
29 | #[napi]
30 | pub enum SupportedEncoding {
31 | Cl100k = 0,
32 | Llama3 = 1,
33 | O200k = 2,
34 | Codestral = 3,
35 | }
36 |
37 | struct TokenizerActor {
38 | receiver: Receiver,
39 | encodings: Arc,
40 | }
41 |
42 | struct Encodings {
43 | cl100k_encoding: tiktoken::Encoding,
44 | llama3_encoding: tiktoken::Encoding,
45 | o200k_encoding: tiktoken::Encoding,
46 | codestral_encoding: tiktoken::Encoding,
47 | }
48 |
49 | enum TokenizerMessage {
50 | ExactNumTokens {
51 | respond_to: oneshot::Sender>,
52 | text: String,
53 | encoding: SupportedEncoding,
54 | special_token_handling: tiktoken::SpecialTokenHandling,
55 | },
56 | EncodeTokens {
57 | respond_to: oneshot::Sender>>,
58 | text: String,
59 | encoding: SupportedEncoding,
60 | special_token_handling: tiktoken::SpecialTokenHandling,
61 | },
62 | // always encodes all special tokens!
63 | EncodeSingleToken {
64 | respond_to: oneshot::Sender>,
65 | bytes: Vec,
66 | encoding: SupportedEncoding,
67 | },
68 | DecodeTokens {
69 | respond_to: oneshot::Sender>,
70 | tokens: Vec,
71 | encoding: SupportedEncoding,
72 | },
73 | DecodeTokenBytes {
74 | respond_to: oneshot::Sender>>,
75 | token: u32,
76 | encoding: SupportedEncoding,
77 | },
78 | ApproximateNumTokens {
79 | respond_to: oneshot::Sender>,
80 | text: String,
81 | encoding: SupportedEncoding,
82 | replace_spaces_with_lower_one_eighth_block: bool,
83 | },
84 | }
85 |
86 | impl TokenizerActor {
87 | fn new(receiver: Receiver, encodings: Arc) -> Self {
88 | TokenizerActor { receiver, encodings }
89 | }
90 |
91 | fn get_encoding(&self, encoding: SupportedEncoding) -> &tiktoken::Encoding {
92 | match encoding {
93 | SupportedEncoding::Cl100k => &self.encodings.cl100k_encoding,
94 | SupportedEncoding::Llama3 => &self.encodings.llama3_encoding,
95 | SupportedEncoding::O200k => &self.encodings.o200k_encoding,
96 | SupportedEncoding::Codestral => &self.encodings.codestral_encoding,
97 | }
98 | }
99 |
100 | fn handle_message(&self, msg: TokenizerMessage) {
101 | match msg {
102 | TokenizerMessage::ExactNumTokens { respond_to, text, encoding, special_token_handling } => {
103 | let tokens = self
104 | .get_encoding(encoding)
105 | .encode(&text, &special_token_handling)
106 | .context("Error encoding string");
107 |
108 | let num_tokens = match tokens {
109 | Ok(t) => Ok(t.len() as i32),
110 | Err(e) => Err(e),
111 | };
112 |
113 | // The `let _ =` ignores any errors when sending.
114 | let _ = respond_to.send(num_tokens);
115 | }
116 | TokenizerMessage::EncodeTokens { respond_to, text, encoding, special_token_handling } => {
117 | let tokens = self
118 | .get_encoding(encoding)
119 | .encode(&text, &special_token_handling)
120 | .context("Error encoding string");
121 |
122 | let tokens = match tokens {
123 | Ok(t) => Ok(t.into_iter().map(|t| t as u32).collect()),
124 | Err(e) => Err(e),
125 | };
126 |
127 | // The `let _ =` ignores any errors when sending.
128 | let _ = respond_to.send(tokens);
129 | }
130 | TokenizerMessage::EncodeSingleToken { respond_to, bytes, encoding } => {
131 | let token = self.get_encoding(encoding).encode_single_token_bytes(&bytes);
132 |
133 | let token = match token {
134 | Ok(t) => Ok(t as u32),
135 | Err(_) => Err(anyhow::anyhow!("Token not recognized")),
136 | };
137 |
138 | // The `let _ =` ignores any errors when sending.
139 | let _ = respond_to.send(token);
140 | }
141 | TokenizerMessage::DecodeTokenBytes { respond_to, token, encoding } => {
142 | let bytes = self.get_encoding(encoding).decode_single_token_bytes(token as usize);
143 | let bytes = match bytes {
144 | Ok(b) => Ok(b),
145 | Err(e) => Err(anyhow::anyhow!(e)),
146 | };
147 | let _ = respond_to.send(bytes);
148 | }
149 | TokenizerMessage::DecodeTokens { respond_to, tokens, encoding } => {
150 | let text = self
151 | .get_encoding(encoding)
152 | .decode(&tokens.into_iter().map(|t| t as usize).collect::>());
153 |
154 | // The `let _ =` ignores any errors when sending.
155 | let _ = respond_to.send(Ok(text));
156 | }
157 | TokenizerMessage::ApproximateNumTokens {
158 | respond_to,
159 | text,
160 | encoding,
161 | replace_spaces_with_lower_one_eighth_block,
162 | } => {
163 | let tokens = self.get_encoding(encoding).estimate_num_tokens_no_special_tokens_fast(
164 | &text,
165 | replace_spaces_with_lower_one_eighth_block,
166 | );
167 |
168 | // The `let _ =` ignores any errors when sending.
169 | let _ = respond_to.send(Ok(tokens as i32));
170 | }
171 | }
172 | }
173 | }
174 |
175 | fn run_tokenizer_actor(actor: TokenizerActor) {
176 | while let Ok(msg) = actor.receiver.recv_blocking() {
177 | actor.handle_message(msg);
178 | }
179 | }
180 |
181 | #[napi]
182 | #[derive(Clone)]
183 | pub struct Tokenizer {
184 | sender: Sender,
185 | }
186 |
187 | #[napi]
188 | pub enum SpecialTokenAction {
189 | /// The special token is forbidden. If it is included in the string, an error will be returned.
190 | Forbidden = 0,
191 | /// The special token is tokenized as normal text.
192 | NormalText = 1,
193 | /// The special token is treated as the special token it is. If this is applied to a specific text and the text is NOT a special token then an error will be returned. If it is the default action no error will be returned, don't worry.
194 | Special = 2,
195 | }
196 |
197 | impl SpecialTokenAction {
198 | pub fn to_tiktoken(&self) -> tiktoken::SpecialTokenAction {
199 | match self {
200 | SpecialTokenAction::Forbidden => tiktoken::SpecialTokenAction::Forbidden,
201 | SpecialTokenAction::NormalText => tiktoken::SpecialTokenAction::NormalText,
202 | SpecialTokenAction::Special => tiktoken::SpecialTokenAction::Special,
203 | }
204 | }
205 | }
206 |
207 | #[napi]
208 | impl Tokenizer {
209 | pub fn new() -> Result {
210 | let (sender, receiver) = bounded(256);
211 | for i in 0..4 {
212 | let actor = TokenizerActor::new(receiver.clone(), ENCODINGS.clone().unwrap());
213 | std::thread::Builder::new()
214 | .name(format!("tokenizer-actor-{}", i))
215 | .spawn(move || run_tokenizer_actor(actor))
216 | .unwrap();
217 | }
218 |
219 | Ok(Self { sender })
220 | }
221 |
222 | #[napi]
223 | pub async fn exact_num_tokens_no_special_tokens(
224 | &self,
225 | text: String,
226 | encoding: SupportedEncoding,
227 | ) -> Result {
228 | let (send, recv) = oneshot::channel();
229 | let msg = TokenizerMessage::ExactNumTokens {
230 | respond_to: send,
231 | text,
232 | encoding,
233 | special_token_handling: tiktoken::SpecialTokenHandling {
234 | // no special tokens!! everything is normal text
235 | // this is how tokenization is handled in the chat model api
236 | default: tiktoken::SpecialTokenAction::NormalText,
237 | ..Default::default()
238 | },
239 | };
240 |
241 | // ignore errors since it can only mean the channel is closed, which will be caught in the recv below
242 | let _ = self.sender.send(msg).await;
243 | match recv.await {
244 | Ok(result) => result.map_err(|e| Error::from_reason(e.to_string())),
245 | Err(e) => Err(Error::from_reason(format!("Actor task has been killed: {}", e.to_string()))),
246 | }
247 | }
248 |
249 | #[napi]
250 | pub async fn exact_num_tokens(
251 | &self,
252 | text: String,
253 | encoding: SupportedEncoding,
254 | special_token_default_action: SpecialTokenAction,
255 | special_token_overrides: HashMap,
256 | ) -> Result {
257 | let (send, recv) = oneshot::channel();
258 | let msg = TokenizerMessage::ExactNumTokens {
259 | respond_to: send,
260 | text,
261 | encoding,
262 | special_token_handling: tiktoken::SpecialTokenHandling {
263 | // no special tokens!! everything is normal text
264 | // this is how tokenization is handled in the chat model api
265 | default: special_token_default_action.to_tiktoken(),
266 | overrides: special_token_overrides.into_iter().map(|(k, v)| (k, v.to_tiktoken())).collect(),
267 | },
268 | };
269 |
270 | // ignore errors since it can only mean the channel is closed, which will be caught in the recv below
271 | let _ = self.sender.send(msg).await;
272 | match recv.await {
273 | Ok(result) => result.map_err(|e| Error::from_reason(e.to_string())),
274 | Err(e) => Err(Error::from_reason(format!("Actor task has been killed: {}", e.to_string()))),
275 | }
276 | }
277 |
278 | #[napi]
279 | pub async fn encode_cl100k_no_special_tokens(&self, text: String) -> Result, Error> {
280 | let (send, recv) = oneshot::channel();
281 | let msg = TokenizerMessage::EncodeTokens {
282 | respond_to: send,
283 | text,
284 | encoding: SupportedEncoding::Cl100k,
285 | special_token_handling: tiktoken::SpecialTokenHandling {
286 | // no special tokens!! everything is normal text
287 | // this is how tokenization is handled in the chat model api
288 | default: tiktoken::SpecialTokenAction::NormalText,
289 | ..Default::default()
290 | },
291 | };
292 |
293 | // ignore errors since it can only mean the channel is closed, which will be caught in the recv below
294 | let _ = self.sender.send(msg).await;
295 | match recv.await {
296 | Ok(result) => result.map_err(|e| Error::from_reason(e.to_string())),
297 | Err(e) => Err(Error::from_reason(format!("Actor task has been killed: {}", e.to_string()))),
298 | }
299 | }
300 |
301 | #[napi]
302 | pub async fn approx_num_tokens(
303 | &self,
304 | text: String,
305 | encoding: SupportedEncoding,
306 | replace_spaces_with_lower_one_eighth_block: bool,
307 | ) -> Result {
308 | let (send, recv) = oneshot::channel();
309 | let msg = TokenizerMessage::ApproximateNumTokens {
310 | respond_to: send,
311 | text,
312 | encoding,
313 | replace_spaces_with_lower_one_eighth_block,
314 | };
315 |
316 | // ignore errors since it can only mean the channel is closed, which will be caught in the recv below
317 | let _ = self.sender.send(msg).await;
318 | match recv.await {
319 | Ok(result) => result.map_err(|e| Error::from_reason(e.to_string())),
320 | Err(e) => Err(Error::from_reason(format!("Actor task has been killed: {}", e.to_string()))),
321 | }
322 | }
323 |
324 | #[napi]
325 | pub async fn encode(
326 | &self,
327 | text: String,
328 | encoding: SupportedEncoding,
329 | special_token_default_action: SpecialTokenAction,
330 | special_token_overrides: HashMap,
331 | ) -> Result, Error> {
332 | let (send, recv) = oneshot::channel();
333 | let msg = TokenizerMessage::EncodeTokens {
334 | respond_to: send,
335 | text,
336 | encoding,
337 | special_token_handling: tiktoken::SpecialTokenHandling {
338 | // no special tokens!! everything is normal text
339 | // this is how tokenization is handled in the chat model api
340 | default: special_token_default_action.to_tiktoken(),
341 | overrides: special_token_overrides.into_iter().map(|(k, v)| (k, v.to_tiktoken())).collect(),
342 | },
343 | };
344 |
345 | // ignore errors since it can only mean the channel is closed, which will be caught in the recv below
346 | let _ = self.sender.send(msg).await;
347 | match recv.await {
348 | Ok(result) => result.map_err(|e| Error::from_reason(e.to_string())),
349 | Err(e) => Err(Error::from_reason(format!("Actor task has been killed: {}", e.to_string()))),
350 | }
351 | }
352 |
353 | #[napi]
354 | pub async fn encode_single_token(
355 | &self,
356 | bytes: napi::bindgen_prelude::Uint8Array,
357 | encoding: SupportedEncoding,
358 | ) -> Result {
359 | let (send, recv) = oneshot::channel();
360 | let msg =
361 | TokenizerMessage::EncodeSingleToken { respond_to: send, bytes: bytes.to_vec(), encoding };
362 |
363 | // ignore errors since it can only mean the channel is closed, which will be caught in the recv below
364 | let _ = self.sender.send(msg).await;
365 | match recv.await {
366 | Ok(result) => result.map_err(|e| Error::from_reason(e.to_string())),
367 | Err(e) => Err(Error::from_reason(format!("Actor task has been killed: {}", e.to_string()))),
368 | }
369 | }
370 | #[napi]
371 | pub async fn decode_byte(
372 | &self,
373 | token: u32,
374 | encoding: SupportedEncoding,
375 | ) -> Result {
376 | let (send, recv) = oneshot::channel();
377 | let msg = TokenizerMessage::DecodeTokenBytes { respond_to: send, token, encoding };
378 |
379 | // ignore errors since it can only mean the channel is closed, which will be caught in the recv below
380 | let _ = self.sender.send(msg).await;
381 | match recv.await {
382 | Ok(result) => result
383 | .map_err(|e| napi::Error::from_reason(e.to_string()))
384 | .map(|v| napi::bindgen_prelude::Uint8Array::new(v.into())),
385 | Err(e) => Err(Error::from_reason(format!("Actor task has been killed: {}", e.to_string()))),
386 | }
387 | }
388 |
389 | #[napi]
390 | pub async fn decode(
391 | &self,
392 | encoded_tokens: Vec,
393 | encoding: SupportedEncoding,
394 | ) -> Result {
395 | let (send, recv) = oneshot::channel();
396 | let msg = TokenizerMessage::DecodeTokens { respond_to: send, tokens: encoded_tokens, encoding };
397 |
398 | // ignore errors since it can only mean the channel is closed, which will be caught in the recv below
399 | let _ = self.sender.send(msg).await;
400 | match recv.await {
401 | Ok(result) => result.map_err(|e| Error::from_reason(e.to_string())),
402 | Err(e) => Err(Error::from_reason(format!("Actor task has been killed: {}", e.to_string()))),
403 | }
404 | }
405 | }
406 |
407 | #[napi]
408 | pub struct SyncTokenizer {
409 | encodings: Arc,
410 | }
411 |
412 | #[napi]
413 | impl SyncTokenizer {
414 | #[napi(constructor)]
415 | pub fn new() -> Result {
416 | Ok(Self { encodings: ENCODINGS.clone().unwrap() })
417 | }
418 |
419 | #[napi]
420 | pub fn approx_num_tokens(&self, text: String, encoding: SupportedEncoding) -> Result {
421 | Ok(self.get_encoding(encoding).estimate_num_tokens_no_special_tokens_fast(&text, false) as i32)
422 | }
423 |
424 | fn get_encoding(&self, encoding: SupportedEncoding) -> &tiktoken::Encoding {
425 | match encoding {
426 | SupportedEncoding::Cl100k => &self.encodings.cl100k_encoding,
427 | SupportedEncoding::Llama3 => &self.encodings.llama3_encoding,
428 | SupportedEncoding::O200k => &self.encodings.o200k_encoding,
429 | SupportedEncoding::Codestral => &self.encodings.codestral_encoding,
430 | }
431 | }
432 | }
433 |
434 | #[napi]
435 | pub fn get_tokenizer() -> Result {
436 | TOKENIZER.clone()
437 | }
438 |
439 | #[allow(clippy::expect_used)]
440 | #[napi::module_init]
441 | fn init() {
442 | let rt = Builder::new_multi_thread()
443 | .enable_all()
444 | .worker_threads(2)
445 | .thread_name("tokenizer-tokio")
446 | .build()
447 | .expect("Failed to build tokio runtime");
448 | create_custom_tokio_runtime(rt);
449 | }
450 |
451 | #[cfg(test)]
452 | mod tests {
453 | use super::*;
454 |
455 | #[tokio::test]
456 | async fn test_num_tokens() {
457 | let tokenizer = get_tokenizer().unwrap();
458 | let num_tokens = tokenizer
459 | .exact_num_tokens_no_special_tokens("hello, world".to_string(), SupportedEncoding::Cl100k)
460 | .await
461 | .unwrap();
462 | assert_eq!(num_tokens, 3);
463 | }
464 | }
465 |
--------------------------------------------------------------------------------