├── .git-blame-ignore-revs
├── .gitignore
├── .npmignore
├── .prettierignore
├── .prettierrc.json
├── .vscode-test.mjs
├── .vscode
├── extensions.json
├── launch.json
└── settings.json
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── SECURITY.md
├── SUPPORT.md
├── build
├── base.yml
├── build-tracer.ts
├── postcompile.ts
└── postinstall.ts
├── examples
├── README.md
├── file-contents.tsx
├── history.tsx
├── package-lock.json
├── package.json
└── tsconfig.json
├── package-lock.json
├── package.json
├── src
├── base
│ ├── htmlTracer.ts
│ ├── htmlTracerTypes.ts
│ ├── index.ts
│ ├── jsonTypes.ts
│ ├── materialized.ts
│ ├── once.ts
│ ├── output
│ │ ├── mode.ts
│ │ ├── openaiConvert.ts
│ │ ├── openaiTypes.ts
│ │ ├── rawTypes.ts
│ │ └── vscode.ts
│ ├── promptElement.ts
│ ├── promptElements.tsx
│ ├── promptRenderer.ts
│ ├── results.ts
│ ├── test
│ │ ├── elements.test.tsx
│ │ ├── materialized.test.ts
│ │ ├── renderer.bench.tsx
│ │ ├── renderer.test.tsx
│ │ └── testUtils.ts
│ ├── tokenizer
│ │ ├── cl100kBaseTokenizer.ts
│ │ ├── cl100k_base.tiktoken
│ │ └── tokenizer.ts
│ ├── tracer.ts
│ ├── tsx-globals.ts
│ ├── tsx.ts
│ ├── types.ts
│ ├── util
│ │ ├── arrays.ts
│ │ ├── assert.ts
│ │ └── vs
│ │ │ ├── common
│ │ │ ├── charCode.ts
│ │ │ ├── marshallingIds.ts
│ │ │ ├── path.ts
│ │ │ ├── platform.ts
│ │ │ ├── process.ts
│ │ │ └── uri.ts
│ │ │ └── nls.ts
│ ├── vscode.d.ts
│ └── vscodeTypes.d.ts
└── tracer
│ ├── hooks.ts
│ ├── i18n.tsx
│ ├── index.css
│ ├── index.tsx
│ ├── node.tsx
│ └── tsconfig.json
└── tsconfig.json
/.git-blame-ignore-revs:
--------------------------------------------------------------------------------
1 | a5803dad4d5e70d74d162e1f423223d68e975587
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 |
4 | # Logs
5 | logs
6 | *.log
7 | npm-debug.log*
8 | yarn-debug.log*
9 | yarn-error.log*
10 | lerna-debug.log*
11 | .pnpm-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 | # Snowpack dependency directory (https://snowpack.dev/)
49 | web_modules/
50 |
51 | # TypeScript cache
52 | *.tsbuildinfo
53 |
54 | # Optional npm cache directory
55 | .npm
56 |
57 | # Optional eslint cache
58 | .eslintcache
59 |
60 | # Optional stylelint cache
61 | .stylelintcache
62 |
63 | # Microbundle cache
64 | .rpt2_cache/
65 | .rts2_cache_cjs/
66 | .rts2_cache_es/
67 | .rts2_cache_umd/
68 |
69 | # Optional REPL history
70 | .node_repl_history
71 |
72 | # Output of 'npm pack'
73 | *.tgz
74 |
75 | # Yarn Integrity file
76 | .yarn-integrity
77 |
78 | # dotenv environment variable files
79 | .env
80 | .env.development.local
81 | .env.test.local
82 | .env.production.local
83 | .env.local
84 |
85 | # parcel-bundler cache (https://parceljs.org/)
86 | .cache
87 | .parcel-cache
88 |
89 | # Next.js build output
90 | .next
91 | out
92 |
93 | # Nuxt.js build / generate output
94 | .nuxt
95 | dist
96 |
97 | # Gatsby files
98 | .cache/
99 | # Comment in the public line in if your project uses Gatsby and not Next.js
100 | # https://nextjs.org/blog/next-9-1#public-directory-support
101 | # public
102 |
103 | # vuepress build output
104 | .vuepress/dist
105 |
106 | # vuepress v2.x temp and cache directory
107 | .temp
108 | .cache
109 |
110 | # Docusaurus cache and generated files
111 | .docusaurus
112 |
113 | # Serverless directories
114 | .serverless/
115 |
116 | # FuseBox cache
117 | .fusebox/
118 |
119 | # DynamoDB Local files
120 | .dynamodb/
121 |
122 | # TernJS port file
123 | .tern-port
124 |
125 | # Stores VSCode versions used for testing VSCode extensions
126 | .vscode-test
127 |
128 | # yarn v2
129 | .yarn/cache
130 | .yarn/unplugged
131 | .yarn/build-state.yml
132 | .yarn/install-state.gz
133 | .pnp.*
134 |
135 | src/base/htmlTracerSrc.ts
136 |
137 | *.cpuprofile
138 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src/
2 | build/
3 | .vscode/
4 | .gitignore
5 | .prettierignore
6 | .prettierrc.json
7 | .vscode-test.mjs
8 | .vscode-test/
9 | *.tgz
10 | tsconfig.json
11 | *.md
12 | dist/base/test/
13 | *.map
14 | dist/base/tokenizer/cl100kBaseTokenizer*.*
15 | dist/base/tokenizer/cl100k_base.tiktoken
16 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist/**
2 | .vscode/**
3 | .vscode-test/**
4 | .git-blame-ignore-revs
5 | **/*.js
6 | src/base/util/vs/**
7 | SECURITY.md
8 | SUPPORT.md
9 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": true,
3 | "arrowParens": "avoid",
4 | "printWidth": 100,
5 | "singleQuote": true
6 | }
7 |
--------------------------------------------------------------------------------
/.vscode-test.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig } from '@vscode/test-cli';
2 |
3 | export default defineConfig({
4 | files: 'dist/base/test/*.test.js',
5 | version: 'insiders',
6 | launchArgs: ['--disable-extensions', '--profile-temp'],
7 | mocha: {
8 | ui: 'tdd',
9 | color: true,
10 | forbidOnly: !!process.env.CI,
11 | timeout: 5000,
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["esbenp.prettier-vscode"]
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "configurations": [
3 | {
4 | "name": "Extension tests",
5 | "type": "extensionHost",
6 | "request": "launch",
7 | "testConfiguration": "${workspaceFolder}/.vscode-test.mjs",
8 | "sourceMaps": true,
9 | "smartStep": true,
10 | "internalConsoleOptions": "openOnSessionStart",
11 | "outFiles": [
12 | "${workspaceFolder}/dist/**/*.js",
13 | "!**/node_modules/**"
14 | ],
15 | },
16 | ]
17 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "git.branchProtection": ["main"],
3 | "git.branchProtectionPrompt": "alwaysCommitToNewBranch",
4 | "files.trimTrailingWhitespace": true,
5 | "editor.defaultFormatter": "esbenp.prettier-vscode",
6 | "editor.formatOnSave": true,
7 | "extension-test-runner.extractSettings": {
8 | "suite": ["suite"],
9 | "test": ["test"],
10 | "extractWith": "syntax"
11 | },
12 | "extension-test-runner.debugOptions": {
13 | "outFiles": ["${workspaceFolder}/dist/**/*.js"]
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 0.3.0-alpha.7
4 |
5 | - **feat:** add a `passPriority` attribute for logical wrapper elements
6 | - **fix:** tool calls not being visible in tracer
7 |
8 | ## 0.3.0-alpha.6
9 |
10 | - **fix:** containers without priority set should have max priority
11 |
12 | ## 0.3.0-alpha.5
13 |
14 | - **feat:** add `Expandable` elements to the renderer. See the [readme](./README.md#expandable-text) for details.
15 |
16 | ## 0.3.0-alpha.4
17 |
18 | - **feat:** enhance the `HTMLTracer` to allow consumers to visualize element pruning order
19 |
20 | ## 0.3.0-alpha.3
21 |
22 | - **feat:** add `MetadataMap.getAll()`
23 | - **fix:** don't drop empty messages that have tool calls
24 |
25 | ## 0.3.0-alpha.2
26 |
27 | - **fix:** update to match proposed VS Code tools API
28 |
29 | ## 0.3.0-alpha.1
30 |
31 | - ⚠️ **breaking refactor:** `priority` is now local within tree elements
32 |
33 | Previously, in order to calculate elements to be pruned if the token budget was exceeded, all text in the prompt was collected into a flat list and lowest `priority` elements were removed first; the priority value was global. However, this made composition difficult because all elements needed to operate within the domain of priorities provided by the prompt.
34 |
35 | In this version, priorities are handled as a tree. To prune elements, the lowest priority element is selected among siblings recursively, until a leaf node is selected and removed. Take the tree of elements:
36 |
37 | ```
38 | A[priority=1]
39 | A1[priority=50]
40 | A2[priority=200]
41 | B[priority=2]
42 | B1[priority=0]
43 | B2[priority=100]
44 | ```
45 |
46 | The pruning order is now `A1`, `A2`, `B1`, then `B2`. Previously it would have been `B1`, `A1`, `B2`, `A2`. In a tiebreaker between two sibling elements with the same priority, the element with the lowest-priority direct child is chosen for pruning. For example, in the case
47 |
48 | ```
49 | A
50 | A1[priority=50]
51 | A2[priority=200]
52 | B
53 | B1[priority=0]
54 | B2[priority=100]
55 | ```
56 |
57 | The pruning order is `B1`, `A1`, `B2`, `A2`.
58 |
59 | - **feature:** new `LegacyPrioritization` element
60 |
61 | There is a new `LegacyPrioritization` which can be used to wrap other elements in order to fall-back to the classic global prioritization model. This is a stepping stone and will be removed in future versions.
62 |
63 | ```tsx
64 |
65 | ...
66 | ...
67 |
68 | ```
69 |
70 | - **feature:** new `Chunk` element
71 |
72 | The new `Chunk` element can be used to group elements that should either be all retained, or all pruned. This is similar to a `TextChunk`, but it also allows for extrinsic children. For example, you might wrap content like this to ensure the `FileLink` isn't present without its `FileContents` and vise-versa:
73 |
74 | ```tsx
75 |
76 | The file I'm editing is:
77 |
78 |
79 |
80 | ```
81 |
82 | - **feature:** `local` metadata
83 |
84 | Previously, metadata in a prompt was always globally available regardless of where it was positioned and whether the position it was in survived pruning. There is a new `local` flag you can apply such that the metadata is only retained if the element it's in was included in the prompt:
85 |
86 | ```tsx
87 |
88 |
89 | Hello world!
90 |
91 | ```
92 |
93 | Internally, references are now represented as local metadata.
94 |
95 | - ⚠️ **breaking refactor:** metadata is now returned from `render()`
96 |
97 | Rather than being a separate property on the renderer, metadata is now returned in the `RenderPromptResult`.
98 |
99 | - **refactor:** whitespace tightening
100 |
101 | The new tree-based rendering allows us to be slightly smarter in how line breaks are inserted and retained. The following rules are in place:
102 |
103 | - Line breaks ` ` always ensure that there's a line break at the location.
104 | - The contents of any tag will add a line break before them if one does not exist (between `HiBye`, for example.)
105 | - A line break is not automatically inserted for siblings directly following other text (for example, there is no line break in `Check out `)
106 | - Leading and trailing whitespace is removed from chat messages.
107 |
108 | This may result in some churn in existing elements.
109 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Microsoft Open Source Code of Conduct
2 |
3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
4 |
5 | Resources:
6 |
7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)
8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Microsoft Corporation.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # @vscode/prompt-tsx
2 |
3 | This library enables you to declare prompts using TSX when you develop VS Code extensions that integrate with Copilot Chat. To learn more, check out our [documentation](https://code.visualstudio.com/api/extension-guides/chat) or fork our quickstart [sample](https://github.com/microsoft/vscode-extension-samples/tree/main/chat-sample).
4 |
5 | ## Why TSX?
6 |
7 | As AI engineers, our products communicate with large language models using chat messages composed of text prompts. While developing Copilot Chat, we've found that composing prompts with just bare strings is unwieldy and frustrating.
8 |
9 | Some of the challenges we ran into include:
10 |
11 | 1. We used either programmatic string concatenation or template strings for composing prompts. Programmatic string concatenation made prompt text increasingly difficult to read, maintain, and update over time. Template string-based prompts were rigid and prone to issues like unnecessary whitespace.
12 | 2. In both cases, our prompts and RAG-generated context could not adapt to changing context window constraints as we upgraded our models. Prompts are ultimately bare strings, which makes them hard to edit once they are composed via string concatenation.
13 |
14 | To improve the developer experience for writing prompts in language model-based VS Code extensions like Copilot Chat, we built the TSX-based prompt renderer that we've extracted in this library. This has enabled us to compose expressive, flexible prompts that cleanly convert to chat messages. Our prompts are now able to evolve with our product and dynamically adapt to each model's context window.
15 |
16 | ### Key concepts
17 |
18 | In this library, prompts are represented as a tree of TSX components that are flattened into a list of chat messages. Each TSX node in the tree has a `priority` that is conceptually similar to a `zIndex` (higher number == higher priority).
19 |
20 | If a rendered prompt has more message tokens than can fit into the available context window, the prompt renderer prunes messages with the lowest priority from the `ChatMessage`s result, preserving the order in which they were declared. This means your extension code can safely declare TSX components for potentially large pieces of context like conversation history and codebase context.
21 |
22 | TSX components at the root level must render to `ChatMessage`s at the root level. `ChatMessage`s may have TSX components as children, but they must ultimately render to text. You can also have `TextChunk`s within `ChatMessage`s, which allows you to reduce less important parts of a chat message under context window limits without losing the full message.
23 |
24 | ## Usage
25 |
26 | ### Workspace Setup
27 |
28 | You can install this library in your extension using the command
29 |
30 | ```
31 | npm install --save @vscode/prompt-tsx
32 | ```
33 |
34 | This library exports a `renderPrompt` utility for rendering a TSX component to `vscode.LanguageModelChatMessage`s.
35 |
36 | To enable TSX use in your extension, add the following configuration options to your `tsconfig.json`:
37 |
38 | ```json
39 | {
40 | "compilerOptions": {
41 | // ...
42 | "jsx": "react",
43 | "jsxFactory": "vscpp",
44 | "jsxFragmentFactory": "vscppf"
45 | }
46 | // ...
47 | }
48 | ```
49 |
50 | Note: if your codebase depends on both `@vscode/prompt-tsx` and another library that uses JSX, for example in a monorepo where a parent folder has dependencies on React, you may encounter compilation errors when trying to add this library to your project. This is because [by default](https://www.typescriptlang.org/tsconfig/#types%5D), TypeScript includes all `@types` packages during compilation. You can address this by explicitly listing the types that you want considered during compilation, e.g.:
51 |
52 | ```json
53 | {
54 | "compilerOptions": {
55 | "types": ["node", "jest", "express"]
56 | }
57 | }
58 | ```
59 |
60 | ### Rendering a Prompt
61 |
62 | Next, your extension can use `renderPrompt` to render a TSX prompt. Here is an example of using TSX prompts in a Copilot chat participant that suggests SQL queries based on database context:
63 |
64 | ```ts
65 | import { renderPrompt } from '@vscode/prompt-tsx';
66 | import * as vscode from 'vscode';
67 | import { TestPrompt } from './prompt';
68 |
69 | const participant = vscode.chat.createChatParticipant(
70 | 'mssql',
71 | async (
72 | request: vscode.ChatRequest,
73 | context: vscode.ChatContext,
74 | response: vscode.ChatResponseStream,
75 | token: vscode.CancellationToken
76 | ) => {
77 | response.progress('Reading database context...');
78 |
79 | const models = await vscode.lm.selectChatModels({ family: 'gpt-4' });
80 | if (models.length === 0) {
81 | // No models available, return early
82 | return;
83 | }
84 | const chatModel = models[0];
85 |
86 | // Render TSX prompt
87 | const { messages } = await renderPrompt(
88 | TestPrompt,
89 | { userQuery: request.prompt },
90 | { modelMaxPromptTokens: 4096 },
91 | chatModel
92 | );
93 |
94 | const chatRequest = await chatModel.sendRequest(messages, {}, token);
95 |
96 | // ... Report stream data to VS Code UI
97 | }
98 | );
99 | ```
100 |
101 | Here is how you would declare the TSX prompt rendered above:
102 |
103 | ````tsx
104 | import {
105 | AssistantMessage,
106 | BasePromptElementProps,
107 | PromptElement,
108 | PromptSizing,
109 | UserMessage,
110 | } from '@vscode/prompt-tsx';
111 | import * as vscode from 'vscode';
112 |
113 | export interface PromptProps extends BasePromptElementProps {
114 | userQuery: string;
115 | }
116 |
117 | export interface PromptState {
118 | creationScript: string;
119 | }
120 |
121 | export class TestPrompt extends PromptElement {
122 | override async prepare() {}
123 |
124 | async render(state: PromptState, sizing: PromptSizing) {
125 | const sqlExtensionApi = await vscode.extensions.getExtension('ms-mssql.mssql')?.activate();
126 | const creationScript = await sqlExtensionApi.getDatabaseCreateScript?.();
127 |
128 | return (
129 | <>
130 |
131 | You are a SQL expert.
132 |
133 | Your task is to help the user craft SQL queries that perform their task.
134 |
135 | You should suggest SQL queries that are performant and correct.
136 |
137 | Return your suggested SQL query in a Markdown code block that begins with ```sql and ends
138 | with ```.
139 |
140 |
141 |
142 | Here are the creation scripts that were used to create the tables in my database. Pay
143 | close attention to the tables and columns that are available in my database:
144 |
145 | {state.creationScript}
146 |
147 | {this.props.userQuery}
148 |
149 | >
150 | );
151 | }
152 | }
153 | ````
154 |
155 | Please note:
156 |
157 | - If your prompt does asynchronous work e.g. VS Code extension API calls or additional requests to the Copilot API for chunk reranking, you can precompute this state in an optional async `prepare` method. `prepare` is called before `render` and the prepared state will be passed back to your prompt component's sync `render` method.
158 | - Newlines are not preserved in JSX text or between JSX elements when rendered, and must be explicitly declared with the builtin ` ` attribute.
159 |
160 | ### Prioritization
161 |
162 | If a rendered prompt has more message tokens than can fit into the available context window, the prompt renderer prunes messages with the lowest priority from the `ChatMessage`s result.
163 |
164 | In the above example, each message had the same priority, so they would be pruned in the order in which they were declared, but we could control that by passing a priority to element:
165 |
166 | ```jsx
167 | <>
168 | You are a SQL expert...
169 |
170 | Here are the creation scripts that were used to create the tables in my database...
171 |
172 | {this.props.userQuery}
173 | >
174 | ```
175 |
176 | In this case, a very long `userQuery` would get pruned from the output first if it's too long. Priorities are local in the element tree, so for example the tree of nodes...
177 |
178 | ```html
179 |
180 | A
181 | B
182 |
183 |
184 | C
185 | D
186 |
187 | ```
188 |
189 | ...would be pruned in the order `B->A->D->C`. If two sibling elements share the same priority, the renderer looks ahead at their direct children and picks whichever one has a child with the lowest priority: if the `SystemMessage` and `UserMessage` in the above example did not declare priorities, the pruning order would be `B->D->A->C`.
190 |
191 | Continuous text strings and elements can both be pruned from the tree. If you have a set of elements that you want to either be include all the time or none of the time, you can use the simple `Chunk` utility element:
192 |
193 | ```html
194 |
195 | The file I'm editing is:
196 |
197 | ```
198 |
199 | #### Passing Priority
200 |
201 | In some cases, you may have logical wrapper elements which contain other elements which should share the parent's priority scope. You can use the `passPriority` attribute for this:
202 |
203 | ```tsx
204 | class MyContainer extends PromptElement {
205 | render() {
206 | return <>{this.props.children}>;
207 | }
208 | }
209 |
210 | const myPrompt = (
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 | );
219 | ```
220 |
221 | In this case where we have a wrapper element which includes the children in its own output, the prune order would be `ChildA`, `ChildC`, then `ChildB`.
222 |
223 | ### Flex Behavior
224 |
225 | Wholesale pruning is not always ideal. Instead, we'd prefer to include as much of the query as possible. To do this, we can use the `flexGrow` property, which allows an element to use the remainder of its parent's token budget when it's rendered.
226 |
227 | `prompt-tsx` provides a utility component that supports this use case: `TextChunk`. Given input text, and optionally a delimiting string or regular expression, it'll include as much of the text as possible to fit within its budget:
228 |
229 | ```tsx
230 | <>
231 | You are a SQL expert...
232 |
233 | Here are the creation scripts that were used to create the tables in my database...
234 |
235 |
236 | {this.props.userQuery}
237 |
238 | >
239 | ```
240 |
241 | When `flexGrow` is set for an element, other elements are rendered first, and then the `flexGrow` element is rendered and given the remaining unused token budget from its container as a parameter in the `PromptSizing` passed to its `prepare` and `render` methods. Here's a simplified version of the `TextChunk` component:
242 |
243 | ```tsx
244 | class SimpleTextChunk extends PromptElement<{ text: string }, string> {
245 | prepare(sizing: PromptSizing): Promise {
246 | const words = this.props.text.split(' ');
247 | let str = '';
248 |
249 | for (const word of words) {
250 | if (tokenizer.tokenLength(str + ' ' + word) > sizing.tokenBudget) {
251 | break;
252 | }
253 |
254 | str += ' ' + word;
255 | }
256 |
257 | return str;
258 | }
259 |
260 | render(content: string) {
261 | return <>{content}>;
262 | }
263 | }
264 | ```
265 |
266 | There are a few similar properties which control budget allocation you might find useful for more advanced cases:
267 |
268 | - `flexReserve`: controls the number of tokens reserved from the container's budget _before_ this element gets rendered. For example, if you have a 100 token budget and the elements `<>>`, then `Foo` would receive a `PromptSizing.tokenBudget` of 70, and `Bar` would receive however many tokens of the 100 that `Foo` didn't use. This is only useful in conjunction with `flexGrow`.
269 |
270 | This may also be set to a string in the form `/N` to take a proportion of the container's budget. For example, `` would reserve a third of the container's budget for this element.
271 |
272 | - `flexBasis`: controls the proportion of tokens allocated from the container's budget to this element. It defaults to `1` on all elements. For example, if you have the elements `<>>` and a 100 token budget, each element would be allocated 50 tokens in its `PromptSizing.tokenBudget`. If you instead render `<>>`, `Bar` would receive 66 tokens and `Foo` would receive 33.
273 |
274 | It's important to note that all of the `flex*` properties allow for cooperative use of the token budget for a prompt, but have no effect on the prioritization and pruning logic undertaken once all elements are rendered.
275 |
276 | ### Local Priority Limits
277 |
278 | `prompt-tsx` provides a `TokenLimit` element that can be used to set a hard cap on the number of tokens that can be consumed by a prompt or part of a prompt. Using it is fairly straightforward:
279 |
280 | ```tsx
281 | class PromptWithLimit extends PromptElement {
282 | render() {
283 | return (
284 |
285 | {/* Your elements here! */}
286 |
287 | );
288 | }
289 | }
290 | ```
291 |
292 | `TokenLimit` subtrees are pruned before the prompt gets pruned. As you would expect, the `PromptSizing` of child elements inside of a limit reflect the reduced budget. If the `TokenLimit` would get `tokenBudget` smaller than its maximum via the usual distribution rules, then that's given it child elements instead (but pruning to the `max` value still happens.)
293 |
294 | ### Expandable Text
295 |
296 | The tools provided by `flex*` attributes are good, but sometimes you may still end up with unused space in your token budget that you'd like to utilize. We provide a special `` element that can be used in this case. It takes a callback that can return a text string.
297 |
298 | ```tsx
299 | {
300 | let data = 'hi';
301 | while (true) {
302 | const more = getMoreUsefulData();
303 | if (await sizing.countTokens(data + more) > sizing.tokenBudget) { break }
304 | data += more;
305 | }
306 | }
307 | return data;
308 | }} />
309 | ```
310 |
311 | After the prompt is rendered, the renderer sums up the tokens used by all messages. If there is unused budget, then any `` elements' values are called again with their `PromptSizing` is increased by the token excess.
312 |
313 | If there are multiple `` elements, then they're re-called in the order in which they were initially rendered. Because they're designed to fill up any remaining space, it usually makes sense to have at most one `` element per prompt.
314 |
315 | ### "Keep With"
316 |
317 | In some cases, content might only be relevant when other content is also included in the request. For example in tool calls, your tool call request should only be rendered if the tool call response survived prioritization.
318 |
319 | You can use the `useKeepWith` function to help with this. It returns a component class which is only visible in the output as none of its usages become empty. For example:
320 |
321 | ```tsx
322 | class MyPromptElement extends PromptElement {
323 | render() {
324 | const KeepWith = useKeepWith();
325 | return (
326 | <>
327 |
328 | ...
329 |
330 |
331 | ...
332 |
333 | >
334 | );
335 | }
336 | }
337 | ```
338 |
339 | Unlike ``, which prevents pruning of any children and simply removes them as a block, `` in this case will allow the `ToolCallResponse` to be pruned, and if it's fully pruned it will also remove the `ToolCallRequest`.
340 |
341 | You can also pass the `KeepWith` instance to `toolCalls` in `AssistantMessage`s.
342 |
343 | #### Debugging Budgeting
344 |
345 | You can set a `tracer` property on the `PromptElement` to debug how your elements are rendered and how this library allocates your budget. We include a basic `HTMLTracer` you can use, which can be served on an address:
346 |
347 | ```js
348 | const renderer = new PromptRenderer(/* ... */);
349 | const tracer = new HTMLTracer();
350 | renderer.tracer = tracer;
351 | renderer.render(/* ... */);
352 |
353 | tracer.serveHTML().then(server => {
354 | console.log('Server address:', server.address);
355 | });
356 | ```
357 |
358 | ### IfEmpty
359 |
360 | The `` helper allows you to provide an alternative element to use if the default children of an element are empty at the time of rendering. This is especially useful when you require fallback logic for opaque child data, such as tool calls.
361 |
362 | ```tsx
363 | class MyPromptElement extends PromptElement {
364 | render() {
365 | const KeepWith = useKeepWith();
366 | return (
367 | <>
368 |
369 | ...
370 |
371 | >
372 | );
373 | }
374 | }
375 | ```
376 |
377 | ### Usage in Tools
378 |
379 | Visual Studio Code's API supports language models tools, sometimes called 'functions'. The tools API allows tools to return multiple content types of data to its consumers, and this library supports both returning rich prompt elements to tool callers, as well as using rich content returned from tools.
380 |
381 | #### As a Tool
382 |
383 | As a tool, you can use this library normally. However, to return data to the tool caller, you will want to use a special function `renderElementJSON` to serialize your elements to a plain, transferrable JSON object that can be used by a consumer if they also leverage prompt-tsx:
384 |
385 | Note that when VS Code invokes your language model tool, the `options` may contain `tokenizationOptions` which you should pass through as the third argument to `renderElementJSON`:
386 |
387 | ```ts
388 | import { LanguageModelPromptTsxPart, LanguageModelToolInvocationOptions, LanguageModelToolResult } from 'vscode'
389 |
390 | async function doToolInvocation(
391 | options: LanguageModelToolInvocationOptions
392 | ): LanguageModelToolResult {
393 | const json = await renderElementJSON(MyElement, { /* props */ }, options.tokenizationOptions)
394 | return new LanguageModelToolResult([new LanguageModelPromptTsxPart(json)])
395 | }
396 | ```
397 |
398 | #### As a Consumer
399 |
400 | You may invoke the `vscode.lm.invokeTool` API however you see fit. If you know your token budget in advance, you should pass it to the tool when you call `invokeTool` via the `tokenOptions` option. You can then render the result using the `` helper element, for example:
401 |
402 | ```tsx
403 | class MyElement extends PromptElement {
404 | async render(_state: void, sizing: PromptSizing) {
405 | const result = await vscode.lm.invokeTool(toolId, {
406 | parameters: getToolParameters(),
407 | tokenizationOptions: {
408 | tokenBudget: sizing.tokenBudget,
409 | countTokens: (text, token) => sizing.countTokens(text, token),
410 | },
411 | });
412 |
413 | return ;
414 | }
415 | }
416 | ```
417 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## Security
4 |
5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin).
6 |
7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below.
8 |
9 | ## Reporting Security Issues
10 |
11 | **Please do not report security vulnerabilities through public GitHub issues.**
12 |
13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report).
14 |
15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp).
16 |
17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).
18 |
19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
20 |
21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
22 | * Full paths of source file(s) related to the manifestation of the issue
23 | * The location of the affected source code (tag/branch/commit or direct URL)
24 | * Any special configuration required to reproduce the issue
25 | * Step-by-step instructions to reproduce the issue
26 | * Proof-of-concept or exploit code (if possible)
27 | * Impact of the issue, including how an attacker might exploit the issue
28 |
29 | This information will help us triage your report more quickly.
30 |
31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs.
32 |
33 | ## Preferred Languages
34 |
35 | We prefer all communications to be in English.
36 |
37 | ## Policy
38 |
39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd).
40 |
41 |
42 |
--------------------------------------------------------------------------------
/SUPPORT.md:
--------------------------------------------------------------------------------
1 | # TODO: The maintainer of this repo has not yet edited this file
2 |
3 | **REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project?
4 |
5 | - **No CSS support:** Fill out this template with information about how to file issues and get help.
6 | - **Yes CSS support:** Fill out an intake form at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). CSS will work with/help you to determine next steps.
7 | - **Not sure?** Fill out an intake as though the answer were "Yes". CSS will help you decide.
8 |
9 | *Then remove this first heading from this SUPPORT.MD file before publishing your repo.*
10 |
11 | # Support
12 |
13 | ## How to file issues and get help
14 |
15 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing
16 | issues before filing new issues to avoid duplicates. For new issues, file your bug or
17 | feature request as a new Issue.
18 |
19 | For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE
20 | FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER
21 | CHANNEL. WHERE WILL YOU HELP PEOPLE?**.
22 |
23 | ## Microsoft Support Policy
24 |
25 | Support for this **PROJECT or PRODUCT** is limited to the resources listed above.
26 |
--------------------------------------------------------------------------------
/build/base.yml:
--------------------------------------------------------------------------------
1 | name: $(Date:yyyyMMdd)$(Rev:.r)
2 |
3 | trigger:
4 | branches:
5 | include:
6 | - main
7 |
8 | resources:
9 | repositories:
10 | - repository: templates
11 | type: github
12 | name: microsoft/vscode-engineering
13 | ref: main
14 | endpoint: Monaco
15 |
16 | parameters:
17 | - name: publishPackage
18 | displayName: 🚀 Publish @vscode/prompt-tsx
19 | type: boolean
20 | default: false
21 |
22 | extends:
23 | template: azure-pipelines/npm-package/pipeline.yml@templates
24 | parameters:
25 | npmPackages:
26 | - name: prompt-tsx
27 |
28 | buildSteps:
29 | - script: npm ci
30 | displayName: Install dependencies
31 |
32 | - script: npm run compile
33 | displayName: Compile
34 |
35 | testPlatforms:
36 | - name: Linux
37 | nodeVersions:
38 | - 20.x
39 | - name: MacOS
40 | nodeVersions:
41 | - 20.x
42 | - name: Windows
43 | nodeVersions:
44 | - 20.x
45 |
46 | testSteps:
47 | - script: npm ci
48 | displayName: Install dependencies
49 |
50 | - bash: |
51 | /usr/bin/Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
52 | echo ">>> Started xvfb"
53 | displayName: Start xvfb
54 | condition: eq(variables['Agent.OS'], 'Linux')
55 |
56 | - script: npm run compile
57 | displayName: Compile npm package
58 | env:
59 | DISPLAY: ':99.0'
60 |
61 | - script: npm run test
62 | displayName: Test npm package
63 | env:
64 | DISPLAY: ':99.0'
65 |
66 | publishPackage: ${{ parameters.publishPackage }}
67 |
--------------------------------------------------------------------------------
/build/build-tracer.ts:
--------------------------------------------------------------------------------
1 | import * as assert from 'assert';
2 | import * as chokidar from 'chokidar';
3 | import * as esbuild from 'esbuild';
4 | import { writeFileSync } from 'fs';
5 |
6 | const watch = process.argv.includes('--watch');
7 | const minify = watch ? process.argv.includes('--minify') : !process.argv.includes('--no-minify');
8 |
9 | const ctx = esbuild.context({
10 | entryPoints: ['src/tracer/index.tsx'],
11 | tsconfig: 'src/tracer/tsconfig.json',
12 | bundle: true,
13 | sourcemap: minify ? false : 'inline',
14 | minify,
15 | platform: 'browser',
16 | outdir: 'out',
17 | write: false,
18 | });
19 |
20 | function build() {
21 | return ctx
22 | .then(ctx => ctx.rebuild())
23 | .then(bundle => {
24 | assert.strictEqual(bundle.outputFiles.length, 2, 'expected to have 2 output files');
25 |
26 | const css = bundle.outputFiles.find(o => o.path.endsWith('.css'));
27 | assert.ok(css, 'expected to have css');
28 | const js = bundle.outputFiles.find(o => o.path.endsWith('.js'));
29 | assert.ok(js, 'expected to have js');
30 | writeFileSync(
31 | 'src/base/htmlTracerSrc.ts',
32 | `export const tracerSrc = ${JSON.stringify(
33 | js.text
34 | )};\nexport const tracerCss = ${JSON.stringify(css.text)};`
35 | );
36 | })
37 | .catch(err => {
38 | if (err.errors) {
39 | console.error(err.errors.join('\n'));
40 | } else {
41 | console.error(err);
42 | }
43 | });
44 | }
45 |
46 | if (watch) {
47 | let timeout: NodeJS.Timeout | null = null;
48 | chokidar.watch('src/tracer/**/*.{tsx,ts,css}', {}).on('all', () => {
49 | if (timeout) {
50 | clearTimeout(timeout);
51 | }
52 | timeout = setTimeout(build, 600);
53 | });
54 | } else {
55 | build().then(() => {
56 | process.exit(0);
57 | });
58 | }
59 |
--------------------------------------------------------------------------------
/build/postcompile.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation and GitHub. All rights reserved.
3 | *--------------------------------------------------------------------------------------------*/
4 |
5 | import { copyStaticAssets } from './postinstall';
6 |
7 | async function main() {
8 | // Ship the vscodeTypes.d.ts file in the dist bundle
9 | await copyStaticAssets(['src/base/vscodeTypes.d.ts'], 'dist/base/');
10 | }
11 |
12 | main();
13 |
--------------------------------------------------------------------------------
/build/postinstall.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation and GitHub. All rights reserved.
3 | *--------------------------------------------------------------------------------------------*/
4 |
5 | import * as fs from 'fs';
6 | import * as path from 'path';
7 |
8 | const REPO_ROOT = path.join(__dirname, '..');
9 |
10 | export async function copyStaticAssets(srcpaths: string[], dst: string): Promise {
11 | await Promise.all(
12 | srcpaths.map(async srcpath => {
13 | const src = path.join(REPO_ROOT, srcpath);
14 | const dest = path.join(REPO_ROOT, dst, path.basename(srcpath));
15 | await fs.promises.mkdir(path.dirname(dest), { recursive: true });
16 | await fs.promises.copyFile(src, dest);
17 | })
18 | );
19 | }
20 |
21 | async function main() {
22 | // Ship the tiktoken file in the dist bundle
23 | await copyStaticAssets(['src/base/tokenizer/cl100k_base.tiktoken'], 'dist/base/tokenizer');
24 | }
25 |
26 | main();
27 |
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | # @vscode/prompt-tsx Samples
2 |
3 | This directory contains samples for common patterns using the `@vscode/prompt-tsx` library. Each file contains an example of a component, with a docblock explaining its design. You can find the same content is a nicely-formatted way in the [VS Code Documentation for extension authors](https://code.visualstudio.com/api/extension-guides/prompt-tsx).
4 |
--------------------------------------------------------------------------------
/examples/file-contents.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | BasePromptElementProps,
3 | PromptElement,
4 | PromptPiece,
5 | PromptSizing,
6 | SystemMessage,
7 | UserMessage,
8 | } from '@vscode/prompt-tsx';
9 | import { ChatContext, TextDocument } from 'vscode';
10 | import { History } from './history';
11 |
12 | interface IFilesToInclude {
13 | document: TextDocument;
14 | line: number;
15 | }
16 |
17 | interface IMyPromptProps extends BasePromptElementProps {
18 | history: ChatContext['history'];
19 | userQuery: string;
20 | files: IFilesToInclude[];
21 | }
22 |
23 | /**
24 | * In this example, we want to include the contents of all files the user is
25 | * currently looking at in their prompt. But, these files could be big, to the
26 | * point where including all of them would lead to their text being pruned!
27 | *
28 | * This example shows you how to use the `flexGrow` property to cooperatively
29 | * size the file contents to fit within the token budget. Each element receives
30 | * information about how much of the token budget it is suggested to consume in
31 | * its `PromptSizing` object, passed to both `prepare` and `render`.
32 | *
33 | * By default, each element has a `flexGrow` value of `0`. This means they're
34 | * all rendered concurrently and split the budget equally (unless modified by
35 | * a `flexBasis` value.) If you assign elements to a higher `flexGrow` value,
36 | * then they're rendered after everything else, and they're given any remaining
37 | * unused budget. This gives you a great way to create elements that size to
38 | * fit but not exceed your total budget.
39 | *
40 | * Let's use this to make the `FileContext` grow to fill the available space.
41 | * We'll assign it a `flexGrow` value of `1`, and then it will be rendered after
42 | * the instructions and query.
43 | *
44 | * History can be big, however, and we'd prefer to bring in more context rather
45 | * than more history. So, we'll assign the `History` element a `flexGrow` value
46 | * of `2` for the sole purpose of keeping its token consumption out of the
47 | * `FileContext` budget. However, we will set `flexReserve="/5"` to have it
48 | * 'reserve' 1/5th of the total budget from being given to the sizing of
49 | * earlier elements, just to make sure we have some amount of history in the
50 | * prompt.
51 | *
52 | * It's important to note that the `flexGrow` value, and `PromptSizing` in
53 | * general, allows **cooperative** use of the token budget. If the prompt is
54 | * over budget after everything is rendered, then pruning still happens as
55 | * usual. `flex*` values have no impact on the priority or pruning process.
56 | *
57 | * While we're using the active files and selections here, these same concepts
58 | * can be applied in other scenarios too.
59 | */
60 | export class MyPrompt extends PromptElement {
61 | render() {
62 | return (
63 | <>
64 | Here are your base instructions.
65 | {/* See `./history.tsx` for an explainer on the history element. */}
66 |
74 | {this.props.userQuery}
75 |
76 | >
77 | );
78 | }
79 | }
80 |
81 | class FileContext extends PromptElement<{ files: IFilesToInclude[] } & BasePromptElementProps> {
82 | async render(_state: void, sizing: PromptSizing): Promise {
83 | const files = await this.getExpandedFiles(sizing);
84 | return <>{files.map(f => f.toString())}>;
85 | }
86 |
87 | /**
88 | * The idea here is:
89 | *
90 | * 1. We wrap each file in markdown-style code fences, so get the base
91 | * token consumption of each of those.
92 | * 2. Keep looping through the files. Each time, add one line from each file
93 | * until either we're out of lines (anyHadLinesToExpand=false) or until
94 | * the next line would cause us to exceed our token budget.
95 | *
96 | * This always will produce files that are under the budget because
97 | * tokenization can cause content on multiple lines to 'merge', but it will
98 | * never exceed the budget.
99 | *
100 | * (`tokenLength(a) + tokenLength(b) <= tokenLength(a + b)` in all current
101 | * tokenizers.)
102 | */
103 | private async getExpandedFiles(sizing: PromptSizing) {
104 | const files = this.props.files.map(f => new FileContextTracker(f.document, f.line));
105 |
106 | let tokenCount = 0;
107 | // count the base amount of tokens used by the files:
108 | for (const file of files) {
109 | tokenCount += await file.tokenCount(sizing);
110 | }
111 |
112 | while (true) {
113 | let anyHadLinesToExpand = false;
114 | for (const file of files) {
115 | const nextLine = file.nextLine();
116 | if (nextLine === undefined) {
117 | continue;
118 | }
119 |
120 | anyHadLinesToExpand = true;
121 | const nextTokenCount = await sizing.countTokens(nextLine);
122 | if (tokenCount + nextTokenCount > sizing.tokenBudget) {
123 | return files;
124 | }
125 |
126 | file.expand();
127 | tokenCount += nextTokenCount;
128 | }
129 |
130 | if (!anyHadLinesToExpand) {
131 | return files;
132 | }
133 | }
134 | }
135 | }
136 |
137 | class FileContextTracker {
138 | private prefix = `# ${this.document.fileName}\n\`\`\`\n`;
139 | private suffix = '\n```\n';
140 | private lines: string[] = [];
141 |
142 | private aboveLine = this.originLine;
143 | private belowLine = this.originLine;
144 | private nextLineIs: 'above' | 'below' | 'none' = 'above';
145 |
146 | constructor(private readonly document: TextDocument, private readonly originLine: number) {}
147 |
148 | /** Counts the length of the current data. */
149 | public async tokenCount(sizing: PromptSizing) {
150 | const before = await sizing.countTokens(this.prefix);
151 | const after = await sizing.countTokens(this.suffix);
152 | return before + after;
153 | }
154 |
155 | /** Gets the next line that will be added on the following `expand` call. */
156 | public nextLine(): string | undefined {
157 | switch (this.nextLineIs) {
158 | case 'above':
159 | return this.document.lineAt(this.aboveLine).text + '\n';
160 | case 'below':
161 | return this.document.lineAt(this.belowLine).text + '\n';
162 | case 'none':
163 | return undefined;
164 | }
165 | }
166 |
167 | /** Adds in the 'next line' */
168 | public expand() {
169 | if (this.nextLineIs === 'above') {
170 | this.lines.unshift(this.document.lineAt(this.aboveLine).text);
171 | if (this.belowLine < this.document.lineCount - 1) {
172 | this.belowLine++;
173 | this.nextLineIs = 'below';
174 | } else if (this.aboveLine > 0) {
175 | this.aboveLine--;
176 | } else {
177 | this.nextLineIs = 'none';
178 | }
179 | } else if (this.nextLineIs === 'below') {
180 | this.lines.push(this.document.lineAt(this.belowLine).text);
181 | if (this.aboveLine > 0) {
182 | this.aboveLine--;
183 | this.nextLineIs = 'above';
184 | } else if (this.belowLine < this.document.lineCount - 1) {
185 | this.belowLine++;
186 | } else {
187 | this.nextLineIs = 'none';
188 | }
189 | }
190 | }
191 |
192 | /** Gets the file content as a string. */
193 | toString() {
194 | return this.prefix + this.lines.join('\n') + this.suffix;
195 | }
196 | }
197 |
--------------------------------------------------------------------------------
/examples/history.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | AssistantMessage,
3 | BasePromptElementProps,
4 | PrioritizedList,
5 | PromptElement,
6 | PromptPiece,
7 | PromptSizing,
8 | SystemMessage,
9 | UserMessage,
10 | } from '@vscode/prompt-tsx';
11 | import {
12 | CancellationToken,
13 | ChatContext,
14 | ChatRequestTurn,
15 | ChatResponseMarkdownPart,
16 | ChatResponseTurn,
17 | Progress,
18 | } from 'vscode';
19 | import { ChatResponsePart } from '../dist/base/vscodeTypes';
20 |
21 | interface IMyPromptProps extends BasePromptElementProps {
22 | history: ChatContext['history'];
23 | userQuery: string;
24 | }
25 |
26 | /**
27 | * Including conversation history in your prompt is important as it allows the
28 | * user to ask followup questions to previous messages. However, you want to
29 | * make sure its priority is treated appropriately because history can
30 | * grow very large over time.
31 | *
32 | * We've found that the pattern which makes the most sense is usually to prioritize, in order:
33 | *
34 | * 1. The base prompt instructions, then
35 | * 1. The current user query, then
36 | * 1. The last couple turns of chat history, then
37 | * 1. Any supporting data, then
38 | * 1. As much of the remaining history as you can fit.
39 | *
40 | * For this reason, we split the history in two parts in the prompt, where
41 | * recent prompt turns are prioritized above general contextual information.
42 | */
43 | export class MyPrompt extends PromptElement {
44 | render() {
45 | return (
46 | <>
47 |
48 | Here are your base instructions. They have the highest priority because you want to make
49 | sure they're always included!
50 |
51 | {/* The remainder of the history has the lowest priority since it's less relevant */}
52 |
53 | {/* The last 2 history messages are preferred over any workspace context we have vlow */}
54 |
55 | {/* The user query is right behind the system message in priority */}
56 | {this.props.userQuery}
57 |
58 | With a slightly lower priority, you can include some contextual data about the workspace
59 | or files here...
60 |
61 | >
62 | );
63 | }
64 | }
65 |
66 | interface IHistoryProps extends BasePromptElementProps {
67 | history: ChatContext['history'];
68 | newer: number; // last 2 message priority values
69 | older: number; // previous message priority values
70 | passPriority: true; // require this prop be set!
71 | }
72 |
73 | /**
74 | * We can wrap up this history element to be a little easier to use. `prompt-tsx`
75 | * has a `passPriority` attribute which allows an element to act as a 'pass-through'
76 | * container, so that its children are pruned as if they were direct children of
77 | * the parent. With this component, the elements
78 | *
79 | * ```
80 | *
81 | *
82 | * ```
83 | *
84 | * ...can equivalently be expressed as:
85 | *
86 | * ```
87 | *
88 | * ```
89 | */
90 | export class History extends PromptElement {
91 | render(): PromptPiece {
92 | return (
93 | <>
94 |
95 |
96 | >
97 | );
98 | }
99 | }
100 |
101 | interface IHistoryMessagesProps extends BasePromptElementProps {
102 | history: ChatContext['history'];
103 | }
104 |
105 | /**
106 | * The History element simply lists user and assistant messages from the chat
107 | * context. If things like tool calls or file trees are relevant for, your
108 | * case, you can make this element more complex to handle those cases.
109 | */
110 | export class HistoryMessages extends PromptElement {
111 | render(): PromptPiece {
112 | const history: (UserMessage | AssistantMessage)[] = [];
113 | for (const turn of this.props.history) {
114 | if (turn instanceof ChatRequestTurn) {
115 | history.push({turn.prompt});
116 | } else if (turn instanceof ChatResponseTurn) {
117 | history.push(
118 |
119 | {chatResponseToMarkdown(turn)}
120 |
121 | );
122 | }
123 | }
124 | return (
125 |
126 | {history}
127 |
128 | );
129 | }
130 | }
131 |
132 | const chatResponseToMarkdown = (response: ChatResponseTurn) => {
133 | let str = '';
134 | for (const part of response.response) {
135 | if (response instanceof ChatResponseMarkdownPart) {
136 | str += part.value;
137 | }
138 | }
139 |
140 | return str;
141 | };
142 |
--------------------------------------------------------------------------------
/examples/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "examples",
3 | "lockfileVersion": 3,
4 | "requires": true,
5 | "packages": {
6 | "": {
7 | "devDependencies": {
8 | "@types/vscode": "^1.95.0",
9 | "@vscode/prompt-tsx": "file:.."
10 | }
11 | },
12 | "..": {
13 | "version": "0.3.0-alpha.13",
14 | "dev": true,
15 | "license": "MIT",
16 | "devDependencies": {
17 | "@microsoft/tiktokenizer": "^1.0.6",
18 | "@types/node": "^20.11.30",
19 | "@vscode/test-cli": "^0.0.9",
20 | "@vscode/test-electron": "^2.4.1",
21 | "concurrently": "^9.0.1",
22 | "cross-env": "^7.0.3",
23 | "esbuild": "^0.24.0",
24 | "mocha": "^10.2.0",
25 | "preact": "^10.24.2",
26 | "prettier": "^2.8.8",
27 | "tsx": "^4.19.1",
28 | "typescript": "^5.6.2"
29 | }
30 | },
31 | "node_modules/@types/vscode": {
32 | "version": "1.95.0",
33 | "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.95.0.tgz",
34 | "integrity": "sha512-0LBD8TEiNbet3NvWsmn59zLzOFu/txSlGxnv5yAFHCrhG9WvAnR3IvfHzMOs2aeWqgvNjq9pO99IUw8d3n+unw==",
35 | "dev": true
36 | },
37 | "node_modules/@vscode/prompt-tsx": {
38 | "resolved": "..",
39 | "link": true
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/examples/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "devDependencies": {
3 | "@types/vscode": "^1.95.0",
4 | "@vscode/prompt-tsx": "file:.."
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/examples/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react",
4 | "jsxFactory": "vscpp",
5 | "jsxFragmentFactory": "vscppf"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@vscode/prompt-tsx",
3 | "version": "0.4.0-alpha.4",
4 | "description": "Declare LLM prompts with TSX",
5 | "main": "./dist/base/index.js",
6 | "types": "./dist/base/index.d.ts",
7 | "scripts": {
8 | "fmt": "prettier . --write",
9 | "prepack": "npm run compile",
10 | "compile": "tsx ./build/build-tracer.ts && tsc -p tsconfig.json && tsx ./build/postcompile.ts",
11 | "watch": "concurrently \"npm run -s watch:base\" \"npm run -s watch:tracer\"",
12 | "watch:tracer": "tsx ./build/build-tracer.ts --watch",
13 | "watch:base": "tsc --watch --sourceMap --preserveWatchOutput",
14 | "test": "vscode-test",
15 | "test:unit": "cross-env IS_OUTSIDE_VSCODE=1 mocha --import=tsx -u tdd \"src/base/test/**/*.test.{ts,tsx}\"",
16 | "test:bench": "tsx ./src/base/test/renderer.bench.tsx",
17 | "prettier": "prettier --list-different --write --cache .",
18 | "prepare": "tsx ./build/postinstall.ts"
19 | },
20 | "keywords": [],
21 | "author": "Microsoft Corporation",
22 | "license": "MIT",
23 | "bugs": {
24 | "url": "https://github.com/microsoft/vscode-prompt-tsx/issues"
25 | },
26 | "repository": {
27 | "type": "git",
28 | "url": "git+https://github.com/microsoft/vscode-prompt-tsx.git"
29 | },
30 | "homepage": "https://github.com/microsoft/vscode-prompt-tsx#readme",
31 | "devDependencies": {
32 | "@microsoft/tiktokenizer": "^1.0.6",
33 | "@types/node": "^20.11.30",
34 | "@vscode/test-cli": "^0.0.9",
35 | "@vscode/test-electron": "^2.4.1",
36 | "concurrently": "^9.0.1",
37 | "cross-env": "^7.0.3",
38 | "esbuild": "^0.25.4",
39 | "mocha": "^10.2.0",
40 | "preact": "^10.24.2",
41 | "prettier": "^2.8.8",
42 | "tinybench": "^3.1.1",
43 | "tsx": "^4.19.1",
44 | "typescript": "^5.6.2"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/base/htmlTracer.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation and GitHub. All rights reserved.
3 | *--------------------------------------------------------------------------------------------*/
4 |
5 | import type { IncomingMessage, OutgoingMessage, Server } from 'http';
6 | import type { AddressInfo } from 'net';
7 | import { tracerCss, tracerSrc } from './htmlTracerSrc';
8 | import {
9 | HTMLTraceEpoch,
10 | IHTMLTraceRenderData,
11 | IMaterializedMetadata,
12 | ITraceMaterializedContainer,
13 | ITraceMaterializedNode,
14 | TraceMaterializedNodeType,
15 | } from './htmlTracerTypes';
16 | import {
17 | MaterializedChatMessageImage,
18 | MaterializedChatMessage,
19 | MaterializedChatMessageTextChunk,
20 | GenericMaterializedContainer,
21 | MaterializedNode,
22 | MaterializedChatMessageOpaque,
23 | MaterializedChatMessageBreakpoint,
24 | } from './materialized';
25 | import { PromptMetadata } from './results';
26 | import { ITokenizer } from './tokenizer/tokenizer';
27 | import { IElementEpochData, ITraceData, ITraceEpoch, ITracer, ITraceRenderData } from './tracer';
28 | import { Raw } from './output/mode';
29 |
30 | /**
31 | * Handler that can trace rendering internals into an HTML summary.
32 | */
33 | export class HTMLTracer implements ITracer {
34 | private traceData?: ITraceData;
35 | private readonly epochs: ITraceEpoch[] = [];
36 |
37 | addRenderEpoch(epoch: ITraceEpoch): void {
38 | this.epochs.push(epoch);
39 | }
40 |
41 | includeInEpoch(data: IElementEpochData): void {
42 | this.epochs[this.epochs.length - 1].elements.push(data);
43 | }
44 |
45 | didMaterializeTree(traceData: ITraceData): void {
46 | this.traceData = traceData;
47 | }
48 |
49 | /**
50 | * Returns HTML to trace the output. Note that is starts a server which is
51 | * used for client interaction to resize the prompt and its `address` should
52 | * be displayed or opened as a link in a browser.
53 | *
54 | * The server runs until it is disposed.
55 | */
56 | public async serveHTML(): Promise {
57 | return RequestServer.create({
58 | epochs: this.epochs,
59 | traceData: mustGet(this.traceData),
60 | });
61 | }
62 |
63 | /**
64 | * Gets an HTML router for a server at the URL. URL is the form `http://127.0.0.1:1234`.
65 | */
66 | public serveRouter(url: string): IHTMLRouter {
67 | return new RequestRouter({
68 | baseAddress: url,
69 | epochs: this.epochs,
70 | traceData: mustGet(this.traceData),
71 | });
72 | }
73 | }
74 |
75 | export interface IHTMLRouter {
76 | address: string;
77 | route(httpIncomingMessage: unknown, httpOutgoingMessage: unknown): boolean;
78 | }
79 |
80 | export interface IHTMLServer {
81 | address: string;
82 | getHTML(): Promise;
83 | dispose(): void;
84 | }
85 |
86 | interface IServerOpts {
87 | epochs: ITraceEpoch[];
88 | traceData: ITraceData;
89 | baseAddress: string;
90 | }
91 |
92 | class RequestRouter implements IHTMLRouter {
93 | private serverToken = crypto.randomUUID();
94 |
95 | constructor(private readonly opts: IServerOpts) {}
96 |
97 | public route(httpIncomingMessage: unknown, httpOutgoingMessage: unknown): boolean {
98 | const req = httpIncomingMessage as IncomingMessage;
99 | const res = httpOutgoingMessage as OutgoingMessage;
100 | const url = new URL(req.url || '/', `http://localhost`);
101 | const prefix = `/${this.serverToken}`;
102 | switch (url.pathname) {
103 | case prefix:
104 | case `${prefix}/`:
105 | this.onRoot(url, req, res);
106 | break;
107 | case `${prefix}/regen`:
108 | this.onRegen(url, req, res);
109 | break;
110 | default:
111 | return false;
112 | }
113 |
114 | return true;
115 | }
116 |
117 | public get address() {
118 | return this.opts.baseAddress + '/' + this.serverToken;
119 | }
120 |
121 | public async getHTML() {
122 | const { traceData, epochs } = this.opts;
123 | return `
124 |
125 |
134 | `;
135 | }
136 |
137 | private async onRegen(url: URL, _req: IncomingMessage, res: OutgoingMessage) {
138 | const { traceData } = this.opts;
139 | const budget = Number(url.searchParams.get('n') || traceData.budget);
140 | const renderedTree = await traceData.renderTree(budget);
141 | const serialized = await serializeRenderData(traceData.tokenizer, renderedTree);
142 | const json = JSON.stringify(serialized);
143 | res.setHeader('Content-Type', 'application/json');
144 | res.setHeader('Content-Length', Buffer.byteLength(json));
145 | res.end(json);
146 | }
147 |
148 | private onRoot(_url: URL, _req: IncomingMessage, res: OutgoingMessage) {
149 | this.getHTML().then(html => {
150 | res.setHeader('Content-Type', 'text/html');
151 | res.setHeader('Content-Length', Buffer.byteLength(html));
152 | res.end(html);
153 | });
154 | }
155 | }
156 |
157 | class RequestServer extends RequestRouter implements IHTMLServer {
158 | public static async create(opts: Omit) {
159 | const { createServer } = await import('http');
160 | const server = createServer((req, res) => {
161 | try {
162 | if (!instance.route(req, res)) {
163 | res.statusCode = 404;
164 | res.end('Not Found');
165 | }
166 | } catch (e) {
167 | res.statusCode = 500;
168 | res.end(String(e));
169 | }
170 | });
171 |
172 | const port = await new Promise((resolve, reject) => {
173 | server
174 | .listen(0, '127.0.0.1', () => resolve((server.address() as AddressInfo).port))
175 | .on('error', reject);
176 | });
177 |
178 | const instance = new RequestServer(
179 | {
180 | ...opts,
181 | baseAddress: `http://127.0.0.1:${port}`,
182 | },
183 | server
184 | );
185 |
186 | return instance;
187 | }
188 |
189 | constructor(opts: IServerOpts, private readonly server: Server) {
190 | super(opts);
191 | }
192 |
193 | dispose() {
194 | this.server.closeAllConnections();
195 | this.server.close();
196 | }
197 | }
198 |
199 | async function serializeRenderData(
200 | tokenizer: ITokenizer,
201 | tree: ITraceRenderData
202 | ): Promise {
203 | return {
204 | container: (await serializeMaterialized(
205 | tokenizer,
206 | tree.container,
207 | false
208 | )) as ITraceMaterializedContainer,
209 | removed: tree.removed,
210 | budget: tree.budget,
211 | };
212 | }
213 |
214 | async function serializeMaterialized(
215 | tokenizer: ITokenizer,
216 | materialized: MaterializedNode,
217 | inChatMessage: boolean
218 | ): Promise {
219 | const common = {
220 | metadata: materialized.metadata.map(serializeMetadata),
221 | priority: materialized.priority,
222 | };
223 |
224 | if (materialized instanceof MaterializedChatMessageTextChunk) {
225 | return {
226 | ...common,
227 | type: TraceMaterializedNodeType.TextChunk,
228 | value: materialized.text,
229 | tokens: await materialized.upperBoundTokenCount(tokenizer),
230 | };
231 | } else if (materialized instanceof MaterializedChatMessageImage) {
232 | return {
233 | ...common,
234 | name: materialized.id.toString(),
235 | id: materialized.id,
236 | type: TraceMaterializedNodeType.Image,
237 | value: materialized.src,
238 | tokens: await materialized.upperBoundTokenCount(tokenizer),
239 | };
240 | } else if (
241 | materialized instanceof MaterializedChatMessageOpaque ||
242 | materialized instanceof MaterializedChatMessageBreakpoint
243 | ) {
244 | // todo: add to visualizer
245 | return undefined;
246 | } else {
247 | const containerCommon = {
248 | ...common,
249 | id: materialized.id,
250 | name: materialized.name,
251 | children: (
252 | await Promise.all(
253 | materialized.children.map(c =>
254 | serializeMaterialized(
255 | tokenizer,
256 | c,
257 | inChatMessage || materialized instanceof MaterializedChatMessage
258 | )
259 | )
260 | )
261 | ).filter(r => !!r),
262 | tokens: inChatMessage
263 | ? await materialized.upperBoundTokenCount(tokenizer)
264 | : await materialized.tokenCount(tokenizer),
265 | };
266 |
267 | if (materialized instanceof GenericMaterializedContainer) {
268 | return {
269 | ...containerCommon,
270 | type: TraceMaterializedNodeType.Container,
271 | };
272 | } else if (materialized instanceof MaterializedChatMessage) {
273 | const content = materialized.text
274 | .filter(element => typeof element === 'string')
275 | .join('')
276 | .trim();
277 | return {
278 | ...containerCommon,
279 | type: TraceMaterializedNodeType.ChatMessage,
280 | role: Raw.ChatRole.display(materialized.role),
281 | text: content,
282 | };
283 | }
284 | }
285 |
286 | assertNever(materialized);
287 | }
288 |
289 | function assertNever(x: never): never {
290 | throw new Error('unreachable');
291 | }
292 |
293 | function serializeMetadata(metadata: PromptMetadata): IMaterializedMetadata {
294 | return { name: metadata.constructor.name, value: JSON.stringify(metadata) };
295 | }
296 |
297 | const mustGet = (value: T | undefined): T => {
298 | if (value === undefined) {
299 | throw new Error('Prompt must be rendered before calling HTMLTRacer.serveHTML');
300 | }
301 |
302 | return value;
303 | };
304 |
--------------------------------------------------------------------------------
/src/base/htmlTracerTypes.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation and GitHub. All rights reserved.
3 | *--------------------------------------------------------------------------------------------*/
4 |
5 | import { ITraceEpoch } from './tracer';
6 |
7 | export type HTMLTraceEpoch = ITraceEpoch;
8 |
9 | export interface IHTMLTraceRenderData {
10 | container: ITraceMaterializedContainer;
11 | removed: number;
12 | budget: number;
13 | }
14 |
15 | export type ITraceMaterializedNode =
16 | | ITraceMaterializedContainer
17 | | ITraceMaterializedChatMessage
18 | | ITraceMaterializedChatMessageTextChunk
19 | | ITraceMaterializedChatMessageImage;
20 |
21 | export const enum TraceMaterializedNodeType {
22 | Container,
23 | ChatMessage,
24 | TextChunk,
25 | Image,
26 | }
27 |
28 | export interface IMaterializedMetadata {
29 | name: string;
30 | value: string;
31 | }
32 |
33 | export interface ITraceMaterializedCommon {
34 | priority: number;
35 | tokens: number;
36 | metadata: IMaterializedMetadata[];
37 | }
38 |
39 | export interface ITraceMaterializedContainer extends ITraceMaterializedCommon {
40 | type: TraceMaterializedNodeType.Container;
41 | id: number;
42 | name: string | undefined;
43 | children: ITraceMaterializedNode[];
44 | }
45 |
46 | export interface ITraceMaterializedChatMessage extends ITraceMaterializedCommon {
47 | type: TraceMaterializedNodeType.ChatMessage;
48 | id: number;
49 | role: string;
50 | name: string | undefined;
51 | priority: number;
52 | text: string;
53 | tokens: number;
54 | children: ITraceMaterializedNode[];
55 | }
56 |
57 | export interface ITraceMaterializedChatMessageTextChunk extends ITraceMaterializedCommon {
58 | type: TraceMaterializedNodeType.TextChunk;
59 | value: string;
60 | priority: number;
61 | tokens: number;
62 | }
63 |
64 | export interface ITraceMaterializedChatMessageImage extends ITraceMaterializedCommon {
65 | id: number;
66 | type: TraceMaterializedNodeType.Image;
67 | name: string
68 | value: string;
69 | priority: number;
70 | tokens: number,
71 | }
72 |
73 |
--------------------------------------------------------------------------------
/src/base/index.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation and GitHub. All rights reserved.
3 | *--------------------------------------------------------------------------------------------*/
4 |
5 | import type {
6 | CancellationToken,
7 | ChatResponsePart,
8 | LanguageModelChat,
9 | LanguageModelChatMessage,
10 | Progress,
11 | } from 'vscode';
12 | import { PromptElementJSON } from './jsonTypes';
13 | import { ModeToChatMessageType, OutputMode, Raw } from './output/mode';
14 | import { ChatMessage } from './output/openaiTypes';
15 | import { MetadataMap, PromptRenderer } from './promptRenderer';
16 | import { PromptReference } from './results';
17 | import { ITokenizer, VSCodeTokenizer } from './tokenizer/tokenizer';
18 | import { BasePromptElementProps, IChatEndpointInfo, PromptElementCtor } from './types';
19 | import { ChatDocumentContext } from './vscodeTypes.d';
20 |
21 | export * from './htmlTracer';
22 | export * as JSONTree from './jsonTypes';
23 | export * from './output/mode';
24 | export * from './promptElements';
25 | export * from './results';
26 | export { ITokenizer } from './tokenizer/tokenizer';
27 | export * from './tracer';
28 | export * from './tsx-globals';
29 | export * from './types';
30 |
31 | export { PromptElement } from './promptElement';
32 | export { MetadataMap, PromptRenderer, QueueItem, RenderPromptResult } from './promptRenderer';
33 |
34 | /**
35 | * Renders a prompt element and returns the result.
36 | *
37 | * @template P - The type of the prompt element props.
38 | * @param ctor - The constructor of the prompt element.
39 | * @param props - The props for the prompt element.
40 | * @param endpoint - The chat endpoint information.
41 | * @param progress - The progress object for reporting progress of the chat response.
42 | * @param token - The cancellation token for cancelling the operation.
43 | * @param tokenizer - The tokenizer for tokenizing the chat response.
44 | * @param mode - The mode to render the chat messages in.
45 | * @returns A promise that resolves to an object containing the rendered {@link LanguageModelChatMessage chat messages}, token count, metadatas, used context, and references.
46 | */
47 | export async function renderPrompt
(
48 | ctor: PromptElementCtor
,
49 | props: P,
50 | endpoint: IChatEndpointInfo,
51 | tokenizerMetadata: ITokenizer | LanguageModelChat,
52 | progress?: Progress,
53 | token?: CancellationToken,
54 | mode?: OutputMode.VSCode
55 | ): Promise<{
56 | messages: LanguageModelChatMessage[];
57 | tokenCount: number;
58 | metadata: MetadataMap;
59 | usedContext: ChatDocumentContext[];
60 | references: PromptReference[];
61 | }>;
62 | /**
63 | * Renders a prompt element and returns the result.
64 | *
65 | * @template P - The type of the prompt element props.
66 | * @param ctor - The constructor of the prompt element.
67 | * @param props - The props for the prompt element.
68 | * @param endpoint - The chat endpoint information.
69 | * @param progress - The progress object for reporting progress of the chat response.
70 | * @param token - The cancellation token for cancelling the operation.
71 | * @param tokenizer - The tokenizer for tokenizing the chat response.
72 | * @param mode - The mode to render the chat messages in.
73 | * @returns A promise that resolves to an object containing the rendered {@link ChatMessage chat messages}, token count, metadatas, used context, and references.
74 | */
75 | export async function renderPrompt
,
91 | props: P,
92 | endpoint: IChatEndpointInfo,
93 | tokenizerMetadata: ITokenizer | LanguageModelChat,
94 | progress?: Progress,
95 | token?: CancellationToken,
96 | mode = OutputMode.VSCode
97 | ): Promise<{
98 | messages: (ChatMessage | LanguageModelChatMessage)[];
99 | tokenCount: number;
100 | metadata: MetadataMap;
101 | usedContext: ChatDocumentContext[];
102 | references: PromptReference[];
103 | }> {
104 | let tokenizer =
105 | 'countTokens' in tokenizerMetadata
106 | ? new VSCodeTokenizer((text, token) => tokenizerMetadata.countTokens(text, token), mode)
107 | : tokenizerMetadata;
108 | const renderer = new PromptRenderer(endpoint, ctor, props, tokenizer);
109 | const renderResult = await renderer.render(progress, token);
110 | const usedContext = renderer.getUsedContext();
111 | return { ...renderResult, usedContext };
112 | }
113 |
114 | /**
115 | * Content type of the return value from {@link renderElementJSON}.
116 | * When responding to a tool invocation, the tool should set this as the
117 | * content type in the returned data:
118 | *
119 | * ```ts
120 | * import { contentType } from '@vscode/prompt-tsx';
121 | *
122 | * async function doToolInvocation(): vscode.LanguageModelToolResult {
123 | * return {
124 | * [contentType]: await renderElementJSON(...),
125 | * toString: () => '...',
126 | * };
127 | * }
128 | * ```
129 | */
130 | export const contentType = 'application/vnd.codechat.prompt+json.1';
131 |
132 | /**
133 | * Renders a prompt element to a serializable state. This type be returned in
134 | * tools results and reused in subsequent render calls via the ``
135 | * element.
136 | *
137 | * In this mode, message chunks are not pruned from the tree; budget
138 | * information is used only to hint to the elements how many tokens they should
139 | * consume when rendered.
140 | *
141 | * @template P - The type of the prompt element props.
142 | * @param ctor - The constructor of the prompt element.
143 | * @param props - The props for the prompt element.
144 | * @param budgetInformation - Information about the token budget.
145 | * `vscode.LanguageModelToolInvocationOptions` is assignable to this object.
146 | * @param token - The cancellation token for cancelling the operation.
147 | * @returns A promise that resolves to an object containing the serialized data.
148 | */
149 | export function renderElementJSON
(
150 | ctor: PromptElementCtor
,
151 | props: P,
152 | budgetInformation:
153 | | {
154 | tokenBudget: number;
155 | countTokens(text: string, token?: CancellationToken): Thenable;
156 | }
157 | | undefined,
158 | token?: CancellationToken
159 | ): Promise {
160 | const renderer = new PromptRenderer(
161 | { modelMaxPromptTokens: budgetInformation?.tokenBudget ?? Number.MAX_SAFE_INTEGER },
162 | ctor,
163 | props,
164 | // note: if tokenBudget is given, countTokens is also give and vise-versa.
165 | // `1` is used only as a dummy fallback to avoid errors if no/unlimited budget is provided.
166 | {
167 | mode: OutputMode.Raw,
168 | countMessageTokens(message) {
169 | throw new Error('Tools may only return text, not messages.'); // for now...
170 | },
171 | tokenLength(part, token) {
172 | if (part.type === Raw.ChatCompletionContentPartKind.Text) {
173 | return Promise.resolve(
174 | budgetInformation?.countTokens(part.text, token) ?? Promise.resolve(1)
175 | );
176 | }
177 | return Promise.resolve(1);
178 | },
179 | }
180 | );
181 |
182 | return renderer.renderElementJSON(token);
183 | }
184 |
--------------------------------------------------------------------------------
/src/base/jsonTypes.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation and GitHub. All rights reserved.
3 | *--------------------------------------------------------------------------------------------*/
4 |
5 | import type { Range } from 'vscode';
6 | import { ChatResponseReferencePartStatusKind } from './results';
7 | import { UriComponents } from './util/vs/common/uri';
8 | import { BasePromptElementProps, PromptElementProps } from './types';
9 |
10 | // Types in this region are the JSON representation of prompt elements. These
11 | // can be transmitted between tools and tool callers.
12 | //
13 | // ⚠️ Changes to these types MUST be made in a backwards-compatible way. ⚠️
14 | // Tools and tool callers may be using different prompt-tsx versions.
15 | //
16 | // All enums in this file have explicitly-assigned values, and authors should
17 | // take care not to change existing enum valus.
18 |
19 | export const enum PromptNodeType {
20 | Piece = 1,
21 | Text = 2,
22 | Opaque = 3,
23 | }
24 |
25 | export interface TextJSON {
26 | type: PromptNodeType.Text;
27 | text: string;
28 | priority: number | undefined;
29 | references: PromptReferenceJSON[] | undefined;
30 | lineBreakBefore: boolean | undefined;
31 | }
32 |
33 | /**
34 | * Constructor kind of the node represented by {@link PieceJSON}. This is
35 | * less descriptive than the actual constructor, as we only care to preserve
36 | * the element data that the renderer cares about.
37 | */
38 | export const enum PieceCtorKind {
39 | BaseChatMessage = 1,
40 | Other = 2,
41 | ImageChatMessage = 3,
42 | }
43 |
44 | export const jsonRetainedProps = Object.keys({
45 | flexBasis: 1,
46 | flexGrow: 1,
47 | flexReserve: 1,
48 | passPriority: 1,
49 | priority: 1,
50 | } satisfies { [key in keyof BasePromptElementProps]: 1 }) as readonly (keyof BasePromptElementProps)[];
51 |
52 | export interface BasePieceJSON {
53 | type: PromptNodeType.Piece;
54 | ctor: PieceCtorKind.BaseChatMessage | PieceCtorKind.Other;
55 | ctorName: string | undefined;
56 | children: PromptNodeJSON[];
57 | references: PromptReferenceJSON[] | undefined;
58 | props: Record;
59 | keepWithId?: number;
60 | flags?: number; // ContainerFlags
61 | }
62 |
63 | export interface ImageChatMessagePieceJSON {
64 | type: PromptNodeType.Piece;
65 | ctor: PieceCtorKind.ImageChatMessage;
66 | children: PromptNodeJSON[];
67 | references: PromptReferenceJSON[] | undefined;
68 | props: {
69 | src: string;
70 | detail?: 'low' | 'high';
71 | };
72 | }
73 |
74 | export interface OpaqueJSON {
75 | type: PromptNodeType.Opaque;
76 | tokenUsage?: number;
77 | value: unknown;
78 | priority?: number;
79 | }
80 |
81 | export type PieceJSON = BasePieceJSON | ImageChatMessagePieceJSON;
82 |
83 | export type PromptNodeJSON = PieceJSON | TextJSON | OpaqueJSON;
84 |
85 | export type UriOrLocationJSON = UriComponents | { uri: UriComponents; range: Range };
86 |
87 | export interface PromptReferenceJSON {
88 | anchor: UriOrLocationJSON | { variableName: string; value?: UriOrLocationJSON };
89 | iconPath?: UriComponents | { id: string } | { light: UriComponents; dark: UriComponents };
90 | options?: { status?: { description: string; kind: ChatResponseReferencePartStatusKind } };
91 | }
92 |
93 | export interface PromptElementJSON {
94 | node: PieceJSON;
95 | }
96 |
97 | /** Iterates over each {@link PromptNodeJSON} in the tree. */
98 | export function forEachNode(node: PromptNodeJSON, fn: (node: PromptNodeJSON) => void) {
99 | fn(node);
100 |
101 | if (node.type === PromptNodeType.Piece) {
102 | for (const child of node.children) {
103 | forEachNode(child, fn);
104 | }
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/base/once.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation and GitHub. All rights reserved.
3 | *--------------------------------------------------------------------------------------------*/
4 |
5 | export function once any>(fn: T): T & { clear: () => void } {
6 | let result: ReturnType;
7 | let called = false;
8 |
9 | const wrappedFunction = ((...args: Parameters): ReturnType => {
10 | if (!called) {
11 | result = fn(...args);
12 | called = true;
13 | }
14 | return result;
15 | }) as T & { clear: () => void };
16 |
17 | wrappedFunction.clear = () => {
18 | called = false;
19 | };
20 |
21 | return wrappedFunction;
22 | }
23 |
--------------------------------------------------------------------------------
/src/base/output/mode.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation and GitHub. All rights reserved.
3 | *--------------------------------------------------------------------------------------------*/
4 |
5 | import type { LanguageModelChatMessage } from 'vscode';
6 | import { toOpenAiChatMessage, toOpenAIChatMessages } from './openaiConvert';
7 | import { ChatMessage as OpenAIChatMessage } from './openaiTypes';
8 | import { ChatMessage as RawChatMessage } from './rawTypes';
9 | import { toVsCodeChatMessage, toVsCodeChatMessages } from './vscode';
10 |
11 | export * as OpenAI from './openaiTypes';
12 | export * as Raw from './rawTypes';
13 |
14 | export enum OutputMode {
15 | Raw = 1,
16 | OpenAI = 1 << 1,
17 | VSCode = 1 << 2,
18 | }
19 |
20 | /** Map of the mode to the type of message it produces. */
21 | export interface ModeToChatMessageType {
22 | [OutputMode.Raw]: RawChatMessage;
23 | [OutputMode.VSCode]: LanguageModelChatMessage;
24 | [OutputMode.OpenAI]: OpenAIChatMessage;
25 | }
26 |
27 | /**
28 | * Converts the raw message representation emitted by this library to the given
29 | * type of chat message. The target chat message may or may not represent all
30 | * data included in the {@link RawChatMessage}.
31 | */
32 | export function toMode(
33 | mode: Mode,
34 | messages: RawChatMessage
35 | ): ModeToChatMessageType[Mode];
36 | export function toMode(
37 | mode: Mode,
38 | messages: readonly RawChatMessage[]
39 | ): ModeToChatMessageType[Mode][];
40 | export function toMode(
41 | mode: Mode,
42 | messages: readonly RawChatMessage[] | RawChatMessage
43 | ): ModeToChatMessageType[Mode][] | ModeToChatMessageType[Mode] {
44 | switch (mode) {
45 | case OutputMode.Raw:
46 | return messages as ModeToChatMessageType[Mode][];
47 | case OutputMode.VSCode:
48 | return (
49 | messages instanceof Array ? toVsCodeChatMessages(messages) : toVsCodeChatMessage(messages)
50 | ) as ModeToChatMessageType[Mode];
51 | case OutputMode.OpenAI:
52 | return (
53 | messages instanceof Array ? toOpenAIChatMessages(messages) : toOpenAiChatMessage(messages)
54 | ) as ModeToChatMessageType[Mode];
55 | default:
56 | throw new Error(`Unknown output mode: ${mode}`);
57 | }
58 | }
59 |
60 | export function toVSCode(messages: RawChatMessage): LanguageModelChatMessage;
61 | export function toVSCode(messages: readonly RawChatMessage[]): LanguageModelChatMessage[];
62 | export function toVSCode(
63 | messages: readonly RawChatMessage[] | RawChatMessage
64 | ): LanguageModelChatMessage | LanguageModelChatMessage[] {
65 | return toMode(OutputMode.VSCode, messages as any);
66 | }
67 |
68 | export function toOpenAI(messages: RawChatMessage): OpenAIChatMessage;
69 | export function toOpenAI(messages: readonly RawChatMessage[]): OpenAIChatMessage[];
70 | export function toOpenAI(
71 | messages: readonly RawChatMessage[] | RawChatMessage
72 | ): OpenAIChatMessage | OpenAIChatMessage[] {
73 | return toMode(OutputMode.OpenAI, messages as any);
74 | }
75 |
--------------------------------------------------------------------------------
/src/base/output/openaiConvert.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation and GitHub. All rights reserved.
3 | *--------------------------------------------------------------------------------------------*/
4 | import * as Raw from './rawTypes';
5 | import * as OpenAI from './openaiTypes';
6 | import { OutputMode } from './mode';
7 |
8 | function onlyStringContent(content: Raw.ChatCompletionContentPart[]): string {
9 | return content
10 | .filter(part => part.type === Raw.ChatCompletionContentPartKind.Text)
11 | .map(part => part.text)
12 | .join('');
13 | }
14 |
15 | function stringAndImageContent(
16 | content: Raw.ChatCompletionContentPart[]
17 | ): string | OpenAI.ChatCompletionContentPart[] {
18 |
19 | const parts = content
20 | .map((part): OpenAI.ChatCompletionContentPart | undefined => {
21 | if (part.type === Raw.ChatCompletionContentPartKind.Text) {
22 | return {
23 | type: 'text',
24 | text: part.text,
25 | };
26 | } else if (part.type === Raw.ChatCompletionContentPartKind.Image) {
27 | return {
28 | image_url: part.imageUrl,
29 | type: 'image_url',
30 | };
31 | } else if (
32 | part.type === Raw.ChatCompletionContentPartKind.Opaque &&
33 | Raw.ChatCompletionContentPartOpaque.usableIn(part, OutputMode.OpenAI)
34 | ) {
35 | return part.value as any;
36 | }
37 | })
38 | .filter(r => !!r);
39 |
40 |
41 | if (parts.every(part => part.type === 'text')) {
42 | return parts.map(p=> (p as OpenAI.ChatCompletionContentPartText).text).join('');
43 | }
44 |
45 | return parts;
46 | }
47 |
48 | export function toOpenAiChatMessage(message: Raw.ChatMessage): OpenAI.ChatMessage | undefined {
49 | switch (message.role) {
50 | case Raw.ChatRole.System:
51 | return {
52 | role: OpenAI.ChatRole.System,
53 | content: onlyStringContent(message.content),
54 | name: message.name,
55 | };
56 | case Raw.ChatRole.User:
57 | return {
58 | role: OpenAI.ChatRole.User,
59 | content: stringAndImageContent(message.content),
60 | name: message.name,
61 | };
62 | case Raw.ChatRole.Assistant:
63 | return {
64 | role: OpenAI.ChatRole.Assistant,
65 | content: onlyStringContent(message.content),
66 | name: message.name,
67 | tool_calls: message.toolCalls?.map(toolCall => ({
68 | id: toolCall.id,
69 | function: toolCall.function,
70 | type: 'function',
71 | })),
72 | };
73 | case Raw.ChatRole.Tool:
74 | return {
75 | role: OpenAI.ChatRole.Tool,
76 | content: stringAndImageContent(message.content),
77 | tool_call_id: message.toolCallId,
78 | };
79 | default:
80 | return undefined;
81 | }
82 | }
83 |
84 | export function toOpenAIChatMessages(messages: readonly Raw.ChatMessage[]): OpenAI.ChatMessage[] {
85 | return messages.map(toOpenAiChatMessage).filter(r => !!r);
86 | }
87 |
--------------------------------------------------------------------------------
/src/base/output/openaiTypes.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation and GitHub. All rights reserved.
3 | *--------------------------------------------------------------------------------------------*/
4 |
5 | /**
6 | * An OpenAI Chat Completion message.
7 | *
8 | * Reference: https://platform.openai.com/docs/api-reference/chat/create
9 | */
10 | export type ChatMessage =
11 | | AssistantChatMessage
12 | | SystemChatMessage
13 | | UserChatMessage
14 | | ToolChatMessage
15 | | FunctionChatMessage;
16 |
17 | export interface SystemChatMessage {
18 | role: ChatRole.System;
19 |
20 | /**
21 | * The content of the chat message.
22 | */
23 | content: string;
24 |
25 | /**
26 | * An optional name for the participant. Provides the model information to differentiate between participants of the same role.
27 | */
28 | name?: string;
29 | }
30 |
31 | export interface UserChatMessage {
32 | role: ChatRole.User;
33 |
34 | /**
35 | * The content of the chat message.
36 | */
37 | content: string | Array;
38 |
39 | /**
40 | * An optional name for the participant. Provides the model information to differentiate between participants of the same role.
41 | */
42 | name?: string;
43 | }
44 |
45 | export type ChatCompletionContentPart = ChatCompletionContentPartImage | ChatCompletionContentPartText;
46 |
47 | export interface ChatCompletionContentPartImage {
48 | image_url: ChatCompletionContentPartImage.ImageURL;
49 |
50 | /**
51 | * The type of the content part.
52 | */
53 | type: 'image_url';
54 | }
55 |
56 | export namespace ChatCompletionContentPartImage {
57 | export interface ImageURL {
58 | /**
59 | * Either a URL of the image or the base64 encoded image data.
60 | */
61 | url: string;
62 |
63 | /**
64 | * Specifies the detail level of the image. Learn more in the
65 | * [Vision guide](https://platform.openai.com/docs/guides/vision/low-or-high-fidelity-image-understanding).
66 | */
67 | detail?: 'low' | 'high';
68 | }
69 | }
70 |
71 | export interface ChatCompletionContentPartText {
72 | /**
73 | * The text content.
74 | */
75 | text: string;
76 |
77 | /**
78 | * The type of the content part.
79 | */
80 | type: 'text';
81 | }
82 |
83 | export interface ChatMessageToolCall {
84 | /**
85 | * The ID of the tool call.
86 | */
87 | id: string;
88 |
89 | /**
90 | * The function that the model called.
91 | */
92 | function: ChatMessageFunction;
93 |
94 | /**
95 | * The type of the tool. Currently, only `function` is supported.
96 | */
97 | type: 'function';
98 | }
99 |
100 | export interface AssistantChatMessage {
101 | role: ChatRole.Assistant;
102 |
103 | /**
104 | * The content of the chat message.
105 | */
106 | content: string;
107 |
108 | /**
109 | * An optional name for the participant. Provides the model information to differentiate between participants of the same role.
110 | */
111 | name?: string;
112 |
113 | /**
114 | * The tool calls generated by the model.
115 | */
116 | tool_calls?: Array;
117 | }
118 |
119 | export interface ToolChatMessage {
120 | role: ChatRole.Tool;
121 |
122 | /**
123 | * Tool call that this message is responding to.
124 | */
125 | tool_call_id?: string;
126 |
127 | /**
128 | * The content of the chat message.
129 | */
130 | content: string | Array;
131 | }
132 |
133 | /**
134 | * @deprecated Use {@link ToolChatMessage} instead.
135 | */
136 | export interface FunctionChatMessage {
137 | role: ChatRole.Function;
138 |
139 | /**
140 | * The content of the chat message.
141 | */
142 | content: string;
143 |
144 | /**
145 | * The name of the function that was called
146 | */
147 | name: string;
148 | }
149 |
150 | /**
151 | * The function that the model called.
152 | */
153 | export interface ChatMessageFunction {
154 | /**
155 | * The arguments to call the function with, as generated by the model in JSON
156 | * format. Note that the model does not always generate valid JSON, and may
157 | * hallucinate parameters not defined by your function schema. Validate the
158 | * arguments in your code before calling your function.
159 | */
160 | arguments: string;
161 |
162 | /**
163 | * The name of the function to call.
164 | */
165 | name: string;
166 | }
167 |
168 | /**
169 | * The role of a message in an OpenAI completions request.
170 | */
171 | export enum ChatRole {
172 | System = 'system',
173 | User = 'user',
174 | Assistant = 'assistant',
175 | Function = 'function',
176 | Tool = 'tool',
177 | }
178 |
179 | /**
180 | * BaseTokensPerCompletion is the minimum tokens for a completion request.
181 | * Replies are primed with <|im_start|>assistant<|message|>, so these tokens represent the
182 | * special token and the role name.
183 | */
184 | export const BaseTokensPerCompletion = 3;
185 | /*
186 | * Each GPT 3.5 / GPT 4 message comes with 3 tokens per message due to special characters
187 | */
188 | export const BaseTokensPerMessage = 3;
189 | /*
190 | * Since gpt-3.5-turbo-0613 each name costs 1 token
191 | */
192 | export const BaseTokensPerName = 1;
193 |
--------------------------------------------------------------------------------
/src/base/output/rawTypes.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation and GitHub. All rights reserved.
3 | *--------------------------------------------------------------------------------------------*/
4 |
5 | import { assertNever } from '../util/assert';
6 | import type { OutputMode, toMode } from './mode';
7 |
8 | /**
9 | * A chat message emitted by this library. This can be mapped to other APIs
10 | * easily using {@link toMode}.
11 | *
12 | * Please note:
13 | * - Enumerations and union types are non-exhaustive. More types may be added
14 | * in the future.
15 | * - Data in this representation is very permissible and converting to API
16 | * representations may be lossy.
17 | */
18 | export type ChatMessage =
19 | | AssistantChatMessage
20 | | SystemChatMessage
21 | | UserChatMessage
22 | | ToolChatMessage;
23 |
24 | /**
25 | * The role of a message in an OpenAI completions request.
26 | */
27 | export enum ChatRole {
28 | System,
29 | User,
30 | Assistant,
31 | Tool,
32 | }
33 |
34 | export namespace ChatRole {
35 | export function display(role: ChatRole): string {
36 | switch (role) {
37 | case ChatRole.System:
38 | return 'system';
39 | case ChatRole.User:
40 | return 'user';
41 | case ChatRole.Assistant:
42 | return 'assistant';
43 | case ChatRole.Tool:
44 | return 'tool';
45 | default:
46 | assertNever(role, `unknown chat role ${role}}`);
47 | }
48 | }
49 | }
50 |
51 | export interface BaseChatMessage {
52 | role: ChatRole;
53 | content: ChatCompletionContentPart[];
54 | /**
55 | * An optional name for the participant. Provides the model information to differentiate between participants of the same role.
56 | */
57 | name?: string;
58 | }
59 |
60 | export interface SystemChatMessage extends BaseChatMessage {
61 | role: ChatRole.System;
62 | }
63 |
64 | export interface UserChatMessage extends BaseChatMessage {
65 | role: ChatRole.User;
66 | }
67 |
68 | export type ChatCompletionContentPart =
69 | | ChatCompletionContentPartImage
70 | | ChatCompletionContentPartText
71 | | ChatCompletionContentPartOpaque
72 | | ChatCompletionContentPartCacheBreakpoint;
73 |
74 | export enum ChatCompletionContentPartKind {
75 | Image,
76 | Text,
77 | Opaque,
78 | CacheBreakpoint,
79 | }
80 |
81 | /** An image completion */
82 | export interface ChatCompletionContentPartImage {
83 | imageUrl: ImageURLReference;
84 | type: ChatCompletionContentPartKind.Image;
85 | }
86 |
87 | export interface ChatCompletionContentPartCacheBreakpoint {
88 | type: ChatCompletionContentPartKind.CacheBreakpoint;
89 | /**
90 | * Optional implementation-specific type of the breakpoint.
91 | */
92 | cacheType?: string;
93 | }
94 |
95 | export interface ImageURLReference {
96 | /**
97 | * Either a URL of the image or the base64 encoded image data.
98 | */
99 | url: string;
100 |
101 | /**
102 | * Specifies the detail level of the image. Learn more in the
103 | * [Vision guide](https://platform.openai.com/docs/guides/vision/low-or-high-fidelity-image-understanding).
104 | */
105 | detail?: 'low' | 'high';
106 | }
107 |
108 | export interface ChatCompletionContentPartText {
109 | /**
110 | * The text content.
111 | */
112 | text: string;
113 |
114 | /**
115 | * The type of the content part.
116 | */
117 | type: ChatCompletionContentPartKind.Text;
118 | }
119 |
120 | export interface ChatCompletionContentPartOpaque {
121 | /**
122 | * A JSON-stringifiable value
123 | */
124 | value: unknown;
125 |
126 | /**
127 | * Constant-value token usage of this content part. If undefined, it will
128 | * be assumed 0.
129 | */
130 | tokenUsage?: number;
131 |
132 | /**
133 | * A bitset of output modes where this content part will be omitted.
134 | * E.g. `scope: OutputMode.Anthropic | OutputMode.VSCode`. Not all outputs
135 | * will support opaque parts everywhere.
136 | */
137 | scope?: number;
138 |
139 | /**
140 | * The type of the content part.
141 | */
142 | type: ChatCompletionContentPartKind.Opaque;
143 | }
144 |
145 | export namespace ChatCompletionContentPartOpaque {
146 | export function usableIn(part: ChatCompletionContentPartOpaque, mode: OutputMode) {
147 | return !part.scope || (part.scope & mode) !== 0;
148 | }
149 | }
150 |
151 | export interface ChatMessageToolCall {
152 | /**
153 | * The ID of the tool call.
154 | */
155 | id: string;
156 |
157 | /**
158 | * The function that the model called.
159 | */
160 | function: ChatMessageFunction;
161 |
162 | /**
163 | * The type of the tool. Currently, only `function` is supported.
164 | */
165 | type: 'function';
166 | }
167 |
168 | export interface AssistantChatMessage extends BaseChatMessage {
169 | role: ChatRole.Assistant;
170 | /**
171 | * An optional name for the participant. Provides the model information to differentiate between participants of the same role.
172 | */
173 | name?: string;
174 |
175 | /**
176 | * The tool calls generated by the model.
177 | */
178 | toolCalls?: ChatMessageToolCall[];
179 | }
180 |
181 | export interface ToolChatMessage extends BaseChatMessage {
182 | role: ChatRole.Tool;
183 |
184 | /**
185 | * Tool call that this message is responding to.
186 | */
187 | toolCallId: string;
188 | }
189 |
190 | /**
191 | * The function that the model called.
192 | */
193 | export interface ChatMessageFunction {
194 | /**
195 | * The arguments to call the function with, as generated by the model in JSON
196 | * format.
197 | */
198 | arguments: string;
199 |
200 | /**
201 | * The name of the function to call.
202 | */
203 | name: string;
204 | }
205 |
--------------------------------------------------------------------------------
/src/base/output/vscode.ts:
--------------------------------------------------------------------------------
1 | import type * as vscodeType from 'vscode';
2 | import * as Raw from './rawTypes';
3 |
4 | function onlyStringContent(content: Raw.ChatCompletionContentPart[]): string {
5 | return content
6 | .filter(part => part.type === Raw.ChatCompletionContentPartKind.Text)
7 | .map(part => (part as Raw.ChatCompletionContentPartText).text)
8 | .join('');
9 | }
10 |
11 | let vscode: typeof vscodeType;
12 |
13 | export function toVsCodeChatMessage(
14 | m: Raw.ChatMessage
15 | ): vscodeType.LanguageModelChatMessage | undefined {
16 | vscode ??= require('vscode');
17 |
18 | switch (m.role) {
19 | case Raw.ChatRole.Assistant:
20 | const message: vscodeType.LanguageModelChatMessage =
21 | vscode.LanguageModelChatMessage.Assistant(onlyStringContent(m.content), m.name);
22 | if (m.toolCalls) {
23 | message.content = [
24 | new vscode.LanguageModelTextPart(onlyStringContent(m.content)),
25 | ...m.toolCalls.map(tc => {
26 | // prompt-tsx got args passed as a string, here we assume they are JSON because the vscode-type wants an object
27 | let parsedArgs: object;
28 | try {
29 | parsedArgs = JSON.parse(tc.function.arguments);
30 | } catch (err) {
31 | throw new Error('Invalid JSON in tool call arguments for tool call: ' + tc.id);
32 | }
33 |
34 | return new vscode.LanguageModelToolCallPart(tc.id, tc.function.name, parsedArgs);
35 | }),
36 | ];
37 | }
38 | return message;
39 | case Raw.ChatRole.User:
40 | return vscode.LanguageModelChatMessage.User(onlyStringContent(m.content), m.name);
41 | case Raw.ChatRole.Tool: {
42 | const message: vscodeType.LanguageModelChatMessage = vscode.LanguageModelChatMessage.User('');
43 | message.content = [
44 | new vscode.LanguageModelToolResultPart(m.toolCallId, [
45 | new vscode.LanguageModelTextPart(onlyStringContent(m.content)),
46 | ]),
47 | ];
48 | return message;
49 | }
50 | default:
51 | return undefined;
52 | }
53 | }
54 | /**
55 | * Converts an array of {@link ChatMessage} objects to an array of corresponding {@link LanguageModelChatMessage VS Code chat messages}.
56 | * @param messages - The array of {@link ChatMessage} objects to convert.
57 | * @returns An array of {@link LanguageModelChatMessage VS Code chat messages}.
58 | */
59 | export function toVsCodeChatMessages(
60 | messages: readonly Raw.ChatMessage[]
61 | ): vscodeType.LanguageModelChatMessage[] {
62 | return messages.map(toVsCodeChatMessage).filter(r => !!r);
63 | }
64 |
--------------------------------------------------------------------------------
/src/base/promptElement.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation and GitHub. All rights reserved.
3 | *--------------------------------------------------------------------------------------------*/
4 |
5 | import type { CancellationToken, Progress } from 'vscode';
6 | import './tsx';
7 | import { BasePromptElementProps, PromptElementProps, PromptPiece, PromptSizing } from './types';
8 | import { ChatResponsePart } from './vscodeTypes';
9 |
10 | /**
11 | * `PromptElement` represents a single element of a prompt.
12 | * A prompt element can be rendered by the {@link PromptRenderer} to produce {@link ChatMessage} chat messages.
13 | *
14 | * @remarks Newlines are not preserved in string literals when rendered, and must be explicitly declared with the builtin ` ` attribute.
15 | *
16 | * @template P - The type of the properties for the prompt element. It extends `BasePromptElementProps`.
17 | * @template S - The type of the state for the prompt element. It defaults to `void`.
18 | *
19 | * @property props - The properties of the prompt element.
20 | * @property priority - The priority of the prompt element. If not provided, defaults to 0.
21 | *
22 | * @method prepare - Optionally prepares asynchronous state before the prompt element is rendered.
23 | * @method render - Renders the prompt element. This method is abstract and must be implemented by subclasses.
24 | */
25 | export abstract class PromptElement<
26 | P extends BasePromptElementProps = BasePromptElementProps,
27 | S = void
28 | > {
29 | public readonly props: PromptElementProps
) {
40 | this.props = props;
41 | }
42 |
43 | /**
44 | * Optionally prepare asynchronous state before the prompt element is rendered.
45 | * @param progress - Optionally report progress to the user for long-running state preparation.
46 | * @param token - A cancellation token that can be used to signal cancellation to the prompt element.
47 | *
48 | * @returns A promise that resolves to the prompt element's state.
49 | */
50 | prepare?(
51 | sizing: PromptSizing,
52 | progress?: Progress,
53 | token?: CancellationToken
54 | ): Promise;
55 |
56 | /**
57 | * Renders the prompt element.
58 | *
59 | * @param state - The state of the prompt element.
60 | * @param sizing - The sizing information for the prompt.
61 | * @param progress - Optionally report progress to the user for long-running state preparation.
62 | * @param token - A cancellation token that can be used to signal cancellation to the prompt element.
63 | * @returns The rendered prompt piece or undefined if the element does not want to render anything.
64 | */
65 | abstract render(
66 | state: S,
67 | sizing: PromptSizing,
68 | progress?: Progress,
69 | token?: CancellationToken
70 | ): Promise | PromptPiece | undefined;
71 | }
72 |
--------------------------------------------------------------------------------
/src/base/promptElements.tsx:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation and GitHub. All rights reserved.
3 | *--------------------------------------------------------------------------------------------*/
4 |
5 | import type {
6 | CancellationToken,
7 | LanguageModelPromptTsxPart,
8 | LanguageModelTextPart,
9 | LanguageModelToolResult,
10 | } from 'vscode';
11 | import { contentType, Raw } from '.';
12 | import { PromptElement } from './promptElement';
13 | import {
14 | BasePromptElementProps,
15 | PromptElementCtor,
16 | PromptElementProps,
17 | PromptPiece,
18 | PromptPieceChild,
19 | PromptSizing,
20 | } from './types';
21 | import { PromptElementJSON } from './jsonTypes';
22 |
23 | export type ChatMessagePromptElement = SystemMessage | UserMessage | AssistantMessage;
24 |
25 | export function isChatMessagePromptElement(element: unknown): element is ChatMessagePromptElement {
26 | return (
27 | element instanceof SystemMessage ||
28 | element instanceof UserMessage ||
29 | element instanceof AssistantMessage
30 | );
31 | }
32 |
33 | export interface ChatMessageProps extends BasePromptElementProps {
34 | role?: Raw.ChatRole;
35 | name?: string;
36 | }
37 |
38 | export class BaseChatMessage<
39 | T extends ChatMessageProps = ChatMessageProps
40 | > extends PromptElement {
41 | render() {
42 | return <>{this.props.children}>;
43 | }
44 | }
45 |
46 | /**
47 | * A {@link PromptElement} which can be rendered to an OpenAI system chat message.
48 | *
49 | * See {@link https://platform.openai.com/docs/api-reference/chat/create#chat-create-messages}
50 | */
51 | export class SystemMessage extends BaseChatMessage {
52 | constructor(props: ChatMessageProps) {
53 | props.role = Raw.ChatRole.System;
54 | super(props);
55 | }
56 | }
57 |
58 | /**
59 | * A {@link PromptElement} which can be rendered to an OpenAI user chat message.
60 | *
61 | * See {@link https://platform.openai.com/docs/api-reference/chat/create#chat-create-messages}
62 | */
63 | export class UserMessage extends BaseChatMessage {
64 | constructor(props: ChatMessageProps) {
65 | props.role = Raw.ChatRole.User;
66 | super(props);
67 | }
68 | }
69 |
70 | export interface ToolCall {
71 | id: string;
72 | function: ToolFunction;
73 | type: 'function';
74 | /**
75 | * A `` element, created from {@link useKeepWith}, that wraps
76 | * the tool result. This will ensure that if the tool result is pruned,
77 | * the tool call is also pruned to avoid errors.
78 | */
79 | keepWith?: KeepWithCtor;
80 | }
81 |
82 | export interface ToolFunction {
83 | arguments: string;
84 | name: string;
85 | }
86 |
87 | export interface AssistantMessageProps extends ChatMessageProps {
88 | toolCalls?: ToolCall[];
89 | }
90 |
91 | /**
92 | * A {@link PromptElement} which can be rendered to an OpenAI assistant chat message.
93 | *
94 | * See {@link https://platform.openai.com/docs/api-reference/chat/create#chat-create-messages}
95 | */
96 | export class AssistantMessage extends BaseChatMessage {
97 | constructor(props: AssistantMessageProps) {
98 | props.role = Raw.ChatRole.Assistant;
99 | super(props);
100 | }
101 | }
102 |
103 | const WHITESPACE_RE = /\s+/g;
104 |
105 | export interface ToolMessageProps extends ChatMessageProps {
106 | toolCallId: string;
107 | }
108 |
109 | /**
110 | * A {@link PromptElement} which can be rendered to an OpenAI tool chat message.
111 | *
112 | * See {@link https://platform.openai.com/docs/api-reference/chat/create#chat-create-messages}
113 | */
114 | export class ToolMessage extends BaseChatMessage {
115 | constructor(props: ToolMessageProps) {
116 | props.role = Raw.ChatRole.Tool;
117 | super(props);
118 | }
119 | }
120 |
121 | export interface TextChunkProps extends BasePromptElementProps {
122 | /**
123 | * If defined, the text chunk will potentially truncate its contents at the
124 | * last occurrence of the string or regular expression to ensure its content
125 | * fits within in token budget.
126 | *
127 | * {@see BasePromptElementProps} for options to control how the token budget
128 | * is allocated.
129 | */
130 | breakOn?: RegExp | string;
131 |
132 | /** A shortcut for setting {@link breakOn} to `/\s+/g` */
133 | breakOnWhitespace?: boolean;
134 | }
135 |
136 | /**
137 | * @property {string} src - The source of the image. This should be a raw base64 string.
138 | * @property {'low' | 'high'} [detail] - Optional. The detail level of the image. Can be either 'low' or 'high'. If not specified, `high` is used.
139 | */
140 | export interface ImageProps extends BasePromptElementProps {
141 | src: string;
142 | detail?: 'low' | 'high';
143 | }
144 |
145 | /**
146 | * A chunk of single-line or multi-line text that is a direct child of a {@link ChatMessagePromptElement}.
147 | *
148 | * TextChunks can only have text literals or intrinsic attributes as children.
149 | * It supports truncating text to fix the token budget if passed a {@link TextChunkProps.tokenizer} and {@link TextChunkProps.breakOn} behavior.
150 | * Like other {@link PromptElement}s, it can specify `priority` to determine how it should be prioritized.
151 | */
152 | export class TextChunk extends PromptElement {
153 | async prepare(
154 | sizing: PromptSizing,
155 | _progress?: unknown,
156 | token?: CancellationToken
157 | ): Promise {
158 | const breakOn = this.props.breakOnWhitespace ? WHITESPACE_RE : this.props.breakOn;
159 | if (!breakOn) {
160 | return <>{this.props.children}>;
161 | }
162 |
163 | let fullText = '';
164 | const intrinsics: PromptPiece[] = [];
165 | for (const child of this.props.children || []) {
166 | if (child && typeof child === 'object') {
167 | if (typeof child.ctor !== 'string') {
168 | throw new Error('TextChunk children must be text literals or intrinsic attributes.');
169 | } else if (child.ctor === 'br') {
170 | fullText += '\n';
171 | } else {
172 | intrinsics.push(child);
173 | }
174 | } else if (child != null) {
175 | fullText += child;
176 | }
177 | }
178 |
179 | const text = await getTextContentBelowBudget(sizing, breakOn, fullText, token);
180 | return (
181 | <>
182 | {intrinsics}
183 | {text}
184 | >
185 | );
186 | }
187 |
188 | render(piece: PromptPiece) {
189 | return piece;
190 | }
191 | }
192 |
193 | async function getTextContentBelowBudget(
194 | sizing: PromptSizing,
195 | breakOn: string | RegExp,
196 | fullText: string,
197 | cancellation: CancellationToken | undefined
198 | ) {
199 | if (breakOn instanceof RegExp) {
200 | if (!breakOn.global) {
201 | throw new Error(`\`breakOn\` expression must have the global flag set (got ${breakOn})`);
202 | }
203 |
204 | breakOn.lastIndex = 0;
205 | }
206 |
207 | let outputText = '';
208 | let lastIndex = -1;
209 | while (lastIndex < fullText.length) {
210 | let index: number;
211 | if (typeof breakOn === 'string') {
212 | index = fullText.indexOf(breakOn, lastIndex === -1 ? 0 : lastIndex + breakOn.length);
213 | } else {
214 | index = breakOn.exec(fullText)?.index ?? -1;
215 | }
216 |
217 | if (index === -1) {
218 | index = fullText.length;
219 | }
220 |
221 | const next = outputText + fullText.slice(Math.max(0, lastIndex), index);
222 | if (
223 | (await sizing.countTokens(
224 | { type: Raw.ChatCompletionContentPartKind.Text, text: next },
225 | cancellation
226 | )) > sizing.tokenBudget
227 | ) {
228 | return outputText;
229 | }
230 |
231 | outputText = next;
232 | lastIndex = index;
233 | }
234 |
235 | return outputText;
236 | }
237 |
238 | export class Image extends PromptElement {
239 | constructor(props: ImageProps) {
240 | super(props);
241 | }
242 |
243 | render() {
244 | return <>{this.props.children}>;
245 | }
246 | }
247 |
248 | export interface PrioritizedListProps extends BasePromptElementProps {
249 | /**
250 | * Priority of the list element.
251 | * All rendered elements in this list receive a priority that is offset from this value.
252 | */
253 | priority?: number;
254 | /**
255 | * If `true`, assign higher priority to elements declared earlier in this list.
256 | */
257 | descending: boolean;
258 | }
259 |
260 | /**
261 | * A utility for assigning priorities to a list of prompt elements.
262 | */
263 | export class PrioritizedList extends PromptElement {
264 | override render() {
265 | const { children, priority = 0, descending } = this.props;
266 | if (!children) {
267 | return;
268 | }
269 |
270 | return (
271 | <>
272 | {children.map((child, i) => {
273 | if (!child) {
274 | return;
275 | }
276 |
277 | const thisPriority = descending
278 | ? // First element in array of children has highest priority
279 | priority - i
280 | : // Last element in array of children has highest priority
281 | priority - children.length + i;
282 |
283 | if (typeof child !== 'object') {
284 | return {child};
285 | }
286 |
287 | child.props ??= {};
288 | child.props.priority = thisPriority;
289 | return child;
290 | })}
291 | >
292 | );
293 | }
294 | }
295 |
296 | export interface IToolResultProps extends BasePromptElementProps {
297 | /**
298 | * Base priority of the tool data. All tool data will be scoped to this priority.
299 | */
300 | priority?: number;
301 |
302 | /**
303 | * Tool result from VS Code.
304 | */
305 | data: LanguageModelToolResult;
306 | }
307 |
308 | /**
309 | * A utility to include the result of a tool called using the `vscode.lm.invokeTool` API.
310 | */
311 | export class ToolResult extends PromptElement {
312 | render(): Promise | PromptPiece | undefined {
313 | // note: future updates to content types should be handled here for backwards compatibility
314 | return (
315 | <>
316 | {this.props.data.content.map(part => {
317 | if (part && typeof (part as LanguageModelTextPart).value === 'string') {
318 | return (part as LanguageModelTextPart).value;
319 | } else if (
320 | part &&
321 | (part as LanguageModelPromptTsxPart).value &&
322 | typeof (part as { value: PromptElementJSON }).value.node === 'object'
323 | ) {
324 | return (
325 |
326 | );
327 | }
328 | })}
329 | >
330 | );
331 | }
332 | }
333 |
334 | /**
335 | * Marker element that uses the legacy global prioritization algorithm (0.2.x
336 | * if this library) for pruning child elements. This will be removed in
337 | * the future.
338 | *
339 | * @deprecated
340 | */
341 | export class LegacyPrioritization extends PromptElement {
342 | render() {
343 | return <>{this.props.children}>;
344 | }
345 | }
346 |
347 | /**
348 | * Marker element that ensures all of its children are either included, or
349 | * not included. This is similar to the `` element, but it is more
350 | * basic and can contain extrinsic children.
351 | */
352 | export class Chunk extends PromptElement {
353 | render() {
354 | return <>{this.props.children}>;
355 | }
356 | }
357 |
358 | export interface ExpandableProps extends BasePromptElementProps {
359 | value: (sizing: PromptSizing) => string | Promise;
360 | }
361 |
362 | /**
363 | * An element that can expand to fill the remaining token budget. Takes
364 | * a `value` function that is initially called with the element's token budget,
365 | * and may be called multiple times with the new token budget as the prompt
366 | * is resized.
367 | */
368 | export class Expandable extends PromptElement {
369 | async render(_state: void, sizing: PromptSizing): Promise {
370 | return <>{await this.props.value(sizing)}>;
371 | }
372 | }
373 |
374 | export interface TokenLimitProps extends BasePromptElementProps {
375 | max: number;
376 | }
377 |
378 | /**
379 | * An element that ensures its children don't exceed a certain number of
380 | * `maxTokens`. Its contents are pruned to fit within the budget before
381 | * the overall prompt pruning is run.
382 | */
383 | export class TokenLimit extends PromptElement {
384 | render(): PromptPiece {
385 | return <>{this.props.children}>;
386 | }
387 | }
388 |
389 | export abstract class AbstractKeepWith extends PromptElement {
390 | public abstract readonly id: number;
391 | }
392 |
393 | let keepWidthId = 0;
394 |
395 | export type KeepWithCtor = {
396 | new (props: PromptElementProps): AbstractKeepWith;
397 | id: number;
398 | };
399 |
400 | /**
401 | * Returns a PromptElement that ensures each wrapped element is retained only
402 | * so long as each other wrapped is not empty.
403 | *
404 | * This is useful when dealing with tool calls, for example. In that case,
405 | * your tool call request should only be rendered if the tool call response
406 | * survived prioritization. In that case, you implement a `render` function
407 | * like so:
408 | *
409 | * ```
410 | * render() {
411 | * const KeepWith = useKeepWith();
412 | * return <>
413 | * ...
414 | * ...
415 | * >;
416 | * }
417 | * ```
418 | *
419 | * Unlike ``, which blocks pruning of any child elements and simply
420 | * removes them as a block, `` in this case will allow the
421 | * `ToolCallResponse` to be pruned, and if it's fully pruned it will also
422 | * remove the `ToolCallRequest`.
423 | */
424 | export function useKeepWith(): KeepWithCtor {
425 | const id = keepWidthId++;
426 | return class KeepWith extends AbstractKeepWith {
427 | public static readonly id = id;
428 |
429 | public readonly id = id;
430 |
431 | render(): PromptPiece {
432 | return <>{this.props.children}>;
433 | }
434 | };
435 | }
436 |
437 | export interface IfEmptyProps extends BasePromptElementProps {
438 | alt: PromptPieceChild;
439 | }
440 |
441 | /**
442 | * An element that returns its `alt` prop if its children are empty at the
443 | * time when it's rendered. This is especially useful when you require
444 | * fallback logic for opaque child data, such as tool calls.
445 | */
446 | export class IfEmpty extends PromptElement {
447 | render(): PromptPiece {
448 | return (
449 | <>
450 | {this.props.alt}
451 | {this.props.children}
452 | >
453 | );
454 | }
455 | }
456 |
457 | export class LogicalWrapper extends PromptElement {
458 | render(): PromptPiece {
459 | return <>{this.props.children}>;
460 | }
461 | }
462 |
--------------------------------------------------------------------------------
/src/base/results.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation and GitHub. All rights reserved.
3 | *--------------------------------------------------------------------------------------------*/
4 |
5 | import type { Location, ThemeIcon, Uri } from 'vscode';
6 | import * as JSON from './jsonTypes';
7 | import { URI } from './util/vs/common/uri';
8 |
9 | /**
10 | * Arbitrary metadata which can be retrieved after the prompt is rendered.
11 | */
12 | export abstract class PromptMetadata {
13 | readonly _marker: undefined;
14 | toString(): string {
15 | return Object.getPrototypeOf(this).constructor.name;
16 | }
17 | }
18 |
19 | export enum ChatResponseReferencePartStatusKind {
20 | Complete = 1,
21 | Partial = 2,
22 | Omitted = 3,
23 | }
24 |
25 | /**
26 | * A reference used for creating the prompt.
27 | */
28 | export class PromptReference {
29 | public static fromJSON(json: JSON.PromptReferenceJSON): PromptReference {
30 | // todo@connor4312: do we need to create concrete Location/Range types?
31 | const uriOrLocation = (v: JSON.UriOrLocationJSON): Uri | Location =>
32 | 'scheme' in v ? URI.from(v) : { uri: URI.from(v.uri), range: v.range };
33 |
34 | return new PromptReference(
35 | 'variableName' in json.anchor
36 | ? {
37 | variableName: json.anchor.variableName,
38 | value: json.anchor.value && uriOrLocation(json.anchor.value),
39 | }
40 | : uriOrLocation(json.anchor),
41 | json.iconPath &&
42 | ('scheme' in json.iconPath
43 | ? URI.from(json.iconPath)
44 | : 'light' in json.iconPath
45 | ? { light: URI.from(json.iconPath.light), dark: URI.from(json.iconPath.dark) }
46 | : json.iconPath),
47 | json.options
48 | );
49 | }
50 |
51 | constructor(
52 | readonly anchor: Uri | Location | { variableName: string; value?: Uri | Location },
53 | readonly iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri },
54 | readonly options?: {
55 | status?: { description: string; kind: ChatResponseReferencePartStatusKind };
56 | isFromTool?: boolean;
57 | }
58 | ) {}
59 |
60 | public toJSON(): JSON.PromptReferenceJSON {
61 | return {
62 | anchor: this.anchor,
63 | iconPath: this.iconPath,
64 | options: this.options,
65 | };
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/base/test/elements.test.tsx:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation and GitHub. All rights reserved.
3 | *--------------------------------------------------------------------------------------------*/
4 |
5 | import * as assert from 'assert';
6 | import { PromptElement } from '../promptElement';
7 | import { TextChunk, UserMessage } from '../promptElements';
8 | import { PromptRenderer } from '../promptRenderer';
9 | import { ITokenizer } from '../tokenizer/tokenizer';
10 | import { IChatEndpointInfo } from '../types';
11 | import { OutputMode, Raw } from '../output/mode';
12 |
13 | suite('PromptElements', () => {
14 | suite('TextChunk', () => {
15 | const tokenizer = new (class TokenPerWordTokenizer implements ITokenizer {
16 | readonly mode = OutputMode.Raw;
17 | baseTokensPerMessage = 0;
18 | baseTokensPerName = 0;
19 | baseTokensPerCompletion = 0;
20 |
21 | tokenLength(part: Raw.ChatCompletionContentPart): number {
22 | if (part.type !== Raw.ChatCompletionContentPartKind.Text) {
23 | return 0;
24 | }
25 | return this.strToken(part.text);
26 | }
27 |
28 | countMessageTokens(message: Raw.ChatMessage): number {
29 | return this.strToken(
30 | message.content
31 | .filter(p => p.type === Raw.ChatCompletionContentPartKind.Text)
32 | .map(p => p.text)
33 | .join('')
34 | );
35 | }
36 |
37 | private strToken(s: string) {
38 | return s.trim() === '' ? 1 : s.split(/\s+/g).length;
39 | }
40 | })();
41 |
42 | const assertThrows = async (message: RegExp, fn: () => Promise) => {
43 | let thrown = false;
44 | try {
45 | await fn();
46 | } catch (e) {
47 | thrown = true;
48 | assert.ok(message.test((e as Error).message));
49 | }
50 | assert.ok(thrown, 'expected to throw');
51 | };
52 |
53 | test('split behavior', async () => {
54 | const inst = new PromptRenderer(
55 | { modelMaxPromptTokens: 11 } satisfies Partial as IChatEndpointInfo,
56 | class extends PromptElement {
57 | render() {
58 | return (
59 |
60 |
61 | 1a
62 |
63 | 1b 1c 1d 1e 1f 1g 1h 1i 1j 1k 1l 1m 1n 1o 1p 1q 1r 1s 1t 1u 1v 1w 1x 1y 1z
64 |
65 |
66 | 2a 2b 2c 2d 2e 2f 2g 2h 2i 2j 2k 2l 2m 2n 2o 2p 2q 2r 2s 2t 2u 2v 2w 2x 2y 2z
67 |
68 |
69 | );
70 | }
71 | },
72 | {},
73 | tokenizer
74 | );
75 | const res = await inst.render(undefined, undefined);
76 | assert.deepStrictEqual(res.messages, [
77 | {
78 | content: [{
79 | type: Raw.ChatCompletionContentPartKind.Text,
80 | text: '1a\n1b 1c 1d 1e\n2a 2b 2c 2d 2e',
81 | }],
82 | role: Raw.ChatRole.User,
83 | },
84 | ]);
85 | });
86 |
87 | test('throws on extrinsic', async () => {
88 | await assertThrows(/must be text literals/, async () => {
89 | const inst = new PromptRenderer(
90 | { modelMaxPromptTokens: 11 } satisfies Partial as IChatEndpointInfo,
91 | class Foo extends PromptElement {
92 | render() {
93 | return (
94 |
95 |
96 |
97 |
98 |
99 | );
100 | }
101 | },
102 | {},
103 | tokenizer
104 | );
105 | await inst.render(undefined, undefined);
106 | });
107 | });
108 | });
109 | });
110 |
--------------------------------------------------------------------------------
/src/base/test/materialized.test.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation and GitHub. All rights reserved.
3 | *--------------------------------------------------------------------------------------------*/
4 |
5 | import * as assert from 'assert';
6 | import {
7 | LineBreakBefore,
8 | MaterializedChatMessage,
9 | MaterializedChatMessageTextChunk,
10 | GenericMaterializedContainer,
11 | } from '../materialized';
12 | import { OutputMode, Raw } from '../output/mode';
13 | import { ITokenizer } from '../tokenizer/tokenizer';
14 | import { strFrom } from './testUtils';
15 |
16 | class MockTokenizer implements ITokenizer {
17 | readonly mode = OutputMode.Raw;
18 | tokenLength(part: Raw.ChatCompletionContentPart): number {
19 | return strFrom(part).length;
20 | }
21 | countMessageTokens(message: Raw.ChatMessage): number {
22 | return strFrom(message).length + 3;
23 | }
24 | }
25 | suite('Materialized', () => {
26 | test('should calculate token count correctly', async () => {
27 | const tokenizer = new MockTokenizer();
28 | const container = new GenericMaterializedContainer(
29 | undefined,
30 | 1,
31 | undefined,
32 | 1,
33 | parent => [
34 | new MaterializedChatMessage(
35 | parent,
36 | 0,
37 | Raw.ChatRole.User,
38 | 'user',
39 | undefined,
40 | undefined,
41 | 1,
42 | [],
43 | parent => [
44 | new MaterializedChatMessageTextChunk(parent, 'Hello', 1, [], LineBreakBefore.None),
45 | new MaterializedChatMessageTextChunk(parent, 'World', 1, [], LineBreakBefore.None),
46 | ]
47 | ),
48 | ],
49 | [],
50 | 0
51 | );
52 |
53 | assert.deepStrictEqual(await container.tokenCount(tokenizer), 13);
54 | container.removeLowestPriorityChild();
55 | assert.deepStrictEqual(await container.tokenCount(tokenizer), 8);
56 | });
57 |
58 | test('should calculate lower bound token count correctly', async () => {
59 | const tokenizer = new MockTokenizer();
60 | const container = new GenericMaterializedContainer(
61 | undefined,
62 | 1,
63 | undefined,
64 | 1,
65 | parent => [
66 | new MaterializedChatMessage(
67 | parent,
68 | 0,
69 | Raw.ChatRole.User,
70 | 'user',
71 | undefined,
72 | undefined,
73 | 1,
74 | [],
75 | parent => [
76 | new MaterializedChatMessageTextChunk(parent, 'Hello', 1, [], LineBreakBefore.None),
77 | new MaterializedChatMessageTextChunk(parent, 'World', 1, [], LineBreakBefore.None),
78 | ]
79 | ),
80 | ],
81 | [],
82 | 0
83 | );
84 |
85 | assert.deepStrictEqual(await container.upperBoundTokenCount(tokenizer), 13);
86 | container.removeLowestPriorityChild();
87 | assert.deepStrictEqual(await container.upperBoundTokenCount(tokenizer), 8);
88 | });
89 | });
90 |
--------------------------------------------------------------------------------
/src/base/test/renderer.bench.tsx:
--------------------------------------------------------------------------------
1 | import { existsSync, readFileSync } from 'fs';
2 | import { Bench } from 'tinybench';
3 | import { Cl100KBaseTokenizer } from '../tokenizer/cl100kBaseTokenizer';
4 | import type * as promptTsx from '..';
5 | import assert = require('assert');
6 |
7 | const comparePathVar = 'PROMPT_TSX_COMPARE_PATH';
8 | const tsxComparePath =
9 | process.env[comparePathVar] ||
10 | `${__dirname}/../../../../vscode-copilot/node_modules/@vscode/prompt-tsx`;
11 | const canCompare = existsSync(tsxComparePath);
12 | if (!canCompare) {
13 | console.error(
14 | `$${comparePathVar} was not set / ${tsxComparePath} doesn't exist, so the benchmark will not compare to past behavior`
15 | );
16 | process.exit(1);
17 | }
18 |
19 | const numberOfRepeats = 1;
20 | const sampleText = readFileSync(`${__dirname}/renderer.test.tsx`, 'utf-8');
21 | const sampleTextLines = readFileSync(`${__dirname}/renderer.test.tsx`, 'utf-8').split('\n');
22 | const tokenizer = new Cl100KBaseTokenizer();
23 | const bench = new Bench({
24 | name: `trim ${tokenizer.tokenLength({ type: 1, text: sampleText }) * numberOfRepeats}->1k tokens`,
25 | time: 100,
26 | });
27 |
28 | async function benchTokenizationTrim({
29 | PromptRenderer,
30 | PromptElement,
31 | UserMessage,
32 | TextChunk,
33 | }: typeof promptTsx) {
34 | const r = await new PromptRenderer(
35 | { modelMaxPromptTokens: 1000 },
36 | class extends PromptElement {
37 | render() {
38 | return (
39 | <>
40 | {Array.from({ length: numberOfRepeats }, () => (
41 |
42 | {sampleTextLines.map(l => (
43 | {l}
44 | ))}
45 |
46 | ))}
47 | >
48 | );
49 | }
50 | },
51 | {},
52 | tokenizer
53 | ).render();
54 | assert(r.tokenCount <= 1000);
55 | assert(r.tokenCount > 100);
56 | }
57 |
58 | bench.add('current', () => benchTokenizationTrim(require('..')));
59 | if (canCompare) {
60 | const fn = require(tsxComparePath);
61 | bench.add('previous', () => benchTokenizationTrim(fn));
62 | }
63 |
64 | bench.run().then(() => {
65 | console.log(bench.name);
66 | console.table(bench.table());
67 | });
68 |
--------------------------------------------------------------------------------
/src/base/test/testUtils.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation and GitHub. All rights reserved.
3 | *--------------------------------------------------------------------------------------------*/
4 |
5 | import { Raw } from '../output/mode';
6 |
7 | export const strFrom = (message: Raw.ChatMessage | Raw.ChatCompletionContentPart): string => {
8 | if ('role' in message) {
9 | return message.content.map(strFrom).join('');
10 | } else if (message.type === Raw.ChatCompletionContentPartKind.Text) {
11 | return message.text;
12 | } else {
13 | return '';
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/base/tokenizer/cl100kBaseTokenizer.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation and GitHub. All rights reserved.
3 | *--------------------------------------------------------------------------------------------*/
4 |
5 | import {
6 | createTokenizer,
7 | getRegexByEncoder,
8 | getSpecialTokensByEncoder,
9 | TikTokenizer,
10 | } from '@microsoft/tiktokenizer';
11 | import { join } from 'path';
12 | import { ITokenizer } from './tokenizer';
13 | import { OutputMode, Raw, OpenAI } from '../output/mode';
14 |
15 | /**
16 | * The Cl100K BPE tokenizer for the `gpt-4`, `gpt-3.5-turbo`, and `text-embedding-ada-002` models.
17 | *
18 | * See https://github.com/microsoft/Tokenizer
19 | */
20 | export class Cl100KBaseTokenizer implements ITokenizer {
21 | private _cl100kTokenizer: TikTokenizer | undefined;
22 |
23 | public readonly mode = OutputMode.OpenAI;
24 | public readonly models = ['gpt-4', 'gpt-3.5-turbo', 'text-embedding-ada-002'];
25 |
26 | private readonly baseTokensPerMessage = OpenAI.BaseTokensPerMessage;
27 | private readonly baseTokensPerName = OpenAI.BaseTokensPerName;
28 |
29 | constructor() {}
30 |
31 | /**
32 | * Tokenizes the given text using the Cl100K tokenizer.
33 | * @param text The text to tokenize.
34 | * @returns The tokenized text.
35 | */
36 | private tokenize(text: string): number[] {
37 | if (!this._cl100kTokenizer) {
38 | this._cl100kTokenizer = this.initTokenizer();
39 | }
40 | return this._cl100kTokenizer.encode(text);
41 | }
42 |
43 | /**
44 | * Calculates the token length of the given text.
45 | * @param text The text to calculate the token length for.
46 | * @returns The number of tokens in the text.
47 | */
48 | tokenLength(part: Raw.ChatCompletionContentPart): number {
49 | if (part.type === Raw.ChatCompletionContentPartKind.Text) {
50 | return part.text ? this.tokenize(part.text).length : 0;
51 | }
52 |
53 | return 0;
54 | }
55 |
56 | /**
57 | * Counts tokens for a single chat message within a completion request.
58 | *
59 | * Follows https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb for GPT 3.5/4 models.
60 | *
61 | * **Note**: The result does not include base tokens for the completion itself.
62 | */
63 | countMessageTokens(message: OpenAI.ChatMessage): number {
64 | return this.baseTokensPerMessage + this.countObjectTokens(message);
65 | }
66 |
67 | protected countObjectTokens(obj: any): number {
68 | let numTokens = 0;
69 | for (const [key, value] of Object.entries(obj)) {
70 | if (!value) {
71 | continue;
72 | }
73 |
74 | if (typeof value === 'string') {
75 | numTokens += this.tokenize(value).length;
76 | } else if (value) {
77 | // TODO@roblourens - count tokens for tool_calls correctly
78 | // TODO@roblourens - tool_call_id is always 1 token
79 | numTokens += this.countObjectTokens(value);
80 | }
81 |
82 | if (key === 'name') {
83 | numTokens += this.baseTokensPerName;
84 | }
85 | }
86 |
87 | return numTokens;
88 | }
89 |
90 | private initTokenizer(): TikTokenizer {
91 | return createTokenizer(
92 | // This file is copied to `dist` via the `build/postinstall.ts` script
93 | join(__dirname, './cl100k_base.tiktoken'),
94 | getSpecialTokensByEncoder('cl100k_base'),
95 | getRegexByEncoder('cl100k_base'),
96 | 64000
97 | );
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/base/tokenizer/tokenizer.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation and GitHub. All rights reserved.
3 | *--------------------------------------------------------------------------------------------*/
4 |
5 | import type { CancellationToken, LanguageModelChatMessage } from 'vscode';
6 | import { ModeToChatMessageType, OutputMode, Raw } from '../output/mode';
7 |
8 | /**
9 | * Represents a tokenizer that can be used to tokenize text in chat messages.
10 | */
11 | export interface ITokenizer {
12 | /**
13 | * This mode this tokenizer operates on.
14 | */
15 | readonly mode: M;
16 |
17 | /**
18 | * Return the length of `part` in number of tokens. If the model does not
19 | * support the given kind of part, it may return 0.
20 | *
21 | * @param {str} text - The input text
22 | * @returns {number}
23 | */
24 | tokenLength(
25 | part: Raw.ChatCompletionContentPart,
26 | token?: CancellationToken
27 | ): Promise | number;
28 |
29 | /**
30 | * Returns the token length of the given message.
31 | */
32 | countMessageTokens(message: ModeToChatMessageType[M]): Promise | number;
33 | }
34 |
35 | export class VSCodeTokenizer implements ITokenizer {
36 | public readonly mode = OutputMode.VSCode;
37 |
38 | constructor(
39 | private countTokens: (
40 | text: string | LanguageModelChatMessage,
41 | token?: CancellationToken
42 | ) => Thenable,
43 | mode: OutputMode
44 | ) {
45 | if (mode !== OutputMode.VSCode) {
46 | throw new Error(
47 | '`mode` must be set to vscode when using vscode.LanguageModelChat as the tokenizer'
48 | );
49 | }
50 | }
51 |
52 | async tokenLength(
53 | part: Raw.ChatCompletionContentPart,
54 | token?: CancellationToken
55 | ): Promise {
56 | if (part.type === Raw.ChatCompletionContentPartKind.Text) {
57 | return this.countTokens(part.text, token);
58 | }
59 |
60 | return Promise.resolve(0);
61 | }
62 |
63 | async countMessageTokens(message: LanguageModelChatMessage): Promise {
64 | return this.countTokens(message);
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/base/tracer.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation and GitHub. All rights reserved.
3 | *--------------------------------------------------------------------------------------------*/
4 |
5 | import { GenericMaterializedContainer } from './materialized';
6 | import { ITokenizer } from './tokenizer/tokenizer';
7 |
8 | export interface ITraceRenderData {
9 | budget: number;
10 | container: GenericMaterializedContainer;
11 | removed: number;
12 | }
13 |
14 | export interface ITraceData {
15 | /** Budget the tree was rendered with initially. */
16 | budget: number;
17 |
18 | /** Tree returned from the prompt. */
19 | renderedTree: ITraceRenderData;
20 |
21 | /** Tokenizer that was used. */
22 | tokenizer: ITokenizer;
23 |
24 | /** Callback the tracer and use to re-render the tree at the given budget. */
25 | renderTree(tokenBudget: number): Promise;
26 | }
27 |
28 | export interface IElementEpochData {
29 | id: number;
30 | tokenBudget: number;
31 | }
32 |
33 | export interface ITraceEpoch {
34 | inNode: number | undefined;
35 | flexValue: number;
36 | tokenBudget: number;
37 | reservedTokens: number;
38 | elements: IElementEpochData[];
39 | }
40 |
41 | /**
42 | * Handler that can trace rendering internals.
43 | */
44 | export interface ITracer {
45 | /**
46 | * Called when a group of elements is rendered.
47 | */
48 | addRenderEpoch?(epoch: ITraceEpoch): void;
49 |
50 | /**
51 | * Adds an element into the current epoch.
52 | */
53 | includeInEpoch?(data: IElementEpochData): void;
54 |
55 | /**
56 | * Called when the elements have been processed into their final tree form.
57 | */
58 | didMaterializeTree?(traceData: ITraceData): void;
59 | }
60 |
--------------------------------------------------------------------------------
/src/base/tsx-globals.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation and GitHub. All rights reserved.
3 | *--------------------------------------------------------------------------------------------*/
4 |
5 | import { PromptElementJSON } from './jsonTypes';
6 | import { PromptMetadata, PromptReference } from './results';
7 | import { URI } from './util/vs/common/uri';
8 | import { ChatDocumentContext } from './vscodeTypes';
9 |
10 | declare global {
11 | namespace JSX {
12 | interface IntrinsicElements {
13 | /**
14 | * Add meta data which can be retrieved after the prompt is rendered.
15 | */
16 | meta: {
17 | value: PromptMetadata;
18 | /**
19 | * If set, the metadata will only be included in the rendered result
20 | * if the chunk it's in survives prioritization.
21 | */
22 | local?: boolean;
23 | };
24 | /**
25 | * `\n` character.
26 | */
27 | br: {};
28 | /**
29 | * Expose context used for creating the prompt.
30 | */
31 | usedContext: {
32 | value: ChatDocumentContext[];
33 | };
34 | /**
35 | * Expose the references used for creating the prompt.
36 | * Will be displayed to the user.
37 | */
38 | references: {
39 | value: PromptReference[];
40 | };
41 | /**
42 | * Files that were excluded from the prompt.
43 | */
44 | ignoredFiles: {
45 | value: URI[];
46 | };
47 | /**
48 | * A JSON element previously rendered in {@link renderElementJSON}.
49 | */
50 | elementJSON: {
51 | data: PromptElementJSON;
52 | };
53 |
54 | /**
55 | * A data object that is emitted directly in the output. You as the
56 | * consumer are responsible for ensuring this data works. This element
57 | * has SHARP EDGES and should be used with great care.
58 | */
59 | opaque: {
60 | /** Value to be inserted in the output */
61 | value: unknown;
62 | /**
63 | * The number of tokens consumed by this fragment. This must be AT
64 | * LEAST the number of tokens this part represents in your tokenizer's
65 | * `countMessageTokens` method, or you will get obscure errors.
66 | */
67 | tokenUsage?: number;
68 | /** Usual priority value. */
69 | priority?: number;
70 | };
71 |
72 | /**
73 | * Adds a 'cache breakpoint' to the output. This is exclusively valid
74 | * as a direct child of message types (UserMessage, SystemMessage, etc.)
75 | */
76 | cacheBreakpoint: {
77 | /** Optional implementation-specific cache type */
78 | type?: string;
79 | };
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/base/tsx.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation and GitHub. All rights reserved.
3 | *--------------------------------------------------------------------------------------------*/
4 |
5 | interface _InternalPromptPiece
{
6 | ctor: string | any;
7 | props: P;
8 | children: string | (_InternalPromptPiece | undefined)[];
9 | }
10 |
11 | /**
12 | * Visual Studio Code Prompt Piece
13 | */
14 | function _vscpp(ctor: any, props: any, ...children: any[]): _InternalPromptPiece {
15 | return { ctor, props, children: children.flat() };
16 | }
17 |
18 | /**
19 | * Visual Studio Code Prompt Piece Fragment
20 | */
21 | function _vscppf() {
22 | throw new Error(`This should not be invoked!`);
23 | }
24 | _vscppf.isFragment = true;
25 |
26 | declare const vscpp: typeof _vscpp;
27 | declare const vscppf: typeof _vscppf;
28 |
29 | (globalThis).vscpp = _vscpp;
30 | (globalThis).vscppf = _vscppf;
31 |
--------------------------------------------------------------------------------
/src/base/types.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation and GitHub. All rights reserved.
3 | *--------------------------------------------------------------------------------------------*/
4 |
5 | import { CancellationToken } from 'vscode';
6 | import { PromptElement } from './promptElement';
7 | import { Raw } from './output/mode';
8 |
9 | /**
10 | * Represents information about a chat endpoint.
11 | */
12 | export interface IChatEndpointInfo {
13 | /**
14 | * The maximum number of tokens allowed in the model prompt.
15 | */
16 | readonly modelMaxPromptTokens: number;
17 | }
18 |
19 | /**
20 | * The sizing hint for the prompt element. Prompt elements should take this into account when rendering.
21 | */
22 | export interface PromptSizing {
23 | /**
24 | * The computed token allocation for this prompt element to adhere to when rendering,
25 | * if it specified {@link BasePromptElementProps.flexBasis}.
26 | */
27 | readonly tokenBudget: number;
28 | /**
29 | * Metadata about the endpoint being used.
30 | */
31 | readonly endpoint: IChatEndpointInfo;
32 |
33 | /**
34 | * Counts the number of tokens the text consumes.
35 | */
36 | countTokens(text: Raw.ChatCompletionContentPart | string, token?: CancellationToken): Promise | number;
37 | }
38 |
39 | export interface BasePromptElementProps {
40 | /**
41 | * The absolute priority of the prompt element.
42 | *
43 | * If the messages to be sent exceed the available token budget, prompt elements will be removed from the rendered result, starting with the element with the lowest priority.
44 | *
45 | * If unset, defaults to `Number.MAX_SAFE_INTEGER`, such that elements with no explicit priority take the highest-priority position.
46 | */
47 | priority?: number;
48 | /**
49 | * If set, the children of the prompt element will be considered children of the parent during pruning. This allows you to create logical wrapper elements, for example:
50 | *
51 | * ```
52 | *
53 | *
54 | *
55 | *
56 | *
57 | *
58 | *
59 | * ```
60 | *
61 | * In this case where we have a wrapper element, the prune order would be `ChildA`, `ChildC`, then `ChildB`.
62 | */
63 | passPriority?: boolean;
64 | /**
65 | * The proportion of the container's {@link PromptSizing.tokenBudget token budget} that is assigned to this prompt element, based on the total weight requested by the prompt element and all its siblings.
66 | *
67 | * This is used to compute the {@link PromptSizing.tokenBudget token budget} hint that the prompt element receives.
68 | *
69 | * If set on a child element, the token budget is calculated with respect to all children under the element's parent, such that a child can never consume more tokens than its parent was allocated.
70 | *
71 | * Defaults to 1.
72 | */
73 | flexBasis?: number;
74 |
75 | /**
76 | * If set, sibling elements will be rendered first, followed by this element. The remaining {@link PromptSizing.tokenBudget token budget} from the container will be distributed among the elements with `flexGrow` set.
77 | *
78 | * If multiple elements are present with different values of `flexGrow` set, this process is repeated for each value of `flexGrow` in descending order.
79 | */
80 | flexGrow?: number;
81 |
82 | /**
83 | * If set with {@link flexGrow}, this defines the number of tokens this element
84 | * will reserve of the container {@link PromptSizing.tokenBudget token budget}
85 | * for sizing purposes in elements rendered before it.
86 | *
87 | * This can be set to a constant number of tokens, or a proportion of the
88 | * container's budget. For example, `/3` would reserve a third of the
89 | * container's budget.
90 | */
91 | flexReserve?: number | `/${number}`;
92 | }
93 |
94 | export interface PromptElementCtor