From e9547712704b7efb1434bdaaab8043dff19656a8 Mon Sep 17 00:00:00 2001 From: Seth Juarez Date: Fri, 10 Apr 2026 15:03:49 -0700 Subject: [PATCH 1/3] chore: bump all runtimes to alpha.8 Bump Python (2.0.0a8), TypeScript (2.0.0-alpha.8), C# (2.0.0-alpha.8), and VS Code extension packages for release including: - fix: recursive array/object JSON Schema in tool params (#332) - feat: Rust runtime parity + optional authenticationMode (#333) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Prompty.Anthropic.csproj | 2 +- .../csharp/Prompty.Core/Prompty.Core.csproj | 2 +- .../Prompty.Foundry/Prompty.Foundry.csproj | 2 +- .../Prompty.OpenAI/Prompty.OpenAI.csproj | 2 +- runtime/python/prompty/prompty/_version.py | 2 +- .../packages/anthropic/package.json | 4 +-- runtime/typescript/packages/core/package.json | 2 +- .../typescript/packages/foundry/package.json | 10 +++--- .../typescript/packages/openai/package.json | 6 ++-- .../prompty/packages/core/package-lock.json | 32 +++++++++---------- vscode/prompty/packages/core/package.json | 8 ++--- .../prompty/packages/server/package-lock.json | 6 ++-- vscode/prompty/packages/server/package.json | 4 +-- 13 files changed, 41 insertions(+), 41 deletions(-) diff --git a/runtime/csharp/Prompty.Anthropic/Prompty.Anthropic.csproj b/runtime/csharp/Prompty.Anthropic/Prompty.Anthropic.csproj index a7673704..5f499882 100644 --- a/runtime/csharp/Prompty.Anthropic/Prompty.Anthropic.csproj +++ b/runtime/csharp/Prompty.Anthropic/Prompty.Anthropic.csproj @@ -4,7 +4,7 @@ net9.0 enable enable - 2.0.0-alpha.7 + 2.0.0-alpha.8 Prompty.Anthropic Microsoft Anthropic provider for Prompty — executor and processor for Claude models via the Anthropic Messages API. diff --git a/runtime/csharp/Prompty.Core/Prompty.Core.csproj b/runtime/csharp/Prompty.Core/Prompty.Core.csproj index f2ec5b48..2db681e7 100644 --- a/runtime/csharp/Prompty.Core/Prompty.Core.csproj +++ b/runtime/csharp/Prompty.Core/Prompty.Core.csproj @@ -4,7 +4,7 @@ net9.0 enable enable - 2.0.0-alpha.7 + 2.0.0-alpha.8 Prompty.Core Microsoft Prompty is an asset class and format for LLM prompts. Core library with loader, pipeline, renderers, parsers, and tracing. diff --git a/runtime/csharp/Prompty.Foundry/Prompty.Foundry.csproj b/runtime/csharp/Prompty.Foundry/Prompty.Foundry.csproj index da398bad..bdd3f6ab 100644 --- a/runtime/csharp/Prompty.Foundry/Prompty.Foundry.csproj +++ b/runtime/csharp/Prompty.Foundry/Prompty.Foundry.csproj @@ -6,7 +6,7 @@ enable OPENAI001;CS1591 true - 2.0.0-alpha.7 + 2.0.0-alpha.8 Prompty.Foundry Microsoft Microsoft Foundry (Azure OpenAI) provider for Prompty — executor and processor for Azure-hosted models. diff --git a/runtime/csharp/Prompty.OpenAI/Prompty.OpenAI.csproj b/runtime/csharp/Prompty.OpenAI/Prompty.OpenAI.csproj index ce7ca434..f625fd70 100644 --- a/runtime/csharp/Prompty.OpenAI/Prompty.OpenAI.csproj +++ b/runtime/csharp/Prompty.OpenAI/Prompty.OpenAI.csproj @@ -6,7 +6,7 @@ enable OPENAI001;CS1591 true - 2.0.0-alpha.7 + 2.0.0-alpha.8 Prompty.OpenAI Microsoft OpenAI provider for Prompty — executor and processor for OpenAI chat, embedding, image, and agent APIs. diff --git a/runtime/python/prompty/prompty/_version.py b/runtime/python/prompty/prompty/_version.py index bb6b6459..747aa68b 100644 --- a/runtime/python/prompty/prompty/_version.py +++ b/runtime/python/prompty/prompty/_version.py @@ -1 +1 @@ -VERSION = "2.0.0a7" +VERSION = "2.0.0a8" diff --git a/runtime/typescript/packages/anthropic/package.json b/runtime/typescript/packages/anthropic/package.json index b96d32dc..4645640b 100644 --- a/runtime/typescript/packages/anthropic/package.json +++ b/runtime/typescript/packages/anthropic/package.json @@ -1,6 +1,6 @@ { "name": "@prompty/anthropic", - "version": "2.0.0-alpha.7", + "version": "2.0.0-alpha.8", "description": "Anthropic provider for Prompty — executor and processor for Anthropic Messages API", "type": "module", "main": "./dist/index.cjs", @@ -46,7 +46,7 @@ "node": ">=18.0.0" }, "peerDependencies": { - "@prompty/core": "^2.0.0-alpha.7", + "@prompty/core": "^2.0.0-alpha.8", "@anthropic-ai/sdk": ">=0.39.0" }, "peerDependenciesMeta": { diff --git a/runtime/typescript/packages/core/package.json b/runtime/typescript/packages/core/package.json index a2156358..346902f9 100644 --- a/runtime/typescript/packages/core/package.json +++ b/runtime/typescript/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@prompty/core", - "version": "2.0.0-alpha.7", + "version": "2.0.0-alpha.8", "description": "Prompty core runtime — load, render, parse, and trace .prompty files", "type": "module", "main": "./dist/index.cjs", diff --git a/runtime/typescript/packages/foundry/package.json b/runtime/typescript/packages/foundry/package.json index 78ee12b8..631c80f8 100644 --- a/runtime/typescript/packages/foundry/package.json +++ b/runtime/typescript/packages/foundry/package.json @@ -1,6 +1,6 @@ { "name": "@prompty/foundry", - "version": "2.0.0-alpha.7", + "version": "2.0.0-alpha.8", "description": "Microsoft Foundry provider for Prompty — executor and processor for Azure AI Foundry", "type": "module", "main": "./dist/index.cjs", @@ -46,8 +46,8 @@ "node": ">=18.0.0" }, "peerDependencies": { - "@prompty/core": "^2.0.0-alpha.7", - "@prompty/openai": "^2.0.0-alpha.7" + "@prompty/core": "^2.0.0-alpha.8", + "@prompty/openai": "^2.0.0-alpha.8" }, "dependencies": { "@azure/ai-projects": "^2.0.1", @@ -55,8 +55,8 @@ "openai": "^4.80.0" }, "devDependencies": { - "@prompty/core": "^2.0.0-alpha.7", - "@prompty/openai": "^2.0.0-alpha.7", + "@prompty/core": "^2.0.0-alpha.8", + "@prompty/openai": "^2.0.0-alpha.8", "@types/node": "^20.11.0", "dotenv": "^16.4.0", "tsup": "^8.4.0", diff --git a/runtime/typescript/packages/openai/package.json b/runtime/typescript/packages/openai/package.json index b34bc831..ac93837e 100644 --- a/runtime/typescript/packages/openai/package.json +++ b/runtime/typescript/packages/openai/package.json @@ -1,6 +1,6 @@ { "name": "@prompty/openai", - "version": "2.0.0-alpha.7", + "version": "2.0.0-alpha.8", "description": "OpenAI provider for Prompty — executor and processor for OpenAI APIs", "type": "module", "main": "./dist/index.cjs", @@ -45,13 +45,13 @@ "node": ">=18.0.0" }, "peerDependencies": { - "@prompty/core": "^2.0.0-alpha.7" + "@prompty/core": "^2.0.0-alpha.8" }, "dependencies": { "openai": "^4.80.0" }, "devDependencies": { - "@prompty/core": "^2.0.0-alpha.7", + "@prompty/core": "^2.0.0-alpha.8", "@types/node": "^20.11.0", "dotenv": "^16.4.0", "tsup": "^8.4.0", diff --git a/vscode/prompty/packages/core/package-lock.json b/vscode/prompty/packages/core/package-lock.json index c5944b2d..9b85b93e 100644 --- a/vscode/prompty/packages/core/package-lock.json +++ b/vscode/prompty/packages/core/package-lock.json @@ -12,10 +12,10 @@ "@anthropic-ai/sdk": "^0.39.0", "@azure/ai-projects": "^2.0.1", "@azure/identity": "^4.6.0", - "@prompty/anthropic": "^2.0.0-alpha.7", - "@prompty/core": "^2.0.0-alpha.7", - "@prompty/foundry": "^2.0.0-alpha.7", - "@prompty/openai": "^2.0.0-alpha.7", + "@prompty/anthropic": "^2.0.0-alpha.8", + "@prompty/core": "^2.0.0-alpha.8", + "@prompty/foundry": "^2.0.0-alpha.8", + "@prompty/openai": "^2.0.0-alpha.8", "glob": "^11.1.0", "openai": "^4.80.0", "vscode-languageclient": "^9.0.1" @@ -408,8 +408,8 @@ } }, "node_modules/@prompty/anthropic": { - "version": "2.0.0-alpha.7", - "resolved": "https://registry.npmjs.org/@prompty/anthropic/-/anthropic-2.0.0-alpha.7.tgz", + "version": "2.0.0-alpha.8", + "resolved": "https://registry.npmjs.org/@prompty/anthropic/-/anthropic-2.0.0-alpha.8.tgz", "integrity": "sha512-vz8Lc7Hp4upvFmMRV9tNsMOhTqvqQjSo0HrdJKi/zWa6XKfeMeewIygN8z56XaPllOeVJPACqzrKB9TUa5GCbA==", "license": "MIT", "engines": { @@ -417,7 +417,7 @@ }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.39.0", - "@prompty/core": "^2.0.0-alpha.7" + "@prompty/core": "^2.0.0-alpha.8" }, "peerDependenciesMeta": { "@anthropic-ai/sdk": { @@ -426,8 +426,8 @@ } }, "node_modules/@prompty/core": { - "version": "2.0.0-alpha.7", - "resolved": "https://registry.npmjs.org/@prompty/core/-/core-2.0.0-alpha.7.tgz", + "version": "2.0.0-alpha.8", + "resolved": "https://registry.npmjs.org/@prompty/core/-/core-2.0.0-alpha.8.tgz", "integrity": "sha512-vHQCzmHv3m3bx/aZRXMYg67idit1xBEPNg6AKKR/0GeUj8Z5l2LAXLEoqK7xo46OBs07RdNeHXcwtWd1nZhZ8g==", "license": "MIT", "peer": true, @@ -450,8 +450,8 @@ } }, "node_modules/@prompty/foundry": { - "version": "2.0.0-alpha.7", - "resolved": "https://registry.npmjs.org/@prompty/foundry/-/foundry-2.0.0-alpha.7.tgz", + "version": "2.0.0-alpha.8", + "resolved": "https://registry.npmjs.org/@prompty/foundry/-/foundry-2.0.0-alpha.8.tgz", "integrity": "sha512-7AiZjzFiKOkY4OQ4egOT4De+W6kpk8MwtcSAuugk82BPq+2XbCZv3SsOhVKmWhoHG16qQ/j2FpPLdbtAtM/Atw==", "license": "MIT", "dependencies": { @@ -463,13 +463,13 @@ "node": ">=18.0.0" }, "peerDependencies": { - "@prompty/core": "^2.0.0-alpha.7", - "@prompty/openai": "^2.0.0-alpha.7" + "@prompty/core": "^2.0.0-alpha.8", + "@prompty/openai": "^2.0.0-alpha.8" } }, "node_modules/@prompty/openai": { - "version": "2.0.0-alpha.7", - "resolved": "https://registry.npmjs.org/@prompty/openai/-/openai-2.0.0-alpha.7.tgz", + "version": "2.0.0-alpha.8", + "resolved": "https://registry.npmjs.org/@prompty/openai/-/openai-2.0.0-alpha.8.tgz", "integrity": "sha512-kJsHlYz/7J++79sw++pymwZW6YYjW78ToxtgnkuS9FDqB3VUHOMEGKjQJzlfj8McmDywmx/zpqHG+PHPr+ZpjA==", "license": "MIT", "peer": true, @@ -480,7 +480,7 @@ "node": ">=18.0.0" }, "peerDependencies": { - "@prompty/core": "^2.0.0-alpha.7" + "@prompty/core": "^2.0.0-alpha.8" } }, "node_modules/@types/node": { diff --git a/vscode/prompty/packages/core/package.json b/vscode/prompty/packages/core/package.json index a8f2e88f..89b27a06 100644 --- a/vscode/prompty/packages/core/package.json +++ b/vscode/prompty/packages/core/package.json @@ -16,10 +16,10 @@ "@anthropic-ai/sdk": "^0.39.0", "@azure/ai-projects": "^2.0.1", "@azure/identity": "^4.6.0", - "@prompty/anthropic": "^2.0.0-alpha.7", - "@prompty/core": "^2.0.0-alpha.7", - "@prompty/foundry": "^2.0.0-alpha.7", - "@prompty/openai": "^2.0.0-alpha.7", + "@prompty/anthropic": "^2.0.0-alpha.8", + "@prompty/core": "^2.0.0-alpha.8", + "@prompty/foundry": "^2.0.0-alpha.8", + "@prompty/openai": "^2.0.0-alpha.8", "glob": "^11.1.0", "openai": "^4.80.0", "vscode-languageclient": "^9.0.1" diff --git a/vscode/prompty/packages/server/package-lock.json b/vscode/prompty/packages/server/package-lock.json index ca0de2d6..0b28b453 100644 --- a/vscode/prompty/packages/server/package-lock.json +++ b/vscode/prompty/packages/server/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@prompty/core": "^2.0.0-alpha.7", + "@prompty/core": "^2.0.0-alpha.8", "jsonc-parser": "^3.3.1", "vscode-json-languageservice": "^5.7.2", "vscode-languageserver": "^9.0.1", @@ -126,8 +126,8 @@ } }, "node_modules/@prompty/core": { - "version": "2.0.0-alpha.7", - "resolved": "https://registry.npmjs.org/@prompty/core/-/core-2.0.0-alpha.7.tgz", + "version": "2.0.0-alpha.8", + "resolved": "https://registry.npmjs.org/@prompty/core/-/core-2.0.0-alpha.8.tgz", "integrity": "sha512-vHQCzmHv3m3bx/aZRXMYg67idit1xBEPNg6AKKR/0GeUj8Z5l2LAXLEoqK7xo46OBs07RdNeHXcwtWd1nZhZ8g==", "license": "MIT", "dependencies": { diff --git a/vscode/prompty/packages/server/package.json b/vscode/prompty/packages/server/package.json index 48003a68..1d596e93 100644 --- a/vscode/prompty/packages/server/package.json +++ b/vscode/prompty/packages/server/package.json @@ -8,7 +8,7 @@ "node": "*" }, "dependencies": { - "@prompty/core": "^2.0.0-alpha.7", + "@prompty/core": "^2.0.0-alpha.8", "jsonc-parser": "^3.3.1", "vscode-json-languageservice": "^5.7.2", "vscode-languageserver": "^9.0.1", @@ -33,4 +33,4 @@ "yaml": "2.8.3" } } -} \ No newline at end of file +} From 8efed02667f2cb17d8fecebb5bc6e7b8b9fe1b2a Mon Sep 17 00:00:00 2001 From: Seth Juarez Date: Fri, 10 Apr 2026 15:53:55 -0700 Subject: [PATCH 2/3] fix: nonce type coercion bug in parser strict mode across all runtimes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When randomBytes/token_hex generates an all-digit hex nonce (e.g. '1234567890123456'), parseAttrs type coercion converts it to a number. The subsequent strict comparison (number !== string) always fails, causing a spurious 'Nonce mismatch' error. Fix: compare nonces as strings in buildMessage/build_message across Python, TypeScript, and Rust parsers. Probability of all-digit 16-char hex: (10/16)^16 ≈ 0.01% — matches the observed flaky CI failure rate. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- runtime/python/prompty/prompty/parsers/prompty.py | 3 ++- runtime/rust/prompty/src/parsers/prompty.rs | 10 +++++++++- .../typescript/packages/core/src/parsers/prompty.ts | 5 +++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/runtime/python/prompty/prompty/parsers/prompty.py b/runtime/python/prompty/prompty/parsers/prompty.py index 384f7c3d..afc97f11 100644 --- a/runtime/python/prompty/prompty/parsers/prompty.py +++ b/runtime/python/prompty/prompty/parsers/prompty.py @@ -173,7 +173,8 @@ def _build_message( # Validate nonce in strict mode if nonce is not None: msg_nonce = attrs.pop("nonce", None) - if msg_nonce != nonce: + # Compare as strings — _parse_attrs may coerce all-digit hex nonces to int + if str(msg_nonce) != nonce: raise ValueError( "Nonce mismatch — possible prompt injection detected " "(strict mode is enabled). A template variable may be " diff --git a/runtime/rust/prompty/src/parsers/prompty.rs b/runtime/rust/prompty/src/parsers/prompty.rs index 5012a6b1..5b6a406d 100644 --- a/runtime/rust/prompty/src/parsers/prompty.rs +++ b/runtime/rust/prompty/src/parsers/prompty.rs @@ -145,7 +145,15 @@ fn build_message( ) -> Result { // Validate nonce in strict mode if let Some(expected) = expected_nonce { - let msg_nonce = attrs.get("nonce").and_then(|v| v.as_str()).unwrap_or(""); + // Compare as string — parse_attrs may coerce all-digit hex nonces to Number + let msg_nonce = attrs + .get("nonce") + .map(|v| match v { + serde_json::Value::String(s) => s.clone(), + serde_json::Value::Number(n) => n.to_string(), + _ => String::new(), + }) + .unwrap_or_default(); if msg_nonce != expected { return Err(InvokerError::Parse( "Nonce mismatch — possible prompt injection detected \ diff --git a/runtime/typescript/packages/core/src/parsers/prompty.ts b/runtime/typescript/packages/core/src/parsers/prompty.ts index 97c81b2e..9784130c 100644 --- a/runtime/typescript/packages/core/src/parsers/prompty.ts +++ b/runtime/typescript/packages/core/src/parsers/prompty.ts @@ -123,9 +123,10 @@ export class PromptyChatParser implements Parser { // Validate nonce in strict mode if (nonce !== undefined) { - const msgNonce = attrs.nonce as string | undefined; + const msgNonce = attrs.nonce; delete attrs.nonce; - if (msgNonce !== nonce) { + // Compare as strings — parseAttrs may coerce all-digit hex nonces to numbers + if (String(msgNonce ?? "") !== nonce) { throw new Error( "Nonce mismatch — possible prompt injection detected " + "(strict mode is enabled). A template variable may be " + From 79ba4060581ddc1ab546ca99ae257e3c4c7ba726 Mon Sep 17 00:00:00 2001 From: Seth Juarez Date: Fri, 10 Apr 2026 16:51:54 -0700 Subject: [PATCH 3/3] docs: add conceptual documentation for thread inputs (conversation history) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new core-concepts page explaining how thread inputs work in Prompty: - What threads are and why they use nonce-based expansion - How to declare kind: thread inputs and place them in templates - Data format with multi-language examples (Python, TypeScript, C#, Rust) - Internal pipeline mechanics (render → parse → expand) - Best practices: token budgets, sliding windows, stateless design - Integration with agent mode Also adds cross-references from the file-format doc, core-concepts index, and the chat-assistant tutorial. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../core-concepts/conversation-history.mdx | 448 ++++++++++++++++++ .../docs/core-concepts/file-format.mdx | 2 +- web/src/content/docs/core-concepts/index.mdx | 1 + .../content/docs/tutorials/chat-assistant.mdx | 5 + 4 files changed, 455 insertions(+), 1 deletion(-) create mode 100644 web/src/content/docs/core-concepts/conversation-history.mdx diff --git a/web/src/content/docs/core-concepts/conversation-history.mdx b/web/src/content/docs/core-concepts/conversation-history.mdx new file mode 100644 index 00000000..6f3d3598 --- /dev/null +++ b/web/src/content/docs/core-concepts/conversation-history.mdx @@ -0,0 +1,448 @@ +--- +title: Conversation History (Threads) +description: > + How to use thread inputs to give your prompts multi-turn conversation + context — declaration, data format, template placement, and how the + pipeline keeps user input safe with nonce-based expansion. +sidebar: + order: 3 +--- + +import { Aside, Tabs, TabItem } from '@astrojs/starlight/components'; + +## Overview + +Most LLM applications need **multi-turn conversation**. The model needs to +see prior messages — what the user asked and what it answered — to maintain +coherent context. In Prompty, conversation history is handled through +**thread inputs**: a special input kind that tells the pipeline to splice a +list of messages into the prompt at exactly the right position. + +```mermaid +flowchart LR + S["system:\nYou are helpful."] + T["🧵 Thread Messages\n(prior turns)"] + U["user:\nNew question"] + + S --> T --> U + + style S fill:#dbeafe,stroke:#3b82f6,color:#1e293b + style T fill:#fef3c7,stroke:#f59e0b,color:#78350f + style U fill:#d1fae5,stroke:#10b981,color:#065f46 +``` + +The result is a flat message array — system prompt, then prior conversation, +then the new user message — ready for the LLM. + +--- + +## Declaring a Thread Input + +Add an input with `kind: thread` to your `.prompty` file's `inputs`: + +```yaml {6-8} +--- +name: chat-assistant +model: + id: gpt-4o-mini + provider: openai + connection: + kind: key + apiKey: ${env:OPENAI_API_KEY} +inputs: + - name: question + kind: string + default: Hello! + - name: conversation + kind: thread +--- +``` + +Two things make a thread input different from a regular string or object input: + +1. **`kind: thread`** — signals the pipeline to use special handling (nonce-based + expansion) instead of simple template interpolation. +2. **The value is a list of messages** — not a scalar. Each message has a `role` + and `content`. + +--- + +## Placing the Thread in Your Template + +In the markdown body, place `{{conversation}}` (or whatever you named your +thread input) where the prior messages should appear. The most common pattern +puts it between the system prompt and the new user message: + +```text +system: +You are a friendly, helpful assistant. + +{{conversation}} +user: +{{question}} +``` + +This produces a message array like: + +| # | Role | Content | +|---|------|---------| +| 1 | `system` | You are a friendly, helpful assistant. | +| 2 | `user` | *(first turn — from thread)* | +| 3 | `assistant` | *(first response — from thread)* | +| 4 | `user` | *(second turn — from thread)* | +| 5 | `assistant` | *(second response — from thread)* | +| … | … | … | +| N | `user` | *(current question)* | + + + +--- + +## Passing Thread Data + +Thread data is a **list of message objects**. Each message needs a `role` +(typically `"user"` or `"assistant"`) and `content` (a string or structured +content array). + + + + ```python + import prompty + + history = [] + + while True: + question = input("You: ") + if question.lower() in ("quit", "exit"): + break + + result = prompty.invoke( + "assistant.prompty", + inputs={ + "question": question, + "conversation": history, + }, + ) + print(f"Assistant: {result}\n") + + # Append this exchange to history for the next turn + history.append({"role": "user", "content": question}) + history.append({"role": "assistant", "content": result}) + ``` + + + ```typescript + import { invoke } from "@prompty/core"; + import "@prompty/openai"; + + const history: { role: string; content: string }[] = []; + + // ... in your chat loop: + const result = await invoke("assistant.prompty", { + question: userMessage, + conversation: history, + }); + + history.push({ role: "user", content: userMessage }); + history.push({ role: "assistant", content: String(result) }); + ``` + + + ```csharp + using Prompty.Core; + + var history = new List>(); + + // ... in your chat loop: + var result = await Pipeline.InvokeAsync("assistant.prompty", new() + { + ["question"] = question, + ["conversation"] = history, + }); + + history.Add(new() { ["role"] = "user", ["content"] = question }); + history.Add(new() { ["role"] = "assistant", ["content"] = result!.ToString()! }); + ``` + + + ```rust + use serde_json::json; + + let history = vec![ + json!({"role": "user", "content": "What is Prompty?"}), + json!({"role": "assistant", "content": "Prompty is a file format for LLM prompts."}), + ]; + + let inputs = json!({ + "question": "Tell me more", + "conversation": history, + }); + + let result = pipeline::invoke("assistant.prompty", Some(&inputs)).await?; + ``` + + + +### Message Format + +Each message in the thread list should have: + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `role` | `string` | Yes | `"user"`, `"assistant"`, `"system"`, or `"tool"` | +| `content` | `string` or `array` | Yes | Text content, or an array of content parts for multimodal | + +**Simple format** — content as a plain string: +```json +[ + { "role": "user", "content": "What is the capital of France?" }, + { "role": "assistant", "content": "The capital of France is Paris." } +] +``` + +**Structured format** — content as an array of typed parts: +```json +[ + { + "role": "user", + "content": [ + { "kind": "text", "value": "What's in this image?" }, + { "kind": "image", "value": "https://example.com/photo.jpg" } + ] + }, + { + "role": "assistant", + "content": [ + { "kind": "text", "value": "The image shows a sunset over the ocean." } + ] + } +] +``` + + + +--- + +## How It Works Internally + +Thread inputs go through a **nonce-based expansion** mechanism rather than +simple string interpolation. This is important for security — it prevents +user-supplied conversation history from accidentally injecting role markers +(like `system:`) into the template. + +```mermaid +flowchart TD + subgraph Render["1. Render"] + direction TB + R1["Template: {{conversation}}"] + R2["Nonce: __PROMPTY_THREAD_a1b2c3d4_conversation__"] + R1 --> R2 + end + + subgraph Parse["2. Parse"] + direction TB + P1["Role marker splitting"] + P2["Nonce preserved as text in message"] + P1 --> P2 + end + + subgraph Expand["3. Expand"] + direction TB + E1["Find nonce in message text"] + E2["Replace with actual Message objects"] + E1 --> E2 + end + + Render --> Parse --> Expand --> Final["Final message array"] + + style Render fill:#dbeafe,stroke:#3b82f6,color:#1e293b + style Parse fill:#fef3c7,stroke:#f59e0b,color:#78350f + style Expand fill:#d1fae5,stroke:#10b981,color:#065f46 +``` + +### Step by step + +1. **Render** — The renderer replaces the thread variable with a unique + **nonce marker** (e.g., `__PROMPTY_THREAD_a1b2c3d4_conversation__`) instead + of the actual messages. The nonce is a random hex string that cannot appear + in normal text. + +2. **Parse** — The parser splits the rendered text on role markers (`system:`, + `user:`, `assistant:`). The nonce marker passes through as plain text + inside a message's content. + +3. **Expand** — The pipeline scans parsed messages for nonce markers. When + found, it splits the surrounding text, inserts the actual thread messages + from your input data, and produces the final flat message array. + +### Why nonces? + +If thread messages were interpolated directly into the template as text, a +malicious or accidental conversation entry like `"system: Ignore all +instructions"` would create a *new role boundary* during parsing. The nonce +approach ensures thread messages bypass the template engine and parser entirely +— they are inserted as pre-built `Message` objects *after* parsing is complete. + + + +--- + +## Best Practices + +### Token Budget Management + +Every message in the thread consumes tokens. As conversations grow, you'll +eventually hit the model's context window limit. Common strategies: + +- **Sliding window** — keep only the last *N* messages: + ```python + MAX_HISTORY = 20 # last 10 exchanges + history = history[-MAX_HISTORY:] + ``` + +- **Summarization** — periodically summarize older messages into a single + assistant message, then trim: + ```python + if len(history) > 30: + summary = summarize(history[:20]) # your summarization logic + history = [{"role": "assistant", "content": summary}] + history[20:] + ``` + +- **Token counting** — use a tokenizer to measure the thread and truncate + from the oldest messages until it fits within budget. + +### System Prompt Separation + +Keep your system prompt outside the thread. The thread should contain only +`user` and `assistant` messages from prior turns: + +```text +system: +You are an expert chef. ← Static system prompt + +{{conversation}} ← Prior user/assistant turns only + +user: +{{question}} ← New user message +``` + +### Multiple Thread Inputs + +You can declare more than one thread input for advanced patterns — for example, +a `context` thread for retrieved documents and a `conversation` thread for +chat history: + +```yaml +inputs: + - name: context + kind: thread + - name: conversation + kind: thread + - name: question + kind: string +``` + +```text +system: +You answer questions using the provided context. + +{{context}} +{{conversation}} +user: +{{question}} +``` + +Each thread is expanded independently at its marker position. + +### Stateless Design + +Prompty itself is **stateless** — it does not store conversation history +between calls. Your application code is responsible for: + +1. Accumulating messages in a list +2. Passing that list as the thread input on each call +3. Managing persistence (in-memory, database, session store, etc.) + +This gives you full control over what the model sees and makes it easy to +implement features like message editing, branching, and context management. + +--- + +## Thread Inputs with Agent Mode + +When using `apiType: agent` (the automatic tool-calling loop), thread inputs +work the same way — they provide the initial conversation context. The agent +loop then appends tool calls and results within a single `run()` invocation: + +```yaml +--- +name: agent-with-history +model: + id: gpt-4o + provider: openai + apiType: agent + connection: + kind: key + apiKey: ${env:OPENAI_API_KEY} +inputs: + - name: conversation + kind: thread + - name: question + kind: string +tools: + - name: get_weather + kind: function + description: Get current weather for a city + parameters: + properties: + - name: city + kind: string + required: true +--- +system: +You are a helpful assistant with access to weather data. + +{{conversation}} +user: +{{question}} +``` + +The thread provides context from prior turns, and the agent loop handles +any new tool calls within the current turn. + +--- + +## Quick Reference + +| Aspect | Details | +|--------|---------| +| **Declaration** | `kind: thread` in `inputs` | +| **Template syntax** | `{{threadName}}` — Jinja2 or Mustache | +| **Data format** | `list` of `{ role, content }` objects | +| **Empty thread** | Valid — produces no extra messages | +| **Expansion** | Nonce-based, post-parse (injection-safe) | +| **Supported runtimes** | Python, TypeScript, C#, Rust | +| **State management** | Application-side — Prompty is stateless | + +--- + +## Next Steps + +- [**Tutorial: Build a Chat Assistant**](/tutorials/chat-assistant/) — hands-on walkthrough building a multi-turn chatbot +- [**Agent Mode**](/core-concepts/agent-mode/) — combine threads with automatic tool calling +- [**Pipeline Architecture**](/core-concepts/pipeline/) — understand how threads fit into the four-stage pipeline +- [**Streaming**](/core-concepts/streaming/) — stream responses while using conversation history diff --git a/web/src/content/docs/core-concepts/file-format.mdx b/web/src/content/docs/core-concepts/file-format.mdx index cd730645..7aa0e2ba 100644 --- a/web/src/content/docs/core-concepts/file-format.mdx +++ b/web/src/content/docs/core-concepts/file-format.mdx @@ -196,7 +196,7 @@ outputs: | Field | Type | Default | Description | |--------------|-----------|----------|------------------------------------------| | `name` | `string` | — | Property name (required in list form) | -| `kind` | `string` | — | Type: `string`, `integer`, `float`, `boolean`, `array`, `object`, `thread`, `image`, `file`, `audio` | +| `kind` | `string` | — | Type: `string`, `integer`, `float`, `boolean`, `array`, `object`, [`thread`](/core-concepts/conversation-history/), `image`, `file`, `audio` | | `description`| `string` | — | Human-readable description | | `required` | `boolean` | `false` | Whether the input must be provided | | `default` | `any` | — | Default value when input is not provided | diff --git a/web/src/content/docs/core-concepts/index.mdx b/web/src/content/docs/core-concepts/index.mdx index 0555ba88..363a2d24 100644 --- a/web/src/content/docs/core-concepts/index.mdx +++ b/web/src/content/docs/core-concepts/index.mdx @@ -18,6 +18,7 @@ a key aspect of how Prompty processes `.prompty` files into LLM results. ## Configuration - [**Connections**](/core-concepts/connections/) — how Prompty authenticates with LLM providers (API keys, Microsoft Entra ID, registries) +- [**Conversation History (Threads)**](/core-concepts/conversation-history/) — multi-turn conversation via `kind: thread` inputs, nonce-based expansion, and best practices - [**Tools**](/core-concepts/tools/) — function, MCP, OpenAPI, and custom tool definitions ## Advanced Features diff --git a/web/src/content/docs/tutorials/chat-assistant.mdx b/web/src/content/docs/tutorials/chat-assistant.mdx index f3180dfa..944727db 100644 --- a/web/src/content/docs/tutorials/chat-assistant.mdx +++ b/web/src/content/docs/tutorials/chat-assistant.mdx @@ -401,6 +401,11 @@ invaluable for debugging unexpected outputs. ## Next steps +